mirror of
https://github.com/mattermost/mattermost.git
synced 2025-02-25 18:55:24 -06:00
Combines the following repositories into one: https://github.com/mattermost/mattermost-server https://github.com/mattermost/mattermost-webapp https://github.com/mattermost/focalboard https://github.com/mattermost/mattermost-plugin-playbooks
428 lines
12 KiB
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
|
|
}
|