From 116849842be59bc6960df415f23155ec8e767f06 Mon Sep 17 00:00:00 2001 From: Jesse Hallam Date: Fri, 6 Apr 2018 17:08:57 -0400 Subject: [PATCH] MM-8678: add CUD support for channel members via plugins (#8565) * add CUD support for channel members via plugins This effectively exposes AddChannelMember, UpdateChannelMemberRoles, UpdateChannelMemberNotifyProps and LeaveChannel via the plugin API. It also modifies the semantics of AddChannelMember to explicitly allow for an empty user requestor, left as such for now via the plugin API. * change the signature of AddChannelMember to accept a channel id instead of a channel --- app/channel.go | 12 ++-- app/channel_test.go | 41 ++++++++++++++ app/plugin_api.go | 25 +++++++++ plugin/api.go | 12 ++++ plugin/plugintest/api.go | 39 +++++++++++++ plugin/rpcplugin/api.go | 103 +++++++++++++++++++++++++++++++++++ plugin/rpcplugin/api_test.go | 25 ++++++++- 7 files changed, 251 insertions(+), 6 deletions(-) diff --git a/app/channel.go b/app/channel.go index 6e11d4e5d75..40f21c3a984 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 } diff --git a/app/channel_test.go b/app/channel_test.go index 69efaeca793..a4e0806a6b2 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/plugin_api.go b/app/plugin_api.go index 21b828368c8..b09a0c41900 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/plugin/api.go b/plugin/api.go index 437188f6ee2..d62c2f06946 100644 --- a/plugin/api.go +++ b/plugin/api.go @@ -77,9 +77,21 @@ type API interface { // UpdateChannel updates a channel. UpdateChannel(channel *model.Channel) (*model.Channel, *model.AppError) + // AddChannelMember creates a channel membership for a user. + AddChannelMember(channelId, userId string) (*model.ChannelMember, *model.AppError) + // GetChannelMember gets a channel membership for a user. GetChannelMember(channelId, userId string) (*model.ChannelMember, *model.AppError) + // UpdateChannelMemberRoles updates a user's roles for a channel. + UpdateChannelMemberRoles(channelId, userId, newRoles string) (*model.ChannelMember, *model.AppError) + + // UpdateChannelMemberNotifications updates a user's notification properties for a channel. + UpdateChannelMemberNotifications(channelId, userId string, notifications map[string]string) (*model.ChannelMember, *model.AppError) + + // DeleteChannelMember deletes a channel membership for a user. + DeleteChannelMember(channelId, userId string) *model.AppError + // CreatePost creates a post. CreatePost(post *model.Post) (*model.Post, *model.AppError) diff --git a/plugin/plugintest/api.go b/plugin/plugintest/api.go index 75174a9a6e9..8f9f4a604dc 100644 --- a/plugin/plugintest/api.go +++ b/plugin/plugintest/api.go @@ -223,6 +223,16 @@ func (m *API) UpdateChannel(channel *model.Channel) (*model.Channel, *model.AppE return channelOut, err } +func (m *API) AddChannelMember(channelId, userId string) (*model.ChannelMember, *model.AppError) { + ret := m.Called(channelId, userId) + if f, ok := ret.Get(0).(func(_, _ string) (*model.ChannelMember, *model.AppError)); ok { + return f(channelId, userId) + } + member, _ := ret.Get(0).(*model.ChannelMember) + err, _ := ret.Get(1).(*model.AppError) + return member, err +} + func (m *API) GetChannelMember(channelId, userId string) (*model.ChannelMember, *model.AppError) { ret := m.Called(channelId, userId) if f, ok := ret.Get(0).(func(_, _ string) (*model.ChannelMember, *model.AppError)); ok { @@ -233,6 +243,35 @@ func (m *API) GetChannelMember(channelId, userId string) (*model.ChannelMember, return member, err } +func (m *API) UpdateChannelMemberRoles(channelId, userId, newRoles string) (*model.ChannelMember, *model.AppError) { + ret := m.Called(channelId, userId, newRoles) + if f, ok := ret.Get(0).(func(_, _, _ string) (*model.ChannelMember, *model.AppError)); ok { + return f(channelId, userId, newRoles) + } + member, _ := ret.Get(0).(*model.ChannelMember) + err, _ := ret.Get(1).(*model.AppError) + return member, err +} + +func (m *API) UpdateChannelMemberNotifications(channelId, userId string, notifications map[string]string) (*model.ChannelMember, *model.AppError) { + ret := m.Called(channelId, userId, notifications) + if f, ok := ret.Get(0).(func(_, _ string, _ map[string]string) (*model.ChannelMember, *model.AppError)); ok { + return f(channelId, userId, notifications) + } + member, _ := ret.Get(0).(*model.ChannelMember) + err, _ := ret.Get(1).(*model.AppError) + return member, err +} + +func (m *API) DeleteChannelMember(channelId, userId string) *model.AppError { + ret := m.Called(channelId, userId) + if f, ok := ret.Get(0).(func(_, _ string) *model.AppError); ok { + return f(channelId, userId) + } + err, _ := ret.Get(0).(*model.AppError) + return err +} + func (m *API) CreatePost(post *model.Post) (*model.Post, *model.AppError) { ret := m.Called(post) if f, ok := ret.Get(0).(func(*model.Post) (*model.Post, *model.AppError)); ok { diff --git a/plugin/rpcplugin/api.go b/plugin/rpcplugin/api.go index d87f65b5568..c81bbb7c5c9 100644 --- a/plugin/rpcplugin/api.go +++ b/plugin/rpcplugin/api.go @@ -163,11 +163,33 @@ type APIGetGroupChannelArgs struct { UserIds []string } +type APIAddChannelMemberArgs struct { + ChannelId string + UserId string +} + type APIGetChannelMemberArgs struct { ChannelId string UserId string } +type APIUpdateChannelMemberRolesArgs struct { + ChannelId string + UserId string + NewRoles string +} + +type APIUpdateChannelMemberNotificationsArgs struct { + ChannelId string + UserId string + Notifications map[string]string +} + +type APIDeleteChannelMemberArgs struct { + ChannelId string + UserId string +} + type APIChannelReply struct { Channel *model.Channel Error *model.AppError @@ -239,6 +261,15 @@ func (api *LocalAPI) UpdateChannel(args *model.Channel, reply *APIChannelReply) return nil } +func (api *LocalAPI) AddChannelMember(args *APIAddChannelMemberArgs, reply *APIChannelMemberReply) error { + member, err := api.api.AddChannelMember(args.ChannelId, args.UserId) + *reply = APIChannelMemberReply{ + ChannelMember: member, + Error: err, + } + return nil +} + func (api *LocalAPI) GetChannelMember(args *APIGetChannelMemberArgs, reply *APIChannelMemberReply) error { member, err := api.api.GetChannelMember(args.ChannelId, args.UserId) *reply = APIChannelMemberReply{ @@ -248,6 +279,32 @@ func (api *LocalAPI) GetChannelMember(args *APIGetChannelMemberArgs, reply *APIC return nil } +func (api *LocalAPI) UpdateChannelMemberRoles(args *APIUpdateChannelMemberRolesArgs, reply *APIChannelMemberReply) error { + member, err := api.api.UpdateChannelMemberRoles(args.ChannelId, args.UserId, args.NewRoles) + *reply = APIChannelMemberReply{ + ChannelMember: member, + Error: err, + } + return nil +} + +func (api *LocalAPI) UpdateChannelMemberNotifications(args *APIUpdateChannelMemberNotificationsArgs, reply *APIChannelMemberReply) error { + member, err := api.api.UpdateChannelMemberNotifications(args.ChannelId, args.UserId, args.Notifications) + *reply = APIChannelMemberReply{ + ChannelMember: member, + Error: err, + } + return nil +} + +func (api *LocalAPI) DeleteChannelMember(args *APIDeleteChannelMemberArgs, reply *APIErrorReply) error { + err := api.api.DeleteChannelMember(args.ChannelId, args.UserId) + *reply = APIErrorReply{ + Error: err, + } + return nil +} + type APIPostReply struct { Post *model.Post Error *model.AppError @@ -520,6 +577,17 @@ func (api *RemoteAPI) UpdateChannel(channel *model.Channel) (*model.Channel, *mo return reply.Channel, reply.Error } +func (api *RemoteAPI) AddChannelMember(channelId, userId string) (*model.ChannelMember, *model.AppError) { + var reply APIChannelMemberReply + if err := api.client.Call("LocalAPI.AddChannelMember", &APIAddChannelMemberArgs{ + ChannelId: channelId, + UserId: userId, + }, &reply); err != nil { + return nil, model.NewAppError("RemoteAPI.AddChannelMember", "plugin.rpcplugin.invocation.error", nil, "err="+err.Error(), http.StatusInternalServerError) + } + return reply.ChannelMember, reply.Error +} + func (api *RemoteAPI) GetChannelMember(channelId, userId string) (*model.ChannelMember, *model.AppError) { var reply APIChannelMemberReply if err := api.client.Call("LocalAPI.GetChannelMember", &APIGetChannelMemberArgs{ @@ -531,6 +599,41 @@ func (api *RemoteAPI) GetChannelMember(channelId, userId string) (*model.Channel return reply.ChannelMember, reply.Error } +func (api *RemoteAPI) UpdateChannelMemberRoles(channelId, userId, newRoles string) (*model.ChannelMember, *model.AppError) { + var reply APIChannelMemberReply + if err := api.client.Call("LocalAPI.UpdateChannelMemberRoles", &APIUpdateChannelMemberRolesArgs{ + ChannelId: channelId, + UserId: userId, + NewRoles: newRoles, + }, &reply); err != nil { + return nil, model.NewAppError("RemoteAPI.UpdateChannelMemberRoles", "plugin.rpcplugin.invocation.error", nil, "err="+err.Error(), http.StatusInternalServerError) + } + return reply.ChannelMember, reply.Error +} + +func (api *RemoteAPI) UpdateChannelMemberNotifications(channelId, userId string, notifications map[string]string) (*model.ChannelMember, *model.AppError) { + var reply APIChannelMemberReply + if err := api.client.Call("LocalAPI.UpdateChannelMemberNotifications", &APIUpdateChannelMemberNotificationsArgs{ + ChannelId: channelId, + UserId: userId, + Notifications: notifications, + }, &reply); err != nil { + return nil, model.NewAppError("RemoteAPI.UpdateChannelMemberNotifications", "plugin.rpcplugin.invocation.error", nil, "err="+err.Error(), http.StatusInternalServerError) + } + return reply.ChannelMember, reply.Error +} + +func (api *RemoteAPI) DeleteChannelMember(channelId, userId string) *model.AppError { + var reply APIErrorReply + if err := api.client.Call("LocalAPI.DeleteChannelMember", &APIDeleteChannelMemberArgs{ + ChannelId: channelId, + UserId: userId, + }, &reply); err != nil { + return model.NewAppError("RemoteAPI.DeleteChannelMember", "plugin.rpcplugin.invocation.error", nil, "err="+err.Error(), http.StatusInternalServerError) + } + return reply.Error +} + func (api *RemoteAPI) CreatePost(post *model.Post) (*model.Post, *model.AppError) { var reply APIPostReply if err := api.client.Call("LocalAPI.CreatePost", post, &reply); err != nil { diff --git a/plugin/rpcplugin/api_test.go b/plugin/rpcplugin/api_test.go index 7fe7a0ff953..d7b3733ea41 100644 --- a/plugin/rpcplugin/api_test.go +++ b/plugin/rpcplugin/api_test.go @@ -128,11 +128,32 @@ func TestAPI(t *testing.T) { assert.Equal(t, testChannel, channel) assert.Nil(t, err) - api.On("GetChannelMember", "thechannelid", "theuserid").Return(testChannelMember, nil).Once() - member, err := remote.GetChannelMember("thechannelid", "theuserid") + api.On("AddChannelMember", testChannel.Id, "theuserid").Return(testChannelMember, nil).Once() + member, err := remote.AddChannelMember(testChannel.Id, "theuserid") assert.Equal(t, testChannelMember, member) assert.Nil(t, err) + api.On("GetChannelMember", "thechannelid", "theuserid").Return(testChannelMember, nil).Once() + member, err = remote.GetChannelMember("thechannelid", "theuserid") + assert.Equal(t, testChannelMember, member) + assert.Nil(t, err) + + api.On("UpdateChannelMemberRoles", testChannel.Id, "theuserid", model.CHANNEL_ADMIN_ROLE_ID).Return(testChannelMember, nil).Once() + member, err = remote.UpdateChannelMemberRoles(testChannel.Id, "theuserid", model.CHANNEL_ADMIN_ROLE_ID) + assert.Equal(t, testChannelMember, member) + assert.Nil(t, err) + + notifications := map[string]string{} + notifications[model.MARK_UNREAD_NOTIFY_PROP] = model.CHANNEL_MARK_UNREAD_MENTION + api.On("UpdateChannelMemberNotifications", testChannel.Id, "theuserid", notifications).Return(testChannelMember, nil).Once() + member, err = remote.UpdateChannelMemberNotifications(testChannel.Id, "theuserid", notifications) + assert.Equal(t, testChannelMember, member) + assert.Nil(t, err) + + api.On("DeleteChannelMember", "thechannelid", "theuserid").Return(nil).Once() + err = remote.DeleteChannelMember("thechannelid", "theuserid") + assert.Nil(t, err) + api.On("CreateUser", mock.AnythingOfType("*model.User")).Return(func(u *model.User) (*model.User, *model.AppError) { u.Id = "theuserid" return u, nil