Files
mattermost/server/channels/api4/resolver.go

428 lines
12 KiB
Go

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"context"
"errors"
"fmt"
"github.com/graph-gophers/dataloader/v6"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/channels/web"
)
// cursorPrefix is used to categorize objects
// sent in a cursor. The type is prepended
// to the string with a - to find which
// object the id belongs to.
//
// And after the type is extracted, object
// specific logic can be applied to extract the id.
type cursorPrefix string
const (
channelMemberCursorPrefix cursorPrefix = "channelMember"
channelCursorPrefix cursorPrefix = "channel"
)
type resolver struct {
}
// match with api4.getChannelsForTeamForUser
func (r *resolver) Channels(ctx context.Context, args struct {
TeamID string
UserID string
IncludeDeleted bool
LastDeleteAt float64
LastUpdateAt float64
First int32
After string
}) ([]*channel, error) {
c, err := getCtx(ctx)
if err != nil {
return nil, err
}
if args.UserID == model.Me {
args.UserID = c.AppContext.Session().UserId
}
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), args.UserID) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return nil, c.Err
}
if args.TeamID != "" && !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), args.TeamID, model.PermissionViewTeam) {
c.SetPermissionError(model.PermissionViewTeam)
return nil, c.Err
}
limit := int(args.First)
// ensure args.First limit
if limit == 0 {
limit = web.PerPageDefault
} else if limit > web.PerPageMaximum {
return nil, fmt.Errorf("first parameter %d higher than allowed maximum of %d", limit, web.PerPageMaximum)
}
// ensure args.After format
var afterChannel string
var ok bool
if args.After != "" {
afterChannel, ok = parseChannelCursor(args.After)
if !ok {
return nil, fmt.Errorf("after cursor not in the correct format: %s", args.After)
}
}
// TODO: convert this to a streaming API.
channels, appErr := c.App.GetChannelsForTeamForUserWithCursor(c.AppContext, args.TeamID, args.UserID, &model.ChannelSearchOpts{
IncludeDeleted: args.IncludeDeleted,
LastDeleteAt: int(args.LastDeleteAt),
LastUpdateAt: int(args.LastUpdateAt),
PerPage: model.NewInt(limit),
}, afterChannel)
if appErr != nil {
return nil, appErr
}
appErr = c.App.FillInChannelsProps(c.AppContext, channels)
if appErr != nil {
return nil, appErr
}
return postProcessChannels(c, channels)
}
// match with api4.getUser
func (r *resolver) User(ctx context.Context, args struct{ ID string }) (*user, error) {
return getGraphQLUser(ctx, args.ID)
}
// match with api4.getClientConfig
func (r *resolver) Config(ctx context.Context) (model.StringMap, error) {
c, err := getCtx(ctx)
if err != nil {
return nil, err
}
if c.AppContext.Session().UserId == "" {
return c.App.Srv().Platform().LimitedClientConfigWithComputed(), nil
}
return c.App.Srv().Platform().ClientConfigWithComputed(), nil
}
// match with api4.getClientLicense
func (r *resolver) License(ctx context.Context) (model.StringMap, error) {
c, err := getCtx(ctx)
if err != nil {
return nil, err
}
if c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionReadLicenseInformation) {
return c.App.Srv().ClientLicense(), nil
}
return c.App.Srv().GetSanitizedClientLicense(), nil
}
// match with api4.getTeamMembersForUser for teamID=""
// and api4.getTeamMember for teamID != ""
func (r *resolver) TeamMembers(ctx context.Context, args struct {
UserID string
TeamID string
ExcludeTeam bool
}) ([]*teamMember, error) {
c, err := getCtx(ctx)
if err != nil {
return nil, err
}
if args.UserID == model.Me {
args.UserID = c.AppContext.Session().UserId
}
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), args.UserID) && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionReadOtherUsersTeams) {
c.SetPermissionError(model.PermissionReadOtherUsersTeams)
return nil, c.Err
}
canSee, appErr := c.App.UserCanSeeOtherUser(c.AppContext.Session().UserId, args.UserID)
if appErr != nil {
return nil, appErr
}
if !canSee {
c.SetPermissionError(model.PermissionViewMembers)
return nil, c.Err
}
if args.TeamID != "" && !args.ExcludeTeam {
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), args.TeamID, model.PermissionViewTeam) {
c.SetPermissionError(model.PermissionViewTeam)
return nil, c.Err
}
tm, appErr2 := c.App.GetTeamMember(args.TeamID, args.UserID)
if appErr2 != nil {
return nil, appErr2
}
return []*teamMember{{*tm}}, nil
}
excludeTeamID := ""
if args.TeamID != "" && args.ExcludeTeam {
excludeTeamID = args.TeamID
}
// Do not return archived team members
members, appErr := c.App.GetTeamMembersForUser(args.UserID, excludeTeamID, false)
if appErr != nil {
return nil, appErr
}
// Convert to the wrapper format.
res := make([]*teamMember, 0, len(members))
for _, tm := range members {
res = append(res, &teamMember{*tm})
}
return res, nil
}
func (*resolver) ChannelsLeft(ctx context.Context, args struct {
UserID string
Since float64
}) ([]string, error) {
c, err := getCtx(ctx)
if err != nil {
return nil, err
}
if args.UserID == model.Me {
args.UserID = c.AppContext.Session().UserId
}
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), args.UserID) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return nil, c.Err
}
return c.App.Srv().Store().ChannelMemberHistory().GetChannelsLeftSince(args.UserID, int64(args.Since))
}
// match with api4.getChannelMember
func (*resolver) ChannelMembers(ctx context.Context, args struct {
UserID string
TeamID string
ChannelID string
ExcludeTeam bool
First int32
After string
LastUpdateAt float64
}) ([]*channelMember, error) {
c, err := getCtx(ctx)
if err != nil {
return nil, err
}
if args.UserID == model.Me {
args.UserID = c.AppContext.Session().UserId
}
// If it's a single channel
if args.ChannelID != "" {
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), args.ChannelID, model.PermissionReadChannel) {
c.SetPermissionError(model.PermissionReadChannel)
return nil, c.Err
}
ctx := c.AppContext
ctx.SetContext(app.WithMaster(ctx.Context()))
member, appErr := c.App.GetChannelMember(ctx, args.ChannelID, args.UserID)
if appErr != nil {
return nil, appErr
}
return []*channelMember{{*member}}, nil
}
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), args.UserID) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return nil, c.Err
}
limit := int(args.First)
// ensure args.First limit
if limit == 0 {
limit = web.PerPageDefault
} else if limit > web.PerPageMaximum {
return nil, fmt.Errorf("first parameter %d higher than allowed maximum of %d", limit, web.PerPageMaximum)
}
// ensure args.After format
var afterChannel, afterUser string
var ok bool
if args.After != "" {
afterChannel, afterUser, ok = parseChannelMemberCursor(args.After)
if !ok {
return nil, fmt.Errorf("after cursor not in the correct format: %s", args.After)
}
}
if args.TeamID != "" {
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), args.TeamID, model.PermissionViewTeam) {
primaryTeam := *c.App.Config().TeamSettings.ExperimentalPrimaryTeam
if primaryTeam != "" {
team, appErr := c.App.GetTeamByName(primaryTeam)
if appErr != nil {
return []*channelMember{}, appErr
}
args.TeamID = team.Id
} else {
return []*channelMember{}, nil
}
}
}
opts := &store.ChannelMemberGraphQLSearchOpts{
AfterChannel: afterChannel,
AfterUser: afterUser,
Limit: limit,
LastUpdateAt: int(args.LastUpdateAt),
ExcludeTeam: args.ExcludeTeam,
}
members, err := c.App.Srv().Store().Channel().GetMembersForUserWithCursor(args.UserID, args.TeamID, opts)
if err != nil {
return nil, err
}
res := make([]*channelMember, 0, len(members))
for _, cm := range members {
res = append(res, &channelMember{cm})
}
return res, nil
}
// match with api4.getCategoriesForTeamForUser
func (*resolver) SidebarCategories(ctx context.Context, args struct {
UserID string
TeamID string
ExcludeTeam bool
}) ([]*model.SidebarCategoryWithChannels, error) {
c, err := getCtx(ctx)
if err != nil {
return nil, err
}
// Fallback to primary team logic
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), args.TeamID, model.PermissionViewTeam) {
primaryTeam := *c.App.Config().TeamSettings.ExperimentalPrimaryTeam
if primaryTeam != "" {
team, appErr := c.App.GetTeamByName(primaryTeam)
if appErr != nil {
return []*model.SidebarCategoryWithChannels{}, appErr
}
args.TeamID = team.Id
} else {
return []*model.SidebarCategoryWithChannels{}, nil
}
}
if args.UserID == model.Me {
args.UserID = c.AppContext.Session().UserId
}
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), args.UserID) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return nil, c.Err
}
// If it's only for a single team.
var categories *model.OrderedSidebarCategories
var appErr *model.AppError
if !args.ExcludeTeam {
categories, appErr = c.App.GetSidebarCategoriesForTeamForUser(c.AppContext, args.UserID, args.TeamID)
if appErr != nil {
return nil, appErr
}
} else {
opts := &store.SidebarCategorySearchOpts{
TeamID: args.TeamID,
ExcludeTeam: args.ExcludeTeam,
}
categories, appErr = c.App.GetSidebarCategories(c.AppContext, args.UserID, opts)
if appErr != nil {
return nil, appErr
}
}
// TODO: look into optimizing this.
// create map
orderMap := make(map[string]*model.SidebarCategoryWithChannels, len(categories.Categories))
for _, category := range categories.Categories {
orderMap[category.Id] = category
}
// create a new slice based on the order
res := make([]*model.SidebarCategoryWithChannels, 0, len(categories.Categories))
for _, categoryId := range categories.Order {
res = append(res, orderMap[categoryId])
}
return res, nil
}
// getCtx extracts web.Context out of the usual request context.
// Kind of an anti-pattern, but there are lots of methods attached to *web.Context
// so we use it for now.
func getCtx(ctx context.Context) (*web.Context, error) {
c, ok := ctx.Value(webCtx).(*web.Context)
if !ok {
return nil, errors.New("no web.Context found in context")
}
return c, nil
}
// getRolesLoader returns the roles loader out of the context.
func getRolesLoader(ctx context.Context) (*dataloader.Loader, error) {
l, ok := ctx.Value(rolesLoaderCtx).(*dataloader.Loader)
if !ok {
return nil, errors.New("no dataloader.Loader found in context")
}
return l, nil
}
// getChannelsLoader returns the channels loader out of the context.
func getChannelsLoader(ctx context.Context) (*dataloader.Loader, error) {
l, ok := ctx.Value(channelsLoaderCtx).(*dataloader.Loader)
if !ok {
return nil, errors.New("no dataloader.Loader found in context")
}
return l, nil
}
// getTeamsLoader returns the teams loader out of the context.
func getTeamsLoader(ctx context.Context) (*dataloader.Loader, error) {
l, ok := ctx.Value(teamsLoaderCtx).(*dataloader.Loader)
if !ok {
return nil, errors.New("no dataloader.Loader found in context")
}
return l, nil
}
// getUsersLoader returns the users loader out of the context.
func getUsersLoader(ctx context.Context) (*dataloader.Loader, error) {
l, ok := ctx.Value(usersLoaderCtx).(*dataloader.Loader)
if !ok {
return nil, errors.New("no dataloader.Loader found in context")
}
return l, nil
}