summaryrefslogtreecommitdiffstats
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/admin.go6
-rw-r--r--app/app.go1
-rw-r--r--app/app_test.go2
-rw-r--r--app/apptestlib.go14
-rw-r--r--app/auto_responder.go70
-rw-r--r--app/auto_responder_test.go160
-rw-r--r--app/channel.go20
-rw-r--r--app/channel_test.go41
-rw-r--r--app/command_invite.go102
-rw-r--r--app/command_invite_test.go108
-rw-r--r--app/command_mute.go5
-rw-r--r--app/command_mute_test.go204
-rw-r--r--app/config.go10
-rw-r--r--app/notification.go24
-rw-r--r--app/notification_test.go24
-rw-r--r--app/plugin_api.go25
-rw-r--r--app/status.go26
17 files changed, 828 insertions, 14 deletions
diff --git a/app/admin.go b/app/admin.go
index 22928390e..60b71505a 100644
--- a/app/admin.go
+++ b/app/admin.go
@@ -156,6 +156,10 @@ func (a *App) GetConfig() *model.Config {
return cfg
}
+func (a *App) GetEnvironmentConfig() map[string]interface{} {
+ return a.EnvironmentConfig()
+}
+
func (a *App) SaveConfig(cfg *model.Config, sendConfigChangeClusterMessage bool) *model.AppError {
oldCfg := a.Config()
cfg.SetDefaults()
@@ -239,7 +243,7 @@ func (a *App) TestEmail(userId string, cfg *model.Config) *model.AppError {
T := utils.GetUserTranslations(user.Locale)
license := a.License()
if err := utils.SendMailUsingConfig(user.Email, T("api.admin.test_email.subject"), T("api.admin.test_email.body"), cfg, license != nil && *license.Features.Compliance); err != nil {
- return err
+ return model.NewAppError("testEmail", "app.admin.test_email.failure", map[string]interface{}{"Error": err.Error()}, "", http.StatusInternalServerError)
}
}
diff --git a/app/app.go b/app/app.go
index 462c45613..51ee37cbd 100644
--- a/app/app.go
+++ b/app/app.go
@@ -59,6 +59,7 @@ type App struct {
Saml einterfaces.SamlInterface
config atomic.Value
+ envConfig map[string]interface{}
configFile string
configListeners map[string]func(*model.Config, *model.Config)
diff --git a/app/app_test.go b/app/app_test.go
index c2841ec53..a726fc2b5 100644
--- a/app/app_test.go
+++ b/app/app_test.go
@@ -195,6 +195,7 @@ func TestDoAdvancedPermissionsMigration(t *testing.T) {
model.PERMISSION_LIST_USERS_WITHOUT_TEAM.Id,
model.PERMISSION_MANAGE_JOBS.Id,
model.PERMISSION_CREATE_POST_PUBLIC.Id,
+ model.PERMISSION_CREATE_POST_EPHEMERAL.Id,
model.PERMISSION_CREATE_USER_ACCESS_TOKEN.Id,
model.PERMISSION_READ_USER_ACCESS_TOKEN.Id,
model.PERMISSION_REVOKE_USER_ACCESS_TOKEN.Id,
@@ -359,6 +360,7 @@ func TestDoAdvancedPermissionsMigration(t *testing.T) {
model.PERMISSION_LIST_USERS_WITHOUT_TEAM.Id,
model.PERMISSION_MANAGE_JOBS.Id,
model.PERMISSION_CREATE_POST_PUBLIC.Id,
+ model.PERMISSION_CREATE_POST_EPHEMERAL.Id,
model.PERMISSION_CREATE_USER_ACCESS_TOKEN.Id,
model.PERMISSION_READ_USER_ACCESS_TOKEN.Id,
model.PERMISSION_REVOKE_USER_ACCESS_TOKEN.Id,
diff --git a/app/apptestlib.go b/app/apptestlib.go
index 6c2273c6e..1b22831c9 100644
--- a/app/apptestlib.go
+++ b/app/apptestlib.go
@@ -218,6 +218,20 @@ func (me *TestHelper) createChannel(team *model.Team, channelType string) *model
return channel
}
+func (me *TestHelper) CreateDmChannel(user *model.User) *model.Channel {
+ utils.DisableDebugLogForTest()
+ var err *model.AppError
+ var channel *model.Channel
+ if channel, err = me.App.CreateDirectChannel(me.BasicUser.Id, user.Id); 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()
diff --git a/app/auto_responder.go b/app/auto_responder.go
new file mode 100644
index 000000000..23402ecd4
--- /dev/null
+++ b/app/auto_responder.go
@@ -0,0 +1,70 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package app
+
+import (
+ l4g "github.com/alecthomas/log4go"
+
+ "github.com/mattermost/mattermost-server/model"
+)
+
+func (a *App) SendAutoResponse(channel *model.Channel, receiver *model.User, rootId string) {
+ if receiver == nil || receiver.NotifyProps == nil {
+ return
+ }
+
+ active := receiver.NotifyProps["auto_responder_active"] == "true"
+ message := receiver.NotifyProps["auto_responder_message"]
+
+ if active && message != "" {
+ autoResponderPost := &model.Post{
+ ChannelId: channel.Id,
+ Message: message,
+ RootId: rootId,
+ ParentId: rootId,
+ Type: model.POST_AUTO_RESPONDER,
+ UserId: receiver.Id,
+ }
+
+ if _, err := a.CreatePost(autoResponderPost, channel, false); err != nil {
+ l4g.Error(err.Error())
+ }
+ }
+}
+
+func (a *App) SetAutoResponderStatus(user *model.User, oldNotifyProps model.StringMap) {
+ active := user.NotifyProps["auto_responder_active"] == "true"
+ oldActive := oldNotifyProps["auto_responder_active"] == "true"
+
+ autoResponderEnabled := !oldActive && active
+ autoResponderDisabled := oldActive && !active
+
+ if autoResponderEnabled {
+ a.SetStatusOutOfOffice(user.Id)
+ } else if autoResponderDisabled {
+ a.SetStatusOnline(user.Id, "", true)
+ }
+}
+
+func (a *App) DisableAutoResponder(userId string, asAdmin bool) *model.AppError {
+ user, err := a.GetUser(userId)
+ if err != nil {
+ return err
+ }
+
+ active := user.NotifyProps["auto_responder_active"] == "true"
+
+ if active {
+ patch := &model.UserPatch{}
+ patch.NotifyProps = user.NotifyProps
+ patch.NotifyProps["auto_responder_active"] = "false"
+
+ _, err := a.PatchUser(userId, patch, asAdmin)
+ if err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
diff --git a/app/auto_responder_test.go b/app/auto_responder_test.go
new file mode 100644
index 000000000..65b466c92
--- /dev/null
+++ b/app/auto_responder_test.go
@@ -0,0 +1,160 @@
+// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package app
+
+import (
+ "testing"
+
+ "github.com/mattermost/mattermost-server/model"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestSetAutoResponderStatus(t *testing.T) {
+ th := Setup().InitBasic()
+ defer th.TearDown()
+
+ user := th.CreateUser()
+ defer th.App.PermanentDeleteUser(user)
+
+ th.App.SetStatusOnline(user.Id, "", true)
+
+ patch := &model.UserPatch{}
+ patch.NotifyProps = make(map[string]string)
+ patch.NotifyProps["auto_responder_active"] = "true"
+ patch.NotifyProps["auto_responder_message"] = "Hello, I'm unavailable today."
+
+ userUpdated1, _ := th.App.PatchUser(user.Id, patch, true)
+
+ // autoResponder is enabled, status should be OOO
+ th.App.SetAutoResponderStatus(userUpdated1, user.NotifyProps)
+
+ status, err := th.App.GetStatus(userUpdated1.Id)
+ require.Nil(t, err)
+ assert.Equal(t, model.STATUS_OUT_OF_OFFICE, status.Status)
+
+ patch2 := &model.UserPatch{}
+ patch2.NotifyProps = make(map[string]string)
+ patch2.NotifyProps["auto_responder_active"] = "false"
+ patch2.NotifyProps["auto_responder_message"] = "Hello, I'm unavailable today."
+
+ userUpdated2, _ := th.App.PatchUser(user.Id, patch2, true)
+
+ // autoResponder is disabled, status should be ONLINE
+ th.App.SetAutoResponderStatus(userUpdated2, userUpdated1.NotifyProps)
+
+ status, err = th.App.GetStatus(userUpdated2.Id)
+ require.Nil(t, err)
+ assert.Equal(t, model.STATUS_ONLINE, status.Status)
+
+}
+
+func TestDisableAutoResponder(t *testing.T) {
+ th := Setup().InitBasic()
+ defer th.TearDown()
+
+ user := th.CreateUser()
+ defer th.App.PermanentDeleteUser(user)
+
+ th.App.SetStatusOnline(user.Id, "", true)
+
+ patch := &model.UserPatch{}
+ patch.NotifyProps = make(map[string]string)
+ patch.NotifyProps["auto_responder_active"] = "true"
+ patch.NotifyProps["auto_responder_message"] = "Hello, I'm unavailable today."
+
+ th.App.PatchUser(user.Id, patch, true)
+
+ th.App.DisableAutoResponder(user.Id, true)
+
+ userUpdated1, err := th.App.GetUser(user.Id)
+ require.Nil(t, err)
+ assert.Equal(t, userUpdated1.NotifyProps["auto_responder_active"], "false")
+
+ th.App.DisableAutoResponder(user.Id, true)
+
+ userUpdated2, err := th.App.GetUser(user.Id)
+ require.Nil(t, err)
+ assert.Equal(t, userUpdated2.NotifyProps["auto_responder_active"], "false")
+}
+
+func TestSendAutoResponseSuccess(t *testing.T) {
+ th := Setup().InitBasic()
+ defer th.TearDown()
+
+ user := th.CreateUser()
+ defer th.App.PermanentDeleteUser(user)
+
+ patch := &model.UserPatch{}
+ patch.NotifyProps = make(map[string]string)
+ patch.NotifyProps["auto_responder_active"] = "true"
+ patch.NotifyProps["auto_responder_message"] = "Hello, I'm unavailable today."
+
+ userUpdated1, err := th.App.PatchUser(user.Id, patch, true)
+ require.Nil(t, err)
+
+ firstPost, err := th.App.CreatePost(&model.Post{
+ ChannelId: th.BasicChannel.Id,
+ Message: "zz" + model.NewId() + "a",
+ UserId: th.BasicUser.Id},
+ th.BasicChannel,
+ false)
+
+ th.App.SendAutoResponse(th.BasicChannel, userUpdated1, firstPost.Id)
+
+ if list, err := th.App.GetPosts(th.BasicChannel.Id, 0, 1); err != nil {
+ require.Nil(t, err)
+ } else {
+ autoResponderPostFound := false
+ autoResponderIsComment := false
+ for _, post := range list.Posts {
+ if post.Type == model.POST_AUTO_RESPONDER {
+ autoResponderIsComment = post.RootId == firstPost.Id
+ autoResponderPostFound = true
+ }
+ }
+ assert.True(t, autoResponderPostFound)
+ assert.True(t, autoResponderIsComment)
+ }
+}
+
+func TestSendAutoResponseFailure(t *testing.T) {
+ th := Setup().InitBasic()
+ defer th.TearDown()
+
+ user := th.CreateUser()
+ defer th.App.PermanentDeleteUser(user)
+
+ patch := &model.UserPatch{}
+ patch.NotifyProps = make(map[string]string)
+ patch.NotifyProps["auto_responder_active"] = "false"
+ patch.NotifyProps["auto_responder_message"] = "Hello, I'm unavailable today."
+
+ userUpdated1, err := th.App.PatchUser(user.Id, patch, true)
+ require.Nil(t, err)
+
+ firstPost, err := th.App.CreatePost(&model.Post{
+ ChannelId: th.BasicChannel.Id,
+ Message: "zz" + model.NewId() + "a",
+ UserId: th.BasicUser.Id},
+ th.BasicChannel,
+ false)
+
+ th.App.SendAutoResponse(th.BasicChannel, userUpdated1, firstPost.Id)
+
+ if list, err := th.App.GetPosts(th.BasicChannel.Id, 0, 1); err != nil {
+ require.Nil(t, err)
+ } else {
+ autoResponderPostFound := false
+ autoResponderIsComment := false
+ for _, post := range list.Posts {
+ if post.Type == model.POST_AUTO_RESPONDER {
+ autoResponderIsComment = post.RootId == firstPost.Id
+ autoResponderPostFound = true
+ }
+ }
+ assert.False(t, autoResponderPostFound)
+ assert.False(t, autoResponderIsComment)
+ }
+}
diff --git a/app/channel.go b/app/channel.go
index 6e11d4e5d..7c14538f4 100644
--- a/app/channel.go
+++ b/app/channel.go
@@ -652,8 +652,10 @@ func (a *App) AddChannelMember(userId string, channel *model.Channel, userReques
}
var userRequestor *model.User
- if userRequestor, err = a.GetUser(userRequestorId); err != nil {
- return nil, err
+ if userRequestorId != "" {
+ if userRequestor, err = a.GetUser(userRequestorId); err != nil {
+ return nil, err
+ }
}
cm, err := a.AddUserToChannel(user, channel)
@@ -661,7 +663,7 @@ func (a *App) AddChannelMember(userId string, channel *model.Channel, userReques
return nil, err
}
- if userId == userRequestorId {
+ if userRequestorId == "" || userId == userRequestorId {
a.postJoinChannelMessage(user, channel)
} else {
a.Go(func() {
@@ -669,7 +671,9 @@ func (a *App) AddChannelMember(userId string, channel *model.Channel, userReques
})
}
- a.UpdateChannelLastViewedAt([]string{channel.Id}, userRequestor.Id)
+ if userRequestor != nil {
+ a.UpdateChannelLastViewedAt([]string{channel.Id}, userRequestor.Id)
+ }
return cm, nil
}
@@ -1487,7 +1491,13 @@ func (a *App) GetDirectChannel(userId1, userId2 string) (*model.Channel, *model.
}
func (a *App) ToggleMuteChannel(channelId string, userId string) *model.ChannelMember {
- member := (<-a.Srv.Store.Channel().GetMember(channelId, userId)).Data.(*model.ChannelMember)
+ result := <-a.Srv.Store.Channel().GetMember(channelId, userId)
+
+ if result.Err != nil {
+ return nil
+ }
+
+ member := result.Data.(*model.ChannelMember)
if member.NotifyProps[model.MARK_UNREAD_NOTIFY_PROP] == model.CHANNEL_NOTIFY_MENTION {
member.NotifyProps[model.MARK_UNREAD_NOTIFY_PROP] = model.CHANNEL_MARK_UNREAD_ALL
diff --git a/app/channel_test.go b/app/channel_test.go
index 69efaeca7..a4e0806a6 100644
--- a/app/channel_test.go
+++ b/app/channel_test.go
@@ -340,3 +340,44 @@ func TestRemoveUserFromChannelUpdatesChannelMemberHistoryRecord(t *testing.T) {
assert.Equal(t, publicChannel.Id, histories[0].ChannelId)
assert.NotNil(t, histories[0].LeaveTime)
}
+
+func TestAddChannelMemberNoUserRequestor(t *testing.T) {
+ th := Setup().InitBasic()
+ defer th.TearDown()
+
+ // create a user and add it to a channel
+ user := th.CreateUser()
+ if _, err := th.App.AddTeamMember(th.BasicTeam.Id, user.Id); err != nil {
+ t.Fatal("Failed to add user to team. Error: " + err.Message)
+ }
+
+ groupUserIds := make([]string, 0)
+ groupUserIds = append(groupUserIds, th.BasicUser.Id)
+ groupUserIds = append(groupUserIds, user.Id)
+
+ channel := th.createChannel(th.BasicTeam, model.CHANNEL_OPEN)
+ userRequestorId := ""
+ postRootId := ""
+ if _, err := th.App.AddChannelMember(user.Id, channel, userRequestorId, postRootId); err != nil {
+ t.Fatal("Failed to add user to channel. Error: " + err.Message)
+ }
+
+ // there should be a ChannelMemberHistory record for the user
+ histories := store.Must(th.App.Srv.Store.ChannelMemberHistory().GetUsersInChannelDuring(model.GetMillis()-100, model.GetMillis()+100, channel.Id)).([]*model.ChannelMemberHistoryResult)
+ assert.Len(t, histories, 2)
+ channelMemberHistoryUserIds := make([]string, 0)
+ for _, history := range histories {
+ assert.Equal(t, channel.Id, history.ChannelId)
+ channelMemberHistoryUserIds = append(channelMemberHistoryUserIds, history.UserId)
+ }
+ assert.Equal(t, groupUserIds, channelMemberHistoryUserIds)
+
+ postList := store.Must(th.App.Srv.Store.Post().GetPosts(channel.Id, 0, 1, false)).(*model.PostList)
+ if assert.Len(t, postList.Order, 1) {
+ post := postList.Posts[postList.Order[0]]
+
+ assert.Equal(t, model.POST_JOIN_CHANNEL, post.Type)
+ assert.Equal(t, user.Id, post.UserId)
+ assert.Equal(t, user.Username, post.Props["username"])
+ }
+}
diff --git a/app/command_invite.go b/app/command_invite.go
new file mode 100644
index 000000000..ce443bf3d
--- /dev/null
+++ b/app/command_invite.go
@@ -0,0 +1,102 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package app
+
+import (
+ "strings"
+
+ l4g "github.com/alecthomas/log4go"
+ "github.com/mattermost/mattermost-server/model"
+ goi18n "github.com/nicksnyder/go-i18n/i18n"
+)
+
+type InviteProvider struct {
+}
+
+const (
+ CMD_INVITE = "invite"
+)
+
+func init() {
+ RegisterCommandProvider(&InviteProvider{})
+}
+
+func (me *InviteProvider) GetTrigger() string {
+ return CMD_INVITE
+}
+
+func (me *InviteProvider) GetCommand(a *App, T goi18n.TranslateFunc) *model.Command {
+ return &model.Command{
+ Trigger: CMD_INVITE,
+ AutoComplete: true,
+ AutoCompleteDesc: T("api.command_invite.desc"),
+ AutoCompleteHint: T("api.command_invite.hint"),
+ DisplayName: T("api.command_invite.name"),
+ }
+}
+
+func (me *InviteProvider) DoCommand(a *App, args *model.CommandArgs, message string) *model.CommandResponse {
+ if message == "" {
+ return &model.CommandResponse{Text: args.T("api.command_invite.missing_message.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL}
+ }
+
+ l4g.Debug(message)
+
+ splitMessage := strings.SplitN(message, " ", 2)
+ targetUsername := splitMessage[0]
+ targetUsername = strings.TrimPrefix(targetUsername, "@")
+
+ var userProfile *model.User
+ if result := <-a.Srv.Store.User().GetByUsername(targetUsername); result.Err != nil {
+ l4g.Error(result.Err.Error())
+ return &model.CommandResponse{Text: args.T("api.command_invite.missing_user.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL}
+ } else {
+ userProfile = result.Data.(*model.User)
+ }
+
+ var channelToJoin *model.Channel
+ var err *model.AppError
+ // User set a channel to add the invited user
+ if len(splitMessage) > 1 && splitMessage[1] != "" {
+ targetChannelName := strings.TrimPrefix(strings.TrimSpace(splitMessage[1]), "~")
+
+ if channelToJoin, err = a.GetChannelByName(targetChannelName, args.TeamId); err != nil {
+ return &model.CommandResponse{Text: args.T("api.command_invite.channel.error", map[string]interface{}{"Channel": targetChannelName}), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL}
+ }
+ } else {
+ channelToJoin, err = a.GetChannel(args.ChannelId)
+ if err != nil {
+ return &model.CommandResponse{Text: args.T("api.command_invite.channel.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL}
+ }
+ }
+
+ // Check if is a Direct Channel
+ if channelToJoin.Type == model.CHANNEL_DIRECT || channelToJoin.Type == model.CHANNEL_GROUP {
+ return &model.CommandResponse{Text: args.T("api.command_invite.directchannel.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL}
+ }
+
+ // Check if user is already in the channel
+ _, err = a.GetChannelMember(channelToJoin.Id, userProfile.Id)
+ if err == nil {
+ return &model.CommandResponse{Text: args.T("api.command_invite.user_already_in_channel.app_error", map[string]interface{}{"User": userProfile.Username}), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL}
+ }
+
+ if channelToJoin.Type == model.CHANNEL_OPEN && !a.SessionHasPermissionToChannel(args.Session, channelToJoin.Id, model.PERMISSION_MANAGE_PUBLIC_CHANNEL_MEMBERS) {
+ return &model.CommandResponse{Text: args.T("api.command_invite.permission.app_error", map[string]interface{}{"User": userProfile.Username, "Channel": channelToJoin.Name}), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL}
+ }
+
+ if channelToJoin.Type == model.CHANNEL_PRIVATE && !a.SessionHasPermissionToChannel(args.Session, channelToJoin.Id, model.PERMISSION_MANAGE_PRIVATE_CHANNEL_MEMBERS) {
+ return &model.CommandResponse{Text: args.T("api.command_invite.permission.app_error", map[string]interface{}{"User": userProfile.Username, "Channel": channelToJoin.Name}), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL}
+ }
+
+ if _, err := a.AddChannelMember(userProfile.Id, channelToJoin, args.Session.UserId, ""); err != nil {
+ return &model.CommandResponse{Text: args.T("api.command_invite.fail.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL}
+ }
+
+ if args.ChannelId != channelToJoin.Id {
+ return &model.CommandResponse{Text: args.T("api.command_invite.success", map[string]interface{}{"User": userProfile.Username, "Channel": channelToJoin.Name}), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL}
+ }
+
+ return &model.CommandResponse{}
+}
diff --git a/app/command_invite_test.go b/app/command_invite_test.go
new file mode 100644
index 000000000..c46bc4628
--- /dev/null
+++ b/app/command_invite_test.go
@@ -0,0 +1,108 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package app
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+
+ "github.com/mattermost/mattermost-server/model"
+)
+
+func TestInviteProvider(t *testing.T) {
+ th := Setup().InitBasic()
+ defer th.TearDown()
+
+ channel := th.createChannel(th.BasicTeam, model.CHANNEL_OPEN)
+ privateChannel := th.createChannel(th.BasicTeam, model.CHANNEL_PRIVATE)
+ dmChannel := th.CreateDmChannel(th.BasicUser2)
+
+ basicUser3 := th.CreateUser()
+ th.LinkUserToTeam(basicUser3, th.BasicTeam)
+ basicUser4 := th.CreateUser()
+
+ InviteP := InviteProvider{}
+ args := &model.CommandArgs{
+ T: func(s string, args ...interface{}) string { return s },
+ ChannelId: th.BasicChannel.Id,
+ TeamId: th.BasicTeam.Id,
+ Session: model.Session{UserId: th.BasicUser.Id, TeamMembers: []*model.TeamMember{{TeamId: th.BasicTeam.Id, Roles: model.TEAM_USER_ROLE_ID}}},
+ }
+
+ userAndWrongChannel := "@" + th.BasicUser2.Username + " wrongchannel1"
+ userAndChannel := "@" + th.BasicUser2.Username + " ~" + channel.Name + " "
+ userAndDisplayChannel := "@" + th.BasicUser2.Username + " ~" + channel.DisplayName + " "
+ userAndPrivateChannel := "@" + th.BasicUser2.Username + " ~" + privateChannel.Name
+ userAndDMChannel := "@" + basicUser3.Username + " ~" + dmChannel.Name
+
+ tests := []struct {
+ desc string
+ expected string
+ msg string
+ }{
+ {
+ desc: "Missing user and channel in the command",
+ expected: "api.command_invite.missing_message.app_error",
+ msg: "",
+ },
+ {
+ desc: "User added in the current channel",
+ expected: "",
+ msg: th.BasicUser2.Username,
+ },
+ {
+ desc: "Add user to another channel not the current",
+ expected: "api.command_invite.success",
+ msg: userAndChannel,
+ },
+ {
+ desc: "try to add a user to a direct channel",
+ expected: "api.command_invite.directchannel.app_error",
+ msg: userAndDMChannel,
+ },
+ {
+ desc: "Try to add a user to a invalid channel",
+ expected: "api.command_invite.channel.error",
+ msg: userAndWrongChannel,
+ },
+ {
+ desc: "Try to add a user to an private channel",
+ expected: "api.command_invite.success",
+ msg: userAndPrivateChannel,
+ },
+ {
+ desc: "Using display channel name which is different form Channel name",
+ expected: "api.command_invite.channel.error",
+ msg: userAndDisplayChannel,
+ },
+ {
+ desc: "Invalid user to current channel",
+ expected: "api.command_invite.missing_user.app_error",
+ msg: "@invalidUser123",
+ },
+ {
+ desc: "Invalid user to current channel without @",
+ expected: "api.command_invite.missing_user.app_error",
+ msg: "invalidUser321",
+ },
+ {
+ desc: "try to add a user which is not part of the team",
+ expected: "api.command_invite.fail.app_error",
+ msg: basicUser4.Username,
+ },
+ {
+ desc: "try to add a user to a direct channel",
+ expected: "api.command_invite.directchannel.app_error",
+ msg: userAndDMChannel,
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.desc, func(t *testing.T) {
+ actual := InviteP.DoCommand(th.App, args, test.msg).Text
+ assert.Equal(t, test.expected, actual)
+ })
+ }
+}
diff --git a/app/command_mute.go b/app/command_mute.go
index fdc698cd9..d3a2fa5f7 100644
--- a/app/command_mute.go
+++ b/app/command_mute.go
@@ -40,7 +40,7 @@ func (me *MuteProvider) DoCommand(a *App, args *model.CommandArgs, message strin
var noChannelErr *model.AppError
if channel, noChannelErr = a.GetChannel(args.ChannelId); noChannelErr != nil {
- return &model.CommandResponse{Text: args.T("api.command_mute.error", map[string]interface{}{"Channel": channel.DisplayName}), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL}
+ return &model.CommandResponse{Text: args.T("api.command_mute.no_channel.error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL}
}
channelName := ""
@@ -63,6 +63,9 @@ func (me *MuteProvider) DoCommand(a *App, args *model.CommandArgs, message strin
}
channelMember := a.ToggleMuteChannel(channel.Id, args.UserId)
+ if channelMember == nil {
+ return &model.CommandResponse{Text: args.T("api.command_mute.not_member.error", map[string]interface{}{"Channel": channelName}), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL}
+ }
// Invalidate cache to allow cache lookups while sending notifications
a.Srv.Store.Channel().InvalidateCacheForChannelMembersNotifyProps(channel.Id)
diff --git a/app/command_mute_test.go b/app/command_mute_test.go
new file mode 100644
index 000000000..9f82c48cc
--- /dev/null
+++ b/app/command_mute_test.go
@@ -0,0 +1,204 @@
+// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package app
+
+import (
+ "github.com/mattermost/mattermost-server/model"
+ "github.com/nicksnyder/go-i18n/i18n"
+ "github.com/stretchr/testify/assert"
+ "testing"
+ "time"
+)
+
+func TestMuteCommandNoChannel(t *testing.T) {
+ th := Setup().InitBasic()
+ defer th.TearDown()
+
+ if testing.Short() {
+ t.SkipNow()
+ }
+
+ channel1 := th.BasicChannel
+ channel1M, channel1MError := th.App.GetChannelMember(channel1.Id, th.BasicUser.Id)
+
+ if channel1MError != nil {
+ t.Fatal("User is not a member of channel 1")
+ }
+
+ if channel1M.NotifyProps[model.MARK_UNREAD_NOTIFY_PROP] == model.CHANNEL_NOTIFY_MENTION {
+ t.Fatal("channel shouldn't be muted on initial setup")
+ }
+
+ cmd := &MuteProvider{}
+ resp := cmd.DoCommand(th.App, &model.CommandArgs{
+ T: i18n.IdentityTfunc(),
+ UserId: th.BasicUser.Id,
+ }, "")
+ assert.Equal(t, "api.command_mute.no_channel.error", resp.Text)
+}
+
+func TestMuteCommandNoArgs(t *testing.T) {
+ th := Setup().InitBasic()
+ defer th.TearDown()
+
+ channel1 := th.BasicChannel
+ channel1M, _ := th.App.GetChannelMember(channel1.Id, th.BasicUser.Id)
+
+ assert.Equal(t, model.CHANNEL_NOTIFY_ALL, channel1M.NotifyProps[model.MARK_UNREAD_NOTIFY_PROP])
+
+ cmd := &MuteProvider{}
+
+ // First mute the channel
+ resp := cmd.DoCommand(th.App, &model.CommandArgs{
+ T: i18n.IdentityTfunc(),
+ ChannelId: channel1.Id,
+ UserId: th.BasicUser.Id,
+ }, "")
+ assert.Equal(t, "api.command_mute.success_mute", resp.Text)
+
+ // Now unmute the channel
+ time.Sleep(time.Millisecond)
+ resp = cmd.DoCommand(th.App, &model.CommandArgs{
+ T: i18n.IdentityTfunc(),
+ ChannelId: channel1.Id,
+ UserId: th.BasicUser.Id,
+ }, "")
+
+ assert.Equal(t, "api.command_mute.success_unmute", resp.Text)
+}
+
+func TestMuteCommandSpecificChannel(t *testing.T) {
+ th := Setup().InitBasic()
+ defer th.TearDown()
+
+ if testing.Short() {
+ t.SkipNow()
+ }
+
+ channel1 := th.BasicChannel
+ channel2, _ := th.App.CreateChannel(&model.Channel{
+ DisplayName: "AA",
+ Name: "aa" + model.NewId() + "a",
+ Type: model.CHANNEL_OPEN,
+ TeamId: th.BasicTeam.Id,
+ CreatorId: th.BasicUser.Id,
+ }, true)
+
+ channel2M, _ := th.App.GetChannelMember(channel2.Id, th.BasicUser.Id)
+
+ assert.Equal(t, model.CHANNEL_NOTIFY_ALL, channel2M.NotifyProps[model.MARK_UNREAD_NOTIFY_PROP])
+
+ cmd := &MuteProvider{}
+
+ // First mute the channel
+ resp := cmd.DoCommand(th.App, &model.CommandArgs{
+ T: i18n.IdentityTfunc(),
+ ChannelId: channel1.Id,
+ UserId: th.BasicUser.Id,
+ }, channel2.Name)
+ assert.Equal(t, "api.command_mute.success_mute", resp.Text)
+ time.Sleep(time.Millisecond)
+ channel2M, _ = th.App.GetChannelMember(channel2.Id, th.BasicUser.Id)
+ assert.Equal(t, model.CHANNEL_NOTIFY_MENTION, channel2M.NotifyProps[model.MARK_UNREAD_NOTIFY_PROP])
+
+ // Now unmute the channel
+ resp = cmd.DoCommand(th.App, &model.CommandArgs{
+ T: i18n.IdentityTfunc(),
+ ChannelId: channel1.Id,
+ UserId: th.BasicUser.Id,
+ }, "~"+channel2.Name)
+
+ assert.Equal(t, "api.command_mute.success_unmute", resp.Text)
+ time.Sleep(time.Millisecond)
+ channel2M, _ = th.App.GetChannelMember(channel2.Id, th.BasicUser.Id)
+ assert.Equal(t, model.CHANNEL_NOTIFY_ALL, channel2M.NotifyProps[model.MARK_UNREAD_NOTIFY_PROP])
+}
+
+func TestMuteCommandNotMember(t *testing.T) {
+ th := Setup().InitBasic()
+ defer th.TearDown()
+
+ if testing.Short() {
+ t.SkipNow()
+ }
+
+ channel1 := th.BasicChannel
+ channel2, _ := th.App.CreateChannel(&model.Channel{
+ DisplayName: "AA",
+ Name: "aa" + model.NewId() + "a",
+ Type: model.CHANNEL_OPEN,
+ TeamId: th.BasicTeam.Id,
+ CreatorId: th.BasicUser.Id,
+ }, false)
+
+ cmd := &MuteProvider{}
+
+ // First mute the channel
+ resp := cmd.DoCommand(th.App, &model.CommandArgs{
+ T: i18n.IdentityTfunc(),
+ ChannelId: channel1.Id,
+ UserId: th.BasicUser.Id,
+ }, channel2.Name)
+ assert.Equal(t, "api.command_mute.not_member.error", resp.Text)
+}
+
+func TestMuteCommandNotChannel(t *testing.T) {
+ th := Setup().InitBasic()
+ defer th.TearDown()
+
+ if testing.Short() {
+ t.SkipNow()
+ }
+
+ channel1 := th.BasicChannel
+
+ cmd := &MuteProvider{}
+
+ // First mute the channel
+ resp := cmd.DoCommand(th.App, &model.CommandArgs{
+ T: i18n.IdentityTfunc(),
+ ChannelId: channel1.Id,
+ UserId: th.BasicUser.Id,
+ }, "~noexists")
+ assert.Equal(t, "api.command_mute.error", resp.Text)
+}
+
+func TestMuteCommandDMChannel(t *testing.T) {
+ th := Setup().InitBasic()
+ defer th.TearDown()
+
+ if testing.Short() {
+ t.SkipNow()
+ }
+
+ channel2, _ := th.App.CreateDirectChannel(th.BasicUser.Id, th.BasicUser2.Id)
+ channel2M, _ := th.App.GetChannelMember(channel2.Id, th.BasicUser.Id)
+
+ assert.Equal(t, model.CHANNEL_NOTIFY_ALL, channel2M.NotifyProps[model.MARK_UNREAD_NOTIFY_PROP])
+
+ cmd := &MuteProvider{}
+
+ // First mute the channel
+ resp := cmd.DoCommand(th.App, &model.CommandArgs{
+ T: i18n.IdentityTfunc(),
+ ChannelId: channel2.Id,
+ UserId: th.BasicUser.Id,
+ }, "")
+ assert.Equal(t, "api.command_mute.success_mute_direct_msg", resp.Text)
+ time.Sleep(time.Millisecond)
+ channel2M, _ = th.App.GetChannelMember(channel2.Id, th.BasicUser.Id)
+ assert.Equal(t, model.CHANNEL_NOTIFY_MENTION, channel2M.NotifyProps[model.MARK_UNREAD_NOTIFY_PROP])
+
+ // Now unmute the channel
+ resp = cmd.DoCommand(th.App, &model.CommandArgs{
+ T: i18n.IdentityTfunc(),
+ ChannelId: channel2.Id,
+ UserId: th.BasicUser.Id,
+ }, "")
+
+ assert.Equal(t, "api.command_mute.success_unmute_direct_msg", resp.Text)
+ time.Sleep(time.Millisecond)
+ channel2M, _ = th.App.GetChannelMember(channel2.Id, th.BasicUser.Id)
+ assert.Equal(t, model.CHANNEL_NOTIFY_ALL, channel2M.NotifyProps[model.MARK_UNREAD_NOTIFY_PROP])
+}
diff --git a/app/config.go b/app/config.go
index 761fe3ec9..75d38e24a 100644
--- a/app/config.go
+++ b/app/config.go
@@ -30,6 +30,13 @@ func (a *App) Config() *model.Config {
return &model.Config{}
}
+func (a *App) EnvironmentConfig() map[string]interface{} {
+ if a.envConfig != nil {
+ return a.envConfig
+ }
+ return map[string]interface{}{}
+}
+
func (a *App) UpdateConfig(f func(*model.Config)) {
old := a.Config()
updated := old.Clone()
@@ -46,7 +53,7 @@ func (a *App) PersistConfig() {
func (a *App) LoadConfig(configFile string) *model.AppError {
old := a.Config()
- cfg, configPath, err := utils.LoadConfig(configFile)
+ cfg, configPath, envConfig, err := utils.LoadConfig(configFile)
if err != nil {
return err
}
@@ -57,6 +64,7 @@ func (a *App) LoadConfig(configFile string) *model.AppError {
l4g.Info("Using config file at %s", configPath)
a.config.Store(cfg)
+ a.envConfig = envConfig
a.siteURL = strings.TrimRight(*cfg.ServiceSettings.SiteURL, "/")
diff --git a/app/notification.go b/app/notification.go
index 5b7ed5ad2..2d56d294a 100644
--- a/app/notification.go
+++ b/app/notification.go
@@ -66,13 +66,25 @@ func (a *App) SendNotifications(post *model.Post, team *model.Team, channel *mod
}
}
- if _, ok := profileMap[otherUserId]; ok {
+ otherUser, ok := profileMap[otherUserId]
+ if ok {
mentionedUserIds[otherUserId] = true
}
if post.Props["from_webhook"] == "true" {
mentionedUserIds[post.UserId] = true
}
+
+ if post.Type != model.POST_AUTO_RESPONDER {
+ a.Go(func() {
+ rootId := post.Id
+ if post.RootId != "" && post.RootId != post.Id {
+ rootId = post.RootId
+ }
+ a.SendAutoResponse(channel, otherUser, rootId)
+ })
+ }
+
} else {
keywords := a.GetMentionKeywordsInChannel(profileMap, post.Type != model.POST_HEADER_CHANGE && post.Type != model.POST_PURPOSE_CHANGE)
@@ -842,15 +854,15 @@ func GetExplicitMentions(message string, keywords map[string][]string) *Explicit
checkForMention := func(word string) bool {
isMention := false
- if word == "@here" {
+ if strings.ToLower(word) == "@here" {
ret.HereMentioned = true
}
- if word == "@channel" {
+ if strings.ToLower(word) == "@channel" {
ret.ChannelMentioned = true
}
- if word == "@all" {
+ if strings.ToLower(word) == "@all" {
ret.AllMentioned = true
}
@@ -1021,8 +1033,8 @@ func DoesNotifyPropsAllowPushNotification(user *model.User, channelNotifyProps m
}
func DoesStatusAllowPushNotification(userNotifyProps model.StringMap, status *model.Status, channelId string) bool {
- // If User status is DND return false right away
- if status.Status == model.STATUS_DND {
+ // If User status is DND or OOO return false right away
+ if status.Status == model.STATUS_DND || status.Status == model.STATUS_OUT_OF_OFFICE {
return false
}
diff --git a/app/notification_test.go b/app/notification_test.go
index 1abdd3d61..8df19e2bf 100644
--- a/app/notification_test.go
+++ b/app/notification_test.go
@@ -186,6 +186,17 @@ func TestGetExplicitMentions(t *testing.T) {
ChannelMentioned: true,
},
},
+ "CapitalizedChannel": {
+ Message: "this is an message for @cHaNNeL",
+ Keywords: map[string][]string{"@channel": {id1, id2}},
+ Expected: &ExplicitMentions{
+ MentionedUserIds: map[string]bool{
+ id1: true,
+ id2: true,
+ },
+ ChannelMentioned: true,
+ },
+ },
"All": {
Message: "this is an message for @all",
Keywords: map[string][]string{"@all": {id1, id2}},
@@ -197,6 +208,17 @@ func TestGetExplicitMentions(t *testing.T) {
AllMentioned: true,
},
},
+ "CapitalizedAll": {
+ Message: "this is an message for @ALL",
+ Keywords: map[string][]string{"@all": {id1, id2}},
+ Expected: &ExplicitMentions{
+ MentionedUserIds: map[string]bool{
+ id1: true,
+ id2: true,
+ },
+ AllMentioned: true,
+ },
+ },
"UserWithPeriod": {
Message: "user.period doesn't complicate things at all by including periods in their username",
Keywords: map[string][]string{"user.period": {id1}, "user": {id2}},
@@ -439,6 +461,8 @@ func TestGetExplicitMentionsAtHere(t *testing.T) {
"?@here?": true,
"`@here`": false, // This case shouldn't mention since it's a code block
"~@here~": true,
+ "@HERE": true,
+ "@hERe": true,
}
for message, shouldMention := range cases {
diff --git a/app/plugin_api.go b/app/plugin_api.go
index 21b828368..b09a0c419 100644
--- a/app/plugin_api.go
+++ b/app/plugin_api.go
@@ -124,10 +124,35 @@ func (api *PluginAPI) UpdateChannel(channel *model.Channel) (*model.Channel, *mo
return api.app.UpdateChannel(channel)
}
+func (api *PluginAPI) AddChannelMember(channelId, userId string) (*model.ChannelMember, *model.AppError) {
+ // For now, don't allow overriding these via the plugin API.
+ userRequestorId := ""
+ postRootId := ""
+
+ channel, err := api.GetChannel(channelId)
+ if err != nil {
+ return nil, err
+ }
+
+ return api.app.AddChannelMember(userId, channel, userRequestorId, postRootId)
+}
+
func (api *PluginAPI) GetChannelMember(channelId, userId string) (*model.ChannelMember, *model.AppError) {
return api.app.GetChannelMember(channelId, userId)
}
+func (api *PluginAPI) UpdateChannelMemberRoles(channelId, userId, newRoles string) (*model.ChannelMember, *model.AppError) {
+ return api.app.UpdateChannelMemberRoles(channelId, userId, newRoles)
+}
+
+func (api *PluginAPI) UpdateChannelMemberNotifications(channelId, userId string, notifications map[string]string) (*model.ChannelMember, *model.AppError) {
+ return api.app.UpdateChannelMemberNotifyProps(notifications, channelId, userId)
+}
+
+func (api *PluginAPI) DeleteChannelMember(channelId, userId string) *model.AppError {
+ return api.app.LeaveChannel(channelId, userId)
+}
+
func (api *PluginAPI) CreatePost(post *model.Post) (*model.Post, *model.AppError) {
return api.app.CreatePostMissingChannel(post, true)
}
diff --git a/app/status.go b/app/status.go
index c8bff0d1a..64d5379ef 100644
--- a/app/status.go
+++ b/app/status.go
@@ -303,6 +303,32 @@ func (a *App) SaveAndBroadcastStatus(status *model.Status) *model.AppError {
return nil
}
+func (a *App) SetStatusOutOfOffice(userId string) {
+ if !*a.Config().ServiceSettings.EnableUserStatuses {
+ return
+ }
+
+ status, err := a.GetStatus(userId)
+
+ if err != nil {
+ status = &model.Status{UserId: userId, Status: model.STATUS_OUT_OF_OFFICE, Manual: false, LastActivityAt: 0, ActiveChannel: ""}
+ }
+
+ status.Status = model.STATUS_OUT_OF_OFFICE
+ status.Manual = true
+
+ a.AddStatusCache(status)
+
+ if result := <-a.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_OUT_OF_OFFICE)
+ event.Add("user_id", status.UserId)
+ a.Publish(event)
+}
+
func GetStatusFromCache(userId string) *model.Status {
if result, ok := statusCache.Get(userId); ok {
status := result.(*model.Status)