mattermost/server/channels/app/group.go
Henrique Machado f418e1398d
[GH-28202]: Added GetGroupsByNames API (#33558)
* feat: Added GetGroupsByNames API
This commit implements the endpoint discussed in issue #28202.

This adds a new API endpoint to get multiple groups by a list of
names.

Previously, when the app received a post with @ mentions that it
didn't recognize, it would attempt to fetch them all as users,
then if some were still missing, it would go one by one attempting
to fetch each as a group. Now we just fetch all the groups at
once, just like we do for users.

Also added unit tests for the new API and it's respective
documentation.

* Added server version to GetGroupsByNames documentation

Co-authored-by: Harrison Healey <harrisonmhealey@gmail.com>

* fix: updated status_profile_polling tests to use new endpoint

* fix: fixed mock test

Was using get for post request

---------

Co-authored-by: Harrison Healey <harrisonmhealey@gmail.com>
2025-08-20 18:20:07 -04:00

884 lines
34 KiB
Go

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"encoding/json"
"errors"
"net/http"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/v8/channels/store"
)
func (a *App) GetGroup(id string, opts *model.GetGroupOpts, viewRestrictions *model.ViewUsersRestrictions) (*model.Group, *model.AppError) {
group, err := a.Srv().Store().Group().Get(id)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("GetGroup", "app.group.no_rows", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("GetGroup", "app.select_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
if opts != nil && opts.IncludeMemberIDs {
users, err := a.Srv().Store().Group().GetMemberUsers(id)
if err != nil {
return nil, model.NewAppError("GetGroup", "app.member_count", nil, "", http.StatusInternalServerError).Wrap(err)
}
for _, user := range users {
group.MemberIDs = append(group.MemberIDs, user.Id)
}
}
if opts != nil && opts.IncludeMemberCount {
memberCount, err := a.Srv().Store().Group().GetMemberCountWithRestrictions(id, viewRestrictions)
if err != nil {
return nil, model.NewAppError("GetGroup", "app.member_count", nil, "", http.StatusInternalServerError).Wrap(err)
}
group.MemberCount = model.NewPointer(int(memberCount))
}
return group, nil
}
func (a *App) GetGroupByName(name string, opts model.GroupSearchOpts) (*model.Group, *model.AppError) {
group, err := a.Srv().Store().Group().GetByName(name, opts)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("GetGroupByName", "app.group.no_rows", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("GetGroupByName", "app.select_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return group, nil
}
func (a *App) GetGroupByRemoteID(remoteID string, groupSource model.GroupSource) (*model.Group, *model.AppError) {
group, err := a.Srv().Store().Group().GetByRemoteID(remoteID, groupSource)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("GetGroupByRemoteID", "app.group.no_rows", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("GetGroupByRemoteID", "app.select_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return group, nil
}
func (a *App) GetGroupsBySource(groupSource model.GroupSource) ([]*model.Group, *model.AppError) {
groups, err := a.Srv().Store().Group().GetAllBySource(groupSource)
if err != nil {
return nil, model.NewAppError("GetGroupsBySource", "app.select_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return groups, nil
}
func (a *App) GetGroupsByUserId(userID string, opts model.GroupSearchOpts) ([]*model.Group, *model.AppError) {
groups, err := a.Srv().Store().Group().GetByUser(userID, opts)
if err != nil {
return nil, model.NewAppError("GetGroupsByUserId", "app.select_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return groups, nil
}
func (a *App) GetGroupsByNames(names []string, restrictions *model.ViewUsersRestrictions) ([]*model.Group, *model.AppError) {
groups, err := a.Srv().Store().Group().GetByNames(names, restrictions)
if err != nil {
return nil, model.NewAppError("GetGroupsByNames", "app.select_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return groups, nil
}
func (a *App) CreateGroup(group *model.Group) (*model.Group, *model.AppError) {
if err := a.isUniqueToUsernames(group.GetName()); err != nil {
err.Where = "CreateGroup"
return nil, err
}
group, err := a.Srv().Store().Group().Create(group)
if err != nil {
var invErr *store.ErrInvalidInput
var appErr *model.AppError
switch {
case errors.As(err, &appErr):
return nil, appErr
case errors.As(err, &invErr):
return nil, model.NewAppError("CreateGroup", "app.group.id.app_error", nil, "", http.StatusBadRequest).Wrap(err)
default:
return nil, model.NewAppError("CreateGroup", "app.insert_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return group, nil
}
func (a *App) isUniqueToUsernames(val string) *model.AppError {
if val == "" {
return nil
}
var notFoundErr *store.ErrNotFound
user, err := a.Srv().Store().User().GetByUsername(val)
if err != nil && !errors.As(err, &notFoundErr) {
return model.NewAppError("isUniqueToUsernames", model.NoTranslation, nil, "", http.StatusInternalServerError).Wrap(err)
}
if user != nil {
return model.NewAppError("isUniqueToUsernames", "app.group.username_conflict", map[string]any{"Username": val}, "", http.StatusBadRequest)
}
return nil
}
func (a *App) CreateGroupWithUserIds(group *model.GroupWithUserIds) (*model.Group, *model.AppError) {
if appErr := a.isUniqueToUsernames(group.GetName()); appErr != nil {
appErr.Where = "CreateGroupWithUserIds"
return nil, appErr
}
newGroup, err := a.Srv().Store().Group().CreateWithUserIds(group)
if err != nil {
var invErr *store.ErrInvalidInput
var appErr *model.AppError
var dupKey *store.ErrUniqueConstraint
switch {
case errors.As(err, &appErr):
return nil, appErr
case errors.As(err, &invErr):
return nil, model.NewAppError("CreateGroupWithUserIds", "app.group.id.app_error", nil, "", http.StatusBadRequest).Wrap(err)
case errors.As(err, &dupKey):
return nil, model.NewAppError("CreateGroupWithUserIds", "app.custom_group.unique_name", nil, "", http.StatusBadRequest).Wrap(err)
default:
return nil, model.NewAppError("CreateGroupWithUserIds", "app.insert_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
messageWs := model.NewWebSocketEvent(model.WebsocketEventReceivedGroup, "", "", "", nil, "")
count, err := a.Srv().Store().Group().GetMemberCount(newGroup.Id)
if err != nil {
return nil, model.NewAppError("CreateGroupWithUserIds", "app.group.id.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
group.MemberCount = model.NewPointer(int(count))
groupJSON, jsonErr := json.Marshal(newGroup)
if jsonErr != nil {
return nil, model.NewAppError("CreateGroupWithUserIds", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(jsonErr)
}
messageWs.Add("group", string(groupJSON))
a.Publish(messageWs)
return newGroup, nil
}
func (a *App) UpdateGroup(group *model.Group) (*model.Group, *model.AppError) {
if appErr := a.isUniqueToUsernames(group.GetName()); appErr != nil {
appErr.Where = "UpdateGroup"
return nil, appErr
}
updatedGroup, err := a.Srv().Store().Group().Update(group)
if err != nil {
var nfErr *store.ErrNotFound
var appErr *model.AppError
var dupKey *store.ErrUniqueConstraint
switch {
case errors.As(err, &appErr):
return nil, appErr
case errors.As(err, &nfErr):
return nil, model.NewAppError("UpdateGroup", "app.group.no_rows", nil, "", http.StatusNotFound).Wrap(err)
case errors.As(err, &dupKey):
return nil, model.NewAppError("CreateGroup", "app.custom_group.unique_name", nil, "", http.StatusBadRequest).Wrap(err)
default:
return nil, model.NewAppError("UpdateGroup", "app.select_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
count, err := a.Srv().Store().Group().GetMemberCount(updatedGroup.Id)
if err != nil {
return nil, model.NewAppError("UpdateGroup", "app.group.id.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
updatedGroup.MemberCount = model.NewPointer(int(count))
messageWs := model.NewWebSocketEvent(model.WebsocketEventReceivedGroup, "", "", "", nil, "")
groupJSON, err := json.Marshal(updatedGroup)
if err != nil {
return nil, model.NewAppError("UpdateGroup", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
messageWs.Add("group", string(groupJSON))
a.Publish(messageWs)
return updatedGroup, nil
}
func (a *App) DeleteGroup(groupID string) (*model.Group, *model.AppError) {
deletedGroup, err := a.Srv().Store().Group().Delete(groupID)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("DeleteGroup", "app.group.no_rows", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("DeleteGroup", "app.update_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
count, err := a.Srv().Store().Group().GetMemberCount(groupID)
if err != nil {
return nil, model.NewAppError("DeleteGroup", "app.group.id.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
deletedGroup.MemberCount = model.NewPointer(int(count))
messageWs := model.NewWebSocketEvent(model.WebsocketEventReceivedGroup, "", "", "", nil, "")
groupJSON, err := json.Marshal(deletedGroup)
if err != nil {
return nil, model.NewAppError("DeleteGroup", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
messageWs.Add("group", string(groupJSON))
a.Publish(messageWs)
return deletedGroup, nil
}
func (a *App) RestoreGroup(groupID string) (*model.Group, *model.AppError) {
restoredGroup, err := a.Srv().Store().Group().Restore(groupID)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("RestoreGroup", "app.group.no_rows", nil, "", http.StatusNotFound).Wrap(nfErr)
default:
return nil, model.NewAppError("RestoreGroup", "app.update_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
count, err := a.Srv().Store().Group().GetMemberCount(groupID)
if err != nil {
return nil, model.NewAppError("RestoreGroup", "app.group.id.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
restoredGroup.MemberCount = model.NewPointer(int(count))
messageWs := model.NewWebSocketEvent(model.WebsocketEventReceivedGroup, "", "", "", nil, "")
groupJSON, err := json.Marshal(restoredGroup)
if err != nil {
return nil, model.NewAppError("RestoreGroup", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
messageWs.Add("group", string(groupJSON))
a.Publish(messageWs)
return restoredGroup, nil
}
func (a *App) GetGroupMemberCount(groupID string, viewRestrictions *model.ViewUsersRestrictions) (int64, *model.AppError) {
count, err := a.Srv().Store().Group().GetMemberCountWithRestrictions(groupID, viewRestrictions)
if err != nil {
return 0, model.NewAppError("GetGroupMemberCount", "app.select_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return count, nil
}
func (a *App) GetGroupMemberUsers(groupID string) ([]*model.User, *model.AppError) {
users, err := a.Srv().Store().Group().GetMemberUsers(groupID)
if err != nil {
return nil, model.NewAppError("GetGroupMemberUsers", "app.select_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return users, nil
}
func (a *App) GetGroupMemberUsersSortedPage(groupID string, page int, perPage int, viewRestrictions *model.ViewUsersRestrictions, teammateNameDisplay string) ([]*model.User, int, *model.AppError) {
members, err := a.Srv().Store().Group().GetMemberUsersSortedPage(groupID, page, perPage, viewRestrictions, teammateNameDisplay)
if err != nil {
return nil, 0, model.NewAppError("GetGroupMemberUsersPage", "app.select_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
count, appErr := a.GetGroupMemberCount(groupID, viewRestrictions)
if appErr != nil {
return nil, 0, appErr
}
return a.sanitizeProfiles(members, false), int(count), nil
}
func (a *App) GetGroupMemberUsersPage(groupID string, page int, perPage int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, int, *model.AppError) {
return a.GetGroupMemberUsersSortedPage(groupID, page, perPage, viewRestrictions, model.ShowUsername)
}
func (a *App) GetUsersNotInGroupPage(groupID string, page int, perPage int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, *model.AppError) {
members, err := a.Srv().Store().Group().GetNonMemberUsersPage(groupID, page, perPage, viewRestrictions)
if err != nil {
return nil, model.NewAppError("GetUsersNotInGroupPage", "app.select_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return a.sanitizeProfiles(members, false), nil
}
func (a *App) UpsertGroupMember(groupID string, userID string) (*model.GroupMember, *model.AppError) {
groupMember, err := a.Srv().Store().Group().UpsertMember(groupID, userID)
if err != nil {
var invErr *store.ErrInvalidInput
var appErr *model.AppError
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &appErr):
return nil, appErr
case errors.As(err, &invErr):
return nil, model.NewAppError("UpsertGroupMember", "app.group.uniqueness_error", nil, "", http.StatusBadRequest).Wrap(err)
case errors.As(err, &nfErr):
return nil, model.NewAppError("UpsertGroupMember", "app.group.user_not_found", map[string]any{"Username": nfErr.ID}, "", http.StatusBadRequest).Wrap(err)
default:
return nil, model.NewAppError("UpsertGroupMember", "app.update_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
if appErr := a.publishGroupMemberEvent(model.WebsocketEventGroupMemberAdd, groupMember); appErr != nil {
return nil, appErr
}
return groupMember, nil
}
func (a *App) DeleteGroupMember(groupID string, userID string) (*model.GroupMember, *model.AppError) {
groupMember, err := a.Srv().Store().Group().DeleteMember(groupID, userID)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("DeleteGroupMember", "app.group.no_rows", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("DeleteGroupMember", "app.update_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
if appErr := a.publishGroupMemberEvent(model.WebsocketEventGroupMemberDelete, groupMember); appErr != nil {
return nil, appErr
}
return groupMember, nil
}
func (a *App) UpsertGroupSyncable(groupSyncable *model.GroupSyncable) (*model.GroupSyncable, *model.AppError) {
gs, err := a.Srv().Store().Group().GetGroupSyncable(groupSyncable.GroupId, groupSyncable.SyncableId, groupSyncable.Type)
var notFoundErr *store.ErrNotFound
if err != nil && !errors.As(err, &notFoundErr) {
return nil, model.NewAppError("UpsertGroupSyncable", "app.select_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
// reject the syncable creation if the group isn't already associated to the parent team
if groupSyncable.Type == model.GroupSyncableTypeChannel {
channel, nErr := a.Srv().Store().Channel().Get(groupSyncable.SyncableId, true)
if nErr != nil {
errCtx := map[string]any{"channel_id": groupSyncable.SyncableId}
var nfErr *store.ErrNotFound
switch {
case errors.As(nErr, &nfErr):
return nil, model.NewAppError("UpsertGroupSyncable", "app.channel.get.existing.app_error", errCtx, "", http.StatusNotFound).Wrap(nErr)
default:
return nil, model.NewAppError("UpsertGroupSyncable", "app.channel.get.find.app_error", errCtx, "", http.StatusInternalServerError).Wrap(nErr)
}
}
var team *model.Team
team, nErr = a.Srv().Store().Team().Get(channel.TeamId)
if nErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(nErr, &nfErr):
return nil, model.NewAppError("UpsertGroupSyncable", "app.team.get.find.app_error", nil, "", http.StatusNotFound).Wrap(nErr)
default:
return nil, model.NewAppError("UpsertGroupSyncable", "app.team.get.finding.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
if team.IsGroupConstrained() {
var teamGroups []*model.GroupWithSchemeAdmin
teamGroups, err = a.Srv().Store().Group().GetGroupsByTeam(channel.TeamId, model.GroupSearchOpts{})
if err != nil {
return nil, model.NewAppError("UpsertGroupSyncable", "app.select_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
var permittedGroup bool
for _, teamGroup := range teamGroups {
if teamGroup.Group.Id == groupSyncable.GroupId {
permittedGroup = true
break
}
}
if !permittedGroup {
return nil, model.NewAppError("UpsertGroupSyncable", "group_not_associated_to_synced_team", nil, "", http.StatusBadRequest)
}
} else {
_, appErr := a.UpsertGroupSyncable(model.NewGroupTeam(groupSyncable.GroupId, team.Id, groupSyncable.AutoAdd))
if appErr != nil {
return nil, appErr
}
}
}
if gs == nil {
gs, err = a.Srv().Store().Group().CreateGroupSyncable(groupSyncable)
if err != nil {
var nfErr *store.ErrNotFound
var appErr *model.AppError
switch {
case errors.As(err, &appErr):
return nil, appErr
case errors.As(err, &nfErr):
return nil, model.NewAppError("UpsertGroupSyncable", "store.sql_channel.get.existing.app_error", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("UpsertGroupSyncable", "app.insert_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
} else {
gs, err = a.Srv().Store().Group().UpdateGroupSyncable(groupSyncable)
if err != nil {
var appErr *model.AppError
switch {
case errors.As(err, &appErr):
return nil, appErr
default:
return nil, model.NewAppError("UpsertGroupSyncable", "app.update_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
}
var messageWs *model.WebSocketEvent
if gs.Type == model.GroupSyncableTypeTeam {
messageWs = model.NewWebSocketEvent(model.WebsocketEventReceivedGroupAssociatedToTeam, gs.SyncableId, "", "", nil, "")
} else {
messageWs = model.NewWebSocketEvent(model.WebsocketEventReceivedGroupAssociatedToChannel, "", gs.SyncableId, "", nil, "")
}
messageWs.Add("group_id", gs.GroupId)
a.Publish(messageWs)
return gs, nil
}
func (a *App) GetGroupSyncable(groupID string, syncableID string, syncableType model.GroupSyncableType) (*model.GroupSyncable, *model.AppError) {
group, err := a.Srv().Store().Group().GetGroupSyncable(groupID, syncableID, syncableType)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("GetGroupSyncable", "app.group.no_rows", nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError("GetGroupSyncable", "app.select_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return group, nil
}
func (a *App) GetGroupSyncables(groupID string, syncableType model.GroupSyncableType) ([]*model.GroupSyncable, *model.AppError) {
groups, err := a.Srv().Store().Group().GetAllGroupSyncablesByGroupId(groupID, syncableType)
if err != nil {
return nil, model.NewAppError("GetGroupSyncables", "app.select_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return groups, nil
}
func (a *App) UpdateGroupSyncable(groupSyncable *model.GroupSyncable) (*model.GroupSyncable, *model.AppError) {
if groupSyncable.DeleteAt == 0 {
// updating a *deleted* GroupSyncable, so no need to ensure the GroupTeam is present (as done in the upsert)
gs, err := a.Srv().Store().Group().UpdateGroupSyncable(groupSyncable)
if err != nil {
var appErr *model.AppError
switch {
case errors.As(err, &appErr):
return nil, appErr
default:
return nil, model.NewAppError("UpdateGroupSyncable", "app.update_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return gs, nil
}
// do an upsert to ensure that there's an associated GroupTeam
gs, err := a.UpsertGroupSyncable(groupSyncable)
if err != nil {
return nil, err
}
return gs, nil
}
func (a *App) DeleteGroupSyncable(groupID string, syncableID string, syncableType model.GroupSyncableType) (*model.GroupSyncable, *model.AppError) {
gs, err := a.Srv().Store().Group().DeleteGroupSyncable(groupID, syncableID, syncableType)
if err != nil {
var invErr *store.ErrInvalidInput
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("DeleteGroupSyncable", "app.group.no_rows", nil, "", http.StatusNotFound).Wrap(err)
case errors.As(err, &invErr):
return nil, model.NewAppError("DeleteGroupSyncable", "app.group.group_syncable_already_deleted", nil, "", http.StatusBadRequest).Wrap(err)
default:
return nil, model.NewAppError("DeleteGroupSyncable", "app.update_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
// if a GroupTeam is being deleted delete all associated GroupChannels
if gs.Type == model.GroupSyncableTypeTeam {
allGroupChannels, err := a.Srv().Store().Group().GetAllGroupSyncablesByGroupId(gs.GroupId, model.GroupSyncableTypeChannel)
if err != nil {
return nil, model.NewAppError("DeleteGroupSyncable", "app.select_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
for _, groupChannel := range allGroupChannels {
_, err = a.Srv().Store().Group().DeleteGroupSyncable(groupChannel.GroupId, groupChannel.SyncableId, groupChannel.Type)
if err != nil {
var invErr *store.ErrInvalidInput
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError("DeleteGroupSyncable", "app.group.no_rows", nil, "", http.StatusNotFound).Wrap(err)
case errors.As(err, &invErr):
return nil, model.NewAppError("DeleteGroupSyncable", "app.group.group_syncable_already_deleted", nil, "", http.StatusBadRequest).Wrap(err)
default:
return nil, model.NewAppError("DeleteGroupSyncable", "app.update_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
}
}
var messageWs *model.WebSocketEvent
if gs.Type == model.GroupSyncableTypeTeam {
messageWs = model.NewWebSocketEvent(model.WebsocketEventReceivedGroupNotAssociatedToTeam, gs.SyncableId, "", "", nil, "")
} else {
messageWs = model.NewWebSocketEvent(model.WebsocketEventReceivedGroupNotAssociatedToChannel, "", gs.SyncableId, "", nil, "")
}
messageWs.Add("group_id", gs.GroupId)
a.Publish(messageWs)
return gs, nil
}
// TeamMembersToAdd returns a slice of UserTeamIDPair that need newly created memberships
// based on the groups configurations. The returned list can be optionally scoped to a single given team.
//
// Typically since will be the last successful group sync time.
// If reAddRemovedMembers is true, then team members who left or were removed from the team will
// be included; otherwise, they will be excluded.
func (a *App) TeamMembersToAdd(since int64, teamID *string, reAddRemovedMembers bool) ([]*model.UserTeamIDPair, *model.AppError) {
userTeams, err := a.Srv().Store().Group().TeamMembersToAdd(since, teamID, reAddRemovedMembers)
if err != nil {
return nil, model.NewAppError("TeamMembersToAdd", "app.select_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return userTeams, nil
}
// ChannelMembersToAdd returns a slice of UserChannelIDPair that need newly created memberships
// based on the groups configurations. The returned list can be optionally scoped to a single given channel.
//
// Typically since will be the last successful group sync time.
// If reAddRemovedMembers is true, then channel members who left or were removed from the channel will
// be included; otherwise, they will be excluded.
func (a *App) ChannelMembersToAdd(since int64, channelID *string, reAddRemovedMembers bool) ([]*model.UserChannelIDPair, *model.AppError) {
userChannels, err := a.Srv().Store().Group().ChannelMembersToAdd(since, channelID, reAddRemovedMembers)
if err != nil {
return nil, model.NewAppError("ChannelMembersToAdd", "app.select_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return userChannels, nil
}
func (a *App) TeamMembersToRemove(teamID *string) ([]*model.TeamMember, *model.AppError) {
teamMembers, err := a.Srv().Store().Group().TeamMembersToRemove(teamID)
if err != nil {
return nil, model.NewAppError("TeamMembersToRemove", "app.select_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return teamMembers, nil
}
func (a *App) ChannelMembersToRemove(teamID *string) ([]*model.ChannelMember, *model.AppError) {
channelMembers, err := a.Srv().Store().Group().ChannelMembersToRemove(teamID)
if err != nil {
return nil, model.NewAppError("ChannelMembersToRemove", "app.select_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return channelMembers, nil
}
func (a *App) GetGroupsByChannel(channelID string, opts model.GroupSearchOpts) ([]*model.GroupWithSchemeAdmin, int, *model.AppError) {
groups, err := a.Srv().Store().Group().GetGroupsByChannel(channelID, opts)
if err != nil {
return nil, 0, model.NewAppError("GetGroupsByChannel", "app.select_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
count, err := a.Srv().Store().Group().CountGroupsByChannel(channelID, opts)
if err != nil {
return nil, 0, model.NewAppError("GetGroupsByChannel", "app.select_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return groups, int(count), nil
}
// GetGroupsByTeam returns the paged list and the total count of group associated to the given team.
func (a *App) GetGroupsByTeam(teamID string, opts model.GroupSearchOpts) ([]*model.GroupWithSchemeAdmin, int, *model.AppError) {
groups, err := a.Srv().Store().Group().GetGroupsByTeam(teamID, opts)
if err != nil {
return nil, 0, model.NewAppError("GetGroupsByTeam", "app.select_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
count, err := a.Srv().Store().Group().CountGroupsByTeam(teamID, opts)
if err != nil {
return nil, 0, model.NewAppError("GetGroupsByTeam", "app.select_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return groups, int(count), nil
}
func (a *App) GetGroupsAssociatedToChannelsByTeam(teamID string, opts model.GroupSearchOpts) (map[string][]*model.GroupWithSchemeAdmin, *model.AppError) {
groupsAssociatedByChannelId, err := a.Srv().Store().Group().GetGroupsAssociatedToChannelsByTeam(teamID, opts)
if err != nil {
return nil, model.NewAppError("GetGroupsAssociatedToChannelsByTeam", "app.select_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return groupsAssociatedByChannelId, nil
}
func (a *App) GetGroups(page, perPage int, opts model.GroupSearchOpts, viewRestrictions *model.ViewUsersRestrictions) ([]*model.Group, *model.AppError) {
groups, err := a.Srv().Store().Group().GetGroups(page, perPage, opts, viewRestrictions)
if err != nil {
return nil, model.NewAppError("GetGroups", "app.select_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if opts.IncludeMemberIDs {
for _, group := range groups {
users, err := a.Srv().Store().Group().GetMemberUsers(group.Id)
if err != nil {
return nil, model.NewAppError("GetGroup", "app.member_count", nil, "", http.StatusInternalServerError).Wrap(err)
}
for _, user := range users {
group.MemberIDs = append(group.MemberIDs, user.Id)
}
}
}
return groups, nil
}
// TeamMembersMinusGroupMembers returns the set of users on the given team minus the set of users in the given
// groups.
//
// The result can be used, for example, to determine the set of users who would be removed from a team if the team
// were group-constrained with the given groups.
func (a *App) TeamMembersMinusGroupMembers(teamID string, groupIDs []string, page, perPage int) ([]*model.UserWithGroups, int64, *model.AppError) {
users, err := a.Srv().Store().Group().TeamMembersMinusGroupMembers(teamID, groupIDs, page, perPage)
if err != nil {
return nil, 0, model.NewAppError("TeamMembersMinusGroupMembers", "app.select_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
for _, u := range users {
a.SanitizeProfile(&u.User, false)
}
// parse all group ids of all users
allUsersGroupIDMap := map[string]bool{}
for _, user := range users {
for _, groupID := range user.GetGroupIDs() {
allUsersGroupIDMap[groupID] = true
}
}
// create a slice of distinct group ids
var allUsersGroupIDSlice []string
for key := range allUsersGroupIDMap {
allUsersGroupIDSlice = append(allUsersGroupIDSlice, key)
}
// retrieve groups from DB
groups, appErr := a.GetGroupsByIDs(allUsersGroupIDSlice)
if appErr != nil {
return nil, 0, appErr
}
// map groups by id
groupMap := map[string]*model.Group{}
for _, group := range groups {
groupMap[group.Id] = group
}
// populate each instance's groups field
for _, user := range users {
user.Groups = []*model.Group{}
for _, groupID := range user.GetGroupIDs() {
group, ok := groupMap[groupID]
if ok {
user.Groups = append(user.Groups, group)
}
}
}
totalCount, err := a.Srv().Store().Group().CountTeamMembersMinusGroupMembers(teamID, groupIDs)
if err != nil {
return nil, 0, model.NewAppError("TeamMembersMinusGroupMembers", "app.select_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return users, totalCount, nil
}
func (a *App) GetGroupsByIDs(groupIDs []string) ([]*model.Group, *model.AppError) {
groups, err := a.Srv().Store().Group().GetByIDs(groupIDs)
if err != nil {
return nil, model.NewAppError("GetGroupsByIDs", "app.select_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return groups, nil
}
// ChannelMembersMinusGroupMembers returns the set of users in the given channel minus the set of users in the given
// groups.
//
// The result can be used, for example, to determine the set of users who would be removed from a channel if the
// channel were group-constrained with the given groups.
func (a *App) ChannelMembersMinusGroupMembers(channelID string, groupIDs []string, page, perPage int) ([]*model.UserWithGroups, int64, *model.AppError) {
users, err := a.Srv().Store().Group().ChannelMembersMinusGroupMembers(channelID, groupIDs, page, perPage)
if err != nil {
return nil, 0, model.NewAppError("ChannelMembersMinusGroupMembers", "app.select_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
for _, u := range users {
a.SanitizeProfile(&u.User, false)
}
// parse all group ids of all users
allUsersGroupIDMap := map[string]bool{}
for _, user := range users {
for _, groupID := range user.GetGroupIDs() {
allUsersGroupIDMap[groupID] = true
}
}
// create a slice of distinct group ids
var allUsersGroupIDSlice []string
for key := range allUsersGroupIDMap {
allUsersGroupIDSlice = append(allUsersGroupIDSlice, key)
}
// retrieve groups from DB
groups, appErr := a.GetGroupsByIDs(allUsersGroupIDSlice)
if appErr != nil {
return nil, 0, appErr
}
// map groups by id
groupMap := map[string]*model.Group{}
for _, group := range groups {
groupMap[group.Id] = group
}
// populate each instance's groups field
for _, user := range users {
user.Groups = []*model.Group{}
for _, groupID := range user.GetGroupIDs() {
group, ok := groupMap[groupID]
if ok {
user.Groups = append(user.Groups, group)
}
}
}
totalCount, err := a.Srv().Store().Group().CountChannelMembersMinusGroupMembers(channelID, groupIDs)
if err != nil {
return nil, 0, model.NewAppError("ChannelMembersMinusGroupMembers", "app.select_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return users, totalCount, nil
}
// UserIsInAdminRoleGroup returns true at least one of the user's groups are configured to set the members as
// admins in the given syncable.
func (a *App) UserIsInAdminRoleGroup(userID, syncableID string, syncableType model.GroupSyncableType) (bool, *model.AppError) {
groupIDs, err := a.Srv().Store().Group().AdminRoleGroupsForSyncableMember(userID, syncableID, syncableType)
if err != nil {
return false, model.NewAppError("UserIsInAdminRoleGroup", "app.select_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if len(groupIDs) == 0 {
return false, nil
}
return true, nil
}
func (a *App) UpsertGroupMembers(groupID string, userIDs []string) ([]*model.GroupMember, *model.AppError) {
members, err := a.Srv().Store().Group().UpsertMembers(groupID, userIDs)
if err != nil {
var invErr *store.ErrInvalidInput
var appErr *model.AppError
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &appErr):
return nil, appErr
case errors.As(err, &invErr):
return nil, model.NewAppError("UpsertGroupMembers", "app.group.uniqueness_error", nil, "", http.StatusBadRequest).Wrap(err)
case errors.As(err, &nfErr):
return nil, model.NewAppError("UpsertGroupMembers", "app.group.user_not_found", map[string]any{"Username": nfErr.ID}, "", http.StatusBadRequest).Wrap(err)
default:
return nil, model.NewAppError("UpsertGroupMembers", "app.update_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
for _, groupMember := range members {
if appErr := a.publishGroupMemberEvent(model.WebsocketEventGroupMemberAdd, groupMember); appErr != nil {
return nil, appErr
}
}
return members, nil
}
func (a *App) DeleteGroupMembers(groupID string, userIDs []string) ([]*model.GroupMember, *model.AppError) {
members, err := a.Srv().Store().Group().DeleteMembers(groupID, userIDs)
if err != nil {
var invErr *store.ErrInvalidInput
var appErr *model.AppError
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &appErr):
return nil, appErr
case errors.As(err, &invErr):
return nil, model.NewAppError("DeleteGroupMember", "app.group.uniqueness_error", nil, "", http.StatusBadRequest).Wrap(err)
case errors.As(err, &nfErr):
return nil, model.NewAppError("DeleteGroupMember", "app.group.user_not_found", map[string]any{"Username": nfErr.ID}, "", http.StatusBadRequest).Wrap(err)
default:
return nil, model.NewAppError("DeleteGroupMember", "app.update_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
for _, groupMember := range members {
if appErr := a.publishGroupMemberEvent(model.WebsocketEventGroupMemberDelete, groupMember); appErr != nil {
return nil, appErr
}
}
return members, nil
}
func (a *App) publishGroupMemberEvent(eventName model.WebsocketEventType, groupMember *model.GroupMember) *model.AppError {
messageWs := model.NewWebSocketEvent(eventName, "", "", groupMember.UserId, nil, "")
groupMemberJSON, jsonErr := json.Marshal(groupMember)
if jsonErr != nil {
return model.NewAppError("publishGroupMemberEvent", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(jsonErr)
}
messageWs.Add("group_member", string(groupMemberJSON))
a.Publish(messageWs)
return nil
}