summaryrefslogtreecommitdiffstats
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/admin.go191
-rw-r--r--app/analytics.go239
-rw-r--r--app/app.go16
-rw-r--r--app/apptestlib.go192
-rw-r--r--app/audit.go16
-rw-r--r--app/authorization.go197
-rw-r--r--app/authorization_test.go36
-rw-r--r--app/brand.go49
-rw-r--r--app/channel.go766
-rw-r--r--app/command.go31
-rw-r--r--app/compliance.go62
-rw-r--r--app/email.go236
-rw-r--r--app/email_batching.go252
-rw-r--r--app/email_batching_test.go193
-rw-r--r--app/email_test.go419
-rw-r--r--app/file.go529
-rw-r--r--app/file_test.go33
-rw-r--r--app/import.go157
-rw-r--r--app/ldap.go40
-rw-r--r--app/notification.go721
-rw-r--r--app/notification_test.go313
-rw-r--r--app/oauth.go34
-rw-r--r--app/post.go501
-rw-r--r--app/preference.go16
-rw-r--r--app/saml.go67
-rw-r--r--app/server.go217
-rw-r--r--app/session.go180
-rw-r--r--app/session_test.go31
-rw-r--r--app/slackimport.go631
-rw-r--r--app/slackimport_test.go240
-rw-r--r--app/status.go255
-rw-r--r--app/team.go563
-rw-r--r--app/user.go982
-rw-r--r--app/user_test.go53
-rw-r--r--app/web_conn.go254
-rw-r--r--app/web_hub.go241
-rw-r--r--app/webhook.go189
-rw-r--r--app/websocket_router.go96
-rw-r--r--app/webtrc.go33
39 files changed, 9271 insertions, 0 deletions
diff --git a/app/admin.go b/app/admin.go
new file mode 100644
index 000000000..51e69da57
--- /dev/null
+++ b/app/admin.go
@@ -0,0 +1,191 @@
+// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package app
+
+import (
+ "bufio"
+ "os"
+ "strings"
+ "time"
+
+ "runtime/debug"
+
+ 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 GetLogs() ([]string, *model.AppError) {
+ lines, err := GetLogsSkipSend()
+ if err != nil {
+ return nil, err
+ }
+
+ if einterfaces.GetClusterInterface() != nil {
+ clines, err := einterfaces.GetClusterInterface().GetLogs()
+ if err != nil {
+ return nil, err
+ }
+
+ lines = append(lines, clines...)
+ }
+
+ return lines, nil
+}
+
+func GetLogsSkipSend() ([]string, *model.AppError) {
+ var lines []string
+
+ if utils.Cfg.LogSettings.EnableFile {
+ file, err := os.Open(utils.GetLogFileLocation(utils.Cfg.LogSettings.FileLocation))
+ if err != nil {
+ return nil, model.NewLocAppError("getLogs", "api.admin.file_read_error", nil, err.Error())
+ }
+
+ defer file.Close()
+
+ scanner := bufio.NewScanner(file)
+ for scanner.Scan() {
+ lines = append(lines, scanner.Text())
+ }
+ } else {
+ lines = append(lines, "")
+ }
+
+ return lines, nil
+}
+
+func GetClusterStatus() []*model.ClusterInfo {
+ infos := make([]*model.ClusterInfo, 0)
+
+ if einterfaces.GetClusterInterface() != nil {
+ infos = einterfaces.GetClusterInterface().GetClusterInfos()
+ }
+
+ return infos
+}
+
+func InvalidateAllCaches() *model.AppError {
+ debug.FreeOSMemory()
+ InvalidateAllCachesSkipSend()
+
+ if einterfaces.GetClusterInterface() != nil {
+ err := einterfaces.GetClusterInterface().InvalidateAllCaches()
+ if err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+func InvalidateAllCachesSkipSend() {
+ l4g.Info(utils.T("api.context.invalidate_all_caches"))
+ sessionCache.Purge()
+ ClearStatusCache()
+ store.ClearChannelCaches()
+ store.ClearUserCaches()
+ store.ClearPostCaches()
+}
+
+func GetConfig() *model.Config {
+ json := utils.Cfg.ToJson()
+ cfg := model.ConfigFromJson(strings.NewReader(json))
+ cfg.Sanitize()
+
+ return cfg
+}
+
+func ReloadConfig() {
+ debug.FreeOSMemory()
+ utils.LoadConfig(utils.CfgFileName)
+
+ // start/restart email batching job if necessary
+ InitEmailBatching()
+}
+
+func SaveConfig(cfg *model.Config) *model.AppError {
+ cfg.SetDefaults()
+ utils.Desanitize(cfg)
+
+ if err := cfg.IsValid(); err != nil {
+ return err
+ }
+
+ if err := utils.ValidateLdapFilter(cfg); err != nil {
+ return err
+ }
+
+ if *utils.Cfg.ClusterSettings.Enable {
+ return model.NewLocAppError("saveConfig", "ent.cluster.save_config.error", nil, "")
+ }
+
+ //oldCfg := utils.Cfg
+ utils.SaveConfig(utils.CfgFileName, cfg)
+ utils.LoadConfig(utils.CfgFileName)
+
+ if einterfaces.GetMetricsInterface() != nil {
+ if *utils.Cfg.MetricsSettings.Enable {
+ einterfaces.GetMetricsInterface().StartServer()
+ } else {
+ einterfaces.GetMetricsInterface().StopServer()
+ }
+ }
+
+ // Future feature is to sync the configuration files
+ // if einterfaces.GetClusterInterface() != nil {
+ // err := einterfaces.GetClusterInterface().ConfigChanged(cfg, oldCfg, true)
+ // if err != nil {
+ // return err
+ // }
+ // }
+
+ // start/restart email batching job if necessary
+ InitEmailBatching()
+
+ return nil
+}
+
+func RecycleDatabaseConnection() {
+ oldStore := Srv.Store
+
+ l4g.Warn(utils.T("api.admin.recycle_db_start.warn"))
+ Srv.Store = store.NewSqlStore()
+
+ time.Sleep(20 * time.Second)
+ oldStore.Close()
+
+ l4g.Warn(utils.T("api.admin.recycle_db_end.warn"))
+}
+
+func TestEmail(userId string, cfg *model.Config) *model.AppError {
+ if len(cfg.EmailSettings.SMTPServer) == 0 {
+ return model.NewLocAppError("testEmail", "api.admin.test_email.missing_server", nil, utils.T("api.context.invalid_param.app_error", map[string]interface{}{"Name": "SMTPServer"}))
+ }
+
+ // if the user hasn't changed their email settings, fill in the actual SMTP password so that
+ // the user can verify an existing SMTP connection
+ if cfg.EmailSettings.SMTPPassword == model.FAKE_SETTING {
+ if cfg.EmailSettings.SMTPServer == utils.Cfg.EmailSettings.SMTPServer &&
+ cfg.EmailSettings.SMTPPort == utils.Cfg.EmailSettings.SMTPPort &&
+ cfg.EmailSettings.SMTPUsername == utils.Cfg.EmailSettings.SMTPUsername {
+ cfg.EmailSettings.SMTPPassword = utils.Cfg.EmailSettings.SMTPPassword
+ } else {
+ return model.NewLocAppError("testEmail", "api.admin.test_email.reenter_password", nil, "")
+ }
+ }
+
+ if user, err := GetUser(userId); err != nil {
+ return err
+ } else {
+ T := utils.GetUserTranslations(user.Locale)
+ if err := utils.SendMailUsingConfig(user.Email, T("api.admin.test_email.subject"), T("api.admin.test_email.body"), cfg); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
diff --git a/app/analytics.go b/app/analytics.go
new file mode 100644
index 000000000..891c0dfae
--- /dev/null
+++ b/app/analytics.go
@@ -0,0 +1,239 @@
+// Copyright (c) 2017 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"
+)
+
+const (
+ DAY_MILLISECONDS = 24 * 60 * 60 * 1000
+ MONTH_MILLISECONDS = 31 * DAY_MILLISECONDS
+)
+
+func GetAnalytics(name string, teamId string) (model.AnalyticsRows, *model.AppError) {
+ skipIntensiveQueries := false
+ var systemUserCount int64
+ if r := <-Srv.Store.User().AnalyticsUniqueUserCount(""); r.Err != nil {
+ return nil, r.Err
+ } else {
+ systemUserCount = r.Data.(int64)
+ if systemUserCount > int64(*utils.Cfg.AnalyticsSettings.MaxUsersForStatistics) {
+ l4g.Debug("More than %v users on the system, intensive queries skipped", *utils.Cfg.AnalyticsSettings.MaxUsersForStatistics)
+ skipIntensiveQueries = true
+ }
+ }
+
+ if name == "standard" {
+ var rows model.AnalyticsRows = make([]*model.AnalyticsRow, 10)
+ rows[0] = &model.AnalyticsRow{"channel_open_count", 0}
+ rows[1] = &model.AnalyticsRow{"channel_private_count", 0}
+ rows[2] = &model.AnalyticsRow{"post_count", 0}
+ rows[3] = &model.AnalyticsRow{"unique_user_count", 0}
+ rows[4] = &model.AnalyticsRow{"team_count", 0}
+ rows[5] = &model.AnalyticsRow{"total_websocket_connections", 0}
+ rows[6] = &model.AnalyticsRow{"total_master_db_connections", 0}
+ rows[7] = &model.AnalyticsRow{"total_read_db_connections", 0}
+ rows[8] = &model.AnalyticsRow{"daily_active_users", 0}
+ rows[9] = &model.AnalyticsRow{"monthly_active_users", 0}
+
+ openChan := Srv.Store.Channel().AnalyticsTypeCount(teamId, model.CHANNEL_OPEN)
+ privateChan := Srv.Store.Channel().AnalyticsTypeCount(teamId, model.CHANNEL_PRIVATE)
+ teamChan := Srv.Store.Team().AnalyticsTeamCount()
+
+ var userChan store.StoreChannel
+ if teamId != "" {
+ userChan = Srv.Store.User().AnalyticsUniqueUserCount(teamId)
+ }
+
+ var postChan store.StoreChannel
+ if !skipIntensiveQueries {
+ postChan = Srv.Store.Post().AnalyticsPostCount(teamId, false, false)
+ }
+
+ dailyActiveChan := Srv.Store.User().AnalyticsActiveCount(DAY_MILLISECONDS)
+ monthlyActiveChan := Srv.Store.User().AnalyticsActiveCount(MONTH_MILLISECONDS)
+
+ if r := <-openChan; r.Err != nil {
+ return nil, r.Err
+ } else {
+ rows[0].Value = float64(r.Data.(int64))
+ }
+
+ if r := <-privateChan; r.Err != nil {
+ return nil, r.Err
+ } else {
+ rows[1].Value = float64(r.Data.(int64))
+ }
+
+ if postChan == nil {
+ rows[2].Value = -1
+ } else {
+ if r := <-postChan; r.Err != nil {
+ return nil, r.Err
+ } else {
+ rows[2].Value = float64(r.Data.(int64))
+ }
+ }
+
+ if userChan == nil {
+ rows[3].Value = float64(systemUserCount)
+ } else {
+ if r := <-userChan; r.Err != nil {
+ return nil, r.Err
+ } else {
+ rows[3].Value = float64(r.Data.(int64))
+ }
+ }
+
+ if r := <-teamChan; r.Err != nil {
+ return nil, r.Err
+ } else {
+ rows[4].Value = float64(r.Data.(int64))
+ }
+
+ // If in HA mode then aggregrate all the stats
+ if einterfaces.GetClusterInterface() != nil && *utils.Cfg.ClusterSettings.Enable {
+ stats, err := einterfaces.GetClusterInterface().GetClusterStats()
+ if err != nil {
+ return nil, err
+ }
+
+ totalSockets := TotalWebsocketConnections()
+ totalMasterDb := Srv.Store.TotalMasterDbConnections()
+ totalReadDb := Srv.Store.TotalReadDbConnections()
+
+ for _, stat := range stats {
+ totalSockets = totalSockets + stat.TotalWebsocketConnections
+ totalMasterDb = totalMasterDb + stat.TotalMasterDbConnections
+ totalReadDb = totalReadDb + stat.TotalReadDbConnections
+ }
+
+ rows[5].Value = float64(totalSockets)
+ rows[6].Value = float64(totalMasterDb)
+ 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())
+ }
+
+ if r := <-dailyActiveChan; r.Err != nil {
+ return nil, r.Err
+ } else {
+ rows[8].Value = float64(r.Data.(int64))
+ }
+
+ if r := <-monthlyActiveChan; r.Err != nil {
+ return nil, r.Err
+ } else {
+ rows[9].Value = float64(r.Data.(int64))
+ }
+
+ return rows, nil
+ } else if name == "post_counts_day" {
+ if skipIntensiveQueries {
+ rows := model.AnalyticsRows{&model.AnalyticsRow{"", -1}}
+ return rows, nil
+ }
+
+ if r := <-Srv.Store.Post().AnalyticsPostCountsByDay(teamId); r.Err != nil {
+ return nil, r.Err
+ } else {
+ return r.Data.(model.AnalyticsRows), nil
+ }
+ } else if name == "user_counts_with_posts_day" {
+ if skipIntensiveQueries {
+ rows := model.AnalyticsRows{&model.AnalyticsRow{"", -1}}
+ return rows, nil
+ }
+
+ if r := <-Srv.Store.Post().AnalyticsUserCountsWithPostsByDay(teamId); r.Err != nil {
+ return nil, r.Err
+ } else {
+ return r.Data.(model.AnalyticsRows), nil
+ }
+ } else if name == "extra_counts" {
+ var rows model.AnalyticsRows = make([]*model.AnalyticsRow, 6)
+ rows[0] = &model.AnalyticsRow{"file_post_count", 0}
+ rows[1] = &model.AnalyticsRow{"hashtag_post_count", 0}
+ rows[2] = &model.AnalyticsRow{"incoming_webhook_count", 0}
+ rows[3] = &model.AnalyticsRow{"outgoing_webhook_count", 0}
+ 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()
+
+ 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)
+ }
+
+ if fileChan == nil {
+ rows[0].Value = -1
+ } else {
+ if r := <-fileChan; r.Err != nil {
+ return nil, r.Err
+ } else {
+ rows[0].Value = float64(r.Data.(int64))
+ }
+ }
+
+ if hashtagChan == nil {
+ rows[1].Value = -1
+ } else {
+ if r := <-hashtagChan; r.Err != nil {
+ return nil, r.Err
+ } else {
+ rows[1].Value = float64(r.Data.(int64))
+ }
+ }
+
+ if r := <-iHookChan; r.Err != nil {
+ return nil, r.Err
+ } else {
+ rows[2].Value = float64(r.Data.(int64))
+ }
+
+ if r := <-oHookChan; r.Err != nil {
+ return nil, r.Err
+ } else {
+ rows[3].Value = float64(r.Data.(int64))
+ }
+
+ if r := <-commandChan; r.Err != nil {
+ return nil, r.Err
+ } else {
+ rows[4].Value = float64(r.Data.(int64))
+ }
+
+ if r := <-sessionChan; r.Err != nil {
+ return nil, r.Err
+ } else {
+ rows[5].Value = float64(r.Data.(int64))
+ }
+
+ return rows, nil
+ }
+
+ return nil, nil
+}
+
+func GetRecentlyActiveUsersForTeam(teamId string) (map[string]*model.User, *model.AppError) {
+ if result := <-Srv.Store.User().GetRecentlyActiveUsersForTeam(teamId); result.Err != nil {
+ return nil, result.Err
+ } else {
+ return result.Data.(map[string]*model.User), nil
+ }
+}
diff --git a/app/app.go b/app/app.go
new file mode 100644
index 000000000..8568c7bba
--- /dev/null
+++ b/app/app.go
@@ -0,0 +1,16 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package app
+
+import (
+ "io/ioutil"
+ "net/http"
+)
+
+func CloseBody(r *http.Response) {
+ if r.Body != nil {
+ ioutil.ReadAll(r.Body)
+ r.Body.Close()
+ }
+}
diff --git a/app/apptestlib.go b/app/apptestlib.go
new file mode 100644
index 000000000..41e234130
--- /dev/null
+++ b/app/apptestlib.go
@@ -0,0 +1,192 @@
+// 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.InitHTML()
+ 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/audit.go b/app/audit.go
new file mode 100644
index 000000000..6978e9bc2
--- /dev/null
+++ b/app/audit.go
@@ -0,0 +1,16 @@
+// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package app
+
+import (
+ "github.com/mattermost/platform/model"
+)
+
+func GetAudits(userId string, limit int) (model.Audits, *model.AppError) {
+ if result := <-Srv.Store.Audit().Get(userId, limit); result.Err != nil {
+ return nil, result.Err
+ } else {
+ return result.Data.(model.Audits), nil
+ }
+}
diff --git a/app/authorization.go b/app/authorization.go
new file mode 100644
index 000000000..b43d64341
--- /dev/null
+++ b/app/authorization.go
@@ -0,0 +1,197 @@
+// 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"
+)
+
+func SessionHasPermissionTo(session model.Session, permission *model.Permission) bool {
+ return CheckIfRolesGrantPermission(session.GetUserRoles(), permission.Id)
+}
+
+func SessionHasPermissionToTeam(session model.Session, teamId string, permission *model.Permission) bool {
+ if teamId == "" {
+ return false
+ }
+
+ teamMember := session.GetTeamByTeamId(teamId)
+ if teamMember != nil {
+ if CheckIfRolesGrantPermission(teamMember.GetRoles(), permission.Id) {
+ return true
+ }
+ }
+
+ return SessionHasPermissionTo(session, permission)
+}
+
+func SessionHasPermissionToChannel(session model.Session, channelId string, permission *model.Permission) bool {
+ if channelId == "" {
+ return false
+ }
+
+ channelMember, err := GetChannelMember(channelId, session.UserId)
+ if err == nil {
+ roles := channelMember.GetRoles()
+ if CheckIfRolesGrantPermission(roles, permission.Id) {
+ return true
+ }
+ }
+
+ var channel *model.Channel
+ channel, err = GetChannel(channelId)
+ if err == nil {
+ return SessionHasPermissionToTeam(session, channel.TeamId, permission)
+ }
+
+ return SessionHasPermissionTo(session, permission)
+}
+
+func SessionHasPermissionToChannelByPost(session model.Session, postId string, permission *model.Permission) bool {
+ var channelMember *model.ChannelMember
+ if result := <-Srv.Store.Channel().GetMemberForPost(postId, session.UserId); result.Err == nil {
+ channelMember = result.Data.(*model.ChannelMember)
+
+ if CheckIfRolesGrantPermission(channelMember.GetRoles(), permission.Id) {
+ return true
+ }
+ }
+
+ if result := <-Srv.Store.Channel().GetForPost(postId); result.Err == nil {
+ channel := result.Data.(*model.Channel)
+ return SessionHasPermissionToTeam(session, channel.TeamId, permission)
+ }
+
+ return SessionHasPermissionTo(session, permission)
+}
+
+func SessionHasPermissionToUser(session model.Session, userId string) bool {
+ if userId == "" {
+ return false
+ }
+
+ if session.UserId == userId {
+ return true
+ }
+
+ if SessionHasPermissionTo(session, model.PERMISSION_EDIT_OTHER_USERS) {
+ return true
+ }
+
+ return false
+}
+
+func SessionHasPermissionToPost(session model.Session, postId string, permission *model.Permission) bool {
+ post, err := GetSinglePost(postId)
+ if err != nil {
+ return false
+ }
+
+ if post.UserId == session.UserId {
+ return true
+ }
+
+ return SessionHasPermissionToChannel(session, post.ChannelId, permission)
+}
+
+func HasPermissionTo(askingUserId string, permission *model.Permission) bool {
+ user, err := GetUser(askingUserId)
+ if err != nil {
+ return false
+ }
+
+ roles := user.GetRoles()
+
+ return CheckIfRolesGrantPermission(roles, permission.Id)
+}
+
+func HasPermissionToTeam(askingUserId string, teamId string, permission *model.Permission) bool {
+ if teamId == "" || askingUserId == "" {
+ return false
+ }
+
+ teamMember, err := GetTeamMember(teamId, askingUserId)
+ if err != nil {
+ return false
+ }
+
+ roles := teamMember.GetRoles()
+
+ if CheckIfRolesGrantPermission(roles, permission.Id) {
+ return true
+ }
+
+ return HasPermissionTo(askingUserId, permission)
+}
+
+func HasPermissionToChannel(askingUserId string, channelId string, permission *model.Permission) bool {
+ if channelId == "" || askingUserId == "" {
+ return false
+ }
+
+ channelMember, err := GetChannelMember(channelId, askingUserId)
+ if err == nil {
+ roles := channelMember.GetRoles()
+ if CheckIfRolesGrantPermission(roles, permission.Id) {
+ return true
+ }
+ }
+
+ var channel *model.Channel
+ channel, err = GetChannel(channelId)
+ if err == nil {
+ return HasPermissionToTeam(askingUserId, channel.TeamId, permission)
+ }
+
+ return HasPermissionTo(askingUserId, permission)
+}
+
+func HasPermissionToChannelByPost(askingUserId string, postId string, permission *model.Permission) bool {
+ var channelMember *model.ChannelMember
+ if result := <-Srv.Store.Channel().GetMemberForPost(postId, askingUserId); result.Err == nil {
+ channelMember = result.Data.(*model.ChannelMember)
+
+ if CheckIfRolesGrantPermission(channelMember.GetRoles(), permission.Id) {
+ return true
+ }
+ }
+
+ if result := <-Srv.Store.Channel().GetForPost(postId); result.Err == nil {
+ channel := result.Data.(*model.Channel)
+ return HasPermissionToTeam(askingUserId, channel.TeamId, permission)
+ }
+
+ return HasPermissionTo(askingUserId, permission)
+}
+
+func HasPermissionToUser(askingUserId string, userId string) bool {
+ if askingUserId == userId {
+ return true
+ }
+
+ if HasPermissionTo(askingUserId, model.PERMISSION_EDIT_OTHER_USERS) {
+ return true
+ }
+
+ return false
+}
+
+func CheckIfRolesGrantPermission(roles []string, permissionId string) bool {
+ for _, roleId := range roles {
+ if role, ok := model.BuiltInRoles[roleId]; !ok {
+ l4g.Debug("Bad role in system " + roleId)
+ return false
+ } else {
+ permissions := role.Permissions
+ for _, permission := range permissions {
+ if permission == permissionId {
+ return true
+ }
+ }
+ }
+ }
+
+ return false
+}
diff --git a/app/authorization_test.go b/app/authorization_test.go
new file mode 100644
index 000000000..049567483
--- /dev/null
+++ b/app/authorization_test.go
@@ -0,0 +1,36 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package app
+
+import (
+ "testing"
+
+ "github.com/mattermost/platform/model"
+)
+
+func TestCheckIfRolesGrantPermission(t *testing.T) {
+ Setup()
+
+ cases := []struct {
+ roles []string
+ permissionId string
+ shouldGrant bool
+ }{
+ {[]string{model.ROLE_SYSTEM_ADMIN.Id}, model.ROLE_SYSTEM_ADMIN.Permissions[0], true},
+ {[]string{model.ROLE_SYSTEM_ADMIN.Id}, "non-existant-permission", false},
+ {[]string{model.ROLE_CHANNEL_USER.Id}, model.ROLE_CHANNEL_USER.Permissions[0], true},
+ {[]string{model.ROLE_CHANNEL_USER.Id}, model.PERMISSION_MANAGE_SYSTEM.Id, false},
+ {[]string{model.ROLE_SYSTEM_ADMIN.Id, model.ROLE_CHANNEL_USER.Id}, model.PERMISSION_MANAGE_SYSTEM.Id, true},
+ {[]string{model.ROLE_CHANNEL_USER.Id, model.ROLE_SYSTEM_ADMIN.Id}, model.PERMISSION_MANAGE_SYSTEM.Id, true},
+ {[]string{model.ROLE_TEAM_USER.Id, model.ROLE_TEAM_ADMIN.Id}, model.PERMISSION_MANAGE_SLASH_COMMANDS.Id, true},
+ {[]string{model.ROLE_TEAM_ADMIN.Id, model.ROLE_TEAM_USER.Id}, model.PERMISSION_MANAGE_SLASH_COMMANDS.Id, true},
+ }
+
+ for testnum, testcase := range cases {
+ if CheckIfRolesGrantPermission(testcase.roles, testcase.permissionId) != testcase.shouldGrant {
+ t.Fatal("Failed test case ", testnum)
+ }
+ }
+
+}
diff --git a/app/brand.go b/app/brand.go
new file mode 100644
index 000000000..aeecc6972
--- /dev/null
+++ b/app/brand.go
@@ -0,0 +1,49 @@
+// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package app
+
+import (
+ "mime/multipart"
+ "net/http"
+
+ "github.com/mattermost/platform/einterfaces"
+ "github.com/mattermost/platform/model"
+ "github.com/mattermost/platform/utils"
+)
+
+func SaveBrandImage(imageData *multipart.FileHeader) *model.AppError {
+ brandInterface := einterfaces.GetBrandInterface()
+ if brandInterface == nil {
+ err := model.NewLocAppError("SaveBrandImage", "api.admin.upload_brand_image.not_available.app_error", nil, "")
+ err.StatusCode = http.StatusNotImplemented
+ return err
+ }
+
+ if err := brandInterface.SaveBrandImage(imageData); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func GetBrandImage() ([]byte, *model.AppError) {
+ if len(utils.Cfg.FileSettings.DriverName) == 0 {
+ err := model.NewLocAppError("GetBrandImage", "api.admin.get_brand_image.storage.app_error", nil, "")
+ err.StatusCode = http.StatusNotImplemented
+ return nil, err
+ }
+
+ brandInterface := einterfaces.GetBrandInterface()
+ if brandInterface == nil {
+ err := model.NewLocAppError("GetBrandImage", "api.admin.get_brand_image.not_available.app_error", nil, "")
+ err.StatusCode = http.StatusNotImplemented
+ return nil, err
+ }
+
+ if img, err := brandInterface.GetBrandImage(); err != nil {
+ return nil, err
+ } else {
+ return img, nil
+ }
+}
diff --git a/app/channel.go b/app/channel.go
new file mode 100644
index 000000000..1844e3177
--- /dev/null
+++ b/app/channel.go
@@ -0,0 +1,766 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package app
+
+import (
+ "fmt"
+ "net/http"
+
+ 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 CreateDirectChannel(userId string, otherUserId string) (*model.Channel, *model.AppError) {
+ uc := 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.Err.Id == store.CHANNEL_EXISTS_ERROR {
+ return result.Data.(*model.Channel), nil
+ } else {
+ return nil, result.Err
+ }
+ } else {
+ channel := result.Data.(*model.Channel)
+
+ InvalidateCacheForUser(userId)
+ InvalidateCacheForUser(otherUserId)
+
+ message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_DIRECT_ADDED, "", channel.Id, "", nil)
+ message.Add("teammate_id", otherUserId)
+ Publish(message)
+
+ return channel, nil
+ }
+}
+
+func UpdateChannel(channel *model.Channel) (*model.Channel, *model.AppError) {
+ if result := <-Srv.Store.Channel().Update(channel); result.Err != nil {
+ return nil, result.Err
+ } else {
+ InvalidateCacheForChannel(channel.Id)
+ return channel, nil
+ }
+}
+
+func UpdateChannelMemberRoles(channelId string, userId string, newRoles string) (*model.ChannelMember, *model.AppError) {
+ var member *model.ChannelMember
+ var err *model.AppError
+ if member, err = GetChannelMember(channelId, userId); err != nil {
+ return nil, err
+ }
+
+ member.Roles = newRoles
+
+ if result := <-Srv.Store.Channel().UpdateMember(member); result.Err != nil {
+ return nil, result.Err
+ }
+
+ InvalidateCacheForUser(userId)
+ return member, nil
+}
+
+func UpdateChannelMemberNotifyProps(data map[string]string, channelId string, userId string) (*model.ChannelMember, *model.AppError) {
+ var member *model.ChannelMember
+ var err *model.AppError
+ if member, err = GetChannelMember(channelId, userId); err != nil {
+ return nil, err
+ }
+
+ // update whichever notify properties have been provided, but don't change the others
+ if markUnread, exists := data["mark_unread"]; exists {
+ member.NotifyProps["mark_unread"] = markUnread
+ }
+
+ if desktop, exists := data["desktop"]; exists {
+ member.NotifyProps["desktop"] = desktop
+ }
+
+ if result := <-Srv.Store.Channel().UpdateMember(member); result.Err != nil {
+ return nil, result.Err
+ } else {
+ InvalidateCacheForUser(userId)
+ return member, nil
+ }
+}
+
+func DeleteChannel(channel *model.Channel, userId string) *model.AppError {
+ uc := Srv.Store.User().Get(userId)
+ ihc := Srv.Store.Webhook().GetIncomingByChannel(channel.Id)
+ ohc := Srv.Store.Webhook().GetOutgoingByChannel(channel.Id)
+
+ if uresult := <-uc; uresult.Err != nil {
+ return uresult.Err
+ } else if ihcresult := <-ihc; ihcresult.Err != nil {
+ return ihcresult.Err
+ } else if ohcresult := <-ohc; ohcresult.Err != nil {
+ return ohcresult.Err
+ } else {
+ user := uresult.Data.(*model.User)
+ incomingHooks := ihcresult.Data.([]*model.IncomingWebhook)
+ outgoingHooks := ohcresult.Data.([]*model.OutgoingWebhook)
+
+ if channel.DeleteAt > 0 {
+ err := model.NewLocAppError("deleteChannel", "api.channel.delete_channel.deleted.app_error", nil, "")
+ err.StatusCode = http.StatusBadRequest
+ return err
+ }
+
+ if channel.Name == model.DEFAULT_CHANNEL {
+ err := model.NewLocAppError("deleteChannel", "api.channel.delete_channel.cannot.app_error", map[string]interface{}{"Channel": model.DEFAULT_CHANNEL}, "")
+ err.StatusCode = http.StatusBadRequest
+ return err
+ }
+
+ T := utils.GetUserTranslations(user.Locale)
+
+ post := &model.Post{
+ ChannelId: channel.Id,
+ Message: fmt.Sprintf(T("api.channel.delete_channel.archived"), user.Username),
+ Type: model.POST_CHANNEL_DELETED,
+ UserId: userId,
+ }
+
+ if _, err := CreatePost(post, channel.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 {
+ 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 {
+ 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 {
+ return dresult.Err
+ }
+ InvalidateCacheForChannel(channel.Id)
+
+ message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_CHANNEL_DELETED, channel.TeamId, "", "", nil)
+ message.Add("channel_id", channel.Id)
+
+ Publish(message)
+ }
+
+ return 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)
+ Publish(message)
+
+ return newMember, nil
+}
+
+func AddDirectChannels(teamId string, user *model.User) *model.AppError {
+ var profiles map[string]*model.User
+ if result := <-Srv.Store.User().GetProfiles(teamId, 0, 100); result.Err != nil {
+ return model.NewLocAppError("AddDirectChannels", "api.user.add_direct_channels_and_forget.failed.error", map[string]interface{}{"UserId": user.Id, "TeamId": teamId, "Error": result.Err.Error()}, "")
+ } else {
+ profiles = result.Data.(map[string]*model.User)
+ }
+
+ var preferences model.Preferences
+
+ for id := range profiles {
+ if id == user.Id {
+ continue
+ }
+
+ profile := profiles[id]
+
+ preference := model.Preference{
+ UserId: user.Id,
+ Category: model.PREFERENCE_CATEGORY_DIRECT_CHANNEL_SHOW,
+ Name: profile.Id,
+ Value: "true",
+ }
+
+ preferences = append(preferences, preference)
+
+ if len(preferences) >= 10 {
+ break
+ }
+ }
+
+ if result := <-Srv.Store.Preference().Save(&preferences); result.Err != nil {
+ return model.NewLocAppError("AddDirectChannels", "api.user.add_direct_channels_and_forget.failed.error", map[string]interface{}{"UserId": user.Id, "TeamId": teamId, "Error": result.Err.Error()}, "")
+ }
+
+ return nil
+}
+
+func PostUpdateChannelHeaderMessage(userId string, channelId string, teamId string, oldChannelHeader, newChannelHeader string) *model.AppError {
+ uc := Srv.Store.User().Get(userId)
+
+ if uresult := <-uc; uresult.Err != nil {
+ return model.NewLocAppError("PostUpdateChannelHeaderMessage", "api.channel.post_update_channel_header_message_and_forget.retrieve_user.error", nil, uresult.Err.Error())
+ } else {
+ user := uresult.Data.(*model.User)
+
+ var message string
+ if oldChannelHeader == "" {
+ message = fmt.Sprintf(utils.T("api.channel.post_update_channel_header_message_and_forget.updated_to"), user.Username, newChannelHeader)
+ } else if newChannelHeader == "" {
+ message = fmt.Sprintf(utils.T("api.channel.post_update_channel_header_message_and_forget.removed"), user.Username, oldChannelHeader)
+ } else {
+ message = fmt.Sprintf(utils.T("api.channel.post_update_channel_header_message_and_forget.updated_from"), user.Username, oldChannelHeader, newChannelHeader)
+ }
+
+ post := &model.Post{
+ ChannelId: channelId,
+ Message: message,
+ Type: model.POST_HEADER_CHANGE,
+ UserId: userId,
+ Props: model.StringInterface{
+ "old_header": oldChannelHeader,
+ "new_header": newChannelHeader,
+ },
+ }
+
+ if _, err := CreatePost(post, teamId, false); err != nil {
+ return model.NewLocAppError("", "api.channel.post_update_channel_header_message_and_forget.post.error", nil, err.Error())
+ }
+ }
+
+ return nil
+}
+
+func PostUpdateChannelPurposeMessage(userId string, channelId string, teamId string, oldChannelPurpose string, newChannelPurpose string) *model.AppError {
+ uc := Srv.Store.User().Get(userId)
+
+ if uresult := <-uc; uresult.Err != nil {
+ return model.NewLocAppError("PostUpdateChannelPurposeMessage", "app.channel.post_update_channel_purpose_message.retrieve_user.error", nil, uresult.Err.Error())
+ } else {
+ user := uresult.Data.(*model.User)
+
+ var message string
+ if oldChannelPurpose == "" {
+ message = fmt.Sprintf(utils.T("app.channel.post_update_channel_purpose_message.updated_to"), user.Username, newChannelPurpose)
+ } else if newChannelPurpose == "" {
+ message = fmt.Sprintf(utils.T("app.channel.post_update_channel_purpose_message.removed"), user.Username, oldChannelPurpose)
+ } else {
+ message = fmt.Sprintf(utils.T("app.channel.post_update_channel_purpose_message.updated_from"), user.Username, oldChannelPurpose, newChannelPurpose)
+ }
+
+ post := &model.Post{
+ ChannelId: channelId,
+ Message: message,
+ Type: model.POST_PURPOSE_CHANGE,
+ UserId: userId,
+ Props: model.StringInterface{
+ "old_purpose": oldChannelPurpose,
+ "new_purpose": newChannelPurpose,
+ },
+ }
+ if _, err := CreatePost(post, teamId, false); err != nil {
+ return model.NewLocAppError("", "app.channel.post_update_channel_purpose_message.post.error", nil, err.Error())
+ }
+ }
+
+ return nil
+}
+
+func PostUpdateChannelDisplayNameMessage(userId string, channelId string, teamId string, oldChannelDisplayName, newChannelDisplayName string) *model.AppError {
+ uc := Srv.Store.User().Get(userId)
+
+ if uresult := <-uc; uresult.Err != nil {
+ return model.NewLocAppError("PostUpdateChannelDisplayNameMessage", "api.channel.post_update_channel_displayname_message_and_forget.retrieve_user.error", nil, uresult.Err.Error())
+ } else {
+ user := uresult.Data.(*model.User)
+
+ message := fmt.Sprintf(utils.T("api.channel.post_update_channel_displayname_message_and_forget.updated_from"), user.Username, oldChannelDisplayName, newChannelDisplayName)
+
+ post := &model.Post{
+ ChannelId: channelId,
+ Message: message,
+ Type: model.POST_DISPLAYNAME_CHANGE,
+ UserId: userId,
+ Props: model.StringInterface{
+ "old_displayname": oldChannelDisplayName,
+ "new_displayname": newChannelDisplayName,
+ },
+ }
+
+ if _, err := CreatePost(post, teamId, false); err != nil {
+ return model.NewLocAppError("PostUpdateChannelDisplayNameMessage", "api.channel.post_update_channel_displayname_message_and_forget.create_post.error", nil, err.Error())
+ }
+ }
+
+ return nil
+}
+
+func GetChannel(channelId string) (*model.Channel, *model.AppError) {
+ if result := <-Srv.Store.Channel().Get(channelId, true); result.Err != nil {
+ return nil, result.Err
+ } else {
+ return result.Data.(*model.Channel), nil
+ }
+}
+
+func GetChannelByName(channelName, teamId string) (*model.Channel, *model.AppError) {
+ if result := <-Srv.Store.Channel().GetByName(teamId, channelName); result.Err != nil {
+ return nil, result.Err
+ } else {
+ return result.Data.(*model.Channel), nil
+ }
+}
+
+func GetChannelsForUser(teamId string, userId string) (*model.ChannelList, *model.AppError) {
+ if result := <-Srv.Store.Channel().GetChannels(teamId, userId); result.Err != nil {
+ return nil, result.Err
+ } else {
+ return result.Data.(*model.ChannelList), nil
+ }
+}
+
+func GetChannelsUserNotIn(teamId string, userId string, offset int, limit int) (*model.ChannelList, *model.AppError) {
+ if result := <-Srv.Store.Channel().GetMoreChannels(teamId, userId, offset, limit); result.Err != nil {
+ return nil, result.Err
+ } else {
+ return result.Data.(*model.ChannelList), nil
+ }
+}
+
+func GetChannelMember(channelId string, userId string) (*model.ChannelMember, *model.AppError) {
+ if result := <-Srv.Store.Channel().GetMember(channelId, userId); result.Err != nil {
+ return nil, result.Err
+ } else {
+ return result.Data.(*model.ChannelMember), nil
+ }
+}
+
+func GetChannelMembersByIds(channelId string, userIds []string) (*model.ChannelMembers, *model.AppError) {
+ if result := <-Srv.Store.Channel().GetMembersByIds(channelId, userIds); result.Err != nil {
+ return nil, result.Err
+ } else {
+ return result.Data.(*model.ChannelMembers), nil
+ }
+}
+
+func GetChannelMembersForUser(teamId string, userId string) (*model.ChannelMembers, *model.AppError) {
+ if result := <-Srv.Store.Channel().GetMembersForUser(teamId, userId); result.Err != nil {
+ return nil, result.Err
+ } else {
+ return result.Data.(*model.ChannelMembers), nil
+ }
+}
+
+func GetChannelMemberCount(channelId string) (int64, *model.AppError) {
+ if result := <-Srv.Store.Channel().GetMemberCount(channelId, true); result.Err != nil {
+ return 0, result.Err
+ } else {
+ return result.Data.(int64), nil
+ }
+}
+
+func GetChannelCounts(teamId string, userId string) (*model.ChannelCounts, *model.AppError) {
+ if result := <-Srv.Store.Channel().GetChannelCounts(teamId, userId); result.Err != nil {
+ return nil, result.Err
+ } else {
+ return result.Data.(*model.ChannelCounts), nil
+ }
+}
+
+func JoinChannel(channel *model.Channel, userId string) *model.AppError {
+ userChan := Srv.Store.User().Get(userId)
+ memberChan := Srv.Store.Channel().GetMember(channel.Id, userId)
+
+ if uresult := <-userChan; uresult.Err != nil {
+ return uresult.Err
+ } else if mresult := <-memberChan; mresult.Err == nil && mresult.Data != nil {
+ // user is already in the channel
+ return nil
+ } else {
+ user := uresult.Data.(*model.User)
+
+ if channel.Type == model.CHANNEL_OPEN {
+ if _, err := AddUserToChannel(user, channel); err != nil {
+ return err
+ }
+ PostUserAddRemoveMessage(userId, channel.Id, channel.TeamId, fmt.Sprintf(utils.T("api.channel.join_channel.post_and_forget"), user.Username), model.POST_JOIN_LEAVE)
+ } else {
+ return model.NewLocAppError("JoinChannel", "api.channel.join_channel.permissions.app_error", nil, "")
+ }
+ }
+
+ return nil
+}
+
+func LeaveChannel(channelId string, userId string) *model.AppError {
+ sc := Srv.Store.Channel().Get(channelId, true)
+ uc := Srv.Store.User().Get(userId)
+ ccm := Srv.Store.Channel().GetMemberCount(channelId, false)
+
+ if cresult := <-sc; cresult.Err != nil {
+ return cresult.Err
+ } else if uresult := <-uc; uresult.Err != nil {
+ return cresult.Err
+ } else if ccmresult := <-ccm; ccmresult.Err != nil {
+ return ccmresult.Err
+ } else {
+ channel := cresult.Data.(*model.Channel)
+ user := uresult.Data.(*model.User)
+ membersCount := ccmresult.Data.(int64)
+
+ if channel.Type == model.CHANNEL_DIRECT {
+ err := model.NewLocAppError("LeaveChannel", "api.channel.leave.direct.app_error", nil, "")
+ err.StatusCode = http.StatusBadRequest
+ return err
+ }
+
+ if channel.Type == model.CHANNEL_PRIVATE && membersCount == 1 {
+ err := model.NewLocAppError("LeaveChannel", "api.channel.leave.last_member.app_error", nil, "userId="+user.Id)
+ err.StatusCode = http.StatusBadRequest
+ return err
+ }
+
+ if err := RemoveUserFromChannel(userId, userId, channel); err != nil {
+ return err
+ }
+
+ go PostUserAddRemoveMessage(userId, channel.Id, channel.TeamId, fmt.Sprintf(utils.T("api.channel.leave.left"), user.Username), model.POST_JOIN_LEAVE)
+ }
+
+ return nil
+}
+
+func RemoveUserFromChannel(userIdToRemove string, removerUserId string, channel *model.Channel) *model.AppError {
+ if channel.DeleteAt > 0 {
+ err := model.NewLocAppError("RemoveUserFromChannel", "api.channel.remove_user_from_channel.deleted.app_error", nil, "")
+ err.StatusCode = http.StatusBadRequest
+ return err
+ }
+
+ if channel.Name == model.DEFAULT_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 {
+ return cmresult.Err
+ }
+
+ InvalidateCacheForUser(userIdToRemove)
+ 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)
+
+ // 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)
+
+ return nil
+}
+
+func PostUserAddRemoveMessage(userId, channelId, teamId, message, postType string) *model.AppError {
+ post := &model.Post{
+ ChannelId: channelId,
+ Message: message,
+ Type: postType,
+ UserId: userId,
+ }
+ if _, err := CreatePost(post, teamId, false); err != nil {
+ return model.NewLocAppError("PostUserAddRemoveMessage", "api.channel.post_user_add_remove_message_and_forget.error", nil, err.Error())
+ }
+
+ return nil
+}
+
+func GetNumberOfChannelsOnTeam(teamId string) (int, *model.AppError) {
+ // Get total number of channels on current team
+ if result := <-Srv.Store.Channel().GetTeamChannels(teamId); result.Err != nil {
+ return 0, result.Err
+ } else {
+ return len(*result.Data.(*model.ChannelList)), nil
+ }
+}
+
+func SetActiveChannel(userId string, channelId string) *model.AppError {
+ status, err := GetStatus(userId)
+ if err != nil {
+ status = &model.Status{userId, model.STATUS_ONLINE, false, model.GetMillis(), channelId}
+ } else {
+ status.ActiveChannel = channelId
+ if !status.Manual {
+ status.Status = model.STATUS_ONLINE
+ }
+ status.LastActivityAt = model.GetMillis()
+ }
+
+ AddStatusCache(status)
+
+ return nil
+}
+
+func UpdateChannelLastViewedAt(channelIds []string, userId string) *model.AppError {
+ if result := <-Srv.Store.Channel().UpdateLastViewedAt(channelIds, userId); result.Err != nil {
+ return result.Err
+ }
+
+ return nil
+}
+
+func SearchChannels(teamId string, term string) (*model.ChannelList, *model.AppError) {
+ if result := <-Srv.Store.Channel().SearchInTeam(teamId, term); result.Err != nil {
+ return nil, result.Err
+ } else {
+ return result.Data.(*model.ChannelList), nil
+ }
+}
+
+func SearchChannelsUserNotIn(teamId string, userId string, term string) (*model.ChannelList, *model.AppError) {
+ if result := <-Srv.Store.Channel().SearchMore(userId, teamId, term); result.Err != nil {
+ return nil, result.Err
+ } else {
+ return result.Data.(*model.ChannelList), nil
+ }
+}
+
+func ViewChannel(view *model.ChannelView, teamId string, userId string, clearPushNotifications bool) *model.AppError {
+ channelIds := []string{view.ChannelId}
+
+ var pchan store.StoreChannel
+ if len(view.PrevChannelId) > 0 {
+ channelIds = append(channelIds, view.PrevChannelId)
+
+ if *utils.Cfg.EmailSettings.SendPushNotifications && clearPushNotifications {
+ pchan = Srv.Store.User().GetUnreadCountForChannel(userId, view.ChannelId)
+ }
+ }
+
+ uchan := Srv.Store.Channel().UpdateLastViewedAt(channelIds, userId)
+
+ if pchan != nil {
+ if result := <-pchan; result.Err != nil {
+ return result.Err
+ } else {
+ if result.Data.(int64) > 0 {
+ ClearPushNotification(userId, view.ChannelId)
+ }
+ }
+ }
+
+ if result := <-uchan; result.Err != nil {
+ return result.Err
+ }
+
+ message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_CHANNEL_VIEWED, teamId, "", userId, nil)
+ message.Add("channel_id", view.ChannelId)
+ go Publish(message)
+
+ return 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/compliance.go b/app/compliance.go
new file mode 100644
index 000000000..ffef69b44
--- /dev/null
+++ b/app/compliance.go
@@ -0,0 +1,62 @@
+// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package app
+
+import (
+ "io/ioutil"
+
+ "github.com/mattermost/platform/einterfaces"
+ "github.com/mattermost/platform/model"
+ "github.com/mattermost/platform/utils"
+)
+
+func GetComplianceReports() (model.Compliances, *model.AppError) {
+ if !*utils.Cfg.ComplianceSettings.Enable || !utils.IsLicensed || !*utils.License.Features.Compliance {
+ return nil, model.NewLocAppError("GetComplianceReports", "ent.compliance.licence_disable.app_error", nil, "")
+ }
+
+ if result := <-Srv.Store.Compliance().GetAll(); result.Err != nil {
+ return nil, result.Err
+ } else {
+ return result.Data.(model.Compliances), nil
+ }
+}
+
+func SaveComplianceReport(job *model.Compliance) (*model.Compliance, *model.AppError) {
+ if !*utils.Cfg.ComplianceSettings.Enable || !utils.IsLicensed || !*utils.License.Features.Compliance || einterfaces.GetComplianceInterface() == nil {
+ return nil, model.NewLocAppError("saveComplianceReport", "ent.compliance.licence_disable.app_error", nil, "")
+ }
+
+ job.Type = model.COMPLIANCE_TYPE_ADHOC
+
+ if result := <-Srv.Store.Compliance().Save(job); result.Err != nil {
+ return nil, result.Err
+ } else {
+ job = result.Data.(*model.Compliance)
+ go einterfaces.GetComplianceInterface().RunComplianceJob(job)
+ }
+
+ return job, nil
+}
+
+func GetComplianceReport(reportId string) (*model.Compliance, *model.AppError) {
+ if !*utils.Cfg.ComplianceSettings.Enable || !utils.IsLicensed || !*utils.License.Features.Compliance || einterfaces.GetComplianceInterface() == nil {
+ return nil, model.NewLocAppError("downloadComplianceReport", "ent.compliance.licence_disable.app_error", nil, "")
+ }
+
+ if result := <-Srv.Store.Compliance().Get(reportId); result.Err != nil {
+ return nil, result.Err
+ } else {
+ return result.Data.(*model.Compliance), nil
+ }
+}
+
+func GetComplianceFile(job *model.Compliance) ([]byte, *model.AppError) {
+ if f, err := ioutil.ReadFile(*utils.Cfg.ComplianceSettings.Directory + "compliance/" + job.JobName() + ".zip"); err != nil {
+ return nil, model.NewLocAppError("readFile", "api.file.read_file.reading_local.app_error", nil, err.Error())
+
+ } else {
+ return f, nil
+ }
+}
diff --git a/app/email.go b/app/email.go
new file mode 100644
index 000000000..007a24505
--- /dev/null
+++ b/app/email.go
@@ -0,0 +1,236 @@
+// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package app
+
+import (
+ "fmt"
+ "html/template"
+ "net/url"
+
+ l4g "github.com/alecthomas/log4go"
+ "github.com/mattermost/platform/model"
+ "github.com/mattermost/platform/utils"
+)
+
+func SendChangeUsernameEmail(oldUsername, newUsername, email, locale, siteURL string) *model.AppError {
+ T := utils.GetUserTranslations(locale)
+
+ subject := fmt.Sprintf("[%v] %v", utils.Cfg.TeamSettings.SiteName, T("api.templates.username_change_subject",
+ map[string]interface{}{"TeamDisplayName": utils.Cfg.TeamSettings.SiteName}))
+
+ bodyPage := utils.NewHTMLTemplate("email_change_body", locale)
+ bodyPage.Props["SiteURL"] = siteURL
+ bodyPage.Props["Title"] = T("api.templates.username_change_body.title")
+ bodyPage.Html["Info"] = template.HTML(T("api.templates.username_change_body.info",
+ map[string]interface{}{"TeamDisplayName": utils.Cfg.TeamSettings.SiteName, "NewUsername": newUsername}))
+
+ if err := utils.SendMail(email, subject, bodyPage.Render()); err != nil {
+ return model.NewLocAppError("SendChangeUsernameEmail", "api.user.send_email_change_username_and_forget.error", nil, err.Error())
+ }
+
+ return nil
+}
+
+func SendEmailChangeVerifyEmail(userId, newUserEmail, locale, siteURL string) *model.AppError {
+ T := utils.GetUserTranslations(locale)
+
+ link := fmt.Sprintf("%s/do_verify_email?uid=%s&hid=%s&email=%s", siteURL, userId, model.HashPassword(userId+utils.Cfg.EmailSettings.InviteSalt), url.QueryEscape(newUserEmail))
+
+ subject := fmt.Sprintf("[%v] %v", utils.Cfg.TeamSettings.SiteName, T("api.templates.email_change_verify_subject",
+ map[string]interface{}{"TeamDisplayName": utils.Cfg.TeamSettings.SiteName}))
+
+ bodyPage := utils.NewHTMLTemplate("email_change_verify_body", locale)
+ bodyPage.Props["SiteURL"] = siteURL
+ bodyPage.Props["Title"] = T("api.templates.email_change_verify_body.title")
+ bodyPage.Props["Info"] = T("api.templates.email_change_verify_body.info",
+ map[string]interface{}{"TeamDisplayName": utils.Cfg.TeamSettings.SiteName})
+ bodyPage.Props["VerifyUrl"] = link
+ bodyPage.Props["VerifyButton"] = T("api.templates.email_change_verify_body.button")
+
+ if err := utils.SendMail(newUserEmail, subject, bodyPage.Render()); err != nil {
+ return model.NewLocAppError("SendEmailChangeVerifyEmail", "api.user.send_email_change_verify_email_and_forget.error", nil, err.Error())
+ }
+
+ return nil
+}
+
+func SendEmailChangeEmail(oldEmail, newEmail, locale, siteURL string) *model.AppError {
+ T := utils.GetUserTranslations(locale)
+
+ subject := fmt.Sprintf("[%v] %v", utils.Cfg.TeamSettings.SiteName, T("api.templates.email_change_subject",
+ map[string]interface{}{"TeamDisplayName": utils.Cfg.TeamSettings.SiteName}))
+
+ bodyPage := utils.NewHTMLTemplate("email_change_body", locale)
+ bodyPage.Props["SiteURL"] = siteURL
+ bodyPage.Props["Title"] = T("api.templates.email_change_body.title")
+ bodyPage.Html["Info"] = template.HTML(T("api.templates.email_change_body.info",
+ map[string]interface{}{"TeamDisplayName": utils.Cfg.TeamSettings.SiteName, "NewEmail": newEmail}))
+
+ if err := utils.SendMail(oldEmail, subject, bodyPage.Render()); err != nil {
+ return model.NewLocAppError("SendEmailChangeEmail", "api.user.send_email_change_email_and_forget.error", nil, err.Error())
+ }
+
+ return nil
+}
+
+func SendVerifyEmail(userId, userEmail, locale, siteURL string) *model.AppError {
+ T := utils.GetUserTranslations(locale)
+
+ link := fmt.Sprintf("%s/do_verify_email?uid=%s&hid=%s&email=%s", siteURL, userId, model.HashPassword(userId+utils.Cfg.EmailSettings.InviteSalt), url.QueryEscape(userEmail))
+
+ url, _ := url.Parse(siteURL)
+
+ subject := T("api.templates.verify_subject",
+ map[string]interface{}{"SiteName": utils.ClientCfg["SiteName"]})
+
+ bodyPage := utils.NewHTMLTemplate("verify_body", locale)
+ bodyPage.Props["SiteURL"] = siteURL
+ bodyPage.Props["Title"] = T("api.templates.verify_body.title", map[string]interface{}{"ServerURL": url.Host})
+ bodyPage.Props["Info"] = T("api.templates.verify_body.info")
+ bodyPage.Props["VerifyUrl"] = link
+ bodyPage.Props["Button"] = T("api.templates.verify_body.button")
+
+ if err := utils.SendMail(userEmail, subject, bodyPage.Render()); err != nil {
+ return model.NewLocAppError("SendVerifyEmail", "api.user.send_verify_email_and_forget.failed.error", nil, err.Error())
+ }
+
+ return nil
+}
+
+func SendSignInChangeEmail(email, method, locale, siteURL string) *model.AppError {
+ T := utils.GetUserTranslations(locale)
+
+ subject := T("api.templates.singin_change_email.subject",
+ map[string]interface{}{"SiteName": utils.ClientCfg["SiteName"]})
+
+ bodyPage := utils.NewHTMLTemplate("signin_change_body", locale)
+ bodyPage.Props["SiteURL"] = siteURL
+ bodyPage.Props["Title"] = T("api.templates.signin_change_email.body.title")
+ bodyPage.Html["Info"] = template.HTML(T("api.templates.singin_change_email.body.info",
+ map[string]interface{}{"SiteName": utils.ClientCfg["SiteName"], "Method": method}))
+
+ if err := utils.SendMail(email, subject, bodyPage.Render()); err != nil {
+ return model.NewLocAppError("SendSignInChangeEmail", "api.user.send_sign_in_change_email_and_forget.error", nil, err.Error())
+ }
+
+ return nil
+}
+
+func SendWelcomeEmail(userId string, email string, verified bool, locale, siteURL string) *model.AppError {
+ T := utils.GetUserTranslations(locale)
+
+ rawUrl, _ := url.Parse(siteURL)
+
+ subject := T("api.templates.welcome_subject", map[string]interface{}{"ServerURL": rawUrl.Host})
+
+ bodyPage := utils.NewHTMLTemplate("welcome_body", locale)
+ bodyPage.Props["SiteURL"] = siteURL
+ bodyPage.Props["Title"] = T("api.templates.welcome_body.title", map[string]interface{}{"ServerURL": rawUrl.Host})
+ bodyPage.Props["Info"] = T("api.templates.welcome_body.info")
+ bodyPage.Props["Button"] = T("api.templates.welcome_body.button")
+ bodyPage.Props["Info2"] = T("api.templates.welcome_body.info2")
+ bodyPage.Props["Info3"] = T("api.templates.welcome_body.info3")
+ bodyPage.Props["SiteURL"] = siteURL
+
+ if *utils.Cfg.NativeAppSettings.AppDownloadLink != "" {
+ bodyPage.Props["AppDownloadInfo"] = T("api.templates.welcome_body.app_download_info")
+ bodyPage.Props["AppDownloadLink"] = *utils.Cfg.NativeAppSettings.AppDownloadLink
+ }
+
+ if !verified {
+ link := fmt.Sprintf("%s/do_verify_email?uid=%s&hid=%s&email=%s", siteURL, userId, model.HashPassword(userId+utils.Cfg.EmailSettings.InviteSalt), url.QueryEscape(email))
+ bodyPage.Props["VerifyUrl"] = link
+ }
+
+ if err := utils.SendMail(email, subject, bodyPage.Render()); err != nil {
+ return model.NewLocAppError("SendWelcomeEmail", "api.user.send_welcome_email_and_forget.failed.error", nil, err.Error())
+ }
+
+ return nil
+}
+
+func SendPasswordChangeEmail(email, method, locale, siteURL string) *model.AppError {
+ T := utils.GetUserTranslations(locale)
+
+ subject := T("api.templates.password_change_subject",
+ map[string]interface{}{"TeamDisplayName": utils.Cfg.TeamSettings.SiteName, "SiteName": utils.Cfg.TeamSettings.SiteName})
+
+ bodyPage := utils.NewHTMLTemplate("password_change_body", locale)
+ bodyPage.Props["SiteURL"] = siteURL
+ bodyPage.Props["Title"] = T("api.templates.password_change_body.title")
+ bodyPage.Html["Info"] = template.HTML(T("api.templates.password_change_body.info",
+ map[string]interface{}{"TeamDisplayName": utils.Cfg.TeamSettings.SiteName, "TeamURL": siteURL, "Method": method}))
+
+ if err := utils.SendMail(email, subject, bodyPage.Render()); err != nil {
+ return model.NewLocAppError("SendPasswordChangeEmail", "api.user.send_password_change_email_and_forget.error", nil, err.Error())
+ }
+
+ return nil
+}
+
+func SendMfaChangeEmail(email string, activated bool, locale, siteURL string) *model.AppError {
+ T := utils.GetUserTranslations(locale)
+
+ subject := T("api.templates.mfa_change_subject",
+ map[string]interface{}{"SiteName": utils.Cfg.TeamSettings.SiteName})
+
+ bodyPage := utils.NewHTMLTemplate("mfa_change_body", locale)
+ bodyPage.Props["SiteURL"] = siteURL
+
+ bodyText := ""
+ if activated {
+ bodyText = "api.templates.mfa_activated_body.info"
+ bodyPage.Props["Title"] = T("api.templates.mfa_activated_body.title")
+ } else {
+ bodyText = "api.templates.mfa_deactivated_body.info"
+ bodyPage.Props["Title"] = T("api.templates.mfa_deactivated_body.title")
+ }
+
+ bodyPage.Html["Info"] = template.HTML(T(bodyText,
+ map[string]interface{}{"SiteURL": siteURL}))
+
+ if err := utils.SendMail(email, subject, bodyPage.Render()); err != nil {
+ return model.NewLocAppError("SendMfaChangeEmail", "api.user.send_mfa_change_email.error", nil, err.Error())
+ }
+
+ return nil
+}
+
+func SendInviteEmails(team *model.Team, senderName string, invites []string, siteURL string) {
+ for _, invite := range invites {
+ if len(invite) > 0 {
+ senderRole := utils.T("api.team.invite_members.member")
+
+ subject := utils.T("api.templates.invite_subject",
+ map[string]interface{}{"SenderName": senderName, "TeamDisplayName": team.DisplayName, "SiteName": utils.ClientCfg["SiteName"]})
+
+ bodyPage := utils.NewHTMLTemplate("invite_body", model.DEFAULT_LOCALE)
+ bodyPage.Props["SiteURL"] = siteURL
+ bodyPage.Props["Title"] = utils.T("api.templates.invite_body.title")
+ bodyPage.Html["Info"] = template.HTML(utils.T("api.templates.invite_body.info",
+ map[string]interface{}{"SenderStatus": senderRole, "SenderName": senderName, "TeamDisplayName": team.DisplayName}))
+ bodyPage.Props["Button"] = utils.T("api.templates.invite_body.button")
+ bodyPage.Html["ExtraInfo"] = template.HTML(utils.T("api.templates.invite_body.extra_info",
+ map[string]interface{}{"TeamDisplayName": team.DisplayName, "TeamURL": siteURL + "/" + team.Name}))
+
+ props := make(map[string]string)
+ props["email"] = invite
+ props["id"] = team.Id
+ props["display_name"] = team.DisplayName
+ props["name"] = team.Name
+ props["time"] = fmt.Sprintf("%v", model.GetMillis())
+ data := model.MapToJson(props)
+ hash := model.HashPassword(fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.InviteSalt))
+ bodyPage.Props["Link"] = fmt.Sprintf("%s/signup_user_complete/?d=%s&h=%s", siteURL, url.QueryEscape(data), url.QueryEscape(hash))
+
+ if !utils.Cfg.EmailSettings.SendEmailNotifications {
+ l4g.Info(utils.T("api.team.invite_members.sending.info"), invite, bodyPage.Props["Link"])
+ }
+
+ if err := utils.SendMail(invite, subject, bodyPage.Render()); err != nil {
+ l4g.Error(utils.T("api.team.invite_members.send.error"), err)
+ }
+ }
+ }
+}
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/email_test.go b/app/email_test.go
new file mode 100644
index 000000000..ecaa389bf
--- /dev/null
+++ b/app/email_test.go
@@ -0,0 +1,419 @@
+// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package app
+
+import (
+ "strings"
+ "testing"
+
+ "github.com/mattermost/platform/utils"
+)
+
+func TestSendChangeUsernameEmail(t *testing.T) {
+ Setup()
+
+ var emailTo string = "test@example.com"
+ var oldUsername string = "myoldusername"
+ var newUsername string = "fancyusername"
+ var locale string = "en"
+ var siteURL string = ""
+ var expectedPartialMessage string = "Your username for Mattermost has been changed to " + newUsername + "."
+ var expectedSubject string = "[" + utils.Cfg.TeamSettings.SiteName + "] Your username has changed for Mattermost"
+
+ //Delete all the messages before check the sample email
+ utils.DeleteMailBox(emailTo)
+
+ if err := SendChangeUsernameEmail(oldUsername, newUsername, emailTo, locale, siteURL); err != nil {
+ t.Log(err)
+ t.Fatal("Should send change username email")
+ } else {
+ //Check if the email was send to the rigth email address
+ if resultsMailbox, err := utils.GetMailBox(emailTo); err != nil && !strings.ContainsAny(resultsMailbox[0].To[0], emailTo) {
+ t.Fatal("Wrong To recipient")
+ } else {
+ if resultsEmail, err := utils.GetMessageFromMailbox(emailTo, resultsMailbox[0].ID); err == nil {
+ if !strings.Contains(resultsEmail.Subject, expectedSubject) {
+ t.Log(resultsEmail.Subject)
+ t.Fatal("Wrong Subject")
+ }
+ if !strings.Contains(resultsEmail.Body.Text, expectedPartialMessage) {
+ t.Log(resultsEmail.Body.Text)
+ t.Fatal("Wrong Body message")
+ }
+ }
+ }
+ }
+}
+
+func TestSendEmailChangeVerifyEmail(t *testing.T) {
+ Setup()
+ utils.LoadConfig("config.json")
+
+ var userId string = "5349853498543jdfvndf9834"
+ var newUserEmail string = "newtest@example.com"
+ var locale string = "en"
+ var siteURL string = ""
+ var expectedPartialMessage string = "You updated your email"
+ var expectedSubject string = "[" + utils.Cfg.TeamSettings.SiteName + "] Verify new email address for Mattermost"
+
+ //Delete all the messages before check the sample email
+ utils.DeleteMailBox(newUserEmail)
+
+ if err := SendEmailChangeVerifyEmail(userId, newUserEmail, locale, siteURL); err != nil {
+ t.Log(err)
+ t.Fatal("Should send change username email")
+ } else {
+ //Check if the email was send to the rigth email address
+ if resultsMailbox, err := utils.GetMailBox(newUserEmail); err != nil && !strings.ContainsAny(resultsMailbox[0].To[0], newUserEmail) {
+ t.Fatal("Wrong To recipient")
+ } else {
+ if resultsEmail, err := utils.GetMessageFromMailbox(newUserEmail, resultsMailbox[0].ID); err == nil {
+ if !strings.Contains(resultsEmail.Subject, expectedSubject) {
+ t.Log(resultsEmail.Subject)
+ t.Fatal("Wrong Subject")
+ }
+ if !strings.Contains(resultsEmail.Body.Text, expectedPartialMessage) {
+ t.Log(resultsEmail.Body.Text)
+ t.Fatal("Wrong Body message")
+ }
+ if !strings.Contains(resultsEmail.Body.Text, utils.UrlEncode(newUserEmail)) {
+ t.Log(resultsEmail.Body.Text)
+ t.Fatal("Wrong new email in the message")
+ }
+ }
+ }
+ }
+}
+
+func TestSendEmailChangeEmail(t *testing.T) {
+ Setup()
+ utils.LoadConfig("config.json")
+
+ var oldEmail string = "test@example.com"
+ var newUserEmail string = "newtest@example.com"
+ var locale string = "en"
+ var siteURL string = ""
+ var expectedPartialMessage string = "Your email address for Mattermost has been changed to " + newUserEmail
+ var expectedSubject string = "[" + utils.Cfg.TeamSettings.SiteName + "] Your email address has changed for Mattermost"
+
+ //Delete all the messages before check the sample email
+ utils.DeleteMailBox(oldEmail)
+
+ if err := SendEmailChangeEmail(oldEmail, newUserEmail, locale, siteURL); err != nil {
+ t.Log(err)
+ t.Fatal("Should send change username email")
+ } else {
+ //Check if the email was send to the rigth email address
+ if resultsMailbox, err := utils.GetMailBox(oldEmail); err != nil && !strings.ContainsAny(resultsMailbox[0].To[0], oldEmail) {
+ t.Fatal("Wrong To recipient")
+ } else {
+ if resultsEmail, err := utils.GetMessageFromMailbox(oldEmail, resultsMailbox[0].ID); err == nil {
+ if !strings.Contains(resultsEmail.Subject, expectedSubject) {
+ t.Log(resultsEmail.Subject)
+ t.Fatal("Wrong Subject")
+ }
+ if !strings.Contains(resultsEmail.Body.Text, expectedPartialMessage) {
+ t.Log(resultsEmail.Body.Text)
+ t.Fatal("Wrong Body message")
+ }
+ }
+ }
+ }
+}
+
+func TestSendVerifyEmail(t *testing.T) {
+ Setup()
+ utils.LoadConfig("config.json")
+
+ var userId string = "5349853498543jdfvndf9834"
+ var userEmail string = "test@example.com"
+ var locale string = "en"
+ var siteURL string = ""
+ var expectedPartialMessage string = "Please verify your email address by clicking below"
+ var expectedSubject string = "[" + utils.Cfg.TeamSettings.SiteName + "] Email Verification"
+
+ //Delete all the messages before check the sample email
+ utils.DeleteMailBox(userEmail)
+
+ if err := SendVerifyEmail(userId, userEmail, locale, siteURL); err != nil {
+ t.Log(err)
+ t.Fatal("Should send change username email")
+ } else {
+ //Check if the email was send to the rigth email address
+ if resultsMailbox, err := utils.GetMailBox(userEmail); err != nil && !strings.ContainsAny(resultsMailbox[0].To[0], userEmail) {
+ t.Fatal("Wrong To recipient")
+ } else {
+ if resultsEmail, err := utils.GetMessageFromMailbox(userEmail, resultsMailbox[0].ID); err == nil {
+ if !strings.Contains(resultsEmail.Subject, expectedSubject) {
+ t.Log(resultsEmail.Subject)
+ t.Fatal("Wrong Subject")
+ }
+ if !strings.Contains(resultsEmail.Body.Text, expectedPartialMessage) {
+ t.Log(resultsEmail.Body.Text)
+ t.Fatal("Wrong Body message")
+ }
+ if !strings.Contains(resultsEmail.Body.Text, utils.UrlEncode(userEmail)) {
+ t.Log(resultsEmail.Body.Text)
+ t.Fatal("Wrong new email in the message")
+ }
+ }
+ }
+ }
+}
+
+func TestSendSignInChangeEmail(t *testing.T) {
+ Setup()
+ utils.LoadConfig("config.json")
+
+ var email string = "test@example.com"
+ var locale string = "en"
+ var siteURL string = ""
+ var method string = "AD/LDAP"
+ var expectedPartialMessage string = "You updated your sign-in method on Mattermost to " + method + "."
+ var expectedSubject string = "You updated your sign-in method on Mattermost"
+
+ //Delete all the messages before check the sample email
+ utils.DeleteMailBox(email)
+
+ if err := SendSignInChangeEmail(email, method, locale, siteURL); err != nil {
+ t.Log(err)
+ t.Fatal("Should send change username email")
+ } else {
+ //Check if the email was send to the rigth email address
+ if resultsMailbox, err := utils.GetMailBox(email); err != nil && !strings.ContainsAny(resultsMailbox[0].To[0], email) {
+ t.Fatal("Wrong To recipient")
+ } else {
+ if resultsEmail, err := utils.GetMessageFromMailbox(email, resultsMailbox[0].ID); err == nil {
+ if !strings.Contains(resultsEmail.Subject, expectedSubject) {
+ t.Log(resultsEmail.Subject)
+ t.Fatal("Wrong Subject")
+ }
+ if !strings.Contains(resultsEmail.Body.Text, expectedPartialMessage) {
+ t.Log(resultsEmail.Body.Text)
+ t.Fatal("Wrong Body message")
+ }
+ }
+ }
+ }
+}
+
+func TestSendWelcomeEmail(t *testing.T) {
+ Setup()
+ utils.LoadConfig("config.json")
+
+ var userId string = "32432nkjnijn432uj32"
+ var email string = "test@example.com"
+ var locale string = "en"
+ var siteURL string = "http://test.mattermost.io"
+ var verified bool = true
+ var expectedPartialMessage string = "Mattermost lets you share messages and files from your PC or phone, with instant search and archiving"
+ var expectedSubject string = "You joined test.mattermost.io"
+
+ //Delete all the messages before check the sample email
+ utils.DeleteMailBox(email)
+
+ if err := SendWelcomeEmail(userId, email, verified, locale, siteURL); err != nil {
+ t.Log(err)
+ t.Fatal("Should send change username email")
+ } else {
+ //Check if the email was send to the rigth email address
+ if resultsMailbox, err := utils.GetMailBox(email); err != nil && !strings.ContainsAny(resultsMailbox[0].To[0], email) {
+ t.Fatal("Wrong To recipient")
+ } else {
+ if resultsEmail, err := utils.GetMessageFromMailbox(email, resultsMailbox[0].ID); err == nil {
+ if !strings.Contains(resultsEmail.Subject, expectedSubject) {
+ t.Log(resultsEmail.Subject)
+ t.Fatal("Wrong Subject")
+ }
+ if !strings.Contains(resultsEmail.Body.Text, expectedPartialMessage) {
+ t.Log(resultsEmail.Body.Text)
+ t.Fatal("Wrong Body message")
+ }
+ }
+ }
+ }
+
+ utils.DeleteMailBox(email)
+ verified = false
+ var expectedVerifyEmail string = "Please verify your email address by clicking below."
+
+ if err := SendWelcomeEmail(userId, email, verified, locale, siteURL); err != nil {
+ t.Log(err)
+ t.Fatal("Should send change username email")
+ } else {
+ //Check if the email was send to the rigth email address
+ if resultsMailbox, err := utils.GetMailBox(email); err != nil && !strings.ContainsAny(resultsMailbox[0].To[0], email) {
+ t.Fatal("Wrong To recipient")
+ } else {
+ if resultsEmail, err := utils.GetMessageFromMailbox(email, resultsMailbox[0].ID); err == nil {
+ if !strings.Contains(resultsEmail.Subject, expectedSubject) {
+ t.Log(resultsEmail.Subject)
+ t.Fatal("Wrong Subject")
+ }
+ if !strings.Contains(resultsEmail.Body.Text, expectedPartialMessage) {
+ t.Log(resultsEmail.Body.Text)
+ t.Fatal("Wrong Body message")
+ }
+ if !strings.Contains(resultsEmail.Body.Text, expectedVerifyEmail) {
+ t.Log(resultsEmail.Body.Text)
+ t.Fatal("Wrong Body message")
+ }
+ if !strings.Contains(resultsEmail.Body.Text, utils.UrlEncode(email)) {
+ t.Log(resultsEmail.Body.Text)
+ t.Fatal("Wrong email in the message")
+ }
+ }
+ }
+ }
+}
+
+func TestSendPasswordChangeEmail(t *testing.T) {
+ Setup()
+ utils.LoadConfig("config.json")
+
+ var email string = "test@example.com"
+ var locale string = "en"
+ var siteURL string = "http://test.mattermost.io"
+ var method string = "using a reset password link"
+ var expectedPartialMessage string = "Your password has been updated for " + utils.Cfg.TeamSettings.SiteName + " on " + siteURL + " by " + method
+ var expectedSubject string = "Your password has been updated for " + utils.Cfg.TeamSettings.SiteName + " on " + utils.Cfg.TeamSettings.SiteName
+
+ //Delete all the messages before check the sample email
+ utils.DeleteMailBox(email)
+
+ if err := SendPasswordChangeEmail(email, method, locale, siteURL); err != nil {
+ t.Log(err)
+ t.Fatal("Should send change username email")
+ } else {
+ //Check if the email was send to the rigth email address
+ if resultsMailbox, err := utils.GetMailBox(email); err != nil && !strings.ContainsAny(resultsMailbox[0].To[0], email) {
+ t.Fatal("Wrong To recipient")
+ } else {
+ if resultsEmail, err := utils.GetMessageFromMailbox(email, resultsMailbox[0].ID); err == nil {
+ if !strings.Contains(resultsEmail.Subject, expectedSubject) {
+ t.Log(resultsEmail.Subject)
+ t.Fatal("Wrong Subject")
+ }
+ if !strings.Contains(resultsEmail.Body.Text, expectedPartialMessage) {
+ t.Log(resultsEmail.Body.Text)
+ t.Fatal("Wrong Body message")
+ }
+ }
+ }
+ }
+}
+
+func TestSendMfaChangeEmail(t *testing.T) {
+ Setup()
+ utils.LoadConfig("config.json")
+
+ var email string = "test@example.com"
+ var locale string = "en"
+ var siteURL string = "http://test.mattermost.io"
+ var activated bool = true
+ var expectedPartialMessage string = "Multi-factor authentication has been added to your account on " + siteURL + "."
+ var expectedSubject string = "Your MFA has been updated on " + utils.Cfg.TeamSettings.SiteName
+
+ //Delete all the messages before check the sample email
+ utils.DeleteMailBox(email)
+
+ if err := SendMfaChangeEmail(email, activated, locale, siteURL); err != nil {
+ t.Log(err)
+ t.Fatal("Should send change username email")
+ } else {
+ //Check if the email was send to the rigth email address
+ if resultsMailbox, err := utils.GetMailBox(email); err != nil && !strings.ContainsAny(resultsMailbox[0].To[0], email) {
+ t.Fatal("Wrong To recipient")
+ } else {
+ if resultsEmail, err := utils.GetMessageFromMailbox(email, resultsMailbox[0].ID); err == nil {
+ if !strings.Contains(resultsEmail.Subject, expectedSubject) {
+ t.Log(resultsEmail.Subject)
+ t.Fatal("Wrong Subject")
+ }
+ if !strings.Contains(resultsEmail.Body.Text, expectedPartialMessage) {
+ t.Log(resultsEmail.Body.Text)
+ t.Fatal("Wrong Body message")
+ }
+ }
+ }
+ }
+
+ activated = false
+ expectedPartialMessage = "Multi-factor authentication has been removed from your account on " + siteURL + "."
+ utils.DeleteMailBox(email)
+
+ if err := SendMfaChangeEmail(email, activated, locale, siteURL); err != nil {
+ t.Log(err)
+ t.Fatal("Should send change username email")
+ } else {
+ //Check if the email was send to the rigth email address
+ if resultsMailbox, err := utils.GetMailBox(email); err != nil && !strings.ContainsAny(resultsMailbox[0].To[0], email) {
+ t.Fatal("Wrong To recipient")
+ } else {
+ if resultsEmail, err := utils.GetMessageFromMailbox(email, resultsMailbox[0].ID); err == nil {
+ if !strings.Contains(resultsEmail.Subject, expectedSubject) {
+ t.Log(resultsEmail.Subject)
+ t.Fatal("Wrong Subject")
+ }
+ if !strings.Contains(resultsEmail.Body.Text, expectedPartialMessage) {
+ t.Log(resultsEmail.Body.Text)
+ t.Fatal("Wrong Body message")
+ }
+ }
+ }
+ }
+}
+
+func TestSendInviteEmails(t *testing.T) {
+ th := Setup().InitBasic()
+ utils.LoadConfig("config.json")
+
+ var email1 string = "test1@example.com"
+ var email2 string = "test2@example.com"
+ var senderName string = "TheBoss"
+ var siteURL string = "http://test.mattermost.io"
+ invites := []string{email1, email2}
+ var expectedPartialMessage string = "The team member *" + senderName + "* , has invited you to join *" + th.BasicTeam.DisplayName + "*"
+ var expectedSubject string = senderName + " invited you to join " + th.BasicTeam.DisplayName + " Team on " + utils.Cfg.TeamSettings.SiteName
+
+ //Delete all the messages before check the sample email
+ utils.DeleteMailBox(email1)
+ utils.DeleteMailBox(email2)
+
+ SendInviteEmails(th.BasicTeam, senderName, invites, siteURL)
+
+ //Check if the email was send to the rigth email address to email1
+ if resultsMailbox, err := utils.GetMailBox(email1); err != nil && !strings.ContainsAny(resultsMailbox[0].To[0], email1) {
+ t.Fatal("Wrong To recipient")
+ } else {
+ if resultsEmail, err := utils.GetMessageFromMailbox(email1, resultsMailbox[0].ID); err == nil {
+ if !strings.Contains(resultsEmail.Subject, expectedSubject) {
+ t.Log(resultsEmail.Subject)
+ t.Log(expectedSubject)
+ t.Fatal("Wrong Subject")
+ }
+ if !strings.Contains(resultsEmail.Body.Text, expectedPartialMessage) {
+ t.Log(resultsEmail.Body.Text)
+ t.Fatal("Wrong Body message")
+ }
+ }
+ }
+
+ //Check if the email was send to the rigth email address to email2
+ if resultsMailbox, err := utils.GetMailBox(email2); err != nil && !strings.ContainsAny(resultsMailbox[0].To[0], email2) {
+ t.Fatal("Wrong To recipient")
+ } else {
+ if resultsEmail, err := utils.GetMessageFromMailbox(email2, resultsMailbox[0].ID); err == nil {
+ if !strings.Contains(resultsEmail.Subject, expectedSubject) {
+ t.Log(resultsEmail.Subject)
+ t.Log(expectedSubject)
+ t.Fatal("Wrong Subject")
+ }
+ if !strings.Contains(resultsEmail.Body.Text, expectedPartialMessage) {
+ t.Log(resultsEmail.Body.Text)
+ t.Fatal("Wrong Body message")
+ }
+ }
+ }
+}
diff --git a/app/file.go b/app/file.go
new file mode 100644
index 000000000..a4419bde8
--- /dev/null
+++ b/app/file.go
@@ -0,0 +1,529 @@
+// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package app
+
+import (
+ "bytes"
+ _ "image/gif"
+ "crypto/sha256"
+ "encoding/base64"
+ "fmt"
+ "image"
+ "image/color"
+ "image/draw"
+ "image/jpeg"
+ "io"
+ "io/ioutil"
+ "net/http"
+ "net/url"
+ "os"
+ "path"
+ "path/filepath"
+ "strings"
+ "sync"
+
+ l4g "github.com/alecthomas/log4go"
+ "github.com/mattermost/platform/model"
+ "github.com/mattermost/platform/utils"
+ "github.com/disintegration/imaging"
+ s3 "github.com/minio/minio-go"
+ "github.com/rwcarlsen/goexif/exif"
+ _ "golang.org/x/image/bmp"
+)
+
+const (
+ /*
+ EXIF Image Orientations
+ 1 2 3 4 5 6 7 8
+
+ 888888 888888 88 88 8888888888 88 88 8888888888
+ 88 88 88 88 88 88 88 88 88 88 88 88
+ 8888 8888 8888 8888 88 8888888888 8888888888 88
+ 88 88 88 88
+ 88 88 888888 888888
+ */
+ Upright = 1
+ UprightMirrored = 2
+ UpsideDown = 3
+ UpsideDownMirrored = 4
+ RotatedCWMirrored = 5
+ RotatedCCW = 6
+ RotatedCCWMirrored = 7
+ RotatedCW = 8
+
+ MaxImageSize = 6048 * 4032 // 24 megapixels, roughly 36MB as a raw image
+)
+
+func ReadFile(path string) ([]byte, *model.AppError) {
+ if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_S3 {
+ endpoint := utils.Cfg.FileSettings.AmazonS3Endpoint
+ accessKey := utils.Cfg.FileSettings.AmazonS3AccessKeyId
+ secretKey := utils.Cfg.FileSettings.AmazonS3SecretAccessKey
+ secure := *utils.Cfg.FileSettings.AmazonS3SSL
+ s3Clnt, err := s3.New(endpoint, accessKey, secretKey, secure)
+ if err != nil {
+ return nil, model.NewLocAppError("ReadFile", "api.file.read_file.s3.app_error", nil, err.Error())
+ }
+ bucket := utils.Cfg.FileSettings.AmazonS3Bucket
+ minioObject, err := s3Clnt.GetObject(bucket, path)
+ defer minioObject.Close()
+ if err != nil {
+ return nil, model.NewLocAppError("ReadFile", "api.file.read_file.s3.app_error", nil, err.Error())
+ }
+ if f, err := ioutil.ReadAll(minioObject); err != nil {
+ return nil, model.NewLocAppError("ReadFile", "api.file.read_file.s3.app_error", nil, err.Error())
+ } else {
+ return f, nil
+ }
+ } else if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_LOCAL {
+ if f, err := ioutil.ReadFile(utils.Cfg.FileSettings.Directory + path); err != nil {
+ return nil, model.NewLocAppError("ReadFile", "api.file.read_file.reading_local.app_error", nil, err.Error())
+ } else {
+ return f, nil
+ }
+ } else {
+ return nil, model.NewLocAppError("ReadFile", "api.file.read_file.configured.app_error", nil, "")
+ }
+}
+
+func MoveFile(oldPath, newPath string) *model.AppError {
+ if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_S3 {
+ endpoint := utils.Cfg.FileSettings.AmazonS3Endpoint
+ accessKey := utils.Cfg.FileSettings.AmazonS3AccessKeyId
+ secretKey := utils.Cfg.FileSettings.AmazonS3SecretAccessKey
+ secure := *utils.Cfg.FileSettings.AmazonS3SSL
+ s3Clnt, err := s3.New(endpoint, accessKey, secretKey, secure)
+ if err != nil {
+ return model.NewLocAppError("moveFile", "api.file.write_file.s3.app_error", nil, err.Error())
+ }
+ bucket := utils.Cfg.FileSettings.AmazonS3Bucket
+
+ var copyConds = s3.NewCopyConditions()
+ if err = s3Clnt.CopyObject(bucket, newPath, "/"+path.Join(bucket, oldPath), copyConds); err != nil {
+ return model.NewLocAppError("moveFile", "api.file.move_file.delete_from_s3.app_error", nil, err.Error())
+ }
+ if err = s3Clnt.RemoveObject(bucket, oldPath); err != nil {
+ return model.NewLocAppError("moveFile", "api.file.move_file.delete_from_s3.app_error", nil, err.Error())
+ }
+ } else if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_LOCAL {
+ if err := os.MkdirAll(filepath.Dir(utils.Cfg.FileSettings.Directory+newPath), 0774); err != nil {
+ return model.NewLocAppError("moveFile", "api.file.move_file.rename.app_error", nil, err.Error())
+ }
+
+ if err := os.Rename(utils.Cfg.FileSettings.Directory+oldPath, utils.Cfg.FileSettings.Directory+newPath); err != nil {
+ return model.NewLocAppError("moveFile", "api.file.move_file.rename.app_error", nil, err.Error())
+ }
+ } else {
+ return model.NewLocAppError("moveFile", "api.file.move_file.configured.app_error", nil, "")
+ }
+
+ return nil
+}
+
+func WriteFile(f []byte, path string) *model.AppError {
+ if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_S3 {
+ endpoint := utils.Cfg.FileSettings.AmazonS3Endpoint
+ accessKey := utils.Cfg.FileSettings.AmazonS3AccessKeyId
+ secretKey := utils.Cfg.FileSettings.AmazonS3SecretAccessKey
+ secure := *utils.Cfg.FileSettings.AmazonS3SSL
+ s3Clnt, err := s3.New(endpoint, accessKey, secretKey, secure)
+ if err != nil {
+ return model.NewLocAppError("WriteFile", "api.file.write_file.s3.app_error", nil, err.Error())
+ }
+ bucket := utils.Cfg.FileSettings.AmazonS3Bucket
+ ext := filepath.Ext(path)
+
+ if model.IsFileExtImage(ext) {
+ _, err = s3Clnt.PutObject(bucket, path, bytes.NewReader(f), model.GetImageMimeType(ext))
+ } else {
+ _, err = s3Clnt.PutObject(bucket, path, bytes.NewReader(f), "binary/octet-stream")
+ }
+ if err != nil {
+ return model.NewLocAppError("WriteFile", "api.file.write_file.s3.app_error", nil, err.Error())
+ }
+ } else if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_LOCAL {
+ if err := writeFileLocally(f, utils.Cfg.FileSettings.Directory+path); err != nil {
+ return err
+ }
+ } else {
+ return model.NewLocAppError("WriteFile", "api.file.write_file.configured.app_error", nil, "")
+ }
+
+ return nil
+}
+
+func writeFileLocally(f []byte, path string) *model.AppError {
+ if err := os.MkdirAll(filepath.Dir(path), 0774); err != nil {
+ directory, _ := filepath.Abs(filepath.Dir(path))
+ return model.NewLocAppError("WriteFile", "api.file.write_file_locally.create_dir.app_error", nil, "directory="+directory+", err="+err.Error())
+ }
+
+ if err := ioutil.WriteFile(path, f, 0644); err != nil {
+ return model.NewLocAppError("WriteFile", "api.file.write_file_locally.writing.app_error", nil, err.Error())
+ }
+
+ return nil
+}
+
+func openFileWriteStream(path string) (io.Writer, *model.AppError) {
+ if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_S3 {
+ return nil, model.NewLocAppError("openFileWriteStream", "api.file.open_file_write_stream.s3.app_error", nil, "")
+ } else if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_LOCAL {
+ if err := os.MkdirAll(filepath.Dir(utils.Cfg.FileSettings.Directory+path), 0774); err != nil {
+ return nil, model.NewLocAppError("openFileWriteStream", "api.file.open_file_write_stream.creating_dir.app_error", nil, err.Error())
+ }
+
+ if fileHandle, err := os.Create(utils.Cfg.FileSettings.Directory + path); err != nil {
+ return nil, model.NewLocAppError("openFileWriteStream", "api.file.open_file_write_stream.local_server.app_error", nil, err.Error())
+ } else {
+ fileHandle.Chmod(0644)
+ return fileHandle, nil
+ }
+ }
+
+ return nil, model.NewLocAppError("openFileWriteStream", "api.file.open_file_write_stream.configured.app_error", nil, "")
+}
+
+func closeFileWriteStream(file io.Writer) {
+ file.(*os.File).Close()
+}
+
+func GetInfoForFilename(post *model.Post, teamId string, filename string) *model.FileInfo {
+ // Find the path from the Filename of the form /{channelId}/{userId}/{uid}/{nameWithExtension}
+ split := strings.SplitN(filename, "/", 5)
+ if len(split) < 5 {
+ l4g.Error(utils.T("api.file.migrate_filenames_to_file_infos.unexpected_filename.error"), post.Id, filename)
+ return nil
+ }
+
+ channelId := split[1]
+ userId := split[2]
+ oldId := split[3]
+ name, _ := url.QueryUnescape(split[4])
+
+ if split[0] != "" || split[1] != post.ChannelId || split[2] != post.UserId || strings.Contains(split[4], "/") {
+ l4g.Warn(utils.T("api.file.migrate_filenames_to_file_infos.mismatched_filename.warn"), post.Id, post.ChannelId, post.UserId, filename)
+ }
+
+ pathPrefix := fmt.Sprintf("teams/%s/channels/%s/users/%s/%s/", teamId, channelId, userId, oldId)
+ path := pathPrefix + name
+
+ // Open the file and populate the fields of the FileInfo
+ var info *model.FileInfo
+ if data, err := ReadFile(path); err != nil {
+ l4g.Error(utils.T("api.file.migrate_filenames_to_file_infos.file_not_found.error"), post.Id, filename, path, err)
+ return nil
+ } else {
+ var err *model.AppError
+ info, err = model.GetInfoForBytes(name, data)
+ if err != nil {
+ l4g.Warn(utils.T("api.file.migrate_filenames_to_file_infos.info.app_error"), post.Id, filename, err)
+ }
+ }
+
+ // Generate a new ID because with the old system, you could very rarely get multiple posts referencing the same file
+ info.Id = model.NewId()
+ info.CreatorId = post.UserId
+ info.PostId = post.Id
+ info.CreateAt = post.CreateAt
+ info.UpdateAt = post.UpdateAt
+ info.Path = path
+
+ if info.IsImage() {
+ nameWithoutExtension := name[:strings.LastIndex(name, ".")]
+ info.PreviewPath = pathPrefix + nameWithoutExtension + "_preview.jpg"
+ info.ThumbnailPath = pathPrefix + nameWithoutExtension + "_thumb.jpg"
+ }
+
+ return info
+}
+
+func FindTeamIdForFilename(post *model.Post, filename string) string {
+ split := strings.SplitN(filename, "/", 5)
+ id := split[3]
+ 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 {
+ 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
+ return teams[0].Id
+ } else {
+ for _, team := range teams {
+ path := fmt.Sprintf("teams/%s/channels/%s/users/%s/%s/%s", team.Id, post.ChannelId, post.UserId, id, name)
+ if _, err := ReadFile(path); err == nil {
+ // Found the team that this file was posted from
+ return team.Id
+ }
+ }
+ }
+
+ return ""
+}
+
+var fileMigrationLock sync.Mutex
+
+// Creates and stores FileInfos for a post created before the FileInfos table existed.
+func MigrateFilenamesToFileInfos(post *model.Post) []*model.FileInfo {
+ if len(post.Filenames) == 0 {
+ l4g.Warn(utils.T("api.file.migrate_filenames_to_file_infos.no_filenames.warn"), post.Id)
+ return []*model.FileInfo{}
+ }
+
+ cchan := 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)
+
+ var channel *model.Channel
+ if result := <-cchan; result.Err != nil {
+ l4g.Error(utils.T("api.file.migrate_filenames_to_file_infos.channel.app_error"), post.Id, post.ChannelId, result.Err)
+ return []*model.FileInfo{}
+ } else {
+ channel = result.Data.(*model.Channel)
+ }
+
+ // Find the team that was used to make this post since its part of the file path that isn't saved in the Filename
+ var teamId string
+ if channel.TeamId == "" {
+ // This post was made in a cross-team DM channel so we need to find where its files were saved
+ teamId = FindTeamIdForFilename(post, filenames[0])
+ } else {
+ teamId = channel.TeamId
+ }
+
+ // Create FileInfo objects for this post
+ infos := make([]*model.FileInfo, 0, len(filenames))
+ if teamId == "" {
+ l4g.Error(utils.T("api.file.migrate_filenames_to_file_infos.team_id.error"), post.Id, filenames)
+ } else {
+ for _, filename := range filenames {
+ info := GetInfoForFilename(post, teamId, filename)
+ if info == nil {
+ continue
+ }
+
+ infos = append(infos, info)
+ }
+ }
+
+ // Lock to prevent only one migration thread from trying to update the post at once, preventing duplicate FileInfos from being created
+ fileMigrationLock.Lock()
+ defer fileMigrationLock.Unlock()
+
+ if result := <-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 {
+ 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 {
+ l4g.Debug(utils.T("api.file.migrate_filenames_to_file_infos.not_migrating_post.debug"), post.Id)
+ return result.Data.([]*model.FileInfo)
+ }
+ }
+
+ l4g.Debug(utils.T("api.file.migrate_filenames_to_file_infos.migrating_post.debug"), post.Id)
+
+ 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 {
+ 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
+ }
+
+ savedInfos = append(savedInfos, info)
+ fileIds = append(fileIds, info.Id)
+ }
+
+ // Copy and save the updated post
+ newPost := &model.Post{}
+ *newPost = *post
+
+ newPost.Filenames = []string{}
+ newPost.FileIds = fileIds
+
+ // Update Posts to clear Filenames and set FileIds
+ if result := <-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 {
+ return savedInfos
+ }
+}
+
+func GeneratePublicLink(siteURL string, info *model.FileInfo) string {
+ hash := GeneratePublicLinkHash(info.Id, *utils.Cfg.FileSettings.PublicLinkSalt)
+ return fmt.Sprintf("%s%s/public/files/%v/get?h=%s", siteURL, model.API_URL_SUFFIX, info.Id, hash)
+}
+
+func GeneratePublicLinkHash(fileId, salt string) string {
+ hash := sha256.New()
+ hash.Write([]byte(salt))
+ hash.Write([]byte(fileId))
+
+ return base64.RawURLEncoding.EncodeToString(hash.Sum(nil))
+}
+
+func DoUploadFile(teamId string, channelId string, userId string, rawFilename string, data []byte) (*model.FileInfo, *model.AppError) {
+ filename := filepath.Base(rawFilename)
+
+ info, err := model.GetInfoForBytes(filename, data)
+ if err != nil {
+ err.StatusCode = http.StatusBadRequest
+ return nil, err
+ }
+
+ info.Id = model.NewId()
+ info.CreatorId = userId
+
+ pathPrefix := "teams/" + teamId + "/channels/" + channelId + "/users/" + userId + "/" + info.Id + "/"
+ info.Path = pathPrefix + filename
+
+ if info.IsImage() {
+ // Check dimensions before loading the whole thing into memory later on
+ if info.Width*info.Height > MaxImageSize {
+ err := model.NewLocAppError("uploadFile", "api.file.upload_file.large_image.app_error", map[string]interface{}{"Filename": filename}, "")
+ err.StatusCode = http.StatusBadRequest
+ return nil, err
+ }
+
+ nameWithoutExtension := filename[:strings.LastIndex(filename, ".")]
+ info.PreviewPath = pathPrefix + nameWithoutExtension + "_preview.jpg"
+ info.ThumbnailPath = pathPrefix + nameWithoutExtension + "_thumb.jpg"
+ }
+
+ if err := WriteFile(data, info.Path); err != nil {
+ return nil, err
+ }
+
+ if result := <-Srv.Store.FileInfo().Save(info); result.Err != nil {
+ return nil, result.Err
+ }
+
+ return info, nil
+}
+
+func HandleImages(previewPathList []string, thumbnailPathList []string, fileData [][]byte) {
+ for i, data := range fileData {
+ go func(i int, data []byte) {
+ img, width, height := prepareImage(fileData[i])
+ if img != nil {
+ go generateThumbnailImage(*img, thumbnailPathList[i], width, height)
+ go generatePreviewImage(*img, previewPathList[i], width)
+ }
+ }(i, data)
+ }
+}
+
+func prepareImage(fileData []byte) (*image.Image, int, int) {
+ // Decode image bytes into Image object
+ img, imgType, err := image.Decode(bytes.NewReader(fileData))
+ if err != nil {
+ l4g.Error(utils.T("api.file.handle_images_forget.decode.error"), err)
+ return nil, 0, 0
+ }
+
+ width := img.Bounds().Dx()
+ height := img.Bounds().Dy()
+
+ // Fill in the background of a potentially-transparent png file as white
+ if imgType == "png" {
+ dst := image.NewRGBA(img.Bounds())
+ draw.Draw(dst, dst.Bounds(), image.NewUniform(color.White), image.Point{}, draw.Src)
+ draw.Draw(dst, dst.Bounds(), img, img.Bounds().Min, draw.Over)
+ img = dst
+ }
+
+ // Flip the image to be upright
+ orientation, _ := getImageOrientation(fileData)
+
+ switch orientation {
+ case UprightMirrored:
+ img = imaging.FlipH(img)
+ case UpsideDown:
+ img = imaging.Rotate180(img)
+ case UpsideDownMirrored:
+ img = imaging.FlipV(img)
+ case RotatedCWMirrored:
+ img = imaging.Transpose(img)
+ case RotatedCCW:
+ img = imaging.Rotate270(img)
+ case RotatedCCWMirrored:
+ img = imaging.Transverse(img)
+ case RotatedCW:
+ img = imaging.Rotate90(img)
+ }
+
+ return &img, width, height
+}
+
+func getImageOrientation(imageData []byte) (int, error) {
+ if exifData, err := exif.Decode(bytes.NewReader(imageData)); err != nil {
+ return Upright, err
+ } else {
+ if tag, err := exifData.Get("Orientation"); err != nil {
+ return Upright, err
+ } else {
+ orientation, err := tag.Int(0)
+ if err != nil {
+ return Upright, err
+ } else {
+ return orientation, nil
+ }
+ }
+ }
+}
+
+func generateThumbnailImage(img image.Image, thumbnailPath string, width int, height int) {
+ thumbWidth := float64(utils.Cfg.FileSettings.ThumbnailWidth)
+ thumbHeight := float64(utils.Cfg.FileSettings.ThumbnailHeight)
+ imgWidth := float64(width)
+ imgHeight := float64(height)
+
+ var thumbnail image.Image
+ if imgHeight < thumbHeight && imgWidth < thumbWidth {
+ thumbnail = img
+ } else if imgHeight/imgWidth < thumbHeight/thumbWidth {
+ thumbnail = imaging.Resize(img, 0, utils.Cfg.FileSettings.ThumbnailHeight, imaging.Lanczos)
+ } else {
+ thumbnail = imaging.Resize(img, utils.Cfg.FileSettings.ThumbnailWidth, 0, imaging.Lanczos)
+ }
+
+ buf := new(bytes.Buffer)
+ if err := jpeg.Encode(buf, thumbnail, &jpeg.Options{Quality: 90}); err != nil {
+ l4g.Error(utils.T("api.file.handle_images_forget.encode_jpeg.error"), thumbnailPath, err)
+ return
+ }
+
+ if err := WriteFile(buf.Bytes(), thumbnailPath); err != nil {
+ l4g.Error(utils.T("api.file.handle_images_forget.upload_thumb.error"), thumbnailPath, err)
+ return
+ }
+}
+
+func generatePreviewImage(img image.Image, previewPath string, width int) {
+ var preview image.Image
+ if width > int(utils.Cfg.FileSettings.PreviewWidth) {
+ preview = imaging.Resize(img, utils.Cfg.FileSettings.PreviewWidth, utils.Cfg.FileSettings.PreviewHeight, imaging.Lanczos)
+ } else {
+ preview = img
+ }
+
+ buf := new(bytes.Buffer)
+
+ if err := jpeg.Encode(buf, preview, &jpeg.Options{Quality: 90}); err != nil {
+ l4g.Error(utils.T("api.file.handle_images_forget.encode_preview.error"), previewPath, err)
+ return
+ }
+
+ if err := WriteFile(buf.Bytes(), previewPath); err != nil {
+ l4g.Error(utils.T("api.file.handle_images_forget.upload_preview.error"), previewPath, err)
+ return
+ }
+}
diff --git a/app/file_test.go b/app/file_test.go
new file mode 100644
index 000000000..9df03315e
--- /dev/null
+++ b/app/file_test.go
@@ -0,0 +1,33 @@
+// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package app
+
+import (
+ "testing"
+
+ "github.com/mattermost/platform/model"
+)
+
+func TestGeneratePublicLinkHash(t *testing.T) {
+ filename1 := model.NewId() + "/" + model.NewRandomString(16) + ".txt"
+ filename2 := model.NewId() + "/" + model.NewRandomString(16) + ".txt"
+ salt1 := model.NewRandomString(32)
+ salt2 := model.NewRandomString(32)
+
+ hash1 := GeneratePublicLinkHash(filename1, salt1)
+ hash2 := GeneratePublicLinkHash(filename2, salt1)
+ hash3 := GeneratePublicLinkHash(filename1, salt2)
+
+ if hash1 != GeneratePublicLinkHash(filename1, salt1) {
+ t.Fatal("hash should be equal for the same file name and salt")
+ }
+
+ if hash1 == hash2 {
+ t.Fatal("hashes for different files should not be equal")
+ }
+
+ if hash1 == hash3 {
+ t.Fatal("hashes for the same file with different salts should not be equal")
+ }
+}
diff --git a/app/import.go b/app/import.go
new file mode 100644
index 000000000..8f2cf552e
--- /dev/null
+++ b/app/import.go
@@ -0,0 +1,157 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package app
+
+import (
+ "bytes"
+ "io"
+ "regexp"
+ "unicode/utf8"
+
+ l4g "github.com/alecthomas/log4go"
+ "github.com/mattermost/platform/model"
+ "github.com/mattermost/platform/utils"
+)
+
+//
+// Import functions are sutible for entering posts and users into the database without
+// some of the usual checks. (IsValid is still run)
+//
+
+func ImportPost(post *model.Post) {
+ // Workaround for empty messages, which may be the case if they are webhook posts.
+ firstIteration := true
+ for messageRuneCount := utf8.RuneCountInString(post.Message); messageRuneCount > 0 || firstIteration; messageRuneCount = utf8.RuneCountInString(post.Message) {
+ firstIteration = false
+ var remainder string
+ if messageRuneCount > model.POST_MESSAGE_MAX_RUNES {
+ remainder = string(([]rune(post.Message))[model.POST_MESSAGE_MAX_RUNES:])
+ post.Message = truncateRunes(post.Message, model.POST_MESSAGE_MAX_RUNES)
+ } else {
+ remainder = ""
+ }
+
+ post.Hashtags, _ = model.ParseHashtags(post.Message)
+
+ if result := <-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 {
+ l4g.Error(utils.T("api.import.import_post.attach_files.error"), post.Id, post.FileIds, result.Err)
+ }
+ }
+
+ post.Id = ""
+ post.CreateAt++
+ post.Message = remainder
+ }
+}
+
+func ImportUser(team *model.Team, user *model.User) *model.User {
+ user.MakeNonNil()
+
+ user.Roles = model.ROLE_SYSTEM_USER.Id
+
+ if result := <-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 {
+ l4g.Error(utils.T("api.import.import_user.set_email.error"), cresult.Err)
+ }
+
+ if err := JoinUserToTeam(team, user); err != nil {
+ l4g.Error(utils.T("api.import.import_user.join_team.error"), err)
+ }
+
+ return ruser
+ }
+}
+
+func ImportChannel(channel *model.Channel) *model.Channel {
+ if result := <-Srv.Store.Channel().Save(channel); result.Err != nil {
+ return nil
+ } else {
+ sc := result.Data.(*model.Channel)
+
+ return sc
+ }
+}
+
+func ImportFile(file io.Reader, teamId string, channelId string, userId string, fileName string) (*model.FileInfo, error) {
+ buf := bytes.NewBuffer(nil)
+ io.Copy(buf, file)
+ data := buf.Bytes()
+
+ fileInfo, err := DoUploadFile(teamId, channelId, userId, fileName, data)
+ if err != nil {
+ return nil, err
+ }
+
+ img, width, height := prepareImage(data)
+ if img != nil {
+ generateThumbnailImage(*img, fileInfo.ThumbnailPath, width, height)
+ generatePreviewImage(*img, fileInfo.PreviewPath, width)
+ }
+
+ return fileInfo, nil
+}
+
+func ImportIncomingWebhookPost(post *model.Post, props model.StringInterface) {
+ linkWithTextRegex := regexp.MustCompile(`<([^<\|]+)\|([^>]+)>`)
+ post.Message = linkWithTextRegex.ReplaceAllString(post.Message, "[${2}](${1})")
+
+ post.AddProp("from_webhook", "true")
+
+ if _, ok := props["override_username"]; !ok {
+ post.AddProp("override_username", model.DEFAULT_WEBHOOK_USERNAME)
+ }
+
+ if len(props) > 0 {
+ for key, val := range props {
+ if key == "attachments" {
+ if list, success := val.([]interface{}); success {
+ // parse attachment links into Markdown format
+ 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(key, list)
+ }
+ } else if key != "from_webhook" {
+ post.AddProp(key, val)
+ }
+ }
+ }
+
+ ImportPost(post)
+}
diff --git a/app/ldap.go b/app/ldap.go
new file mode 100644
index 000000000..fe68dfa81
--- /dev/null
+++ b/app/ldap.go
@@ -0,0 +1,40 @@
+// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package app
+
+import (
+ "net/http"
+
+ l4g "github.com/alecthomas/log4go"
+ "github.com/mattermost/platform/einterfaces"
+ "github.com/mattermost/platform/model"
+ "github.com/mattermost/platform/utils"
+)
+
+func SyncLdap() {
+ go func() {
+ if utils.IsLicensed && *utils.License.Features.LDAP && *utils.Cfg.LdapSettings.Enable {
+ if ldapI := einterfaces.GetLdapInterface(); ldapI != nil {
+ ldapI.SyncNow()
+ } else {
+ l4g.Error("%v", model.NewLocAppError("ldapSyncNow", "ent.ldap.disabled.app_error", nil, "").Error())
+ }
+ }
+ }()
+}
+
+func TestLdap() *model.AppError {
+ if ldapI := einterfaces.GetLdapInterface(); ldapI != nil && utils.IsLicensed && *utils.License.Features.LDAP && *utils.Cfg.LdapSettings.Enable {
+ if err := ldapI.RunTest(); err != nil {
+ err.StatusCode = 500
+ return err
+ }
+ } else {
+ err := model.NewLocAppError("ldapTest", "ent.ldap.disabled.app_error", nil, "")
+ err.StatusCode = http.StatusNotImplemented
+ return err
+ }
+
+ return nil
+}
diff --git a/app/notification.go b/app/notification.go
new file mode 100644
index 000000000..ec78c416b
--- /dev/null
+++ b/app/notification.go
@@ -0,0 +1,721 @@
+// 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, sender *model.User) ([]string, *model.AppError) {
+ 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 && post.Props["from_webhook"] != "true" {
+ 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 {
+ profile := profileMap[threadPost.UserId]
+ 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(sender, 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") &&
+ !post.IsSystemMessage() {
+ 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))
+ }
+
+ senderName := make(map[string]string)
+ for _, id := range mentionedUsersList {
+ senderName[id] = ""
+ if post.IsSystemMessage() {
+ senderName[id] = utils.T("system.message.name")
+ } else {
+ 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] = sender.Username
+ } else {
+ senderName[id] = sender.GetDisplayNameForPreference(result.Data.(model.Preference).Value)
+ }
+ }
+ }
+ }
+
+ var senderUsername string
+ if value, ok := post.Props["override_username"]; ok && post.Props["from_webhook"] == "true" {
+ senderUsername = value.(string)
+ } else {
+ senderUsername = sender.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 {
+ sendNotificationEmail(post, profileMap[id], channel, team, senderName[id], sender)
+ }
+ }
+ }
+
+ T := utils.GetUserTranslations(sender.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 {
+ if result := <-Srv.Store.Status().GetOnline(); result.Err != nil {
+ return nil, result.Err
+ } else {
+ statuses := result.Data.([]*model.Status)
+ 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 {
+ 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
+ break
+ }
+ }
+
+ if !found && len(teams) > 0 {
+ team = teams[0]
+ } else {
+ // in case the user hasn't joined any teams we send them to the select_team page
+ team = &model.Team{Name: "select_team", DisplayName: utils.Cfg.TeamSettings.SiteName}
+ }
+ }
+ }
+ 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 {
+ return err
+ }
+
+ 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 {
+ return err
+ }
+ }
+
+ 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(sender *model.User, 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(sender.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..10eb09247
--- /dev/null
+++ b/app/notification_test.go
@@ -0,0 +1,313 @@
+// 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, th.BasicUser)
+ 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) {
+ Setup()
+ // 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/oauth.go b/app/oauth.go
new file mode 100644
index 000000000..3e8b0b8d2
--- /dev/null
+++ b/app/oauth.go
@@ -0,0 +1,34 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package app
+
+import (
+ "github.com/mattermost/platform/model"
+)
+
+func RevokeAccessToken(token string) *model.AppError {
+
+ session, _ := GetSession(token)
+ schan := Srv.Store.Session().Remove(token)
+
+ if result := <-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)
+
+ if result := <-tchan; result.Err != nil {
+ return model.NewLocAppError("RevokeAccessToken", "api.oauth.revoke_access_token.del_token.app_error", nil, "")
+ }
+
+ if result := <-schan; result.Err != nil {
+ return model.NewLocAppError("RevokeAccessToken", "api.oauth.revoke_access_token.del_session.app_error", nil, "")
+ }
+
+ if session != nil {
+ ClearSessionCacheForUser(session.UserId)
+ }
+
+ return nil
+}
diff --git a/app/post.go b/app/post.go
new file mode 100644
index 000000000..6d34cc035
--- /dev/null
+++ b/app/post.go
@@ -0,0 +1,501 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package app
+
+import (
+ "net/http"
+ "regexp"
+
+ l4g "github.com/alecthomas/log4go"
+ "github.com/dyatlov/go-opengraph/opengraph"
+ "github.com/mattermost/platform/einterfaces"
+ "github.com/mattermost/platform/model"
+ "github.com/mattermost/platform/store"
+ "github.com/mattermost/platform/utils"
+)
+
+func CreatePostAsUser(post *model.Post, teamId string) (*model.Post, *model.AppError) {
+ // Check that channel has not been deleted
+ var channel *model.Channel
+ if result := <-Srv.Store.Channel().Get(post.ChannelId, true); result.Err != nil {
+ err := model.NewLocAppError("CreatePostAsUser", "api.context.invalid_param.app_error", map[string]interface{}{"Name": "post.channel_id"}, result.Err.Error())
+ err.StatusCode = http.StatusBadRequest
+ return nil, err
+ } else {
+ channel = result.Data.(*model.Channel)
+ }
+
+ if channel.DeleteAt != 0 {
+ err := model.NewLocAppError("createPost", "api.post.create_post.can_not_post_to_deleted.error", nil, "")
+ err.StatusCode = http.StatusBadRequest
+ return nil, err
+ }
+
+ if rp, err := CreatePost(post, teamId, true); err != nil {
+ if err.Id == "api.post.create_post.root_id.app_error" ||
+ err.Id == "api.post.create_post.channel_root_id.app_error" ||
+ err.Id == "api.post.create_post.parent_id.app_error" {
+ err.StatusCode = http.StatusBadRequest
+ }
+
+ return nil, err
+ } 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}, post.UserId); result.Err != nil {
+ l4g.Error(utils.T("api.post.create_post.last_viewed.error"), post.ChannelId, post.UserId, result.Err)
+ }
+ }
+
+ return rp, nil
+ }
+
+}
+
+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)
+ }
+
+ var user *model.User
+ if result := <-uchan; result.Err != nil {
+ return result.Err
+ } else {
+ user = result.Data.(*model.User)
+ }
+
+ if _, err := SendNotifications(post, team, channel, user); err != nil {
+ return err
+ }
+
+ 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
+}
+
+func UpdatePost(post *model.Post) (*model.Post, *model.AppError) {
+ if utils.IsLicensed {
+ if *utils.Cfg.ServiceSettings.AllowEditPost == model.ALLOW_EDIT_POST_NEVER {
+ err := model.NewLocAppError("updatePost", "api.post.update_post.permissions_denied.app_error", nil, "")
+ err.StatusCode = http.StatusForbidden
+ return nil, err
+ }
+ }
+
+ var oldPost *model.Post
+ if result := <-Srv.Store.Post().Get(post.Id); result.Err != nil {
+ return nil, result.Err
+ } else {
+ oldPost = result.Data.(*model.PostList).Posts[post.Id]
+
+ if oldPost == nil {
+ err := model.NewLocAppError("updatePost", "api.post.update_post.find.app_error", nil, "id="+post.Id)
+ err.StatusCode = http.StatusBadRequest
+ return nil, err
+ }
+
+ if oldPost.UserId != post.UserId {
+ err := model.NewLocAppError("updatePost", "api.post.update_post.permissions.app_error", nil, "oldUserId="+oldPost.UserId)
+ err.StatusCode = http.StatusBadRequest
+ return nil, err
+ }
+
+ if oldPost.DeleteAt != 0 {
+ err := model.NewLocAppError("updatePost", "api.post.update_post.permissions_details.app_error", map[string]interface{}{"PostId": post.Id}, "")
+ err.StatusCode = http.StatusBadRequest
+ return nil, err
+ }
+
+ if oldPost.IsSystemMessage() {
+ err := model.NewLocAppError("updatePost", "api.post.update_post.system_message.app_error", nil, "id="+post.Id)
+ err.StatusCode = http.StatusBadRequest
+ return nil, err
+ }
+
+ if utils.IsLicensed {
+ if *utils.Cfg.ServiceSettings.AllowEditPost == model.ALLOW_EDIT_POST_TIME_LIMIT && model.GetMillis() > oldPost.CreateAt+int64(*utils.Cfg.ServiceSettings.PostEditTimeLimit*1000) {
+ err := model.NewLocAppError("updatePost", "api.post.update_post.permissions_time_limit.app_error", map[string]interface{}{"timeLimit": *utils.Cfg.ServiceSettings.PostEditTimeLimit}, "")
+ err.StatusCode = http.StatusBadRequest
+ return nil, err
+ }
+ }
+ }
+
+ newPost := &model.Post{}
+ *newPost = *oldPost
+
+ newPost.Message = post.Message
+ newPost.EditAt = model.GetMillis()
+ newPost.Hashtags, _ = model.ParseHashtags(post.Message)
+
+ if result := <-Srv.Store.Post().Update(newPost, oldPost); result.Err != nil {
+ return nil, result.Err
+ } else {
+ rpost := result.Data.(*model.Post)
+
+ message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_POST_EDITED, "", rpost.ChannelId, "", nil)
+ message.Add("post", rpost.ToJson())
+
+ go Publish(message)
+
+ InvalidateCacheForChannelPosts(rpost.ChannelId)
+
+ return rpost, nil
+ }
+}
+
+func GetPosts(channelId string, offset int, limit int) (*model.PostList, *model.AppError) {
+ if result := <-Srv.Store.Post().GetPosts(channelId, offset, limit, true); result.Err != nil {
+ return nil, result.Err
+ } else {
+ return result.Data.(*model.PostList), nil
+ }
+}
+
+func GetPostsEtag(channelId string) string {
+ return (<-Srv.Store.Post().GetEtag(channelId, true)).Data.(string)
+}
+
+func GetPostsSince(channelId string, time int64) (*model.PostList, *model.AppError) {
+ if result := <-Srv.Store.Post().GetPostsSince(channelId, time, true); result.Err != nil {
+ return nil, result.Err
+ } else {
+ return result.Data.(*model.PostList), nil
+ }
+}
+
+func GetSinglePost(postId string) (*model.Post, *model.AppError) {
+ if result := <-Srv.Store.Post().GetSingle(postId); result.Err != nil {
+ return nil, result.Err
+ } else {
+ return result.Data.(*model.Post), nil
+ }
+}
+
+func GetPostThread(postId string) (*model.PostList, *model.AppError) {
+ if result := <-Srv.Store.Post().Get(postId); result.Err != nil {
+ return nil, result.Err
+ } else {
+ return result.Data.(*model.PostList), nil
+ }
+}
+
+func GetFlaggedPosts(userId string, offset int, limit int) (*model.PostList, *model.AppError) {
+ if result := <-Srv.Store.Post().GetFlaggedPosts(userId, offset, limit); result.Err != nil {
+ return nil, result.Err
+ } else {
+ return result.Data.(*model.PostList), nil
+ }
+}
+
+func GetPermalinkPost(postId string, userId string) (*model.PostList, *model.AppError) {
+ if result := <-Srv.Store.Post().Get(postId); result.Err != nil {
+ return nil, result.Err
+ } else {
+ list := result.Data.(*model.PostList)
+
+ if len(list.Order) != 1 {
+ return nil, model.NewLocAppError("getPermalinkTmp", "api.post_get_post_by_id.get.app_error", nil, "")
+ }
+ post := list.Posts[list.Order[0]]
+
+ var channel *model.Channel
+ var err *model.AppError
+ if channel, err = GetChannel(post.ChannelId); err != nil {
+ return nil, err
+ }
+
+ if err = JoinChannel(channel, userId); err != nil {
+ return nil, err
+ }
+
+ return list, nil
+ }
+}
+
+func GetPostsAroundPost(postId, channelId string, offset, limit int, before bool) (*model.PostList, *model.AppError) {
+ var pchan store.StoreChannel
+ if before {
+ pchan = Srv.Store.Post().GetPostsBefore(channelId, postId, limit, offset)
+ } else {
+ pchan = Srv.Store.Post().GetPostsAfter(channelId, postId, limit, offset)
+ }
+
+ if result := <-pchan; result.Err != nil {
+ return nil, result.Err
+ } else {
+ return result.Data.(*model.PostList), nil
+ }
+}
+
+func DeletePost(postId string) (*model.Post, *model.AppError) {
+ if result := <-Srv.Store.Post().GetSingle(postId); result.Err != nil {
+ return nil, result.Err
+ } else {
+ post := result.Data.(*model.Post)
+
+ if result := <-Srv.Store.Post().Delete(postId, model.GetMillis()); result.Err != nil {
+ return nil, result.Err
+ }
+
+ message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_POST_DELETED, "", post.ChannelId, "", nil)
+ message.Add("post", post.ToJson())
+
+ go Publish(message)
+ go DeletePostFiles(post)
+ go DeleteFlaggedPosts(post.Id)
+
+ InvalidateCacheForChannelPosts(post.ChannelId)
+
+ return post, nil
+ }
+}
+
+func DeleteFlaggedPosts(postId string) {
+ if result := <-Srv.Store.Preference().DeleteCategoryAndName(model.PREFERENCE_CATEGORY_FLAGGED_POST, postId); result.Err != nil {
+ l4g.Warn(utils.T("api.post.delete_flagged_post.app_error.warn"), result.Err)
+ return
+ }
+}
+
+func DeletePostFiles(post *model.Post) {
+ if len(post.FileIds) != 0 {
+ return
+ }
+
+ if result := <-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)
+ }
+}
+
+func SearchPostsInTeam(terms string, userId string, teamId string, isOrSearch bool) (*model.PostList, *model.AppError) {
+ paramsList := model.ParseSearchParams(terms)
+ channels := []store.StoreChannel{}
+
+ for _, params := range paramsList {
+ params.OrTerms = isOrSearch
+ // don't allow users to search for everything
+ if params.Terms != "*" {
+ channels = append(channels, Srv.Store.Post().Search(teamId, userId, params))
+ }
+ }
+
+ posts := &model.PostList{}
+ for _, channel := range channels {
+ if result := <-channel; result.Err != nil {
+ return nil, result.Err
+ } else {
+ data := result.Data.(*model.PostList)
+ posts.Extend(data)
+ }
+ }
+
+ return posts, nil
+}
+
+func GetFileInfosForPost(postId string) ([]*model.FileInfo, *model.AppError) {
+ pchan := Srv.Store.Post().Get(postId)
+ fchan := Srv.Store.FileInfo().GetForPost(postId)
+
+ var infos []*model.FileInfo
+ if result := <-fchan; result.Err != nil {
+ return nil, result.Err
+ } else {
+ infos = result.Data.([]*model.FileInfo)
+ }
+
+ if len(infos) == 0 {
+ // No FileInfos were returned so check if they need to be created for this post
+ var post *model.Post
+ if result := <-pchan; result.Err != nil {
+ return nil, result.Err
+ } else {
+ post = result.Data.(*model.PostList).Posts[postId]
+ }
+
+ if len(post.Filenames) > 0 {
+ // The post has Filenames that need to be replaced with FileInfos
+ infos = MigrateFilenamesToFileInfos(post)
+ }
+ }
+
+ return infos, nil
+}
+
+func GetOpenGraphMetadata(url string) *opengraph.OpenGraph {
+ og := opengraph.NewOpenGraph()
+
+ res, err := http.Get(url)
+ defer CloseBody(res)
+ if err != nil {
+ return og
+ }
+
+ if err := og.ProcessHTML(res.Body); err != nil {
+ return og
+ }
+
+ return og
+}
diff --git a/app/preference.go b/app/preference.go
new file mode 100644
index 000000000..4e492c4a8
--- /dev/null
+++ b/app/preference.go
@@ -0,0 +1,16 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package app
+
+import (
+ "github.com/mattermost/platform/model"
+)
+
+func GetPreferencesForUser(userId string) (model.Preferences, *model.AppError) {
+ if result := <-Srv.Store.Preference().GetAll(userId); result.Err != nil {
+ return nil, result.Err
+ } else {
+ return result.Data.(model.Preferences), nil
+ }
+}
diff --git a/app/saml.go b/app/saml.go
new file mode 100644
index 000000000..cc39d4540
--- /dev/null
+++ b/app/saml.go
@@ -0,0 +1,67 @@
+// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package app
+
+import (
+ "io"
+ "mime/multipart"
+ "net/http"
+ "os"
+
+ "github.com/mattermost/platform/einterfaces"
+ "github.com/mattermost/platform/model"
+ "github.com/mattermost/platform/utils"
+)
+
+func GetSamlMetadata() (string, *model.AppError) {
+ samlInterface := einterfaces.GetSamlInterface()
+
+ if samlInterface == nil {
+ err := model.NewLocAppError("GetSamlMetadata", "api.admin.saml.not_available.app_error", nil, "")
+ err.StatusCode = http.StatusNotImplemented
+ return "", err
+ }
+
+ if result, err := samlInterface.GetMetadata(); err != nil {
+ return "", model.NewLocAppError("GetSamlMetadata", "api.admin.saml.metadata.app_error", nil, "err="+err.Message)
+ } else {
+ return result, nil
+ }
+}
+
+func AddSamlCertificate(fileData *multipart.FileHeader) *model.AppError {
+ file, err := fileData.Open()
+ defer file.Close()
+ if err != nil {
+ return model.NewLocAppError("AddSamlCertificate", "api.admin.add_certificate.open.app_error", nil, err.Error())
+ }
+
+ out, err := os.Create(utils.FindDir("config") + fileData.Filename)
+ if err != nil {
+ return model.NewLocAppError("AddSamlCertificate", "api.admin.add_certificate.saving.app_error", nil, err.Error())
+ }
+ defer out.Close()
+
+ io.Copy(out, file)
+ return nil
+}
+
+func RemoveSamlCertificate(filename string) *model.AppError {
+ if err := os.Remove(utils.FindConfigFile(filename)); err != nil {
+ return model.NewLocAppError("removeCertificate", "api.admin.remove_certificate.delete.app_error",
+ map[string]interface{}{"Filename": filename}, err.Error())
+ }
+
+ return nil
+}
+
+func GetSamlCertificateStatus() map[string]interface{} {
+ status := make(map[string]interface{})
+
+ status["IdpCertificateFile"] = utils.FileExistsInConfigFolder(*utils.Cfg.SamlSettings.IdpCertificateFile)
+ status["PrivateKeyFile"] = utils.FileExistsInConfigFolder(*utils.Cfg.SamlSettings.PrivateKeyFile)
+ status["PublicCertificateFile"] = utils.FileExistsInConfigFolder(*utils.Cfg.SamlSettings.PublicCertificateFile)
+
+ return status
+}
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..83e5f343a
--- /dev/null
+++ b/app/session.go
@@ -0,0 +1,180 @@
+// 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/utils"
+
+ l4g "github.com/alecthomas/log4go"
+)
+
+var sessionCache *utils.Cache = utils.NewLru(model.SESSION_CACHE_SIZE)
+
+func CreateSession(session *model.Session) (*model.Session, *model.AppError) {
+ if result := <-Srv.Store.Session().Save(session); result.Err != nil {
+ return nil, result.Err
+ } else {
+ session := result.Data.(*model.Session)
+
+ AddSessionToCache(session)
+
+ return session, nil
+ }
+}
+
+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 GetSessions(userId string) ([]*model.Session, *model.AppError) {
+ if result := <-Srv.Store.Session().GetSessions(userId); result.Err != nil {
+ return nil, result.Err
+ } else {
+ return result.Data.([]*model.Session), nil
+ }
+}
+
+func RevokeAllSessions(userId string) *model.AppError {
+ if result := <-Srv.Store.Session().GetSessions(userId); result.Err != nil {
+ return result.Err
+ } else {
+ sessions := result.Data.([]*model.Session)
+
+ for _, session := range sessions {
+ if session.IsOAuth {
+ RevokeAccessToken(session.Token)
+ } else {
+ if result := <-Srv.Store.Session().Remove(session.Id); result.Err != nil {
+ return result.Err
+ }
+ }
+
+ RevokeWebrtcToken(session.Id)
+ }
+ }
+
+ ClearSessionCacheForUser(userId)
+
+ return nil
+}
+
+func ClearSessionCacheForUser(userId string) {
+
+ ClearSessionCacheForUserSkipClusterSend(userId)
+
+ if einterfaces.GetClusterInterface() != nil {
+ einterfaces.GetClusterInterface().ClearSessionCacheForUser(userId)
+ }
+}
+
+func ClearSessionCacheForUserSkipClusterSend(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 SessionCacheLength() int {
+ return sessionCache.Len()
+}
+
+func RevokeSessionsForDeviceId(userId string, deviceId string, currentSessionId string) *model.AppError {
+ if result := <-Srv.Store.Session().GetSessions(userId); result.Err != nil {
+ return result.Err
+ } else {
+ sessions := result.Data.([]*model.Session)
+ for _, session := range sessions {
+ if session.DeviceId == deviceId && session.Id != currentSessionId {
+ l4g.Debug(utils.T("api.user.login.revoking.app_error"), session.Id, userId)
+ if err := RevokeSession(session); err != nil {
+ // Soft error so we still remove the other sessions
+ l4g.Error(err.Error())
+ }
+ }
+ }
+ }
+
+ return nil
+}
+
+func RevokeSessionById(sessionId string) *model.AppError {
+ if result := <-Srv.Store.Session().Get(sessionId); result.Err != nil {
+ return result.Err
+ } else {
+ return RevokeSession(result.Data.(*model.Session))
+ }
+}
+
+func RevokeSession(session *model.Session) *model.AppError {
+ if session.IsOAuth {
+ if err := RevokeAccessToken(session.Token); err != nil {
+ return err
+ }
+ } else {
+ if result := <-Srv.Store.Session().Remove(session.Id); result.Err != nil {
+ return result.Err
+ }
+ }
+
+ RevokeWebrtcToken(session.Id)
+ ClearSessionCacheForUser(session.UserId)
+
+ return nil
+}
+
+func AttachDeviceId(sessionId string, deviceId string, expiresAt int64) *model.AppError {
+ if result := <-Srv.Store.Session().UpdateDeviceId(sessionId, deviceId, expiresAt); result.Err != nil {
+ return result.Err
+ }
+
+ return nil
+}
diff --git a/app/session_test.go b/app/session_test.go
new file mode 100644
index 000000000..aea31cf86
--- /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")
+ }
+
+ ClearSessionCacheForUser(session.UserId)
+
+ rkeys := sessionCache.Keys()
+ if len(rkeys) != len(keys)-1 {
+ t.Fatal("should have one less")
+ }
+}
diff --git a/app/slackimport.go b/app/slackimport.go
new file mode 100644
index 000000000..508803126
--- /dev/null
+++ b/app/slackimport.go
@@ -0,0 +1,631 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package app
+
+import (
+ "archive/zip"
+ "bytes"
+ "encoding/json"
+ "io"
+ "mime/multipart"
+ "path/filepath"
+ "regexp"
+ "strconv"
+ "strings"
+ "unicode/utf8"
+
+ l4g "github.com/alecthomas/log4go"
+ "github.com/mattermost/platform/model"
+ "github.com/mattermost/platform/utils"
+)
+
+type SlackChannel struct {
+ Id string `json:"id"`
+ Name string `json:"name"`
+ Members []string `json:"members"`
+ Topic map[string]string `json:"topic"`
+ Purpose map[string]string `json:"purpose"`
+}
+
+type SlackUser struct {
+ Id string `json:"id"`
+ Username string `json:"name"`
+ Profile map[string]string `json:"profile"`
+}
+
+type SlackFile struct {
+ Id string `json:"id"`
+ Title string `json:"title"`
+}
+
+type SlackPost struct {
+ User string `json:"user"`
+ BotId string `json:"bot_id"`
+ BotUsername string `json:"username"`
+ Text string `json:"text"`
+ TimeStamp string `json:"ts"`
+ Type string `json:"type"`
+ SubType string `json:"subtype"`
+ Comment *SlackComment `json:"comment"`
+ Upload bool `json:"upload"`
+ File *SlackFile `json:"file"`
+ Attachments []SlackAttachment `json:"attachments"`
+}
+
+type SlackComment struct {
+ User string `json:"user"`
+ Comment string `json:"comment"`
+}
+
+type SlackAttachment struct {
+ Id int `json:"id"`
+ Text string `json:"text"`
+ Pretext string `json:"pretext"`
+ Fields []map[string]interface{} `json:"fields"`
+}
+
+func truncateRunes(s string, i int) string {
+ runes := []rune(s)
+ if len(runes) > i {
+ return string(runes[:i])
+ }
+ return s
+}
+
+func SlackConvertTimeStamp(ts string) int64 {
+ timeString := strings.SplitN(ts, ".", 2)[0]
+
+ timeStamp, err := strconv.ParseInt(timeString, 10, 64)
+ if err != nil {
+ l4g.Warn(utils.T("api.slackimport.slack_convert_timestamp.bad.warn"))
+ return 1
+ }
+ return timeStamp * 1000 // Convert to milliseconds
+}
+
+func SlackConvertChannelName(channelName string) string {
+ newName := strings.Trim(channelName, "_-")
+ if len(newName) == 1 {
+ return "slack-channel-" + newName
+ }
+
+ return newName
+}
+
+func SlackParseChannels(data io.Reader) ([]SlackChannel, error) {
+ decoder := json.NewDecoder(data)
+
+ var channels []SlackChannel
+ if err := decoder.Decode(&channels); err != nil {
+ l4g.Warn(utils.T("api.slackimport.slack_parse_channels.error"))
+ return channels, err
+ }
+ return channels, nil
+}
+
+func SlackParseUsers(data io.Reader) ([]SlackUser, error) {
+ decoder := json.NewDecoder(data)
+
+ var users []SlackUser
+ if err := decoder.Decode(&users); err != nil {
+ // This actually returns errors that are ignored.
+ // In this case it is erroring because of a null that Slack
+ // introduced. So we just return the users here.
+ return users, err
+ }
+ return users, nil
+}
+
+func SlackParsePosts(data io.Reader) ([]SlackPost, error) {
+ decoder := json.NewDecoder(data)
+
+ var posts []SlackPost
+ if err := decoder.Decode(&posts); err != nil {
+ l4g.Warn(utils.T("api.slackimport.slack_parse_posts.error"))
+ return posts, err
+ }
+ return posts, nil
+}
+
+func SlackAddUsers(teamId string, slackusers []SlackUser, log *bytes.Buffer) map[string]*model.User {
+ // Log header
+ log.WriteString(utils.T("api.slackimport.slack_add_users.created"))
+ log.WriteString("===============\r\n\r\n")
+
+ addedUsers := make(map[string]*model.User)
+
+ // Need the team
+ var team *model.Team
+ if result := <-Srv.Store.Team().Get(teamId); result.Err != nil {
+ log.WriteString(utils.T("api.slackimport.slack_import.team_fail"))
+ return addedUsers
+ } else {
+ team = result.Data.(*model.Team)
+ }
+
+ for _, sUser := range slackusers {
+ firstName := ""
+ lastName := ""
+ if name, ok := sUser.Profile["first_name"]; ok {
+ firstName = name
+ }
+ if name, ok := sUser.Profile["last_name"]; ok {
+ lastName = name
+ }
+
+ email := sUser.Profile["email"]
+
+ password := model.NewId()
+
+ // Check for email conflict and use existing user if found
+ if result := <-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 {
+ 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}))
+ }
+ continue
+ }
+
+ newUser := model.User{
+ Username: sUser.Username,
+ FirstName: firstName,
+ LastName: lastName,
+ Email: email,
+ Password: password,
+ }
+
+ if mUser := ImportUser(team, &newUser); mUser != nil {
+ addedUsers[sUser.Id] = mUser
+ log.WriteString(utils.T("api.slackimport.slack_add_users.email_pwd", map[string]interface{}{"Email": newUser.Email, "Password": password}))
+ } else {
+ log.WriteString(utils.T("api.slackimport.slack_add_users.unable_import", map[string]interface{}{"Username": sUser.Username}))
+ }
+ }
+
+ return addedUsers
+}
+
+func SlackAddBotUser(teamId string, log *bytes.Buffer) *model.User {
+ var team *model.Team
+ if result := <-Srv.Store.Team().Get(teamId); result.Err != nil {
+ log.WriteString(utils.T("api.slackimport.slack_import.team_fail"))
+ return nil
+ } else {
+ team = result.Data.(*model.Team)
+ }
+
+ password := model.NewId()
+ username := "slackimportuser_" + model.NewId()
+ email := username + "@localhost"
+
+ botUser := model.User{
+ Username: username,
+ FirstName: "",
+ LastName: "",
+ Email: email,
+ Password: password,
+ }
+
+ if mUser := ImportUser(team, &botUser); mUser != nil {
+ log.WriteString(utils.T("api.slackimport.slack_add_bot_user.email_pwd", map[string]interface{}{"Email": botUser.Email, "Password": password}))
+ return mUser
+ } else {
+ log.WriteString(utils.T("api.slackimport.slack_add_bot_user.unable_import", map[string]interface{}{"Username": username}))
+ return nil
+ }
+}
+
+func SlackAddPosts(teamId string, channel *model.Channel, posts []SlackPost, users map[string]*model.User, uploads map[string]*zip.File, botUser *model.User) {
+ for _, sPost := range posts {
+ switch {
+ case sPost.Type == "message" && (sPost.SubType == "" || sPost.SubType == "file_share"):
+ if sPost.User == "" {
+ l4g.Debug(utils.T("api.slackimport.slack_add_posts.without_user.debug"))
+ continue
+ } else if users[sPost.User] == nil {
+ l4g.Debug(utils.T("api.slackimport.slack_add_posts.user_no_exists.debug"), sPost.User)
+ continue
+ }
+ newPost := model.Post{
+ UserId: users[sPost.User].Id,
+ ChannelId: channel.Id,
+ Message: sPost.Text,
+ CreateAt: SlackConvertTimeStamp(sPost.TimeStamp),
+ }
+ if sPost.Upload {
+ if fileInfo, ok := SlackUploadFile(sPost, uploads, teamId, newPost.ChannelId, newPost.UserId); ok == true {
+ newPost.FileIds = append(newPost.FileIds, fileInfo.Id)
+ newPost.Message = sPost.File.Title
+ }
+ }
+ ImportPost(&newPost)
+ for _, fileId := range newPost.FileIds {
+ if result := <-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)
+ }
+ }
+
+ case sPost.Type == "message" && sPost.SubType == "file_comment":
+ if sPost.Comment == nil {
+ l4g.Debug(utils.T("api.slackimport.slack_add_posts.msg_no_comment.debug"))
+ continue
+ } else if sPost.Comment.User == "" {
+ l4g.Debug(utils.T("api.slackimport.slack_add_posts.msg_no_usr.debug"))
+ continue
+ } else if users[sPost.Comment.User] == nil {
+ l4g.Debug(utils.T("api.slackimport.slack_add_posts.user_no_exists.debug"), sPost.User)
+ continue
+ }
+ newPost := model.Post{
+ UserId: users[sPost.Comment.User].Id,
+ ChannelId: channel.Id,
+ Message: sPost.Comment.Comment,
+ CreateAt: SlackConvertTimeStamp(sPost.TimeStamp),
+ }
+ ImportPost(&newPost)
+ case sPost.Type == "message" && sPost.SubType == "bot_message":
+ if botUser == nil {
+ l4g.Warn(utils.T("api.slackimport.slack_add_posts.bot_user_no_exists.warn"))
+ continue
+ } else if sPost.BotId == "" {
+ l4g.Warn(utils.T("api.slackimport.slack_add_posts.no_bot_id.warn"))
+ continue
+ }
+
+ props := make(model.StringInterface)
+ props["override_username"] = sPost.BotUsername
+ if len(sPost.Attachments) > 0 {
+ var mAttachments []interface{}
+ for _, attachment := range sPost.Attachments {
+ mAttachments = append(mAttachments, map[string]interface{}{
+ "text": attachment.Text,
+ "pretext": attachment.Pretext,
+ "fields": attachment.Fields,
+ })
+ }
+ props["attachments"] = mAttachments
+ }
+
+ post := &model.Post{
+ UserId: botUser.Id,
+ ChannelId: channel.Id,
+ CreateAt: SlackConvertTimeStamp(sPost.TimeStamp),
+ Message: sPost.Text,
+ Type: model.POST_SLACK_ATTACHMENT,
+ }
+
+ ImportIncomingWebhookPost(post, props)
+ case sPost.Type == "message" && (sPost.SubType == "channel_join" || sPost.SubType == "channel_leave"):
+ if sPost.User == "" {
+ l4g.Debug(utils.T("api.slackimport.slack_add_posts.msg_no_usr.debug"))
+ continue
+ } else if users[sPost.User] == nil {
+ l4g.Debug(utils.T("api.slackimport.slack_add_posts.user_no_exists.debug"), sPost.User)
+ continue
+ }
+ newPost := model.Post{
+ UserId: users[sPost.User].Id,
+ ChannelId: channel.Id,
+ Message: sPost.Text,
+ CreateAt: SlackConvertTimeStamp(sPost.TimeStamp),
+ Type: model.POST_JOIN_LEAVE,
+ }
+ ImportPost(&newPost)
+ case sPost.Type == "message" && sPost.SubType == "me_message":
+ if sPost.User == "" {
+ l4g.Debug(utils.T("api.slackimport.slack_add_posts.without_user.debug"))
+ continue
+ } else if users[sPost.User] == nil {
+ l4g.Debug(utils.T("api.slackimport.slack_add_posts.user_no_exists.debug"), sPost.User)
+ continue
+ }
+ newPost := model.Post{
+ UserId: users[sPost.User].Id,
+ ChannelId: channel.Id,
+ Message: "*" + sPost.Text + "*",
+ CreateAt: SlackConvertTimeStamp(sPost.TimeStamp),
+ }
+ ImportPost(&newPost)
+ case sPost.Type == "message" && sPost.SubType == "channel_topic":
+ if sPost.User == "" {
+ l4g.Debug(utils.T("api.slackimport.slack_add_posts.msg_no_usr.debug"))
+ continue
+ } else if users[sPost.User] == nil {
+ l4g.Debug(utils.T("api.slackimport.slack_add_posts.user_no_exists.debug"), sPost.User)
+ continue
+ }
+ newPost := model.Post{
+ UserId: users[sPost.User].Id,
+ ChannelId: channel.Id,
+ Message: sPost.Text,
+ CreateAt: SlackConvertTimeStamp(sPost.TimeStamp),
+ Type: model.POST_HEADER_CHANGE,
+ }
+ ImportPost(&newPost)
+ case sPost.Type == "message" && sPost.SubType == "channel_purpose":
+ if sPost.User == "" {
+ l4g.Debug(utils.T("api.slackimport.slack_add_posts.msg_no_usr.debug"))
+ continue
+ } else if users[sPost.User] == nil {
+ l4g.Debug(utils.T("api.slackimport.slack_add_posts.user_no_exists.debug"), sPost.User)
+ continue
+ }
+ newPost := model.Post{
+ UserId: users[sPost.User].Id,
+ ChannelId: channel.Id,
+ Message: sPost.Text,
+ CreateAt: SlackConvertTimeStamp(sPost.TimeStamp),
+ Type: model.POST_PURPOSE_CHANGE,
+ }
+ ImportPost(&newPost)
+ case sPost.Type == "message" && sPost.SubType == "channel_name":
+ if sPost.User == "" {
+ l4g.Debug(utils.T("api.slackimport.slack_add_posts.msg_no_usr.debug"))
+ continue
+ } else if users[sPost.User] == nil {
+ l4g.Debug(utils.T("api.slackimport.slack_add_posts.user_no_exists.debug"), sPost.User)
+ continue
+ }
+ newPost := model.Post{
+ UserId: users[sPost.User].Id,
+ ChannelId: channel.Id,
+ Message: sPost.Text,
+ CreateAt: SlackConvertTimeStamp(sPost.TimeStamp),
+ Type: model.POST_DISPLAYNAME_CHANGE,
+ }
+ ImportPost(&newPost)
+ default:
+ l4g.Warn(utils.T("api.slackimport.slack_add_posts.unsupported.warn"), sPost.Type, sPost.SubType)
+ }
+ }
+}
+
+func SlackUploadFile(sPost SlackPost, uploads map[string]*zip.File, teamId string, channelId string, userId string) (*model.FileInfo, bool) {
+ if sPost.File != nil {
+ if file, ok := uploads[sPost.File.Id]; ok == true {
+ openFile, err := file.Open()
+ if err != nil {
+ l4g.Warn(utils.T("api.slackimport.slack_add_posts.upload_file_open_failed.warn", map[string]interface{}{"FileId": sPost.File.Id, "Error": err.Error()}))
+ return nil, false
+ }
+ defer openFile.Close()
+
+ uploadedFile, err := ImportFile(openFile, teamId, channelId, userId, filepath.Base(file.Name))
+ if err != nil {
+ l4g.Warn(utils.T("api.slackimport.slack_add_posts.upload_file_upload_failed.warn", map[string]interface{}{"FileId": sPost.File.Id, "Error": err.Error()}))
+ return nil, false
+ }
+
+ return uploadedFile, true
+ } else {
+ l4g.Warn(utils.T("api.slackimport.slack_add_posts.upload_file_not_found.warn", map[string]interface{}{"FileId": sPost.File.Id}))
+ return nil, false
+ }
+ } else {
+ l4g.Warn(utils.T("api.slackimport.slack_add_posts.upload_file_not_in_json.warn"))
+ return nil, false
+ }
+}
+
+func deactivateSlackBotUser(user *model.User) {
+ _, err := UpdateActive(user, false)
+ if err != nil {
+ l4g.Warn(utils.T("api.slackimport.slack_deactivate_bot_user.failed_to_deactivate", err))
+ }
+}
+
+func addSlackUsersToChannel(members []string, users map[string]*model.User, channel *model.Channel, log *bytes.Buffer) {
+ for _, member := range members {
+ 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 {
+ log.WriteString(utils.T("api.slackimport.slack_add_channels.failed_to_add_user", map[string]interface{}{"Username": user.Username}))
+ }
+ }
+ }
+}
+
+func SlackSanitiseChannelProperties(channel model.Channel) model.Channel {
+ if utf8.RuneCountInString(channel.DisplayName) > model.CHANNEL_DISPLAY_NAME_MAX_RUNES {
+ l4g.Warn("api.slackimport.slack_sanitise_channel_properties.display_name_too_long.warn", map[string]interface{}{"ChannelName": channel.DisplayName})
+ channel.DisplayName = truncateRunes(channel.DisplayName, model.CHANNEL_DISPLAY_NAME_MAX_RUNES)
+ }
+
+ if len(channel.Name) > model.CHANNEL_NAME_MAX_LENGTH {
+ l4g.Warn("api.slackimport.slack_sanitise_channel_properties.name_too_long.warn", map[string]interface{}{"ChannelName": channel.DisplayName})
+ channel.Name = channel.Name[0:model.CHANNEL_NAME_MAX_LENGTH]
+ }
+
+ if utf8.RuneCountInString(channel.Purpose) > model.CHANNEL_PURPOSE_MAX_RUNES {
+ l4g.Warn("api.slackimport.slack_sanitise_channel_properties.purpose_too_long.warn", map[string]interface{}{"ChannelName": channel.DisplayName})
+ channel.Purpose = truncateRunes(channel.Purpose, model.CHANNEL_PURPOSE_MAX_RUNES)
+ }
+
+ if utf8.RuneCountInString(channel.Header) > model.CHANNEL_HEADER_MAX_RUNES {
+ l4g.Warn("api.slackimport.slack_sanitise_channel_properties.header_too_long.warn", map[string]interface{}{"ChannelName": channel.DisplayName})
+ channel.Header = truncateRunes(channel.Header, model.CHANNEL_HEADER_MAX_RUNES)
+ }
+
+ return channel
+}
+
+func SlackAddChannels(teamId string, slackchannels []SlackChannel, posts map[string][]SlackPost, users map[string]*model.User, uploads map[string]*zip.File, botUser *model.User, log *bytes.Buffer) map[string]*model.Channel {
+ // Write Header
+ log.WriteString(utils.T("api.slackimport.slack_add_channels.added"))
+ log.WriteString("=================\r\n\r\n")
+
+ addedChannels := make(map[string]*model.Channel)
+ for _, sChannel := range slackchannels {
+ newChannel := model.Channel{
+ TeamId: teamId,
+ Type: model.CHANNEL_OPEN,
+ DisplayName: sChannel.Name,
+ Name: SlackConvertChannelName(sChannel.Name),
+ Purpose: sChannel.Purpose["value"],
+ Header: sChannel.Topic["value"],
+ }
+ newChannel = SlackSanitiseChannelProperties(newChannel)
+
+ var mChannel *model.Channel
+ if result := <-Srv.Store.Channel().GetByName(teamId, sChannel.Name); result.Err == nil {
+ // The channel already exists as an active channel. Merge with the existing one.
+ mChannel = result.Data.(*model.Channel)
+ log.WriteString(utils.T("api.slackimport.slack_add_channels.merge", map[string]interface{}{"DisplayName": newChannel.DisplayName}))
+ } else if result := <-Srv.Store.Channel().GetDeletedByName(teamId, sChannel.Name); result.Err == nil {
+ // The channel already exists but has been deleted. Generate a random string for the handle instead.
+ newChannel.Name = model.NewId()
+ newChannel = SlackSanitiseChannelProperties(newChannel)
+ }
+
+ if mChannel == nil {
+ // Haven't found an existing channel to merge with. Try importing it as a new one.
+ mChannel = ImportChannel(&newChannel)
+ if mChannel == 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
+ }
+ }
+
+ addSlackUsersToChannel(sChannel.Members, users, mChannel, log)
+ log.WriteString(newChannel.DisplayName + "\r\n")
+ addedChannels[sChannel.Id] = mChannel
+ SlackAddPosts(teamId, mChannel, posts[sChannel.Name], users, uploads, botUser)
+ }
+
+ return addedChannels
+}
+
+func SlackConvertUserMentions(users []SlackUser, posts map[string][]SlackPost) map[string][]SlackPost {
+ var regexes = make(map[string]*regexp.Regexp, len(users))
+ for _, user := range users {
+ r, err := regexp.Compile("<@" + user.Id + `(\|` + user.Username + ")?>")
+ if err != nil {
+ l4g.Warn(utils.T("api.slackimport.slack_convert_user_mentions.compile_regexp_failed.warn"), user.Id, user.Username)
+ continue
+ }
+ regexes["@"+user.Username] = r
+ }
+
+ // Special cases.
+ regexes["@here"], _ = regexp.Compile(`<!here\|@here>`)
+ regexes["@channel"], _ = regexp.Compile("<!channel>")
+ regexes["@all"], _ = regexp.Compile("<!everyone>")
+
+ for channelName, channelPosts := range posts {
+ for postIdx, post := range channelPosts {
+ for mention, r := range regexes {
+ post.Text = r.ReplaceAllString(post.Text, mention)
+ posts[channelName][postIdx] = post
+ }
+ }
+ }
+
+ return posts
+}
+
+func SlackConvertChannelMentions(channels []SlackChannel, posts map[string][]SlackPost) map[string][]SlackPost {
+ var regexes = make(map[string]*regexp.Regexp, len(channels))
+ for _, channel := range channels {
+ r, err := regexp.Compile("<#" + channel.Id + `(\|` + channel.Name + ")?>")
+ if err != nil {
+ l4g.Warn(utils.T("api.slackimport.slack_convert_channel_mentions.compile_regexp_failed.warn"), channel.Id, channel.Name)
+ continue
+ }
+ regexes["~"+channel.Name] = r
+ }
+
+ for channelName, channelPosts := range posts {
+ for postIdx, post := range channelPosts {
+ for channelReplace, r := range regexes {
+ post.Text = r.ReplaceAllString(post.Text, channelReplace)
+ posts[channelName][postIdx] = post
+ }
+ }
+ }
+
+ return posts
+}
+
+func SlackConvertPostsMarkup(posts map[string][]SlackPost) map[string][]SlackPost {
+ // Convert URLs in Slack's format to Markdown format.
+ regex := regexp.MustCompile(`<([^|<>]+)\|([^|<>]+)>`)
+
+ for channelName, channelPosts := range posts {
+ for postIdx, post := range channelPosts {
+ posts[channelName][postIdx].Text = regex.ReplaceAllString(post.Text, "[$2]($1)")
+ }
+ }
+
+ return posts
+}
+
+func SlackImport(fileData multipart.File, fileSize int64, teamID string) (*model.AppError, *bytes.Buffer) {
+ // Create log file
+ log := bytes.NewBufferString(utils.T("api.slackimport.slack_import.log"))
+
+ zipreader, err := zip.NewReader(fileData, fileSize)
+ if err != nil || zipreader.File == nil {
+ log.WriteString(utils.T("api.slackimport.slack_import.zip.app_error"))
+ return model.NewLocAppError("SlackImport", "api.slackimport.slack_import.zip.app_error", nil, err.Error()), log
+ }
+
+ var channels []SlackChannel
+ var users []SlackUser
+ posts := make(map[string][]SlackPost)
+ uploads := make(map[string]*zip.File)
+ for _, file := range zipreader.File {
+ reader, err := file.Open()
+ if err != nil {
+ log.WriteString(utils.T("api.slackimport.slack_import.open.app_error", map[string]interface{}{"Filename": file.Name}))
+ return model.NewLocAppError("SlackImport", "api.slackimport.slack_import.open.app_error", map[string]interface{}{"Filename": file.Name}, err.Error()), log
+ }
+ if file.Name == "channels.json" {
+ channels, _ = SlackParseChannels(reader)
+ } else if file.Name == "users.json" {
+ users, _ = SlackParseUsers(reader)
+ } else {
+ spl := strings.Split(file.Name, "/")
+ if len(spl) == 2 && strings.HasSuffix(spl[1], ".json") {
+ newposts, _ := SlackParsePosts(reader)
+ channel := spl[0]
+ if _, ok := posts[channel]; ok == false {
+ posts[channel] = newposts
+ } else {
+ posts[channel] = append(posts[channel], newposts...)
+ }
+ } else if len(spl) == 3 && spl[0] == "__uploads" {
+ uploads[spl[1]] = file
+ }
+ }
+ }
+
+ posts = SlackConvertUserMentions(users, posts)
+ posts = SlackConvertChannelMentions(channels, posts)
+ posts = SlackConvertPostsMarkup(posts)
+
+ addedUsers := SlackAddUsers(teamID, users, log)
+ botUser := SlackAddBotUser(teamID, log)
+
+ SlackAddChannels(teamID, channels, posts, addedUsers, uploads, botUser, log)
+
+ if botUser != nil {
+ deactivateSlackBotUser(botUser)
+ }
+
+ InvalidateAllCaches()
+
+ log.WriteString(utils.T("api.slackimport.slack_import.notes"))
+ log.WriteString("=======\r\n\r\n")
+
+ log.WriteString(utils.T("api.slackimport.slack_import.note1"))
+ log.WriteString(utils.T("api.slackimport.slack_import.note2"))
+ log.WriteString(utils.T("api.slackimport.slack_import.note3"))
+
+ return nil, log
+}
diff --git a/app/slackimport_test.go b/app/slackimport_test.go
new file mode 100644
index 000000000..3389c5217
--- /dev/null
+++ b/app/slackimport_test.go
@@ -0,0 +1,240 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package app
+
+import (
+ "github.com/mattermost/platform/model"
+ "os"
+ "strings"
+ "testing"
+)
+
+func TestSlackConvertTimeStamp(t *testing.T) {
+
+ testTimeStamp := "1469785419.000033"
+
+ result := SlackConvertTimeStamp(testTimeStamp)
+
+ if result != 1469785419000 {
+ t.Fatalf("Unexpected timestamp value %v returned.", result)
+ }
+}
+
+func TestSlackConvertChannelName(t *testing.T) {
+ var testData = []struct {
+ input string
+ output string
+ }{
+ {"test-channel", "test-channel"},
+ {"_test_channel_", "test_channel"},
+ {"__test", "test"},
+ {"-t", "slack-channel-t"},
+ {"a", "slack-channel-a"},
+ }
+
+ for _, td := range testData {
+ if td.output != SlackConvertChannelName(td.input) {
+ t.Fatalf("Did not convert channel name correctly: %v", td.input)
+ }
+ }
+}
+
+func TestSlackConvertUserMentions(t *testing.T) {
+ users := []SlackUser{
+ {Id: "U00000A0A", Username: "firstuser"},
+ {Id: "U00000B1B", Username: "seconduser"},
+ }
+
+ posts := map[string][]SlackPost{
+ "test-channel": {
+ {
+ Text: "<!channel>: Hi guys.",
+ },
+ {
+ Text: "Calling <!here|@here>.",
+ },
+ {
+ Text: "Yo <!everyone>.",
+ },
+ {
+ Text: "Regular user test <@U00000B1B|seconduser> and <@U00000A0A>.",
+ },
+ },
+ }
+
+ expectedPosts := map[string][]SlackPost{
+ "test-channel": {
+ {
+ Text: "@channel: Hi guys.",
+ },
+ {
+ Text: "Calling @here.",
+ },
+ {
+ Text: "Yo @all.",
+ },
+ {
+ Text: "Regular user test @seconduser and @firstuser.",
+ },
+ },
+ }
+
+ convertedPosts := SlackConvertUserMentions(users, posts)
+
+ for channelName, channelPosts := range convertedPosts {
+ for postIdx, post := range channelPosts {
+ if post.Text != expectedPosts[channelName][postIdx].Text {
+ t.Fatalf("Converted post text not as expected: %v", post.Text)
+ }
+ }
+ }
+}
+
+func TestSlackConvertChannelMentions(t *testing.T) {
+ channels := []SlackChannel{
+ {Id: "C000AA00A", Name: "one"},
+ {Id: "C000BB11B", Name: "two"},
+ }
+
+ posts := map[string][]SlackPost{
+ "test-channel": {
+ {
+ Text: "Go to <#C000AA00A>.",
+ },
+ {
+ User: "U00000A0A",
+ Text: "Try <#C000BB11B|two> for this.",
+ },
+ },
+ }
+
+ expectedPosts := map[string][]SlackPost{
+ "test-channel": {
+ {
+ Text: "Go to ~one.",
+ },
+ {
+ Text: "Try ~two for this.",
+ },
+ },
+ }
+
+ convertedPosts := SlackConvertChannelMentions(channels, posts)
+
+ for channelName, channelPosts := range convertedPosts {
+ for postIdx, post := range channelPosts {
+ if post.Text != expectedPosts[channelName][postIdx].Text {
+ t.Fatalf("Converted post text not as expected: %v", post.Text)
+ }
+ }
+ }
+
+}
+
+func TestSlackParseChannels(t *testing.T) {
+ file, err := os.Open("../tests/slack-import-test-channels.json")
+ if err != nil {
+ t.Fatalf("Failed to open data file: %v", err)
+ }
+
+ channels, err := SlackParseChannels(file)
+ if err != nil {
+ t.Fatalf("Error occurred parsing channels: %v", err)
+ }
+
+ if len(channels) != 6 {
+ t.Fatalf("Unexpected number of channels: %v", len(channels))
+ }
+}
+
+func TestSlackParseUsers(t *testing.T) {
+ file, err := os.Open("../tests/slack-import-test-users.json")
+ if err != nil {
+ t.Fatalf("Failed to open data file: %v", err)
+ }
+
+ users, err := SlackParseUsers(file)
+ if err != nil {
+ t.Fatalf("Error occurred parsing users: %v", err)
+ }
+
+ if len(users) != 11 {
+ t.Fatalf("Unexpected number of users: %v", len(users))
+ }
+}
+
+func TestSlackParsePosts(t *testing.T) {
+ file, err := os.Open("../tests/slack-import-test-posts.json")
+ if err != nil {
+ t.Fatalf("Failed to open data file: %v", err)
+ }
+
+ posts, err := SlackParsePosts(file)
+ if err != nil {
+ t.Fatalf("Error occurred parsing posts: %v", err)
+ }
+
+ if len(posts) != 8 {
+ t.Fatalf("Unexpected number of posts: %v", len(posts))
+ }
+}
+
+func TestSlackSanitiseChannelProperties(t *testing.T) {
+ c1 := model.Channel{
+ DisplayName: "display-name",
+ Name: "name",
+ Purpose: "The channel purpose",
+ Header: "The channel header",
+ }
+
+ c1s := SlackSanitiseChannelProperties(c1)
+ if c1.DisplayName != c1s.DisplayName || c1.Name != c1s.Name || c1.Purpose != c1s.Purpose || c1.Header != c1s.Header {
+ t.Fatalf("Unexpected alterations to the channel properties.")
+ }
+
+ c2 := model.Channel{
+ DisplayName: strings.Repeat("abcdefghij", 7),
+ Name: strings.Repeat("abcdefghij", 7),
+ Purpose: strings.Repeat("0123456789", 30),
+ Header: strings.Repeat("0123456789", 120),
+ }
+
+ c2s := SlackSanitiseChannelProperties(c2)
+ if c2s.DisplayName != strings.Repeat("abcdefghij", 6)+"abcd" {
+ t.Fatalf("Unexpected alterations to the channel properties: %v", c2s.DisplayName)
+ }
+
+ if c2s.Name != strings.Repeat("abcdefghij", 6)+"abcd" {
+ t.Fatalf("Unexpected alterations to the channel properties: %v", c2s.Name)
+ }
+
+ if c2s.Purpose != strings.Repeat("0123456789", 25) {
+ t.Fatalf("Unexpected alterations to the channel properties: %v", c2s.Purpose)
+ }
+
+ if c2s.Header != strings.Repeat("0123456789", 102)+"0123" {
+ t.Fatalf("Unexpected alterations to the channel properties: %v", c2s.Header)
+ }
+}
+
+func TestSlackConvertPostsMarkup(t *testing.T) {
+ input := make(map[string][]SlackPost)
+ input["test"] = []SlackPost{
+ {
+ Text: "This message contains a link to <https://google.com|Google>.",
+ },
+ {
+ Text: "This message contains a mailto link to <mailto:me@example.com|me@example.com> in it.",
+ },
+ }
+
+ output := SlackConvertPostsMarkup(input)
+
+ if output["test"][0].Text != "This message contains a link to [Google](https://google.com)." {
+ t.Fatalf("Unexpected message after markup translation: %v", output["test"][0].Text)
+ }
+ if output["test"][1].Text != "This message contains a mailto link to [me@example.com](mailto:me@example.com) in it." {
+ t.Fatalf("Unexpected message after markup translation: %v", output["test"][0].Text)
+ }
+}
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..aabdc0bfd
--- /dev/null
+++ b/app/team.go
@@ -0,0 +1,563 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package app
+
+import (
+ "fmt"
+ "net/http"
+ "strconv"
+ "strings"
+
+ 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 CreateTeamWithUser(team *model.Team, userId string) (*model.Team, *model.AppError) {
+ var user *model.User
+ var err *model.AppError
+ if user, err = GetUser(userId); err != nil {
+ return nil, err
+ } else {
+ team.Email = user.Email
+ }
+
+ if !isTeamEmailAllowed(user) {
+ return nil, model.NewLocAppError("isTeamEmailAllowed", "api.team.is_team_creation_allowed.domain.app_error", nil, "")
+ }
+
+ var rteam *model.Team
+ if rteam, err = CreateTeam(team); err != nil {
+ return nil, err
+ }
+
+ if err = JoinUserToTeam(rteam, user); err != nil {
+ return nil, err
+ }
+
+ return rteam, nil
+}
+
+func isTeamEmailAllowed(user *model.User) bool {
+ email := strings.ToLower(user.Email)
+
+ if len(user.AuthService) > 0 && len(*user.AuthData) > 0 {
+ return true
+ }
+
+ // commas and @ signs are optional
+ // can be in the form of "@corp.mattermost.com, mattermost.com mattermost.org" -> corp.mattermost.com mattermost.com mattermost.org
+ domains := strings.Fields(strings.TrimSpace(strings.ToLower(strings.Replace(strings.Replace(utils.Cfg.TeamSettings.RestrictCreationToDomains, "@", " ", -1), ",", " ", -1))))
+
+ matched := false
+ for _, d := range domains {
+ if strings.HasSuffix(email, "@"+d) {
+ matched = true
+ break
+ }
+ }
+
+ if len(utils.Cfg.TeamSettings.RestrictCreationToDomains) > 0 && !matched {
+ return false
+ }
+
+ return true
+}
+
+func UpdateTeam(team *model.Team) (*model.Team, *model.AppError) {
+ var oldTeam *model.Team
+ var err *model.AppError
+ if oldTeam, err = GetTeam(team.Id); err != nil {
+ return nil, err
+ }
+
+ oldTeam.DisplayName = team.DisplayName
+ oldTeam.Description = team.Description
+ oldTeam.InviteId = team.InviteId
+ oldTeam.AllowOpenInvite = team.AllowOpenInvite
+ oldTeam.CompanyName = team.CompanyName
+ oldTeam.AllowedDomains = team.AllowedDomains
+
+ if result := <-Srv.Store.Team().Update(oldTeam); result.Err != nil {
+ return nil, result.Err
+ }
+
+ oldTeam.Sanitize()
+
+ message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_UPDATE_TEAM, "", "", "", nil)
+ message.Add("team", oldTeam.ToJson())
+ go Publish(message)
+
+ return oldTeam, nil
+}
+
+func UpdateTeamMemberRoles(teamId string, userId string, newRoles string) (*model.TeamMember, *model.AppError) {
+ var member *model.TeamMember
+ if result := <-Srv.Store.Team().GetTeamsForUser(userId); result.Err != nil {
+ return nil, result.Err
+ } else {
+ members := result.Data.([]*model.TeamMember)
+ for _, m := range members {
+ if m.TeamId == teamId {
+ member = m
+ }
+ }
+ }
+
+ if member == nil {
+ err := model.NewLocAppError("UpdateTeamMemberRoles", "api.team.update_member_roles.not_a_member", nil, "userId="+userId+" teamId="+teamId)
+ err.StatusCode = http.StatusBadRequest
+ return nil, err
+ }
+
+ member.Roles = newRoles
+
+ if result := <-Srv.Store.Team().UpdateMember(member); result.Err != nil {
+ return nil, result.Err
+ }
+
+ ClearSessionCacheForUser(userId)
+
+ return member, nil
+}
+
+func AddUserToTeam(teamId string, userId string) (*model.Team, *model.AppError) {
+ tchan := Srv.Store.Team().Get(teamId)
+ uchan := Srv.Store.User().Get(userId)
+
+ var team *model.Team
+ if result := <-tchan; result.Err != nil {
+ return nil, result.Err
+ } else {
+ team = result.Data.(*model.Team)
+ }
+
+ var user *model.User
+ if result := <-uchan; result.Err != nil {
+ return nil, result.Err
+ } else {
+ user = result.Data.(*model.User)
+ }
+
+ if err := JoinUserToTeam(team, user); err != nil {
+ return nil, err
+ }
+
+ return team, nil
+}
+
+func AddUserToTeamByTeamId(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 AddUserToTeamByHash(userId string, hash string, data string) (*model.Team, *model.AppError) {
+ props := model.MapFromJson(strings.NewReader(data))
+
+ if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.InviteSalt)) {
+ return nil, model.NewLocAppError("JoinUserToTeamByHash", "api.user.create_user.signup_link_invalid.app_error", nil, "")
+ }
+
+ t, timeErr := strconv.ParseInt(props["time"], 10, 64)
+ if timeErr != nil || model.GetMillis()-t > 1000*60*60*48 { // 48 hours
+ return nil, model.NewLocAppError("JoinUserToTeamByHash", "api.user.create_user.signup_link_expired.app_error", nil, "")
+ }
+
+ tchan := Srv.Store.Team().Get(props["id"])
+ uchan := Srv.Store.User().Get(userId)
+
+ var team *model.Team
+ if result := <-tchan; result.Err != nil {
+ return nil, result.Err
+ } else {
+ team = result.Data.(*model.Team)
+ }
+
+ var user *model.User
+ if result := <-uchan; result.Err != nil {
+ return nil, result.Err
+ } else {
+ user = result.Data.(*model.User)
+ }
+
+ if err := JoinUserToTeam(team, user); err != nil {
+ return nil, err
+ }
+
+ return team, nil
+}
+
+func AddUserToTeamByInviteId(inviteId string, userId string) (*model.Team, *model.AppError) {
+ tchan := Srv.Store.Team().GetByInviteId(inviteId)
+ uchan := Srv.Store.User().Get(userId)
+
+ var team *model.Team
+ if result := <-tchan; result.Err != nil {
+ return nil, result.Err
+ } else {
+ team = result.Data.(*model.Team)
+ }
+
+ var user *model.User
+ if result := <-uchan; result.Err != nil {
+ return nil, result.Err
+ } else {
+ user = result.Data.(*model.User)
+ }
+
+ if err := JoinUserToTeam(team, user); err != nil {
+ return nil, err
+ }
+
+ return team, nil
+}
+
+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)
+ }
+
+ ClearSessionCacheForUser(user.Id)
+ InvalidateCacheForUser(user.Id)
+
+ return nil
+}
+
+func GetTeam(teamId string) (*model.Team, *model.AppError) {
+ if result := <-Srv.Store.Team().Get(teamId); result.Err != nil {
+ return nil, result.Err
+ } else {
+ return result.Data.(*model.Team), nil
+ }
+}
+
+func GetTeamByName(name string) (*model.Team, *model.AppError) {
+ if result := <-Srv.Store.Team().GetByName(name); result.Err != nil {
+ return nil, result.Err
+ } else {
+ return result.Data.(*model.Team), nil
+ }
+}
+
+func GetTeamByInviteId(inviteId string) (*model.Team, *model.AppError) {
+ if result := <-Srv.Store.Team().GetByInviteId(inviteId); result.Err != nil {
+ return nil, result.Err
+ } else {
+ return result.Data.(*model.Team), nil
+ }
+}
+
+func GetAllTeams() ([]*model.Team, *model.AppError) {
+ if result := <-Srv.Store.Team().GetAll(); result.Err != nil {
+ return nil, result.Err
+ } else {
+ return result.Data.([]*model.Team), nil
+ }
+}
+
+func GetAllOpenTeams() ([]*model.Team, *model.AppError) {
+ if result := <-Srv.Store.Team().GetAllTeamListing(); result.Err != nil {
+ return nil, result.Err
+ } else {
+ return result.Data.([]*model.Team), nil
+ }
+}
+
+func GetTeamsForUser(userId string) ([]*model.Team, *model.AppError) {
+ if result := <-Srv.Store.Team().GetTeamsByUserId(userId); result.Err != nil {
+ return nil, result.Err
+ } else {
+ return result.Data.([]*model.Team), nil
+ }
+}
+
+func GetTeamMember(teamId, userId string) (*model.TeamMember, *model.AppError) {
+ if result := <-Srv.Store.Team().GetMember(teamId, userId); result.Err != nil {
+ return nil, result.Err
+ } else {
+ return result.Data.(*model.TeamMember), nil
+ }
+}
+
+func GetTeamMembersForUser(userId string) ([]*model.TeamMember, *model.AppError) {
+ if result := <-Srv.Store.Team().GetTeamsForUser(userId); result.Err != nil {
+ return nil, result.Err
+ } else {
+ return result.Data.([]*model.TeamMember), nil
+ }
+}
+
+func GetTeamMembers(teamId string, offset int, limit int) ([]*model.TeamMember, *model.AppError) {
+ if result := <-Srv.Store.Team().GetMembers(teamId, offset, limit); result.Err != nil {
+ return nil, result.Err
+ } else {
+ return result.Data.([]*model.TeamMember), nil
+ }
+}
+
+func GetTeamMembersByIds(teamId string, userIds []string) ([]*model.TeamMember, *model.AppError) {
+ if result := <-Srv.Store.Team().GetMembersByIds(teamId, userIds); result.Err != nil {
+ return nil, result.Err
+ } else {
+ return result.Data.([]*model.TeamMember), nil
+ }
+}
+
+func RemoveUserFromTeam(teamId string, userId string) *model.AppError {
+ tchan := Srv.Store.Team().Get(teamId)
+ uchan := Srv.Store.User().Get(userId)
+
+ var team *model.Team
+ if result := <-tchan; result.Err != nil {
+ return result.Err
+ } else {
+ team = result.Data.(*model.Team)
+ }
+
+ var user *model.User
+ if result := <-uchan; result.Err != nil {
+ return result.Err
+ } else {
+ user = result.Data.(*model.User)
+ }
+
+ if err := LeaveTeam(team, user); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func LeaveTeam(team *model.Team, user *model.User) *model.AppError {
+ var teamMember *model.TeamMember
+ var err *model.AppError
+
+ if teamMember, err = GetTeamMember(team.Id, user.Id); err != nil {
+ return model.NewLocAppError("LeaveTeam", "api.team.remove_user_from_team.missing.app_error", nil, err.Error())
+ }
+
+ var channelList *model.ChannelList
+
+ if result := <-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 {
+ return result.Err
+ }
+
+ } else {
+ channelList = result.Data.(*model.ChannelList)
+ }
+
+ 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 {
+ return result.Err
+ }
+ }
+ }
+
+ // Send the websocket message before we actually do the remove so the user being removed gets it.
+ 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)
+
+ teamMember.Roles = ""
+ teamMember.DeleteAt = model.GetMillis()
+
+ if result := <-Srv.Store.Team().UpdateMember(teamMember); result.Err != nil {
+ return result.Err
+ }
+
+ if uua := <-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 {
+ return result.Err
+ }
+
+ ClearSessionCacheForUser(user.Id)
+ InvalidateCacheForUser(user.Id)
+
+ return nil
+}
+
+func InviteNewUsersToTeam(emailList []string, teamId, senderId, siteURL string) *model.AppError {
+ if len(emailList) == 0 {
+ err := model.NewLocAppError("InviteNewUsersToTeam", "api.team.invite_members.no_one.app_error", nil, "")
+ err.StatusCode = http.StatusBadRequest
+ return err
+ }
+
+ tchan := Srv.Store.Team().Get(teamId)
+ uchan := Srv.Store.User().Get(senderId)
+
+ var team *model.Team
+ if result := <-tchan; result.Err != nil {
+ return result.Err
+ } else {
+ team = result.Data.(*model.Team)
+ }
+
+ var user *model.User
+ if result := <-uchan; result.Err != nil {
+ return result.Err
+ } else {
+ user = result.Data.(*model.User)
+ }
+
+ SendInviteEmails(team, user.GetDisplayName(), emailList, siteURL)
+
+ return nil
+}
+
+func FindTeamByName(name string) bool {
+ if result := <-Srv.Store.Team().GetByName(name); result.Err != nil {
+ return false
+ } else {
+ return true
+ }
+}
+
+func GetTeamsUnreadForUser(teamId string, userId string) ([]*model.TeamUnread, *model.AppError) {
+ if result := <-Srv.Store.Team().GetTeamsUnreadForUser(teamId, userId); result.Err != nil {
+ return nil, result.Err
+ } else {
+ data := result.Data.([]*model.ChannelUnread)
+ var members []*model.TeamUnread
+ membersMap := make(map[string]*model.TeamUnread)
+
+ unreads := func(cu *model.ChannelUnread, tu *model.TeamUnread) *model.TeamUnread {
+ tu.MentionCount += cu.MentionCount
+
+ if cu.NotifyProps["mark_unread"] != model.CHANNEL_MARK_UNREAD_MENTION {
+ tu.MsgCount += (cu.TotalMsgCount - cu.MsgCount)
+ }
+
+ return tu
+ }
+
+ for i := range data {
+ id := data[i].TeamId
+ if mu, ok := membersMap[id]; ok {
+ membersMap[id] = unreads(data[i], mu)
+ } else {
+ membersMap[id] = unreads(data[i], &model.TeamUnread{
+ MsgCount: 0,
+ MentionCount: 0,
+ TeamId: id,
+ })
+ }
+ }
+
+ for _, val := range membersMap {
+ members = append(members, val)
+ }
+
+ return members, nil
+ }
+}
+
+func PermanentDeleteTeam(team *model.Team) *model.AppError {
+ team.DeleteAt = model.GetMillis()
+ if result := <-Srv.Store.Team().Update(team); result.Err != nil {
+ return result.Err
+ }
+
+ if result := <-Srv.Store.Channel().PermanentDeleteByTeam(team.Id); result.Err != nil {
+ return result.Err
+ }
+
+ if result := <-Srv.Store.Team().RemoveAllMembersByTeam(team.Id); result.Err != nil {
+ return result.Err
+ }
+
+ if result := <-Srv.Store.Team().PermanentDelete(team.Id); result.Err != nil {
+ return result.Err
+ }
+
+ return nil
+}
+
+func GetTeamStats(teamId string) (*model.TeamStats, *model.AppError) {
+ tchan := Srv.Store.Team().GetTotalMemberCount(teamId)
+ achan := Srv.Store.Team().GetActiveMemberCount(teamId)
+
+ stats := &model.TeamStats{}
+ stats.TeamId = teamId
+
+ if result := <-tchan; result.Err != nil {
+ return nil, result.Err
+ } else {
+ stats.TotalMemberCount = result.Data.(int64)
+ }
+
+ if result := <-achan; result.Err != nil {
+ return nil, result.Err
+ } else {
+ stats.ActiveMemberCount = result.Data.(int64)
+ }
+
+ return stats, nil
+}
diff --git a/app/user.go b/app/user.go
new file mode 100644
index 000000000..8fbed301d
--- /dev/null
+++ b/app/user.go
@@ -0,0 +1,982 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package app
+
+import (
+ "bytes"
+ "fmt"
+ "hash/fnv"
+ "html/template"
+ "image"
+ "image/color"
+ "image/draw"
+ _ "image/gif"
+ _ "image/jpeg"
+ "image/png"
+ "io"
+ "io/ioutil"
+ "mime/multipart"
+ "net/http"
+ "net/url"
+ "strconv"
+ "strings"
+
+ l4g "github.com/alecthomas/log4go"
+ "github.com/disintegration/imaging"
+ "github.com/golang/freetype"
+ "github.com/mattermost/platform/einterfaces"
+ "github.com/mattermost/platform/model"
+ "github.com/mattermost/platform/utils"
+)
+
+func CreateUserWithHash(user *model.User, hash string, data string) (*model.User, *model.AppError) {
+ props := model.MapFromJson(strings.NewReader(data))
+
+ if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.InviteSalt)) {
+ return nil, model.NewLocAppError("CreateUserWithHash", "api.user.create_user.signup_link_invalid.app_error", nil, "")
+ }
+
+ if t, err := strconv.ParseInt(props["time"], 10, 64); err != nil || model.GetMillis()-t > 1000*60*60*48 { // 48 hours
+ return nil, model.NewLocAppError("CreateUserWithHash", "api.user.create_user.signup_link_expired.app_error", nil, "")
+ }
+
+ teamId := props["id"]
+
+ var team *model.Team
+ if result := <-Srv.Store.Team().Get(teamId); result.Err != nil {
+ return nil, result.Err
+ } else {
+ team = result.Data.(*model.Team)
+ }
+
+ user.Email = props["email"]
+ user.EmailVerified = true
+
+ var ruser *model.User
+ var err *model.AppError
+ if ruser, err = CreateUser(user); err != nil {
+ return nil, err
+ }
+
+ if err := JoinUserToTeam(team, ruser); err != nil {
+ return nil, err
+ }
+
+ AddDirectChannels(team.Id, ruser)
+
+ return ruser, nil
+}
+
+func CreateUserWithInviteId(user *model.User, inviteId string, siteURL string) (*model.User, *model.AppError) {
+ var team *model.Team
+ if result := <-Srv.Store.Team().GetByInviteId(inviteId); result.Err != nil {
+ return nil, result.Err
+ } else {
+ team = result.Data.(*model.Team)
+ }
+
+ var ruser *model.User
+ var err *model.AppError
+ if ruser, err = CreateUser(user); err != nil {
+ return nil, err
+ }
+
+ if err := JoinUserToTeam(team, ruser); err != nil {
+ return nil, err
+ }
+
+ AddDirectChannels(team.Id, ruser)
+
+ if err := SendWelcomeEmail(ruser.Id, ruser.Email, ruser.EmailVerified, ruser.Locale, siteURL); err != nil {
+ l4g.Error(err.Error())
+ }
+
+ return ruser, nil
+}
+
+func IsFirstUserAccount() bool {
+ if SessionCacheLength() == 0 {
+ if cr := <-Srv.Store.User().GetTotalUsersCount(); cr.Err != nil {
+ l4g.Error(cr.Err)
+ return false
+ } else {
+ count := cr.Data.(int64)
+ if count <= 0 {
+ return true
+ }
+ }
+ }
+
+ return false
+}
+
+func CreateUser(user *model.User) (*model.User, *model.AppError) {
+ if !user.IsSSOUser() && !CheckUserDomain(user, utils.Cfg.TeamSettings.RestrictCreationToDomains) {
+ return nil, model.NewLocAppError("CreateUser", "api.user.create_user.accepted_domain.app_error", nil, "")
+ }
+
+ 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 err := VerifyUserEmail(ruser.Id); err != nil {
+ l4g.Error(utils.T("api.user.create_user.verified.error"), 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(service string, userData io.Reader, teamId string) (*model.User, *model.AppError) {
+ var user *model.User
+ provider := einterfaces.GetOauthProvider(service)
+ if provider == nil {
+ return nil, model.NewLocAppError("CreateOAuthUser", "api.user.create_oauth_user.not_available.app_error", map[string]interface{}{"Service": strings.Title(service)}, "")
+ } else {
+ user = provider.GetUserFromJson(userData)
+ }
+
+ if user == nil {
+ return nil, model.NewLocAppError("CreateOAuthUser", "api.user.create_oauth_user.create.app_error", map[string]interface{}{"Service": service}, "")
+ }
+
+ suchan := Srv.Store.User().GetByAuth(user.AuthData, service)
+ euchan := Srv.Store.User().GetByEmail(user.Email)
+
+ found := true
+ count := 0
+ for found {
+ if found = IsUsernameTaken(user.Username); found {
+ user.Username = user.Username + strconv.Itoa(count)
+ count += 1
+ }
+ }
+
+ if result := <-suchan; result.Err == nil {
+ return nil, model.NewLocAppError("CreateOAuthUser", "api.user.create_oauth_user.already_used.app_error", map[string]interface{}{"Service": service}, "email="+user.Email)
+ }
+
+ if result := <-euchan; result.Err == nil {
+ authService := result.Data.(*model.User).AuthService
+ if authService == "" {
+ return nil, model.NewLocAppError("CreateOAuthUser", "api.user.create_oauth_user.already_attached.app_error",
+ map[string]interface{}{"Service": service, "Auth": model.USER_AUTH_SERVICE_EMAIL}, "email="+user.Email)
+ } else {
+ return nil, model.NewLocAppError("CreateOAuthUser", "api.user.create_oauth_user.already_attached.app_error",
+ map[string]interface{}{"Service": service, "Auth": authService}, "email="+user.Email)
+ }
+ }
+
+ user.EmailVerified = true
+
+ ruser, err := CreateUser(user)
+ if err != nil {
+ return nil, err
+ }
+
+ if len(teamId) > 0 {
+ err = AddUserToTeamByTeamId(teamId, user)
+ if err != nil {
+ return nil, err
+ }
+
+ err = AddDirectChannels(teamId, user)
+ if err != nil {
+ l4g.Error(err.Error())
+ }
+ }
+
+ return ruser, nil
+}
+
+// Check that a user's email domain matches a list of space-delimited domains as a string.
+func CheckUserDomain(user *model.User, domains string) bool {
+ if len(domains) == 0 {
+ return true
+ }
+
+ domainArray := strings.Fields(strings.TrimSpace(strings.ToLower(strings.Replace(strings.Replace(domains, "@", " ", -1), ",", " ", -1))))
+
+ matched := false
+ for _, d := range domainArray {
+ if strings.HasSuffix(strings.ToLower(user.Email), "@"+d) {
+ matched = true
+ break
+ }
+ }
+
+ return matched
+}
+
+// Check if the username is already used by another user. Return false if the username is invalid.
+func IsUsernameTaken(name string) bool {
+
+ if !model.IsValidUsername(name) {
+ return false
+ }
+
+ if result := <-Srv.Store.User().GetByUsername(name); result.Err != nil {
+ return false
+ } else {
+ return true
+ }
+
+ return false
+}
+
+func GetUser(userId string) (*model.User, *model.AppError) {
+ if result := <-Srv.Store.User().Get(userId); result.Err != nil {
+ return nil, result.Err
+ } else {
+ return result.Data.(*model.User), nil
+ }
+}
+
+func GetUserByUsername(username string) (*model.User, *model.AppError) {
+ if result := <-Srv.Store.User().GetByUsername(username); result.Err != nil {
+ return nil, result.Err
+ } else {
+ return result.Data.(*model.User), nil
+ }
+}
+
+func GetUserByEmail(email string) (*model.User, *model.AppError) {
+ if result := <-Srv.Store.User().GetByEmail(email); result.Err != nil {
+ return nil, result.Err
+ } else {
+ return result.Data.(*model.User), nil
+ }
+}
+
+func GetUserByAuth(authData *string, authService string) (*model.User, *model.AppError) {
+ if result := <-Srv.Store.User().GetByAuth(authData, authService); result.Err != nil {
+ return nil, result.Err
+ } else {
+ return result.Data.(*model.User), nil
+ }
+}
+
+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(
+ loginId,
+ *utils.Cfg.EmailSettings.EnableSignInWithUsername && !onlyLdap,
+ *utils.Cfg.EmailSettings.EnableSignInWithEmail && !onlyLdap,
+ ldapAvailable,
+ ); result.Err != nil && result.Err.Id == "store.sql_user.get_for_login.multiple_users" {
+ // don't fall back to LDAP in this case since we already know there's an LDAP user, but that it shouldn't work
+ result.Err.StatusCode = http.StatusBadRequest
+ return nil, result.Err
+ } else if result.Err != nil {
+ if !ldapAvailable {
+ // failed to find user and no LDAP server to fall back on
+ result.Err.StatusCode = http.StatusBadRequest
+ return nil, result.Err
+ }
+
+ // fall back to LDAP server to see if we can find a user
+ if ldapUser, ldapErr := einterfaces.GetLdapInterface().GetUser(loginId); ldapErr != nil {
+ ldapErr.StatusCode = http.StatusBadRequest
+ return nil, ldapErr
+ } else {
+ return ldapUser, nil
+ }
+ } else {
+ return result.Data.(*model.User), nil
+ }
+}
+
+func GetUsers(offset int, limit int) (map[string]*model.User, *model.AppError) {
+ if result := <-Srv.Store.User().GetAllProfiles(offset, limit); result.Err != nil {
+ return nil, result.Err
+ } else {
+ return result.Data.(map[string]*model.User), nil
+ }
+}
+
+func GetUsersEtag() string {
+ return (<-Srv.Store.User().GetEtagForAllProfiles()).Data.(string)
+}
+
+func GetUsersInTeam(teamId string, offset int, limit int) (map[string]*model.User, *model.AppError) {
+ if result := <-Srv.Store.User().GetProfiles(teamId, offset, limit); result.Err != nil {
+ return nil, result.Err
+ } else {
+ return result.Data.(map[string]*model.User), nil
+ }
+}
+
+func GetUsersInTeamEtag(teamId string) string {
+ return (<-Srv.Store.User().GetEtagForProfiles(teamId)).Data.(string)
+}
+
+func GetUsersInChannel(channelId string, offset int, limit int) (map[string]*model.User, *model.AppError) {
+ if result := <-Srv.Store.User().GetProfilesInChannel(channelId, offset, limit, false); result.Err != nil {
+ return nil, result.Err
+ } else {
+ return result.Data.(map[string]*model.User), nil
+ }
+}
+
+func GetUsersNotInChannel(teamId string, channelId string, offset int, limit int) (map[string]*model.User, *model.AppError) {
+ if result := <-Srv.Store.User().GetProfilesNotInChannel(teamId, channelId, offset, limit); result.Err != nil {
+ return nil, result.Err
+ } else {
+ return result.Data.(map[string]*model.User), nil
+ }
+}
+
+func GetUsersByIds(userIds []string) (map[string]*model.User, *model.AppError) {
+ if result := <-Srv.Store.User().GetProfileByIds(userIds, true); result.Err != nil {
+ return nil, result.Err
+ } else {
+ return result.Data.(map[string]*model.User), nil
+ }
+}
+
+func ActivateMfa(userId, token string) *model.AppError {
+ mfaInterface := einterfaces.GetMfaInterface()
+ if mfaInterface == nil {
+ err := model.NewLocAppError("ActivateMfa", "api.user.update_mfa.not_available.app_error", nil, "")
+ err.StatusCode = http.StatusNotImplemented
+ return err
+ }
+
+ var user *model.User
+ if result := <-Srv.Store.User().Get(userId); result.Err != nil {
+ return result.Err
+ } else {
+ user = result.Data.(*model.User)
+ }
+
+ if len(user.AuthService) > 0 && user.AuthService != model.USER_AUTH_SERVICE_LDAP {
+ return model.NewLocAppError("ActivateMfa", "api.user.activate_mfa.email_and_ldap_only.app_error", nil, "")
+ }
+
+ if err := mfaInterface.Activate(user, token); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func DeactivateMfa(userId string) *model.AppError {
+ mfaInterface := einterfaces.GetMfaInterface()
+ if mfaInterface == nil {
+ err := model.NewLocAppError("DeactivateMfa", "api.user.update_mfa.not_available.app_error", nil, "")
+ err.StatusCode = http.StatusNotImplemented
+ return err
+ }
+
+ if err := mfaInterface.Deactivate(userId); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func CreateProfileImage(username string, userId string) ([]byte, *model.AppError) {
+ colors := []color.NRGBA{
+ {197, 8, 126, 255},
+ {227, 207, 18, 255},
+ {28, 181, 105, 255},
+ {35, 188, 224, 255},
+ {116, 49, 196, 255},
+ {197, 8, 126, 255},
+ {197, 19, 19, 255},
+ {250, 134, 6, 255},
+ {227, 207, 18, 255},
+ {123, 201, 71, 255},
+ {28, 181, 105, 255},
+ {35, 188, 224, 255},
+ {116, 49, 196, 255},
+ {197, 8, 126, 255},
+ {197, 19, 19, 255},
+ {250, 134, 6, 255},
+ {227, 207, 18, 255},
+ {123, 201, 71, 255},
+ {28, 181, 105, 255},
+ {35, 188, 224, 255},
+ {116, 49, 196, 255},
+ {197, 8, 126, 255},
+ {197, 19, 19, 255},
+ {250, 134, 6, 255},
+ {227, 207, 18, 255},
+ {123, 201, 71, 255},
+ }
+
+ h := fnv.New32a()
+ h.Write([]byte(userId))
+ seed := h.Sum32()
+
+ initial := string(strings.ToUpper(username)[0])
+
+ fontBytes, err := ioutil.ReadFile(utils.FindDir("fonts") + utils.Cfg.FileSettings.InitialFont)
+ if err != nil {
+ return nil, model.NewLocAppError("CreateProfileImage", "api.user.create_profile_image.default_font.app_error", nil, err.Error())
+ }
+ font, err := freetype.ParseFont(fontBytes)
+ if err != nil {
+ return nil, model.NewLocAppError("CreateProfileImage", "api.user.create_profile_image.default_font.app_error", nil, err.Error())
+ }
+
+ width := int(utils.Cfg.FileSettings.ProfileWidth)
+ height := int(utils.Cfg.FileSettings.ProfileHeight)
+ color := colors[int64(seed)%int64(len(colors))]
+ dstImg := image.NewRGBA(image.Rect(0, 0, width, height))
+ srcImg := image.White
+ draw.Draw(dstImg, dstImg.Bounds(), &image.Uniform{color}, image.ZP, draw.Src)
+ size := float64((width + height) / 4)
+
+ c := freetype.NewContext()
+ c.SetFont(font)
+ c.SetFontSize(size)
+ c.SetClip(dstImg.Bounds())
+ c.SetDst(dstImg)
+ c.SetSrc(srcImg)
+
+ pt := freetype.Pt(width/6, height*2/3)
+ _, err = c.DrawString(initial, pt)
+ if err != nil {
+ return nil, model.NewLocAppError("CreateProfileImage", "api.user.create_profile_image.initial.app_error", nil, err.Error())
+ }
+
+ buf := new(bytes.Buffer)
+
+ if imgErr := png.Encode(buf, dstImg); imgErr != nil {
+ return nil, model.NewLocAppError("CreateProfileImage", "api.user.create_profile_image.encode.app_error", nil, imgErr.Error())
+ } else {
+ return buf.Bytes(), nil
+ }
+}
+
+func GetProfileImage(user *model.User) ([]byte, *model.AppError) {
+ var img []byte
+
+ if len(utils.Cfg.FileSettings.DriverName) == 0 {
+ var err *model.AppError
+ if img, err = CreateProfileImage(user.Username, user.Id); err != nil {
+ return nil, err
+ }
+ } else {
+ path := "users/" + user.Id + "/profile.png"
+
+ if data, err := ReadFile(path); err != nil {
+ if img, err = CreateProfileImage(user.Username, user.Id); err != nil {
+ return nil, err
+ }
+
+ if user.LastPictureUpdate == 0 {
+ if err := WriteFile(img, path); err != nil {
+ return nil, err
+ }
+ }
+
+ } else {
+ img = data
+ }
+ }
+
+ return img, nil
+}
+
+func SetProfileImage(userId string, imageData *multipart.FileHeader) *model.AppError {
+ file, err := imageData.Open()
+ defer file.Close()
+ if err != nil {
+ return model.NewLocAppError("SetProfileImage", "api.user.upload_profile_user.open.app_error", nil, err.Error())
+ }
+
+ // Decode image config first to check dimensions before loading the whole thing into memory later on
+ config, _, err := image.DecodeConfig(file)
+ if err != nil {
+ return model.NewLocAppError("SetProfileImage", "api.user.upload_profile_user.decode_config.app_error", nil, err.Error())
+ } else if config.Width*config.Height > model.MaxImageSize {
+ return model.NewLocAppError("SetProfileImage", "api.user.upload_profile_user.too_large.app_error", nil, err.Error())
+ }
+
+ file.Seek(0, 0)
+
+ // Decode image into Image object
+ img, _, err := image.Decode(file)
+ if err != nil {
+ return model.NewLocAppError("SetProfileImage", "api.user.upload_profile_user.decode.app_error", nil, err.Error())
+ }
+
+ // Scale profile image
+ img = imaging.Resize(img, utils.Cfg.FileSettings.ProfileWidth, utils.Cfg.FileSettings.ProfileHeight, imaging.Lanczos)
+
+ buf := new(bytes.Buffer)
+ err = png.Encode(buf, img)
+ if err != nil {
+ return model.NewLocAppError("SetProfileImage", "api.user.upload_profile_user.encode.app_error", nil, err.Error())
+ }
+
+ path := "users/" + userId + "/profile.png"
+
+ if err := WriteFile(buf.Bytes(), path); err != nil {
+ return model.NewLocAppError("SetProfileImage", "api.user.upload_profile_user.upload_profile.app_error", nil, "")
+ }
+
+ Srv.Store.User().UpdateLastPictureUpdate(userId)
+
+ if user, err := GetUser(userId); err != nil {
+ l4g.Error(utils.T("api.user.get_me.getting.error"), userId)
+ } else {
+ options := utils.Cfg.GetSanitizeOptions()
+ user.SanitizeProfile(options)
+
+ omitUsers := make(map[string]bool, 1)
+ omitUsers[userId] = true
+ message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_USER_UPDATED, "", "", "", omitUsers)
+ message.Add("user", user)
+
+ Publish(message)
+ }
+
+ return nil
+}
+
+func UpdateActiveNoLdap(userId string, active bool) (*model.User, *model.AppError) {
+ var user *model.User
+ var err *model.AppError
+ if user, err = GetUser(userId); err != nil {
+ return nil, err
+ }
+
+ if user.IsLDAPUser() {
+ err := model.NewLocAppError("UpdateActive", "api.user.update_active.no_deactivate_ldap.app_error", nil, "userId="+user.Id)
+ err.StatusCode = http.StatusBadRequest
+ return nil, err
+ }
+
+ return UpdateActive(user, active)
+}
+
+func UpdateActive(user *model.User, active bool) (*model.User, *model.AppError) {
+ if active {
+ user.DeleteAt = 0
+ } else {
+ user.DeleteAt = model.GetMillis()
+ }
+
+ if result := <-Srv.Store.User().Update(user, true); result.Err != nil {
+ return nil, result.Err
+ } else {
+ if user.DeleteAt > 0 {
+ if err := RevokeAllSessions(user.Id); err != nil {
+ return nil, err
+ }
+ }
+
+ if extra := <-Srv.Store.Channel().ExtraUpdateByUser(user.Id, model.GetMillis()); extra.Err != nil {
+ return nil, extra.Err
+ }
+
+ ruser := result.Data.([2]*model.User)[0]
+ options := utils.Cfg.GetSanitizeOptions()
+ options["passwordupdate"] = false
+ ruser.Sanitize(options)
+
+ if !active {
+ SetStatusOffline(ruser.Id, false)
+ }
+
+ return ruser, nil
+ }
+}
+
+func UpdateUser(user *model.User, siteURL string) (*model.User, *model.AppError) {
+ if result := <-Srv.Store.User().Update(user, false); result.Err != nil {
+ return nil, result.Err
+ } else {
+ rusers := result.Data.([2]*model.User)
+
+ if rusers[0].Email != rusers[1].Email {
+ go func() {
+ if err := SendEmailChangeEmail(rusers[1].Email, rusers[0].Email, rusers[0].Locale, siteURL); err != nil {
+ l4g.Error(err.Error())
+ }
+ }()
+
+ if utils.Cfg.EmailSettings.RequireEmailVerification {
+ go func() {
+ if err := SendEmailChangeVerifyEmail(rusers[0].Id, rusers[0].Email, rusers[0].Locale, siteURL); err != nil {
+ l4g.Error(err.Error())
+ }
+ }()
+ }
+ }
+
+ if rusers[0].Username != rusers[1].Username {
+ go func() {
+ if err := SendChangeUsernameEmail(rusers[1].Username, rusers[0].Username, rusers[0].Email, rusers[0].Locale, siteURL); err != nil {
+ l4g.Error(err.Error())
+ }
+ }()
+ }
+
+ InvalidateCacheForUser(user.Id)
+
+ return rusers[0], nil
+ }
+}
+
+func UpdateUserNotifyProps(userId string, props map[string]string, siteURL string) (*model.User, *model.AppError) {
+ var user *model.User
+ var err *model.AppError
+ if user, err = GetUser(userId); err != nil {
+ return nil, err
+ }
+
+ user.NotifyProps = props
+
+ var ruser *model.User
+ if ruser, err = UpdateUser(user, siteURL); err != nil {
+ return nil, err
+ }
+
+ return ruser, nil
+}
+
+func UpdatePasswordByUserIdSendEmail(userId, newPassword, method, siteURL string) *model.AppError {
+ var user *model.User
+ var err *model.AppError
+ if user, err = GetUser(userId); err != nil {
+ return err
+ }
+
+ return UpdatePasswordSendEmail(user, newPassword, method, siteURL)
+}
+
+func UpdatePassword(user *model.User, newPassword string) *model.AppError {
+ if err := utils.IsPasswordValid(newPassword); err != nil {
+ return err
+ }
+
+ hashedPassword := model.HashPassword(newPassword)
+
+ if result := <-Srv.Store.User().UpdatePassword(user.Id, hashedPassword); result.Err != nil {
+ return model.NewLocAppError("UpdatePassword", "api.user.update_password.failed.app_error", nil, result.Err.Error())
+ }
+
+ return nil
+}
+
+func UpdatePasswordSendEmail(user *model.User, newPassword, method, siteURL string) *model.AppError {
+ if err := UpdatePassword(user, newPassword); err != nil {
+ return err
+ }
+
+ go func() {
+ if err := SendPasswordChangeEmail(user.Email, method, user.Locale, siteURL); err != nil {
+ l4g.Error(err.Error())
+ }
+ }()
+
+ return nil
+}
+
+func SendPasswordReset(email string, siteURL string) (bool, *model.AppError) {
+ var user *model.User
+ var err *model.AppError
+ if user, err = GetUserByEmail(email); err != nil {
+ return false, nil
+ }
+
+ if user.AuthData != nil && len(*user.AuthData) != 0 {
+ return false, model.NewLocAppError("SendPasswordReset", "api.user.send_password_reset.sso.app_error", nil, "userId="+user.Id)
+ }
+
+ var recovery *model.PasswordRecovery
+ if recovery, err = CreatePasswordRecovery(user.Id); err != nil {
+ return false, err
+ }
+
+ T := utils.GetUserTranslations(user.Locale)
+
+ link := fmt.Sprintf("%s/reset_password_complete?code=%s", siteURL, url.QueryEscape(recovery.Code))
+
+ subject := T("api.templates.reset_subject")
+
+ bodyPage := utils.NewHTMLTemplate("reset_body", user.Locale)
+ bodyPage.Props["SiteURL"] = siteURL
+ bodyPage.Props["Title"] = T("api.templates.reset_body.title")
+ bodyPage.Html["Info"] = template.HTML(T("api.templates.reset_body.info"))
+ bodyPage.Props["ResetUrl"] = link
+ bodyPage.Props["Button"] = T("api.templates.reset_body.button")
+
+ if err := utils.SendMail(email, subject, bodyPage.Render()); err != nil {
+ return false, model.NewLocAppError("SendPasswordReset", "api.user.send_password_reset.send.app_error", nil, "err="+err.Message)
+ }
+
+ return true, nil
+}
+
+func ResetPasswordFromCode(code, newPassword, siteURL string) *model.AppError {
+ var recovery *model.PasswordRecovery
+ var err *model.AppError
+ if recovery, err = GetPasswordRecovery(code); err != nil {
+ return err
+ } else {
+ if model.GetMillis()-recovery.CreateAt >= model.PASSWORD_RECOVER_EXPIRY_TIME {
+ return model.NewLocAppError("resetPassword", "api.user.reset_password.link_expired.app_error", nil, "")
+ }
+ }
+
+ var user *model.User
+ if user, err = GetUser(recovery.UserId); err != nil {
+ return err
+ }
+
+ if user.IsSSOUser() {
+ return model.NewLocAppError("ResetPasswordFromCode", "api.user.reset_password.sso.app_error", nil, "userId="+user.Id)
+ }
+
+ T := utils.GetUserTranslations(user.Locale)
+
+ if err := UpdatePasswordSendEmail(user, newPassword, T("api.user.reset_password.method"), siteURL); err != nil {
+ return err
+ }
+
+ if err := DeletePasswordRecoveryForUser(recovery.UserId); err != nil {
+ l4g.Error(err.Error())
+ }
+
+ return nil
+}
+
+func CreatePasswordRecovery(userId string) (*model.PasswordRecovery, *model.AppError) {
+ recovery := &model.PasswordRecovery{}
+ recovery.UserId = userId
+
+ if result := <-Srv.Store.PasswordRecovery().SaveOrUpdate(recovery); result.Err != nil {
+ return nil, result.Err
+ }
+
+ return recovery, nil
+}
+
+func GetPasswordRecovery(code string) (*model.PasswordRecovery, *model.AppError) {
+ if result := <-Srv.Store.PasswordRecovery().GetByCode(code); result.Err != nil {
+ return nil, model.NewLocAppError("GetPasswordRecovery", "api.user.reset_password.invalid_link.app_error", nil, result.Err.Error())
+ } else {
+ return result.Data.(*model.PasswordRecovery), nil
+ }
+}
+
+func DeletePasswordRecoveryForUser(userId string) *model.AppError {
+ if result := <-Srv.Store.PasswordRecovery().Delete(userId); result.Err != nil {
+ return result.Err
+ }
+
+ return nil
+}
+
+func UpdateUserRoles(userId string, newRoles string) (*model.User, *model.AppError) {
+ var user *model.User
+ var err *model.AppError
+ if user, err = GetUser(userId); err != nil {
+ err.StatusCode = http.StatusBadRequest
+ return nil, err
+ }
+
+ user.Roles = newRoles
+ uchan := Srv.Store.User().Update(user, true)
+ schan := Srv.Store.Session().UpdateRoles(user.Id, newRoles)
+
+ var ruser *model.User
+ if result := <-uchan; result.Err != nil {
+ return nil, result.Err
+ } else {
+ ruser = result.Data.([2]*model.User)[0]
+ }
+
+ if result := <-schan; result.Err != nil {
+ // soft error since the user roles were still updated
+ l4g.Error(result.Err)
+ }
+
+ ClearSessionCacheForUser(user.Id)
+
+ return ruser, nil
+}
+
+func PermanentDeleteUser(user *model.User) *model.AppError {
+ l4g.Warn(utils.T("api.user.permanent_delete_user.attempting.warn"), user.Email, user.Id)
+ if user.IsInRole(model.ROLE_SYSTEM_ADMIN.Id) {
+ l4g.Warn(utils.T("api.user.permanent_delete_user.system_admin.warn"), user.Email)
+ }
+
+ if _, err := UpdateActive(user, false); err != nil {
+ return err
+ }
+
+ if result := <-Srv.Store.Session().PermanentDeleteSessionsByUser(user.Id); result.Err != nil {
+ return result.Err
+ }
+
+ if result := <-Srv.Store.OAuth().PermanentDeleteAuthDataByUser(user.Id); result.Err != nil {
+ return result.Err
+ }
+
+ if result := <-Srv.Store.Webhook().PermanentDeleteIncomingByUser(user.Id); result.Err != nil {
+ return result.Err
+ }
+
+ if result := <-Srv.Store.Webhook().PermanentDeleteOutgoingByUser(user.Id); result.Err != nil {
+ return result.Err
+ }
+
+ if result := <-Srv.Store.Command().PermanentDeleteByUser(user.Id); result.Err != nil {
+ return result.Err
+ }
+
+ if result := <-Srv.Store.Preference().PermanentDeleteByUser(user.Id); result.Err != nil {
+ return result.Err
+ }
+
+ if result := <-Srv.Store.Channel().PermanentDeleteMembersByUser(user.Id); result.Err != nil {
+ return result.Err
+ }
+
+ if result := <-Srv.Store.Post().PermanentDeleteByUser(user.Id); result.Err != nil {
+ return result.Err
+ }
+
+ if result := <-Srv.Store.User().PermanentDelete(user.Id); result.Err != nil {
+ return result.Err
+ }
+
+ if result := <-Srv.Store.Audit().PermanentDeleteByUser(user.Id); result.Err != nil {
+ return result.Err
+ }
+
+ if result := <-Srv.Store.Team().RemoveAllMembersByUser(user.Id); result.Err != nil {
+ return result.Err
+ }
+
+ if result := <-Srv.Store.PasswordRecovery().Delete(user.Id); result.Err != nil {
+ return result.Err
+ }
+
+ l4g.Warn(utils.T("api.user.permanent_delete_user.deleted.warn"), user.Email, user.Id)
+
+ return nil
+}
+
+func PermanentDeleteAllUsers() *model.AppError {
+ if result := <-Srv.Store.User().GetAll(); result.Err != nil {
+ return result.Err
+ } else {
+ users := result.Data.([]*model.User)
+ for _, user := range users {
+ PermanentDeleteUser(user)
+ }
+ }
+
+ return nil
+}
+
+func VerifyUserEmail(userId string) *model.AppError {
+ if err := (<-Srv.Store.User().VerifyEmail(userId)).Err; err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func SearchUsersInChannel(channelId string, term string, searchOptions map[string]bool) ([]*model.User, *model.AppError) {
+ if result := <-Srv.Store.User().SearchInChannel(channelId, term, searchOptions); result.Err != nil {
+ return nil, result.Err
+ } else {
+ return result.Data.([]*model.User), nil
+ }
+}
+
+func SearchUsersNotInChannel(teamId string, channelId string, term string, searchOptions map[string]bool) ([]*model.User, *model.AppError) {
+ if result := <-Srv.Store.User().SearchNotInChannel(teamId, channelId, term, searchOptions); result.Err != nil {
+ return nil, result.Err
+ } else {
+ return result.Data.([]*model.User), nil
+ }
+}
+
+func SearchUsersInTeam(teamId string, term string, searchOptions map[string]bool) ([]*model.User, *model.AppError) {
+ if result := <-Srv.Store.User().Search(teamId, term, searchOptions); result.Err != nil {
+ return nil, result.Err
+ } else {
+ return result.Data.([]*model.User), nil
+ }
+}
+
+func AutocompleteUsersInChannel(teamId string, channelId string, term string, searchOptions map[string]bool) (*model.UserAutocompleteInChannel, *model.AppError) {
+ uchan := Srv.Store.User().SearchInChannel(channelId, term, searchOptions)
+ nuchan := Srv.Store.User().SearchNotInChannel(teamId, channelId, term, searchOptions)
+
+ autocomplete := &model.UserAutocompleteInChannel{}
+
+ if result := <-uchan; result.Err != nil {
+ return nil, result.Err
+ } else {
+ autocomplete.InChannel = result.Data.([]*model.User)
+ }
+
+ if result := <-nuchan; result.Err != nil {
+ return nil, result.Err
+ } else {
+ autocomplete.OutOfChannel = result.Data.([]*model.User)
+ }
+
+ return autocomplete, nil
+}
+
+func AutocompleteUsersInTeam(teamId string, term string, searchOptions map[string]bool) (*model.UserAutocompleteInTeam, *model.AppError) {
+ autocomplete := &model.UserAutocompleteInTeam{}
+
+ if result := <-Srv.Store.User().Search(teamId, term, searchOptions); result.Err != nil {
+ return nil, result.Err
+ } else {
+ autocomplete.InTeam = result.Data.([]*model.User)
+ }
+
+ return autocomplete, nil
+}
diff --git a/app/user_test.go b/app/user_test.go
new file mode 100644
index 000000000..5b994d219
--- /dev/null
+++ b/app/user_test.go
@@ -0,0 +1,53 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package app
+
+import (
+ "testing"
+)
+
+func TestIsUsernameTaken(t *testing.T) {
+ th := Setup().InitBasic()
+ user := th.BasicUser
+ taken := IsUsernameTaken(user.Username)
+
+ if !taken {
+ t.Logf("the username '%v' should be taken", user.Username)
+ t.FailNow()
+ }
+
+ newUsername := "randomUsername"
+ taken = IsUsernameTaken(newUsername)
+
+ if taken {
+ t.Logf("the username '%v' should not be taken", newUsername)
+ t.FailNow()
+ }
+}
+
+func TestCheckUserDomain(t *testing.T) {
+ th := Setup().InitBasic()
+ user := th.BasicUser
+
+ cases := []struct {
+ domains string
+ matched bool
+ }{
+ {"simulator.amazonses.com", true},
+ {"gmail.com", false},
+ {"", true},
+ {"gmail.com simulator.amazonses.com", true},
+ }
+ for _, c := range cases {
+ matched := CheckUserDomain(user, c.domains)
+ if matched != c.matched {
+ if c.matched {
+ t.Logf("'%v' should have matched '%v'", user.Email, c.domains)
+ } else {
+ t.Logf("'%v' should not have matched '%v'", user.Email, c.domains)
+ }
+ t.FailNow()
+ }
+ }
+}
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..70ba1d07a
--- /dev/null
+++ b/app/webhook.go
@@ -0,0 +1,189 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package app
+
+import (
+ "crypto/tls"
+ "io"
+ "io/ioutil"
+ "net/http"
+ "regexp"
+ "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) {
+ // parse links into Markdown format
+ linkWithTextRegex := regexp.MustCompile(`<([^<\|]+)\|([^>]+)>`)
+ text = linkWithTextRegex.ReplaceAllString(text, "[${2}](${1})")
+
+ 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)
+ }
+ }
+
+ if len(props) > 0 {
+ for key, val := range props {
+ if key == "attachments" {
+ if list, success := val.([]interface{}); success {
+ // parse attachment links into Markdown format
+ 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(key, list)
+ }
+ } 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/app/webtrc.go b/app/webtrc.go
new file mode 100644
index 000000000..b526c96a6
--- /dev/null
+++ b/app/webtrc.go
@@ -0,0 +1,33 @@
+// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package app
+
+import (
+ "crypto/tls"
+ "encoding/base64"
+ "net/http"
+ "strings"
+
+ "github.com/mattermost/platform/model"
+ "github.com/mattermost/platform/utils"
+)
+
+func RevokeWebrtcToken(sessionId string) {
+ token := base64.StdEncoding.EncodeToString([]byte(sessionId))
+ data := make(map[string]string)
+ data["janus"] = "remove_token"
+ data["token"] = token
+ data["transaction"] = model.NewId()
+ data["admin_secret"] = *utils.Cfg.WebrtcSettings.GatewayAdminSecret
+
+ rq, _ := http.NewRequest("POST", *utils.Cfg.WebrtcSettings.GatewayAdminUrl, strings.NewReader(model.MapToJson(data)))
+ rq.Header.Set("Content-Type", "application/json")
+
+ // we do not care about the response
+ tr := &http.Transport{
+ TLSClientConfig: &tls.Config{InsecureSkipVerify: *utils.Cfg.ServiceSettings.EnableInsecureOutgoingConnections},
+ }
+ httpClient := &http.Client{Transport: tr}
+ httpClient.Do(rq)
+}