From 224b72c61e9c42a7c801854d851a678adc324990 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Tue, 28 Apr 2020 12:52:43 +0200 Subject: [PATCH] MM-24037 Adding getKnowUsers API endpoint (#14332) * Adding getKnowUsers API endpoint * Adding i18n strings * Fixing golint errors * Adding doc strings * Remove debug line * Updating app_iface * Fixing gofmt --- api4/user.go | 13 +++ api4/user_test.go | 100 ++++++++++++++++++++++ app/app_iface.go | 4 + app/opentracing_layer.go | 22 +++++ app/user.go | 7 ++ i18n/en.json | 4 + model/client4.go | 11 +++ store/opentracing_layer.go | 18 ++++ store/sqlstore/user_store.go | 20 +++++ store/store.go | 1 + store/storetest/mocks/UserStore.go | 25 ++++++ store/storetest/user_store.go | 132 +++++++++++++++++++++++++++++ store/timer_layer.go | 16 ++++ 13 files changed, 373 insertions(+) diff --git a/api4/user.go b/api4/user.go index 0cade5761e..be5bb2c206 100644 --- a/api4/user.go +++ b/api4/user.go @@ -26,6 +26,7 @@ func (api *API) InitUser() { api.BaseRoutes.Users.Handle("", api.ApiSessionRequired(getUsers)).Methods("GET") api.BaseRoutes.Users.Handle("/ids", api.ApiSessionRequired(getUsersByIds)).Methods("POST") api.BaseRoutes.Users.Handle("/usernames", api.ApiSessionRequired(getUsersByNames)).Methods("POST") + api.BaseRoutes.Users.Handle("/known", api.ApiSessionRequired(getKnownUsers)).Methods("GET") api.BaseRoutes.Users.Handle("/search", api.ApiSessionRequiredDisableWhenBusy(searchUsers)).Methods("POST") api.BaseRoutes.Users.Handle("/autocomplete", api.ApiSessionRequired(autocompleteUsers)).Methods("GET") api.BaseRoutes.Users.Handle("/stats", api.ApiSessionRequired(getTotalUsersStats)).Methods("GET") @@ -717,6 +718,18 @@ func getUsersByNames(c *Context, w http.ResponseWriter, r *http.Request) { w.Write([]byte(model.UserListToJson(users))) } +func getKnownUsers(c *Context, w http.ResponseWriter, r *http.Request) { + userIds, err := c.App.GetKnownUsers(c.App.Session().UserId) + if err != nil { + c.Err = err + return + } + + data, _ := json.Marshal(userIds) + + w.Write(data) +} + func searchUsers(c *Context, w http.ResponseWriter, r *http.Request) { props := model.UserSearchFromJson(r.Body) if props == nil { diff --git a/api4/user_test.go b/api4/user_test.go index 29dc9505b3..f060dc8dbd 100644 --- a/api4/user_test.go +++ b/api4/user_test.go @@ -4528,3 +4528,103 @@ func TestPromoteGuestToUser(t *testing.T) { }) }) } + +func TestGetKnownUsers(t *testing.T) { + th := Setup(t) + defer th.TearDown() + + t1, err := th.App.CreateTeam(&model.Team{ + DisplayName: "dn_" + model.NewId(), + Name: GenerateTestTeamName(), + Email: th.GenerateTestEmail(), + Type: model.TEAM_OPEN, + }) + require.Nil(t, err, "failed to create team") + + t2, err := th.App.CreateTeam(&model.Team{ + DisplayName: "dn_" + model.NewId(), + Name: GenerateTestTeamName(), + Email: th.GenerateTestEmail(), + Type: model.TEAM_OPEN, + }) + require.Nil(t, err, "failed to create team") + + t3, err := th.App.CreateTeam(&model.Team{ + DisplayName: "dn_" + model.NewId(), + Name: GenerateTestTeamName(), + Email: th.GenerateTestEmail(), + Type: model.TEAM_OPEN, + }) + require.Nil(t, err, "failed to create team") + + c1, err := th.App.CreateChannel(&model.Channel{ + DisplayName: "dn_" + model.NewId(), + Name: "name_" + model.NewId(), + Type: model.CHANNEL_OPEN, + TeamId: t1.Id, + CreatorId: model.NewId(), + }, false) + require.Nil(t, err, "failed to create channel") + + c2, err := th.App.CreateChannel(&model.Channel{ + DisplayName: "dn_" + model.NewId(), + Name: "name_" + model.NewId(), + Type: model.CHANNEL_OPEN, + TeamId: t2.Id, + CreatorId: model.NewId(), + }, false) + require.Nil(t, err, "failed to create channel") + + c3, err := th.App.CreateChannel(&model.Channel{ + DisplayName: "dn_" + model.NewId(), + Name: "name_" + model.NewId(), + Type: model.CHANNEL_OPEN, + TeamId: t3.Id, + CreatorId: model.NewId(), + }, false) + require.Nil(t, err, "failed to create channel") + + u1 := th.CreateUser() + defer th.App.PermanentDeleteUser(u1) + u2 := th.CreateUser() + defer th.App.PermanentDeleteUser(u2) + u3 := th.CreateUser() + defer th.App.PermanentDeleteUser(u3) + u4 := th.CreateUser() + defer th.App.PermanentDeleteUser(u4) + + th.LinkUserToTeam(u1, t1) + th.LinkUserToTeam(u1, t2) + th.LinkUserToTeam(u2, t1) + th.LinkUserToTeam(u3, t2) + th.LinkUserToTeam(u4, t3) + + th.App.AddUserToChannel(u1, c1) + th.App.AddUserToChannel(u1, c2) + th.App.AddUserToChannel(u2, c1) + th.App.AddUserToChannel(u3, c2) + th.App.AddUserToChannel(u4, c3) + + t.Run("get know users sharing no channels", func(t *testing.T) { + _, _ = th.Client.Login(u4.Email, u4.Password) + userIds, resp := th.Client.GetKnownUsers() + CheckNoError(t, resp) + assert.Empty(t, userIds) + }) + + t.Run("get know users sharing one channel", func(t *testing.T) { + _, _ = th.Client.Login(u3.Email, u3.Password) + userIds, resp := th.Client.GetKnownUsers() + CheckNoError(t, resp) + assert.Len(t, userIds, 1) + assert.Equal(t, userIds[0], u1.Id) + }) + + t.Run("get know users sharing multiple channels", func(t *testing.T) { + _, _ = th.Client.Login(u1.Email, u1.Password) + userIds, resp := th.Client.GetKnownUsers() + CheckNoError(t, resp) + assert.Len(t, userIds, 2) + assert.ElementsMatch(t, userIds, []string{u2.Id, u3.Id}) + }) +} diff --git a/app/app_iface.go b/app/app_iface.go index 8cde6f109c..e09b2f5656 100644 --- a/app/app_iface.go +++ b/app/app_iface.go @@ -156,6 +156,10 @@ type AppIface interface { GetEnvironmentConfig() map[string]interface{} // GetHubForUserId returns the hub for a given user id. GetHubForUserId(userId string) *Hub + // GetKnownUsers returns the list of user ids of users with any direct + // relationship with a user. That means any user sharing any channel, including + // direct and group channels. + GetKnownUsers(userID string) ([]string, *model.AppError) // GetLdapGroup retrieves a single LDAP group by the given LDAP group id. GetLdapGroup(ldapGroupID string) (*model.Group, *model.AppError) // GetMarketplacePlugins returns a list of plugins from the marketplace-server, diff --git a/app/opentracing_layer.go b/app/opentracing_layer.go index 912ee630eb..8838276a79 100644 --- a/app/opentracing_layer.go +++ b/app/opentracing_layer.go @@ -5702,6 +5702,28 @@ func (a *OpenTracingAppLayer) GetJobsPage(page int, perPage int) ([]*model.Job, return resultVar0, resultVar1 } +func (a *OpenTracingAppLayer) GetKnownUsers(userID string) ([]string, *model.AppError) { + origCtx := a.ctx + span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetKnownUsers") + + a.ctx = newCtx + a.app.Srv().Store.SetContext(newCtx) + defer func() { + a.app.Srv().Store.SetContext(origCtx) + a.ctx = origCtx + }() + + defer span.Finish() + resultVar0, resultVar1 := a.app.GetKnownUsers(userID) + + if resultVar1 != nil { + span.LogFields(spanlog.Error(resultVar1)) + ext.Error.Set(span, true) + } + + return resultVar0, resultVar1 +} + func (a *OpenTracingAppLayer) GetLatestTermsOfService() (*model.TermsOfService, *model.AppError) { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetLatestTermsOfService") diff --git a/app/user.go b/app/user.go index 60871dbe00..d0633ba366 100644 --- a/app/user.go +++ b/app/user.go @@ -2083,3 +2083,10 @@ func (a *App) invalidateUserCacheAndPublish(userId string) { message.Add("user", user) a.Publish(message) } + +// GetKnownUsers returns the list of user ids of users with any direct +// relationship with a user. That means any user sharing any channel, including +// direct and group channels. +func (a *App) GetKnownUsers(userID string) ([]string, *model.AppError) { + return a.Srv().Store.User().GetKnownUsers(userID) +} diff --git a/i18n/en.json b/i18n/en.json index 3f78acebc1..48aee99c5d 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -7242,6 +7242,10 @@ "id": "store.sql_user.get_for_login.multiple_users", "translation": "We found multiple users matching your credentials and were unable to log you in. Please contact an administrator." }, + { + "id": "store.sql_user.get_known_users.get_users.app_error", + "translation": "Unable to get know users from the database." + }, { "id": "store.sql_user.get_new_users.app_error", "translation": "We encountered an error while finding the new users." diff --git a/model/client4.go b/model/client4.go index 3fbae16568..230efb1995 100644 --- a/model/client4.go +++ b/model/client4.go @@ -5005,6 +5005,17 @@ func (c *Client4) PatchChannelModerations(channelID string, patch []*ChannelMode return ChannelModerationsFromJson(r.Body), BuildResponse(r) } +func (c *Client4) GetKnownUsers() ([]string, *Response) { + r, err := c.DoApiGet(c.GetUsersRoute()+"/known", "") + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + var userIds []string + json.NewDecoder(r.Body).Decode(&userIds) + return userIds, BuildResponse(r) +} + func (c *Client4) GetChannelMemberCountsByGroup(channelID string, includeTimezones bool, etag string) ([]*ChannelMemberCountByGroup, *Response) { r, err := c.DoApiGet(c.GetChannelRoute(channelID)+"/member_counts_by_group?include_timezones="+strconv.FormatBool(includeTimezones), etag) if err != nil { diff --git a/store/opentracing_layer.go b/store/opentracing_layer.go index 5f292e06d4..38a1b1584e 100644 --- a/store/opentracing_layer.go +++ b/store/opentracing_layer.go @@ -7641,6 +7641,24 @@ func (s *OpenTracingLayerUserStore) GetForLogin(loginId string, allowSignInWithU return resultVar0, resultVar1 } +func (s *OpenTracingLayerUserStore) GetKnownUsers(userID string) ([]string, *model.AppError) { + origCtx := s.Root.Store.Context() + span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.GetKnownUsers") + s.Root.Store.SetContext(newCtx) + defer func() { + s.Root.Store.SetContext(origCtx) + }() + + defer span.Finish() + resultVar0, resultVar1 := s.UserStore.GetKnownUsers(userID) + if resultVar1 != nil { + span.LogFields(spanlog.Error(resultVar1)) + ext.Error.Set(span, true) + } + + return resultVar0, resultVar1 +} + func (s *OpenTracingLayerUserStore) GetNewUsersForTeam(teamId string, offset int, limit int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, *model.AppError) { origCtx := s.Root.Store.Context() span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.GetNewUsersForTeam") diff --git a/store/sqlstore/user_store.go b/store/sqlstore/user_store.go index 54dd7ed6f5..4007f009ee 100644 --- a/store/sqlstore/user_store.go +++ b/store/sqlstore/user_store.go @@ -1770,3 +1770,23 @@ func (us SqlUserStore) AutocompleteUsersInChannel(teamId, channelId, term string autocomplete.OutOfChannel = users return autocomplete, nil } + +// GetKnownUsers returns the list of user ids of users with any direct +// relationship with a user. That means any user sharing any channel, including +// direct and group channels. +func (us SqlUserStore) GetKnownUsers(userId string) ([]string, *model.AppError) { + var userIds []string + usersQuery, args, _ := us.getQueryBuilder(). + Select("DISTINCT ocm.UserId"). + From("ChannelMembers AS cm"). + Join("ChannelMembers AS ocm ON ocm.ChannelId = cm.ChannelId"). + Where(sq.NotEq{"ocm.UserId": userId}). + Where(sq.Eq{"cm.UserId": userId}). + ToSql() + _, err := us.GetSearchReplica().Select(&userIds, usersQuery, args...) + if err != nil { + return nil, model.NewAppError("SqlUserStore.GetKnownUsers", "store.sql_user.get_known_users.get_users.app_error", nil, err.Error(), http.StatusInternalServerError) + } + + return userIds, nil +} diff --git a/store/store.go b/store/store.go index 111513e34a..f08986b1be 100644 --- a/store/store.go +++ b/store/store.go @@ -332,6 +332,7 @@ type UserStore interface { DemoteUserToGuest(userID string) *model.AppError DeactivateGuests() ([]string, *model.AppError) AutocompleteUsersInChannel(teamId, channelId, term string, options *model.UserSearchOptions) (*model.UserAutocompleteInChannel, *model.AppError) + GetKnownUsers(userID string) ([]string, *model.AppError) } type BotStore interface { diff --git a/store/storetest/mocks/UserStore.go b/store/storetest/mocks/UserStore.go index 8f41f19506..142965ae4a 100644 --- a/store/storetest/mocks/UserStore.go +++ b/store/storetest/mocks/UserStore.go @@ -582,6 +582,31 @@ func (_m *UserStore) GetForLogin(loginId string, allowSignInWithUsername bool, a return r0, r1 } +// GetKnownUsers provides a mock function with given fields: userID +func (_m *UserStore) GetKnownUsers(userID string) ([]string, *model.AppError) { + ret := _m.Called(userID) + + var r0 []string + if rf, ok := ret.Get(0).(func(string) []string); ok { + r0 = rf(userID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]string) + } + } + + var r1 *model.AppError + if rf, ok := ret.Get(1).(func(string) *model.AppError); ok { + r1 = rf(userID) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*model.AppError) + } + } + + return r0, r1 +} + // GetNewUsersForTeam provides a mock function with given fields: teamId, offset, limit, viewRestrictions func (_m *UserStore) GetNewUsersForTeam(teamId string, offset int, limit int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, *model.AppError) { ret := _m.Called(teamId, offset, limit, viewRestrictions) diff --git a/store/storetest/user_store.go b/store/storetest/user_store.go index a43fc10e87..6fc8b35452 100644 --- a/store/storetest/user_store.go +++ b/store/storetest/user_store.go @@ -82,6 +82,7 @@ func TestUserStore(t *testing.T, ss store.Store, s SqlSupplier) { t.Run("DemoteUserToGuest", func(t *testing.T) { testUserStoreDemoteUserToGuest(t, ss) }) t.Run("DeactivateGuests", func(t *testing.T) { testDeactivateGuests(t, ss) }) t.Run("ResetLastPictureUpdate", func(t *testing.T) { testUserStoreResetLastPictureUpdate(t, ss) }) + t.Run("GetKnownUsers", func(t *testing.T) { testGetKnownUsers(t, ss) }) } func testUserStoreSave(t *testing.T, ss store.Store) { @@ -4624,3 +4625,134 @@ func testUserStoreResetLastPictureUpdate(t *testing.T, ss store.Store) { assert.True(t, user2.UpdateAt > user.UpdateAt) assert.Zero(t, user2.LastPictureUpdate) } + +func testGetKnownUsers(t *testing.T, ss store.Store) { + teamId := model.NewId() + + u1, err := ss.User().Save(&model.User{ + Email: MakeEmail(), + Username: "u1" + model.NewId(), + }) + require.Nil(t, err) + defer func() { require.Nil(t, ss.User().PermanentDelete(u1.Id)) }() + _, err = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u1.Id}, -1) + require.Nil(t, err) + + u2, err := ss.User().Save(&model.User{ + Email: MakeEmail(), + Username: "u2" + model.NewId(), + }) + require.Nil(t, err) + defer func() { require.Nil(t, ss.User().PermanentDelete(u2.Id)) }() + _, err = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u2.Id}, -1) + require.Nil(t, err) + + u3, err := ss.User().Save(&model.User{ + Email: MakeEmail(), + Username: "u3" + model.NewId(), + }) + require.Nil(t, err) + defer func() { require.Nil(t, ss.User().PermanentDelete(u3.Id)) }() + _, err = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u3.Id}, -1) + require.Nil(t, err) + _, err = ss.Bot().Save(&model.Bot{ + UserId: u3.Id, + Username: u3.Username, + OwnerId: u1.Id, + }) + require.Nil(t, err) + u3.IsBot = true + + defer func() { require.Nil(t, ss.Bot().PermanentDelete(u3.Id)) }() + + u4, err := ss.User().Save(&model.User{ + Email: MakeEmail(), + Username: "u4" + model.NewId(), + }) + require.Nil(t, err) + defer func() { require.Nil(t, ss.User().PermanentDelete(u4.Id)) }() + _, err = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u4.Id}, -1) + require.Nil(t, err) + + ch1 := &model.Channel{ + TeamId: teamId, + DisplayName: "Profiles in channel", + Name: "profiles-" + model.NewId(), + Type: model.CHANNEL_OPEN, + } + c1, err := ss.Channel().Save(ch1, -1) + require.Nil(t, err) + + ch2 := &model.Channel{ + TeamId: teamId, + DisplayName: "Profiles in private", + Name: "profiles-" + model.NewId(), + Type: model.CHANNEL_PRIVATE, + } + c2, err := ss.Channel().Save(ch2, -1) + require.Nil(t, err) + + ch3 := &model.Channel{ + TeamId: teamId, + DisplayName: "Profiles in private", + Name: "profiles-" + model.NewId(), + Type: model.CHANNEL_PRIVATE, + } + c3, err := ss.Channel().Save(ch3, -1) + require.Nil(t, err) + + _, err = ss.Channel().SaveMember(&model.ChannelMember{ + ChannelId: c1.Id, + UserId: u1.Id, + NotifyProps: model.GetDefaultChannelNotifyProps(), + }) + require.Nil(t, err) + + _, err = ss.Channel().SaveMember(&model.ChannelMember{ + ChannelId: c1.Id, + UserId: u2.Id, + NotifyProps: model.GetDefaultChannelNotifyProps(), + }) + require.Nil(t, err) + + _, err = ss.Channel().SaveMember(&model.ChannelMember{ + ChannelId: c2.Id, + UserId: u3.Id, + NotifyProps: model.GetDefaultChannelNotifyProps(), + }) + require.Nil(t, err) + + _, err = ss.Channel().SaveMember(&model.ChannelMember{ + ChannelId: c2.Id, + UserId: u1.Id, + NotifyProps: model.GetDefaultChannelNotifyProps(), + }) + require.Nil(t, err) + + _, err = ss.Channel().SaveMember(&model.ChannelMember{ + ChannelId: c3.Id, + UserId: u4.Id, + NotifyProps: model.GetDefaultChannelNotifyProps(), + }) + require.Nil(t, err) + + t.Run("get know users sharing no channels", func(t *testing.T) { + userIds, err := ss.User().GetKnownUsers(u4.Id) + require.Nil(t, err) + assert.Empty(t, userIds) + }) + + t.Run("get know users sharing one channel", func(t *testing.T) { + userIds, err := ss.User().GetKnownUsers(u3.Id) + require.Nil(t, err) + assert.Len(t, userIds, 1) + assert.Equal(t, userIds[0], u1.Id) + }) + + t.Run("get know users sharing multiple channels", func(t *testing.T) { + userIds, err := ss.User().GetKnownUsers(u1.Id) + require.Nil(t, err) + assert.Len(t, userIds, 2) + assert.ElementsMatch(t, userIds, []string{u2.Id, u3.Id}) + }) +} diff --git a/store/timer_layer.go b/store/timer_layer.go index 47548e6680..b4341aa59d 100644 --- a/store/timer_layer.go +++ b/store/timer_layer.go @@ -6919,6 +6919,22 @@ func (s *TimerLayerUserStore) GetForLogin(loginId string, allowSignInWithUsernam return resultVar0, resultVar1 } +func (s *TimerLayerUserStore) GetKnownUsers(userID string) ([]string, *model.AppError) { + start := timemodule.Now() + + resultVar0, resultVar1 := s.UserStore.GetKnownUsers(userID) + + elapsed := float64(timemodule.Since(start)) / float64(timemodule.Second) + if s.Root.Metrics != nil { + success := "false" + if resultVar1 == nil { + success = "true" + } + s.Root.Metrics.ObserveStoreMethodDuration("UserStore.GetKnownUsers", success, elapsed) + } + return resultVar0, resultVar1 +} + func (s *TimerLayerUserStore) GetNewUsersForTeam(teamId string, offset int, limit int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, *model.AppError) { start := timemodule.Now()