// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. package app import ( "encoding/json" "errors" "fmt" "net/http" "slices" "strings" "time" "github.com/mattermost/mattermost/server/v8/channels/utils" "github.com/mattermost/mattermost/server/v8/platform/services/sharedchannel" "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/plugin" "github.com/mattermost/mattermost/server/public/shared/i18n" "github.com/mattermost/mattermost/server/public/shared/mlog" "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/store" "github.com/mattermost/mattermost/server/v8/channels/store/sqlstore" ) const ( UpdateMultipleMaximum = 200 ) // DefaultChannelNames returns the list of system-wide default channel names. // // By default the list will be (not necessarily in this order): // // ['town-square', 'off-topic'] // // However, if TeamSettings.ExperimentalDefaultChannels contains a list of channels then that list will replace // 'off-topic' and be included in the return results in addition to 'town-square'. For example: // // ['town-square', 'game-of-thrones', 'wow'] func (a *App) DefaultChannelNames(rctx request.CTX) []string { names := []string{"town-square"} if len(a.Config().TeamSettings.ExperimentalDefaultChannels) == 0 { names = append(names, "off-topic") } else { seenChannels := map[string]bool{"town-square": true} for _, channelName := range a.Config().TeamSettings.ExperimentalDefaultChannels { if !seenChannels[channelName] { names = append(names, channelName) seenChannels[channelName] = true } } } return names } func (a *App) JoinDefaultChannels(rctx request.CTX, teamID string, user *model.User, shouldBeAdmin bool, userRequestorId string) *model.AppError { var requestor *model.User var nErr error if userRequestorId != "" { requestor, nErr = a.Srv().Store().User().Get(rctx, userRequestorId) if nErr != nil { var nfErr *store.ErrNotFound switch { case errors.As(nErr, &nfErr): return model.NewAppError("JoinDefaultChannels", MissingAccountError, nil, "", http.StatusNotFound).Wrap(nErr) default: return model.NewAppError("JoinDefaultChannels", "app.user.get.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr) } } } for _, channelName := range a.DefaultChannelNames(rctx) { channel, channelErr := a.Srv().Store().Channel().GetByName(teamID, channelName, true) if channelErr != nil { rctx.Logger().Warn("No default channel with this name", mlog.String("channel_name", channelName), mlog.String("team_id", teamID), mlog.Err(channelErr)) continue } if channel.Type != model.ChannelTypeOpen { continue } cm := &model.ChannelMember{ ChannelId: channel.Id, UserId: user.Id, SchemeGuest: user.IsGuest(), SchemeUser: !user.IsGuest(), SchemeAdmin: shouldBeAdmin, NotifyProps: model.GetDefaultChannelNotifyProps(), } _, nErr = a.Srv().Store().Channel().SaveMember(rctx, cm) if histErr := a.Srv().Store().ChannelMemberHistory().LogJoinEvent(user.Id, channel.Id, model.GetMillis()); histErr != nil { return model.NewAppError("JoinDefaultChannels", "app.channel_member_history.log_join_event.internal_error", nil, "", http.StatusInternalServerError).Wrap(histErr) } if *a.Config().ServiceSettings.ExperimentalEnableDefaultChannelLeaveJoinMessages { if aErr := a.postJoinMessageForDefaultChannel(rctx, user, requestor, channel); aErr != nil { rctx.Logger().Warn("Failed to post join/leave message", mlog.Err(aErr)) } } a.invalidateCacheForChannelMembers(channel.Id) message := model.NewWebSocketEvent(model.WebsocketEventUserAdded, "", channel.Id, "", nil, "") message.Add("user_id", user.Id) message.Add("team_id", channel.TeamId) a.Publish(message) } if nErr != nil { var appErr *model.AppError var cErr *store.ErrConflict switch { case errors.As(nErr, &cErr): if cErr.Resource == "ChannelMembers" { return model.NewAppError("JoinDefaultChannels", "app.channel.save_member.exists.app_error", nil, "", http.StatusBadRequest).Wrap(nErr) } case errors.As(nErr, &appErr): return appErr default: return model.NewAppError("JoinDefaultChannels", "app.channel.create_direct_channel.internal_error", nil, "", http.StatusInternalServerError).Wrap(nErr) } } return nil } func (a *App) postJoinMessageForDefaultChannel(rctx request.CTX, user *model.User, requestor *model.User, channel *model.Channel) *model.AppError { if channel.Name == model.DefaultChannelName { if requestor == nil { if err := a.postJoinTeamMessage(rctx, user, channel); err != nil { return err } } else { if err := a.postAddToTeamMessage(rctx, requestor, user, channel, ""); err != nil { return err } } } else { if requestor == nil { if err := a.postJoinChannelMessage(rctx, user, channel); err != nil { return err } } else { if err := a.PostAddToChannelMessage(rctx, requestor, user, channel, ""); err != nil { return err } } } return nil } func (a *App) CreateChannelWithUser(rctx request.CTX, channel *model.Channel, userID string) (*model.Channel, *model.AppError) { return a.CreateChannelWithUserAndPropertyValues(rctx, channel, userID, nil) } // CreateChannelWithUserAndPropertyValues creates a channel and its attribute // values together. propertyValues are already validated and group-stamped by the // caller, and are written before this returns so nothing reacting to channel // creation observes a channel whose attributes are missing. // // A required attribute's "must have a value" rule is enforced only by the api4 // handler that builds propertyValues (api4/channel.go:createChannel), not here. // Every other caller of this function or of CreateChannel — plugin_api.go, // import_functions.go, api4/channel_local.go, sharedchannel/channelinvite.go, // slashcommands/auto_channels.go — reaches it with no propertyValues at all, // which this layer accepts. That is deliberate: required is a creation-time UX // gate for the public API, not a data invariant, and these are trusted paths // the UX gate was never meant to police. func (a *App) CreateChannelWithUserAndPropertyValues(rctx request.CTX, channel *model.Channel, userID string, propertyValues []*model.PropertyValue) (*model.Channel, *model.AppError) { if channel.IsGroupOrDirect() { return nil, model.NewAppError("CreateChannelWithUser", "api.channel.create_channel.direct_channel.app_error", nil, "", http.StatusBadRequest) } if channel.IsBoard() { return nil, model.NewAppError("CreateChannelWithUser", "app.channel.create_channel.board_type.app_error", nil, "use CreateBoardChannel instead", http.StatusBadRequest) } if channel.IsSpace() { return nil, model.NewAppError("CreateChannelWithUser", "app.channel.create_channel.space_type.app_error", nil, "use CreateChannel instead", http.StatusBadRequest) } if channel.TeamId == "" { return nil, model.NewAppError("CreateChannelWithUser", "app.channel.create_channel.no_team_id.app_error", nil, "", http.StatusBadRequest) } // Get total number of channels on current team count, err := a.GetNumberOfChannelsOnTeam(rctx, channel.TeamId) if err != nil { return nil, err } if int64(count+1) > *a.Config().TeamSettings.MaxChannelsPerTeam { return nil, model.NewAppError("CreateChannelWithUser", "api.channel.create_channel.max_channel_limit.app_error", map[string]any{"MaxChannelsPerTeam": *a.Config().TeamSettings.MaxChannelsPerTeam}, "", http.StatusBadRequest) } channel.CreatorId = userID rchannel, err := a.CreateChannel(rctx, channel, true, propertyValues...) if err != nil { return nil, err } a.addChannelToDefaultCategory(rctx, userID, channel) var user *model.User if user, err = a.GetUser(rctx, userID); err != nil { return nil, err } if err = a.postJoinChannelMessage(rctx, user, channel); err != nil { return nil, err } message := model.NewWebSocketEvent(model.WebsocketEventChannelCreated, "", "", userID, nil, "") message.Add("channel_id", channel.Id) message.Add("team_id", channel.TeamId) a.Publish(message) return rchannel, nil } // RenameChannel is used to rename the channel Name and the DisplayName fields func (a *App) RenameChannel(rctx request.CTX, channel *model.Channel, newChannelName string, newDisplayName string) (*model.Channel, *model.AppError) { if channel.Type == model.ChannelTypeDirect { return nil, model.NewAppError("RenameChannel", "api.channel.rename_channel.cant_rename_direct_messages.app_error", nil, "", http.StatusBadRequest) } if channel.Type == model.ChannelTypeGroup { return nil, model.NewAppError("RenameChannel", "api.channel.rename_channel.cant_rename_group_messages.app_error", nil, "", http.StatusBadRequest) } // Clean up the channel name and display name newChannelName = strings.TrimSpace(newChannelName) newDisplayName = strings.TrimSpace(newDisplayName) channel.Name = newChannelName if newDisplayName != "" { channel.DisplayName = newDisplayName } newChannel, err := a.UpdateChannel(rctx, channel) if err != nil { return nil, err } return newChannel, nil } func (a *App) CreateChannel(rctx request.CTX, channel *model.Channel, addMember bool, propertyValues ...*model.PropertyValue) (*model.Channel, *model.AppError) { if channel.IsBoard() { return nil, model.NewAppError("CreateChannel", "app.channel.create_channel.board_type.app_error", nil, "use CreateBoardChannel instead", http.StatusBadRequest) } if channel.IsSpace() && !a.Config().FeatureFlags.EnableDocs { return nil, model.NewAppError("CreateChannel", "app.channel.create_channel.spaces_not_enabled.app_error", nil, "", http.StatusForbidden) } channel.DisplayName = strings.TrimSpace(channel.DisplayName) channel.DefaultCategoryName = strings.TrimSpace(channel.DefaultCategoryName) channel.ManagedCategoryName = strings.TrimSpace(channel.ManagedCategoryName) sc, nErr := a.Srv().Store().Channel().Save(rctx, channel, *a.Config().TeamSettings.MaxChannelsPerTeam) if nErr != nil { var invErr *store.ErrInvalidInput var cErr *store.ErrConflict var ltErr *store.ErrLimitExceeded var appErr *model.AppError switch { case errors.As(nErr, &invErr): switch { case invErr.Entity == "Channel" && invErr.Field == "DeleteAt": return nil, model.NewAppError("CreateChannel", "store.sql_channel.save.archived_channel.app_error", nil, "", http.StatusBadRequest).Wrap(nErr) case invErr.Entity == "Channel" && invErr.Field == "Type": return nil, model.NewAppError("CreateChannel", "store.sql_channel.save.direct_channel.app_error", nil, "", http.StatusBadRequest).Wrap(nErr) case invErr.Entity == "Channel" && invErr.Field == "Id": return nil, model.NewAppError("CreateChannel", "store.sql_channel.save_channel.existing.app_error", nil, "id="+invErr.Value.(string), http.StatusBadRequest).Wrap(nErr) } case errors.As(nErr, &cErr): return sc, model.NewAppError("CreateChannel", store.ChannelExistsError, nil, "", http.StatusBadRequest).Wrap(nErr) case errors.As(nErr, <Err): return nil, model.NewAppError("CreateChannel", "store.sql_channel.save_channel.limit.app_error", nil, "", http.StatusBadRequest).Wrap(nErr) case errors.As(nErr, &appErr): // in case we haven't converted to plain error. return nil, appErr default: // last fallback in case it doesn't map to an existing app error. return nil, model.NewAppError("CreateChannel", "app.channel.create_channel.internal_error", nil, "", http.StatusInternalServerError).Wrap(nErr) } } if addMember { user, nErr := a.Srv().Store().User().Get(rctx, channel.CreatorId) if nErr != nil { var nfErr *store.ErrNotFound switch { case errors.As(nErr, &nfErr): return nil, model.NewAppError("CreateChannel", MissingAccountError, nil, "", http.StatusNotFound).Wrap(nErr) default: return nil, model.NewAppError("CreateChannel", "app.user.get.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr) } } cm := &model.ChannelMember{ ChannelId: sc.Id, UserId: user.Id, SchemeGuest: user.IsGuest(), SchemeUser: !user.IsGuest(), SchemeAdmin: true, NotifyProps: model.GetDefaultChannelNotifyProps(), } if _, nErr := a.Srv().Store().Channel().SaveMember(rctx, cm); nErr != nil { var appErr *model.AppError var cErr *store.ErrConflict switch { case errors.As(nErr, &cErr): switch cErr.Resource { case "ChannelMembers": return nil, model.NewAppError("CreateChannel", "app.channel.save_member.exists.app_error", nil, "", http.StatusBadRequest).Wrap(nErr) } case errors.As(nErr, &appErr): return nil, appErr default: return nil, model.NewAppError("CreateChannel", "app.channel.create_direct_channel.internal_error", nil, "", http.StatusInternalServerError).Wrap(nErr) } } if err := a.Srv().Store().ChannelMemberHistory().LogJoinEvent(channel.CreatorId, sc.Id, model.GetMillis()); err != nil { return nil, model.NewAppError("CreateChannel", "app.channel_member_history.log_join_event.internal_error", nil, "", http.StatusInternalServerError).Wrap(err) } a.Srv().Platform().InvalidateChannelCacheForUser(channel.CreatorId) } if channel.ManagedCategoryName != "" { if !model.MinimumEnterpriseLicense(a.Channels().License()) || !a.Config().FeatureFlags.ManagedChannelCategories { rctx.Logger().Warn("Managed category update ignored: feature not available") sc.ManagedCategoryName = "" } else { if appErr := a.SetChannelManagedCategory(rctx, sc.Id, channel.ManagedCategoryName); appErr != nil { rctx.Logger().Error("Failed to set managed category on new channel", mlog.String("channel_id", sc.Id), mlog.String("category_name", channel.ManagedCategoryName), mlog.Err(appErr)) sc.ManagedCategoryName = "" } } } if len(propertyValues) > 0 { if appErr := a.setChannelPropertyValuesOnCreate(rctx, sc, propertyValues); appErr != nil { return nil, appErr } } if sc.IsSpace() { return sc, nil } a.Srv().Go(func() { pluginContext := pluginContext(rctx) a.ch.RunMultiHook(func(hooks plugin.Hooks, _ *model.Manifest) bool { hooks.ChannelHasBeenCreated(pluginContext, sc) return true }, plugin.ChannelHasBeenCreatedID) }) return sc, nil } // setChannelPropertyValuesOnCreate writes a new channel's attribute values, and // removes the channel again if that write fails. Compensating is safe only here: // the values go in before the join message, the channel_created event and the // plugin hook, so nobody has been told the channel exists and nobody but the // creator is in it. There is no transaction spanning the channel and property // stores, so this is the only way a caller gets all-or-nothing. func (a *App) setChannelPropertyValuesOnCreate(rctx request.CTX, channel *model.Channel, values []*model.PropertyValue) *model.AppError { for _, value := range values { value.TargetID = channel.Id value.TargetType = model.PropertyValueTargetTypeChannel value.CreatedBy = channel.CreatorId value.UpdatedBy = channel.CreatorId } _, appErr := a.UpsertPropertyValues(rctx, values, model.PropertyFieldObjectTypeChannel, channel.Id, "") if appErr == nil { return nil } if deleteErr := a.PermanentDeleteChannel(rctx, channel); deleteErr != nil { // The channel now exists without its required values, which is the state // this whole path exists to prevent — so it is logged loudly rather than // folded into the error the caller sees. rctx.Logger().Error("Failed to remove channel after its attribute values could not be written", mlog.String("channel_id", channel.Id), mlog.Err(deleteErr)) } return appErr } func (a *App) GetOrCreateDirectChannel(rctx request.CTX, userID, otherUserID string, channelOptions ...model.ChannelOption) (*model.Channel, *model.AppError) { channel, nErr := a.getDirectChannel(rctx, userID, otherUserID) if nErr != nil { return nil, nErr } if channel != nil { return channel, nil } if *a.Config().TeamSettings.RestrictDirectMessage == model.DirectMessageTeam && !a.SessionHasPermissionTo(*rctx.Session(), model.PermissionManageSystem) { users, err := a.GetUsersByIds(rctx, []string{userID, otherUserID}, &store.UserGetByIdsOpts{}) if err != nil { return nil, err } var isBotExempt bool for _, user := range users { if user.IsBot { exempt, err := a.IsBotExemptFromDMRestrictions(rctx, user.Id) if err != nil { return nil, err } if exempt { isBotExempt = true break } } } // if one of the users is an exempt bot, don't restrict to team members if !isBotExempt { commonTeamIDs, err := a.GetCommonTeamIDsForTwoUsers(userID, otherUserID) if err != nil { return nil, err } if len(commonTeamIDs) == 0 { return nil, model.NewAppError("createDirectChannel", "api.channel.create_channel.direct_channel.team_restricted_error", nil, "", http.StatusForbidden) } } } channel, err := a.createDirectChannel(rctx, userID, otherUserID, channelOptions...) if err != nil { if err.Id == store.ChannelExistsError { return channel, nil } return nil, err } a.handleCreationEvent(rctx, userID, otherUserID, channel) return channel, nil } func (a *App) getOrCreateDirectChannelWithUser(rctx request.CTX, user, otherUser *model.User) (*model.Channel, *model.AppError) { channel, nErr := a.getDirectChannel(rctx, user.Id, otherUser.Id) if nErr != nil { return nil, nErr } if channel != nil { return channel, nil } channel, err := a.createDirectChannelWithUser(rctx, user, otherUser) if err != nil { if err.Id == store.ChannelExistsError { return channel, nil } return nil, err } a.handleCreationEvent(rctx, user.Id, otherUser.Id, channel) return channel, nil } func (a *App) handleCreationEvent(rctx request.CTX, userID, otherUserID string, channel *model.Channel) { a.Srv().Platform().InvalidateChannelCacheForUser(userID) a.Srv().Platform().InvalidateChannelCacheForUser(otherUserID) a.Srv().Go(func() { pluginContext := pluginContext(rctx) a.ch.RunMultiHook(func(hooks plugin.Hooks, _ *model.Manifest) bool { hooks.ChannelHasBeenCreated(pluginContext, channel) return true }, plugin.ChannelHasBeenCreatedID) }) message := model.NewWebSocketEvent(model.WebsocketEventDirectAdded, "", channel.Id, "", nil, "") message.Add("creator_id", userID) message.Add("teammate_id", otherUserID) a.Publish(message) } func (a *App) createDirectChannel(rctx request.CTX, userID string, otherUserID string, channelOptions ...model.ChannelOption) (*model.Channel, *model.AppError) { users, err := a.Srv().Store().User().GetMany(rctx, []string{userID, otherUserID}) if err != nil { return nil, model.NewAppError("CreateDirectChannel", "api.channel.create_direct_channel.invalid_user.app_error", nil, "", http.StatusBadRequest).Wrap(err) } if len(users) == 0 { return nil, model.NewAppError("CreateDirectChannel", "api.channel.create_direct_channel.invalid_user.app_error", nil, fmt.Sprintf("No users found for ids: %s. %s", userID, otherUserID), http.StatusBadRequest) } // We are doing this because we allow a user to create a direct channel with themselves if userID == otherUserID { users = append(users, users[0]) } // After we counted for direct channels with the same user, if we do not have two users then we failed to find one if len(users) != 2 { return nil, model.NewAppError("CreateDirectChannel", "api.channel.create_direct_channel.invalid_user.app_error", nil, fmt.Sprintf("No users found for ids: %s. %s", userID, otherUserID), http.StatusBadRequest) } // The potential swap dance below is necessary in order to guarantee determinism when creating a direct channel. // When we query the database for some given user ids, the database result is not deterministic, meaning we can get // the same results but in different order. In order to conform the contract of Channel.CreateDirectChannel method // below we need to identify which user is who. user := users[0] otherUser := users[1] if user.Id != userID { user = users[1] otherUser = users[0] } return a.createDirectChannelWithUser(rctx, user, otherUser, channelOptions...) } func (a *App) createDirectChannelWithUser(rctx request.CTX, user, otherUser *model.User, channelOptions ...model.ChannelOption) (*model.Channel, *model.AppError) { if !a.Config().FeatureFlags.EnableSharedChannelsDMs && (user.IsRemote() || otherUser.IsRemote()) { return nil, model.NewAppError("createDirectChannelWithUser", "api.channel.create_channel.direct_channel.remote_restricted.app_error", nil, "", http.StatusForbidden) } channel, nErr := a.Srv().Store().Channel().CreateDirectChannel(rctx, user, otherUser, channelOptions...) if nErr != nil { var invErr *store.ErrInvalidInput var cErr *store.ErrConflict var ltErr *store.ErrLimitExceeded var appErr *model.AppError switch { case errors.As(nErr, &invErr): switch { case invErr.Entity == "Channel" && invErr.Field == "DeleteAt": return nil, model.NewAppError("createDirectChannelWithUser", "store.sql_channel.save.archived_channel.app_error", nil, "", http.StatusBadRequest).Wrap(nErr) case invErr.Entity == "Channel" && invErr.Field == "Type": return nil, model.NewAppError("createDirectChannelWithUser", "store.sql_channel.save_direct_channel.not_direct.app_error", nil, "", http.StatusBadRequest).Wrap(nErr) case invErr.Entity == "Channel" && invErr.Field == "Id": return nil, model.NewAppError("SqlChannelStore.Save", "store.sql_channel.save_channel.existing.app_error", nil, "id="+invErr.Value.(string), http.StatusBadRequest).Wrap(nErr) } case errors.As(nErr, &cErr): switch cErr.Resource { case "Channel": return channel, model.NewAppError("createDirectChannelWithUser", store.ChannelExistsError, nil, "", http.StatusBadRequest).Wrap(nErr) case "ChannelMembers": return nil, model.NewAppError("createDirectChannelWithUser", "app.channel.save_member.exists.app_error", nil, "", http.StatusBadRequest).Wrap(nErr) } case errors.As(nErr, <Err): return nil, model.NewAppError("createDirectChannelWithUser", "store.sql_channel.save_channel.limit.app_error", nil, "", http.StatusBadRequest).Wrap(nErr) case errors.As(nErr, &appErr): // in case we haven't converted to plain error. return nil, appErr default: // last fallback in case it doesn't map to an existing app error. return nil, model.NewAppError("createDirectChannelWithUser", "app.channel.create_direct_channel.internal_error", nil, "", http.StatusInternalServerError).Wrap(nErr) } } if err := a.Srv().Store().ChannelMemberHistory().LogJoinEvent(user.Id, channel.Id, model.GetMillis()); err != nil { return nil, model.NewAppError("createDirectChannelWithUser", "app.channel_member_history.log_join_event.internal_error", nil, "", http.StatusInternalServerError).Wrap(err) } if user.Id != otherUser.Id { if err := a.Srv().Store().ChannelMemberHistory().LogJoinEvent(otherUser.Id, channel.Id, model.GetMillis()); err != nil { return nil, model.NewAppError("createDirectChannelWithUser", "app.channel_member_history.log_join_event.internal_error", nil, "", http.StatusInternalServerError).Wrap(err) } } // When the newly created channel is shared and the creator is local // create a local shared channel record if channel.IsShared() && !user.IsRemote() { sc := &model.SharedChannel{ ChannelId: channel.Id, TeamId: channel.TeamId, Home: true, ReadOnly: false, ShareName: channel.Name, ShareDisplayName: channel.DisplayName, SharePurpose: channel.Purpose, ShareHeader: channel.Header, CreatorId: user.Id, Type: channel.Type, } if _, err := a.ShareChannel(rctx, sc); err != nil { rctx.Logger().Error("Failed to share newly created direct channel", mlog.String("channel_id", channel.Id), mlog.Err(err)) } } return channel, nil } func (a *App) CreateGroupChannel(rctx request.CTX, userIDs []string, creatorId string, channelOptions ...model.ChannelOption) (*model.Channel, *model.AppError) { channel, err := a.createGroupChannel(rctx, userIDs, creatorId, channelOptions...) if err != nil { if err.Id == store.ChannelExistsError { return channel, nil } return nil, err } jsonIDs := model.ArrayToJSON(userIDs) for _, userID := range userIDs { a.Srv().Platform().InvalidateChannelCacheForUser(userID) message := model.NewWebSocketEvent(model.WebsocketEventGroupAdded, "", channel.Id, userID, nil, "") message.Add("teammate_ids", jsonIDs) a.Publish(message) } return channel, nil } // creatorId is used to determine if the group channel should have a // shared channel record attached. It can be empty if the caller // doesn't know who the creator is (e.g. the import process) and the // resulting group channel will not be shared func (a *App) createGroupChannel(rctx request.CTX, userIDs []string, creatorID string, channelOptions ...model.ChannelOption) (*model.Channel, *model.AppError) { if len(userIDs) > model.ChannelGroupMaxUsers || len(userIDs) < model.ChannelGroupMinUsers { return nil, model.NewAppError("CreateGroupChannel", "api.channel.create_group.bad_size.app_error", nil, "", http.StatusBadRequest) } // we skip cache and use master when fetching profiles to avoid // issues in shared channels and HA, when users are created from a // shared channels GM invite right before creating the GM users, err := a.Srv().Store().User().GetProfileByIds(sqlstore.RequestContextWithMaster(rctx), userIDs, nil, false) if err != nil { return nil, model.NewAppError("createGroupChannel", "app.user.get_profiles.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } if len(users) != len(userIDs) { return nil, model.NewAppError("CreateGroupChannel", "api.channel.create_group.bad_user.app_error", nil, "user_ids="+model.ArrayToJSON(userIDs), http.StatusBadRequest) } // extracts the creator and the remotes involved in the GM to // decide how to handle the shared part of the creation var creator *model.User remoteIDs := map[string]bool{} for _, user := range users { if user.Id == creatorID { creator = user } if user.IsRemote() { remoteIDs[*user.RemoteId] = true } } channelIsShared := len(remoteIDs) > 0 if channelIsShared && !a.Config().FeatureFlags.EnableSharedChannelsDMs { for _, user := range users { if user.IsRemote() { return nil, model.NewAppError("createGroupChannel", "api.channel.create_group.remote_restricted.app_error", nil, "", http.StatusForbidden) } } } group := &model.Channel{ Name: model.GetGroupNameFromUserIds(userIDs), DisplayName: model.GetGroupDisplayNameFromUsers(users, true), Type: model.ChannelTypeGroup, Shared: new(channelIsShared), } channel, nErr := a.Srv().Store().Channel().Save(rctx, group, *a.Config().TeamSettings.MaxChannelsPerTeam, channelOptions...) if nErr != nil { var invErr *store.ErrInvalidInput var cErr *store.ErrConflict var ltErr *store.ErrLimitExceeded var appErr *model.AppError switch { case errors.As(nErr, &invErr): switch { case invErr.Entity == "Channel" && invErr.Field == "DeleteAt": return nil, model.NewAppError("CreateChannel", "store.sql_channel.save.archived_channel.app_error", nil, "", http.StatusBadRequest).Wrap(nErr) case invErr.Entity == "Channel" && invErr.Field == "Type": return nil, model.NewAppError("CreateChannel", "store.sql_channel.save.direct_channel.app_error", nil, "", http.StatusBadRequest).Wrap(nErr) case invErr.Entity == "Channel" && invErr.Field == "Id": return nil, model.NewAppError("CreateChannel", "store.sql_channel.save_channel.existing.app_error", nil, "id="+invErr.Value.(string), http.StatusBadRequest).Wrap(nErr) } case errors.As(nErr, &cErr): return channel, model.NewAppError("CreateChannel", store.ChannelExistsError, nil, "", http.StatusBadRequest).Wrap(nErr) case errors.As(nErr, <Err): return nil, model.NewAppError("CreateChannel", "store.sql_channel.save_channel.limit.app_error", nil, "", http.StatusBadRequest).Wrap(nErr) case errors.As(nErr, &appErr): // in case we haven't converted to plain error. return nil, appErr default: // last fallback in case it doesn't map to an existing app error. return nil, model.NewAppError("CreateChannel", "app.channel.create_channel.internal_error", nil, "", http.StatusInternalServerError).Wrap(nErr) } } for _, user := range users { cm := &model.ChannelMember{ UserId: user.Id, ChannelId: channel.Id, NotifyProps: model.GetDefaultChannelNotifyProps(), SchemeGuest: user.IsGuest(), SchemeUser: !user.IsGuest(), } if _, nErr = a.Srv().Store().Channel().SaveMember(rctx, cm); nErr != nil { var appErr *model.AppError var cErr *store.ErrConflict switch { case errors.As(nErr, &cErr): switch cErr.Resource { case "ChannelMembers": return nil, model.NewAppError("createGroupChannel", "app.channel.save_member.exists.app_error", nil, "", http.StatusBadRequest).Wrap(nErr) } case errors.As(nErr, &appErr): return nil, appErr default: return nil, model.NewAppError("createGroupChannel", "app.channel.create_direct_channel.internal_error", nil, "", http.StatusInternalServerError).Wrap(nErr) } } if err := a.Srv().Store().ChannelMemberHistory().LogJoinEvent(user.Id, channel.Id, model.GetMillis()); err != nil { return nil, model.NewAppError("createGroupChannel", "app.channel_member_history.log_join_event.internal_error", nil, "", http.StatusInternalServerError).Wrap(err) } } // When the newly created channel is shared, the creator is local // and one of the participants is remote create a local shared // channel record if channel.IsShared() && creator != nil && !creator.IsRemote() { sc := &model.SharedChannel{ ChannelId: channel.Id, TeamId: channel.TeamId, Home: true, ReadOnly: false, ShareName: channel.Name, ShareDisplayName: channel.DisplayName, SharePurpose: channel.Purpose, ShareHeader: channel.Header, CreatorId: creatorID, Type: channel.Type, } if _, err := a.ShareChannel(rctx, sc); err != nil { rctx.Logger().Error("Failed to share newly created group channel", mlog.String("channel_id", channel.Id), mlog.Err(err)) } else { // if we could successfully share the channel, we invite // the remotes involved to it if sc, _ := a.getSharedChannelsService(false); sc != nil { for remoteID := range remoteIDs { rc, err := a.Srv().Store().RemoteCluster().Get(remoteID, false) if err != nil { rctx.Logger().Error("Failed to send invite to group message channel, can't retrieve remote cluster", mlog.String("channel_id", channel.Id), mlog.String("remote_id", remoteID), mlog.Err(err)) continue } opts := []sharedchannel.InviteOption{sharedchannel.WithCreator(creatorID)} for _, user := range users { opts = append(opts, sharedchannel.WithDirectParticipant(user, remoteID)) } if err := sc.SendChannelInvite(channel, creatorID, rc, opts...); err != nil { rctx.Logger().Error("Failed to send invite to group message channel, error sending the invite", mlog.String("channel_id", channel.Id), mlog.String("remote_id", remoteID), mlog.Err(err)) } } } } } a.Srv().Go(func() { pluginContext := pluginContext(rctx) a.ch.RunMultiHook(func(hooks plugin.Hooks, _ *model.Manifest) bool { hooks.ChannelHasBeenCreated(pluginContext, channel) return true }, plugin.ChannelHasBeenCreatedID) }) return channel, nil } func (a *App) GetGroupChannel(rctx request.CTX, userIDs []string) (*model.Channel, *model.AppError) { if len(userIDs) > model.ChannelGroupMaxUsers || len(userIDs) < model.ChannelGroupMinUsers { return nil, model.NewAppError("GetGroupChannel", "api.channel.create_group.bad_size.app_error", nil, "", http.StatusBadRequest) } users, err := a.Srv().Store().User().GetProfileByIds(rctx, userIDs, nil, true) if err != nil { return nil, model.NewAppError("GetGroupChannel", "app.user.get_profiles.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } if len(users) != len(userIDs) { return nil, model.NewAppError("GetGroupChannel", "api.channel.create_group.bad_user.app_error", nil, "user_ids="+model.ArrayToJSON(userIDs), http.StatusBadRequest) } channel, appErr := a.GetChannelByName(rctx, model.GetGroupNameFromUserIds(userIDs), "", true) if appErr != nil { return nil, appErr } return channel, nil } // UpdateChannel updates a given channel by its Id. It also publishes the CHANNEL_UPDATED event. func (a *App) UpdateChannel(rctx request.CTX, channel *model.Channel) (*model.Channel, *model.AppError) { // The generic Get excludes spaces, so fetch a space by its exact type instead; otherwise // UpdateChannel can't load the existing channel and a rename or header edit would fail with // a not-found before it reaches the store. // Read from master: spaces are uncached, so an update right after create would otherwise // miss against a lagging replica. var oldChannel *model.Channel var getErr error if channel.IsSpace() { oldChannel, getErr = a.Srv().Store().Channel().GetChannelOfType(RequestContextWithMaster(rctx), channel.Id, model.ChannelTypeSpace) } else { oldChannel, getErr = a.Srv().Store().Channel().Get(channel.Id, true) } if getErr != nil { errCtx := map[string]any{"channel_id": channel.Id} var nfErr *store.ErrNotFound switch { case errors.As(getErr, &nfErr): return nil, model.NewAppError("UpdateChannel", "app.channel.get.existing.app_error", errCtx, "", http.StatusNotFound).Wrap(getErr) default: return nil, model.NewAppError("UpdateChannel", "app.channel.get.find.app_error", errCtx, "", http.StatusInternalServerError).Wrap(getErr) } } // Space backing channels are internal: skip ABAC enforcement and the plugin // ChannelWillBeUpdated hook, matching the other space lifecycle paths. if !channel.IsSpace() { enforced, appErr := a.ChannelAccessControlled(rctx, channel.Id) if appErr != nil { return nil, appErr } if enforced { if channel.Type != model.ChannelTypePrivate && channel.Type != model.ChannelTypeOpen { return nil, model.NewAppError("UpdateChannel", "api.channel.update_channel.not_allowed.app_error", nil, "", http.StatusForbidden) } // Block public ↔ private conversion while an ABAC policy is attached. // Public-channel and private-channel ABAC have asymmetric semantics // (advisory recommend/auto-add vs hard-gate with member removal); a // silent type flip would change what the existing policy actually // does to members. The admin must remove the policy first and // re-apply it after the conversion if they still want it. if oldChannel.Type != channel.Type { return nil, model.NewAppError("UpdateChannel", "api.channel.update_channel.policy_enforced_type_conversion.app_error", nil, "channel has an active ABAC policy; remove the policy before converting between public and private", http.StatusBadRequest) } } var channelErr *model.AppError channel, channelErr = a.runGuardedChannelWillBeUpdated(rctx, channel, oldChannel) if channelErr != nil { return nil, channelErr } } _, err := a.Srv().Store().Channel().Update(rctx, channel) if err != nil { var appErr *model.AppError var uniqueConstraintErr *store.ErrUniqueConstraint var invErr *store.ErrInvalidInput switch { case errors.As(err, &uniqueConstraintErr): return nil, model.NewAppError("UpdateChannel", store.ChannelExistsError, nil, "", http.StatusBadRequest).Wrap(err) case errors.As(err, &invErr): return nil, model.NewAppError("UpdateChannel", "app.channel.update.bad_id", nil, "", http.StatusBadRequest).Wrap(err) case errors.As(err, &appErr): return nil, appErr default: return nil, model.NewAppError("UpdateChannel", "app.channel.update_channel.internal_error", nil, "", http.StatusInternalServerError).Wrap(err) } } a.Srv().Platform().InvalidateCacheForChannel(channel) // Space backing channels are internal: skip the channel_updated broadcast. if channel.IsSpace() { return channel, nil } messageWs := model.NewWebSocketEvent(model.WebsocketEventChannelUpdated, "", channel.Id, "", nil, "") channelJSON, jsonErr := json.Marshal(channel) if jsonErr != nil { return nil, model.NewAppError("UpdateChannel", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(jsonErr) } messageWs.Add("channel", string(channelJSON)) a.Publish(messageWs) return channel, nil } // CreateChannelScheme creates a new Scheme of scope channel and assigns it to the channel. func (a *App) CreateChannelScheme(rctx request.CTX, channel *model.Channel) (*model.Scheme, *model.AppError) { scheme, err := a.CreateScheme(&model.Scheme{ Name: model.NewId(), DisplayName: model.NewId(), Scope: model.SchemeScopeChannel, }) if err != nil { return nil, err } channel.SchemeId = &scheme.Id if _, err := a.UpdateChannelScheme(rctx, channel); err != nil { return nil, err } return scheme, nil } // DeleteChannelScheme deletes a channels scheme and sets its SchemeId to nil. func (a *App) DeleteChannelScheme(rctx request.CTX, channel *model.Channel) (*model.Channel, *model.AppError) { if channel.SchemeId != nil && *channel.SchemeId != "" { if _, err := a.DeleteScheme(*channel.SchemeId); err != nil { return nil, err } } channel.SchemeId = nil return a.UpdateChannelScheme(rctx, channel) } // UpdateChannelScheme saves the new SchemeId of the channel passed. func (a *App) UpdateChannelScheme(rctx request.CTX, channel *model.Channel) (*model.Channel, *model.AppError) { var oldChannel *model.Channel var err *model.AppError if oldChannel, err = a.GetChannel(rctx, channel.Id); err != nil { return nil, err } oldChannel.SchemeId = channel.SchemeId return a.UpdateChannel(rctx, oldChannel) } func (a *App) UpdateChannelPrivacy(rctx request.CTX, oldChannel *model.Channel, user *model.User) (*model.Channel, *model.AppError) { if oldChannel.IsSpace() { return nil, model.NewAppError("UpdateChannelPrivacy", "app.channel.update_channel_privacy.space.app_error", nil, "", http.StatusBadRequest) } wasDiscoverable := oldChannel.Discoverable // Public channels are inherently joinable; the discoverable flag only // has meaning for private channels. Clear it eagerly so callers reading // the row mid-conversion don't see an inconsistent state. if oldChannel.Type == model.ChannelTypeOpen { oldChannel.Discoverable = false } channel, err := a.UpdateChannel(rctx, oldChannel) if err != nil { return channel, err } postErr := a.postChannelPrivacyMessage(rctx, user, channel) if postErr != nil { if channel.Type == model.ChannelTypeOpen { channel.Type = model.ChannelTypePrivate // Restore the discoverable flag we eagerly cleared above so // the rollback fully undoes the conversion. Without this the // caller would see a private channel with discoverable=false // (and would have to re-toggle it). channel.Discoverable = wasDiscoverable } else { channel.Type = model.ChannelTypeOpen } // revert to previous channel privacy if _, err = a.UpdateChannel(rctx, channel); err != nil { a.Log().Error("Failed to revert channel privacy after posting an update message failed", mlog.Err(err)) } return channel, postErr } // Now that the conversion is fully committed, cancel pending join // requests for the formerly discoverable private channel — the WS // broadcast inside the helper updates each requester's My Pending // Requests list in real-time. Doing this after the privacy-message // step ensures a transient post failure (which triggers the rollback // above) cannot leave requests cancelled against a still-private // channel. if wasDiscoverable && channel.Type == model.ChannelTypeOpen { a.Srv().Go(func() { a.CancelPendingChannelJoinRequestsOnConvert(rctx, channel) }) } a.Srv().Platform().InvalidateCacheForChannel(channel) messageWs := model.NewWebSocketEvent(model.WebsocketEventChannelConverted, channel.TeamId, "", "", nil, "") messageWs.Add("channel_id", channel.Id) messageWs.Add("channel_type", string(channel.Type)) a.Publish(messageWs) return channel, nil } func (a *App) postChannelPrivacyMessage(rctx request.CTX, user *model.User, channel *model.Channel) *model.AppError { var authorId string var authorUsername string if user != nil { authorId = user.Id authorUsername = user.Username } else { systemBot, err := a.GetSystemBot(rctx) if err != nil { return model.NewAppError("postChannelPrivacyMessage", "api.channel.post_channel_privacy_message.error", nil, "", http.StatusInternalServerError).Wrap(err) } authorId = systemBot.UserId authorUsername = systemBot.Username } message := (map[model.ChannelType]string{ model.ChannelTypeOpen: i18n.T("api.channel.change_channel_privacy.private_to_public"), model.ChannelTypePrivate: i18n.T("api.channel.change_channel_privacy.public_to_private"), })[channel.Type] post := &model.Post{ ChannelId: channel.Id, Message: message, Type: model.PostTypeChangeChannelPrivacy, UserId: authorId, Props: model.StringInterface{ "username": authorUsername, }, } if _, _, err := a.CreatePost(rctx, post, channel, model.CreatePostFlags{SetOnline: true}); err != nil { return model.NewAppError("postChannelPrivacyMessage", "api.channel.post_channel_privacy_message.error", nil, "", http.StatusInternalServerError).Wrap(err) } return nil } func (a *App) RestoreChannel(rctx request.CTX, channel *model.Channel, userID string) (*model.Channel, *model.AppError) { if channel.DeleteAt == 0 { return nil, model.NewAppError("restoreChannel", "api.channel.restore_channel.restored.app_error", nil, "", http.StatusBadRequest) } // Space backing channels are internal; plugin ChannelWillBeRestored hooks are skipped. if !channel.IsSpace() { if appErr := a.runGuardedChannelWillBeRestored(rctx, channel); appErr != nil { return nil, appErr } } if err := a.Srv().Store().Channel().Restore(channel.Id, model.GetMillis()); err != nil { return nil, model.NewAppError("RestoreChannel", "app.channel.restore.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } channel.DeleteAt = 0 a.Srv().Platform().InvalidateCacheForChannel(channel) // Space backing channels are internal: skip the channel_restored chat event and system post. if channel.IsSpace() { return channel, nil } var message *model.WebSocketEvent if channel.Type == model.ChannelTypeOpen { message = model.NewWebSocketEvent(model.WebsocketEventChannelRestored, channel.TeamId, "", "", nil, "") } else { message = model.NewWebSocketEvent(model.WebsocketEventChannelRestored, "", channel.Id, "", nil, "") } message.Add("channel_id", channel.Id) a.Publish(message) var user *model.User if userID != "" { var nErr error user, nErr = a.Srv().Store().User().Get(rctx, userID) if nErr != nil { var nfErr *store.ErrNotFound switch { case errors.As(nErr, &nfErr): return nil, model.NewAppError("RestoreChannel", MissingAccountError, nil, "", http.StatusNotFound).Wrap(nErr) default: return nil, model.NewAppError("RestoreChannel", "app.user.get.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr) } } } if user != nil { T := i18n.GetUserTranslations(user.Locale) post := &model.Post{ ChannelId: channel.Id, Message: T("api.channel.restore_channel.unarchived", map[string]any{"Username": user.Username}), Type: model.PostTypeChannelRestored, UserId: userID, Props: model.StringInterface{ "username": user.Username, }, } if _, _, err := a.CreatePost(rctx, post, channel, model.CreatePostFlags{SetOnline: true}); err != nil { rctx.Logger().Warn("Failed to post unarchive message", mlog.Err(err)) } } else { a.Srv().Go(func() { systemBot, err := a.GetSystemBot(rctx) if err != nil { rctx.Logger().Error("Failed to post unarchive message", mlog.Err(err)) return } post := &model.Post{ ChannelId: channel.Id, Message: i18n.T("api.channel.restore_channel.unarchived", map[string]any{"Username": systemBot.Username}), Type: model.PostTypeChannelRestored, UserId: systemBot.UserId, Props: model.StringInterface{ "username": systemBot.Username, }, } if _, _, err := a.CreatePost(rctx, post, channel, model.CreatePostFlags{SetOnline: true}); err != nil { rctx.Logger().Error("Failed to post unarchive message", mlog.Err(err)) } }) } return channel, nil } func (a *App) PatchChannel(rctx request.CTX, channel *model.Channel, patch *model.ChannelPatch, userID string) (*model.Channel, *model.AppError) { restrictDM, err := a.CheckIfChannelIsRestrictedDM(rctx, channel) if err != nil { return nil, err } if restrictDM { return nil, model.NewAppError("PatchChannel", "api.channel.patch_update_channel.restricted_dm.app_error", nil, "", http.StatusBadRequest) } oldChannelDisplayName := channel.DisplayName oldChannelHeader := channel.Header oldChannelPurpose := channel.Purpose oldChannelAutotranslation := channel.AutoTranslation channel.Patch(patch) channel, err = a.UpdateChannel(rctx, channel) if err != nil { return nil, err } a.addChannelToDefaultCategory(rctx, userID, channel) if oldChannelDisplayName != channel.DisplayName { if err = a.PostUpdateChannelDisplayNameMessage(rctx, userID, channel, oldChannelDisplayName, channel.DisplayName); err != nil { rctx.Logger().Warn(err.Error()) } } if channel.Header != oldChannelHeader { if err = a.PostUpdateChannelHeaderMessage(rctx, userID, channel, oldChannelHeader, channel.Header); err != nil { rctx.Logger().Warn(err.Error()) } } if channel.Purpose != oldChannelPurpose { if err = a.PostUpdateChannelPurposeMessage(rctx, userID, channel, oldChannelPurpose, channel.Purpose); err != nil { rctx.Logger().Warn(err.Error()) } } if channel.AutoTranslation != oldChannelAutotranslation { if err = a.postUpdateChannelAutotranslationMessage(rctx, userID, channel, channel.AutoTranslation); err != nil { rctx.Logger().Warn(err.Error()) } } return channel, nil } // GetSchemeRolesForChannel Checks if a channel or its team has an override scheme for channel roles and returns the scheme roles or default channel roles. func (a *App) GetSchemeRolesForChannel(rctx request.CTX, channelID string) (guestRoleName, userRoleName, adminRoleName string, err *model.AppError) { channel, err := a.GetChannel(rctx, channelID) if err != nil { return } if channel.SchemeId != nil && *channel.SchemeId != "" { var scheme *model.Scheme scheme, err = a.GetScheme(*channel.SchemeId) if err != nil { return } guestRoleName = scheme.DefaultChannelGuestRole userRoleName = scheme.DefaultChannelUserRole adminRoleName = scheme.DefaultChannelAdminRole return } return a.GetTeamSchemeChannelRoles(rctx, channel.TeamId) } // GetTeamSchemeChannelRoles Checks if a team has an override scheme and returns the scheme channel role names or default channel role names. func (a *App) GetTeamSchemeChannelRoles(rctx request.CTX, teamID string) (guestRoleName, userRoleName, adminRoleName string, err *model.AppError) { team, err := a.GetTeam(teamID) if err != nil { return } if team.SchemeId != nil && *team.SchemeId != "" { var scheme *model.Scheme scheme, err = a.GetScheme(*team.SchemeId) if err != nil { return } guestRoleName = scheme.DefaultChannelGuestRole userRoleName = scheme.DefaultChannelUserRole adminRoleName = scheme.DefaultChannelAdminRole } else { guestRoleName = model.ChannelGuestRoleId userRoleName = model.ChannelUserRoleId adminRoleName = model.ChannelAdminRoleId } return } // GetChannelModerationsForChannel Gets a channels ChannelModerations from either the higherScoped roles or from the channel scheme roles. func (a *App) GetChannelModerationsForChannel(rctx request.CTX, channel *model.Channel) ([]*model.ChannelModeration, *model.AppError) { guestRoleName, memberRoleName, _, err := a.GetSchemeRolesForChannel(rctx, channel.Id) if err != nil { return nil, err } memberRole, err := a.GetRoleByName(rctx, memberRoleName) if err != nil { return nil, err } var guestRole *model.Role if guestRoleName != "" { guestRole, err = a.GetRoleByName(rctx, guestRoleName) if err != nil { return nil, err } } higherScopedGuestRoleName, higherScopedMemberRoleName, _, err := a.GetTeamSchemeChannelRoles(rctx, channel.TeamId) if err != nil { return nil, err } higherScopedMemberRole, err := a.GetRoleByName(rctx, higherScopedMemberRoleName) if err != nil { return nil, err } var higherScopedGuestRole *model.Role if higherScopedGuestRoleName != "" { higherScopedGuestRole, err = a.GetRoleByName(rctx, higherScopedGuestRoleName) if err != nil { return nil, err } } return buildChannelModerations(rctx, channel.Type, memberRole, guestRole, higherScopedMemberRole, higherScopedGuestRole), nil } // PatchChannelModerationsForChannel Updates a channels scheme roles based on a given ChannelModerationPatch, if the permissions match the higher scoped role the scheme is deleted. func (a *App) PatchChannelModerationsForChannel(rctx request.CTX, channel *model.Channel, channelModerationsPatch []*model.ChannelModerationPatch) ([]*model.ChannelModeration, *model.AppError) { higherScopedGuestRoleName, higherScopedMemberRoleName, _, err := a.GetTeamSchemeChannelRoles(rctx, channel.TeamId) if err != nil { return nil, err } higherScopedMemberRole, err := a.GetRoleByName(RequestContextWithMaster(rctx), higherScopedMemberRoleName) if err != nil { return nil, err } var higherScopedGuestRole *model.Role if higherScopedGuestRoleName != "" { higherScopedGuestRole, err = a.GetRoleByName(RequestContextWithMaster(rctx), higherScopedGuestRoleName) if err != nil { return nil, err } } higherScopedMemberPermissions := higherScopedMemberRole.GetChannelModeratedPermissions(channel.Type) var higherScopedGuestPermissions map[string]bool if higherScopedGuestRole != nil { higherScopedGuestPermissions = higherScopedGuestRole.GetChannelModeratedPermissions(channel.Type) } for _, moderationPatch := range channelModerationsPatch { if moderationPatch.Roles.Members != nil && *moderationPatch.Roles.Members && !higherScopedMemberPermissions[*moderationPatch.Name] { return nil, model.NewAppError("PatchChannelModerationsForChannel", "api.channel.patch_channel_moderations_for_channel.restricted_permission.app_error", nil, "", http.StatusForbidden) } if moderationPatch.Roles.Guests != nil && *moderationPatch.Roles.Guests && !higherScopedGuestPermissions[*moderationPatch.Name] { return nil, model.NewAppError("PatchChannelModerationsForChannel", "api.channel.patch_channel_moderations_for_channel.restricted_permission.app_error", nil, "", http.StatusForbidden) } } var scheme *model.Scheme // Channel has no scheme so create one if channel.SchemeId == nil || *channel.SchemeId == "" { scheme, err = a.CreateChannelScheme(rctx, channel) if err != nil { return nil, err } // Send a websocket event about this new role. The other new roles—member and guest—get emitted when they're updated. var adminRole *model.Role adminRole, err = a.GetRoleByName(rctx, scheme.DefaultChannelAdminRole) if err != nil { return nil, err } if appErr := a.sendUpdatedRoleEvent(adminRole); appErr != nil { return nil, appErr } message := model.NewWebSocketEvent(model.WebsocketEventChannelSchemeUpdated, "", channel.Id, "", nil, "") a.Publish(message) rctx.Logger().Info("Permission scheme created.", mlog.String("channel_id", channel.Id), mlog.String("channel_name", channel.Name)) } else { scheme, err = a.GetScheme(*channel.SchemeId) if err != nil { return nil, err } } guestRoleName := scheme.DefaultChannelGuestRole memberRoleName := scheme.DefaultChannelUserRole memberRole, err := a.GetRoleByName(rctx, memberRoleName) if err != nil { return nil, err } var guestRole *model.Role if guestRoleName != "" { guestRole, err = a.GetRoleByName(rctx, guestRoleName) if err != nil { return nil, err } } memberRolePatch := memberRole.RolePatchFromChannelModerationsPatch(channelModerationsPatch, "members") var guestRolePatch *model.RolePatch if guestRole != nil { guestRolePatch = guestRole.RolePatchFromChannelModerationsPatch(channelModerationsPatch, "guests") } for _, channelModerationPatch := range channelModerationsPatch { permissionModified := *channelModerationPatch.Name if channelModerationPatch.Roles.Guests != nil && slices.Contains(model.ChannelModeratedPermissionsChangedByPatch(guestRole, guestRolePatch), permissionModified) { if *channelModerationPatch.Roles.Guests { rctx.Logger().Info("Permission enabled for guests.", mlog.String("permission", permissionModified), mlog.String("channel_id", channel.Id), mlog.String("channel_name", channel.Name)) } else { rctx.Logger().Info("Permission disabled for guests.", mlog.String("permission", permissionModified), mlog.String("channel_id", channel.Id), mlog.String("channel_name", channel.Name)) } } if channelModerationPatch.Roles.Members != nil && slices.Contains(model.ChannelModeratedPermissionsChangedByPatch(memberRole, memberRolePatch), permissionModified) { if *channelModerationPatch.Roles.Members { rctx.Logger().Info("Permission enabled for members.", mlog.String("permission", permissionModified), mlog.String("channel_id", channel.Id), mlog.String("channel_name", channel.Name)) } else { rctx.Logger().Info("Permission disabled for members.", mlog.String("permission", permissionModified), mlog.String("channel_id", channel.Id), mlog.String("channel_name", channel.Name)) } } } memberRolePermissionsUnmodified := len(model.ChannelModeratedPermissionsChangedByPatch(higherScopedMemberRole, memberRolePatch)) == 0 guestRolePermissionsUnmodified := len(model.ChannelModeratedPermissionsChangedByPatch(higherScopedGuestRole, guestRolePatch)) == 0 if memberRolePermissionsUnmodified && guestRolePermissionsUnmodified { // The channel scheme matches the permissions of its higherScoped scheme so delete the scheme if _, err = a.DeleteChannelScheme(rctx, channel); err != nil { return nil, err } message := model.NewWebSocketEvent(model.WebsocketEventChannelSchemeUpdated, "", channel.Id, "", nil, "") a.Publish(message) memberRole = higherScopedMemberRole guestRole = higherScopedGuestRole rctx.Logger().Info("Permission scheme deleted.", mlog.String("channel_id", channel.Id), mlog.String("channel_name", channel.Name)) } else { memberRole, err = a.PatchRole(memberRole, memberRolePatch) if err != nil { return nil, err } guestRole, err = a.PatchRole(guestRole, guestRolePatch) if err != nil { return nil, err } } cErr := a.forEachChannelMember(rctx, channel.Id, func(channelMember model.ChannelMember) error { a.Srv().Store().Channel().InvalidateAllChannelMembersForUser(channelMember.UserId) evt := model.NewWebSocketEvent(model.WebsocketEventChannelMemberUpdated, "", "", channelMember.UserId, nil, "") memberJSON, jsonErr := json.Marshal(channelMember) if jsonErr != nil { return jsonErr } evt.Add("channelMember", string(memberJSON)) a.Publish(evt) return nil }) if cErr != nil { return nil, model.NewAppError("PatchChannelModerationsForChannel", "api.channel.patch_channel_moderations.cache_invalidation.error", nil, "", http.StatusInternalServerError).Wrap(cErr) } return buildChannelModerations(rctx, channel.Type, memberRole, guestRole, higherScopedMemberRole, higherScopedGuestRole), nil } func buildChannelModerations(rctx request.CTX, channelType model.ChannelType, memberRole *model.Role, guestRole *model.Role, higherScopedMemberRole *model.Role, higherScopedGuestRole *model.Role) []*model.ChannelModeration { var memberPermissions, guestPermissions, higherScopedMemberPermissions, higherScopedGuestPermissions map[string]bool if memberRole != nil { memberPermissions = memberRole.GetChannelModeratedPermissions(channelType) } if guestRole != nil { guestPermissions = guestRole.GetChannelModeratedPermissions(channelType) } if higherScopedMemberRole != nil { higherScopedMemberPermissions = higherScopedMemberRole.GetChannelModeratedPermissions(channelType) } if higherScopedGuestRole != nil { higherScopedGuestPermissions = higherScopedGuestRole.GetChannelModeratedPermissions(channelType) } var channelModerations []*model.ChannelModeration for _, permissionKey := range model.ChannelModeratedPermissions { roles := &model.ChannelModeratedRoles{} roles.Members = &model.ChannelModeratedRole{ Value: memberPermissions[permissionKey], Enabled: higherScopedMemberPermissions[permissionKey], } if permissionKey == "manage_members" || permissionKey == "manage_bookmarks" { roles.Guests = nil } else { roles.Guests = &model.ChannelModeratedRole{ Value: guestPermissions[permissionKey], Enabled: higherScopedGuestPermissions[permissionKey], } } moderation := &model.ChannelModeration{ Name: permissionKey, Roles: roles, } channelModerations = append(channelModerations, moderation) } return channelModerations } // UpdateChannelMemberRoles updates the roles for a channel member. // This is the public API used by REST endpoints and plugins. // It enforces strict validation requiring either SchemeUser or SchemeGuest to be true. func (a *App) UpdateChannelMemberRoles(rctx request.CTX, channelID string, userID string, newRoles string) (*model.ChannelMember, *model.AppError) { return a.updateChannelMemberRolesInternal(rctx, channelID, userID, newRoles, false) } // updateChannelMemberRolesInternal is the internal implementation of UpdateChannelMemberRoles. // The allowSchemeUserUnset parameter controls whether to enforce the requirement that members // must have either SchemeUser or SchemeGuest set to true. // // When allowSchemeUserUnset is false (default for API/plugin calls), the function enforces // that members must have a base scheme role. When true (bulk import only), this validation // is skipped to support the two-phase import pattern where explicit roles are set first, // then scheme roles are set via UpdateChannelMemberSchemeRoles immediately after. func (a *App) updateChannelMemberRolesInternal(rctx request.CTX, channelID string, userID string, newRoles string, allowSchemeUserUnset bool) (*model.ChannelMember, *model.AppError) { var member *model.ChannelMember var err *model.AppError if member, err = a.GetChannelMember(rctx, channelID, userID); err != nil { return nil, err } schemeGuestRole, schemeUserRole, schemeAdminRole, err := a.GetSchemeRolesForChannel(rctx, channelID) if err != nil { return nil, err } prevSchemeGuestValue := member.SchemeGuest var newExplicitRoles []string member.SchemeGuest = false member.SchemeUser = false member.SchemeAdmin = false for roleName := range strings.FieldsSeq(newRoles) { var role *model.Role role, err = a.GetRoleByName(rctx, roleName) if err != nil { err.StatusCode = http.StatusBadRequest return nil, err } if !role.SchemeManaged { if model.IsBuiltInRole(roleName) && !model.IsChannelScopedBuiltInRole(roleName) { err = model.NewAppError("UpdateChannelMemberRoles", "api.channel.update_channel_member_roles.scheme_role.app_error", nil, "role_name="+roleName, http.StatusBadRequest) return nil, err } newExplicitRoles = append(newExplicitRoles, roleName) } else { // The role is scheme-managed, so need to check if it is part of the scheme for this channel or not. switch roleName { case schemeAdminRole: member.SchemeAdmin = true case schemeUserRole: member.SchemeUser = true case schemeGuestRole: member.SchemeGuest = true default: // If not part of the scheme for this channel, then it is not allowed to apply it as an explicit role. return nil, model.NewAppError("UpdateChannelMemberRoles", "api.channel.update_channel_member_roles.scheme_role.app_error", nil, "role_name="+roleName, http.StatusBadRequest) } } } if member.SchemeUser && member.SchemeGuest { return nil, model.NewAppError("UpdateChannelMemberRoles", "api.channel.update_channel_member_roles.guest_and_user.app_error", nil, "", http.StatusBadRequest) } if member.SchemeGuest && member.SchemeAdmin { return nil, model.NewAppError("UpdateChannelMemberRoles", "api.channel.update_channel_member_roles.guest_and_admin.app_error", nil, "", http.StatusBadRequest) } if prevSchemeGuestValue != member.SchemeGuest { return nil, model.NewAppError("UpdateChannelMemberRoles", "api.channel.update_channel_member_roles.changing_guest_role.app_error", nil, "", http.StatusBadRequest) } // Validate that the member has a base scheme role (SchemeUser or SchemeGuest). // This ensures members always have the minimum required permissions. // Bulk import operations may skip this validation temporarily. if !allowSchemeUserUnset && !member.SchemeGuest && !member.SchemeUser { return nil, model.NewAppError("UpdateChannelMemberRoles", "api.channel.update_channel_member_roles.unset_user_scheme.app_error", nil, "", http.StatusBadRequest) } member.ExplicitRoles = strings.Join(newExplicitRoles, " ") return a.updateChannelMember(rctx, member) } func (a *App) UpdateChannelMemberSchemeRoles(rctx request.CTX, channelID string, userID string, isSchemeGuest bool, isSchemeUser bool, isSchemeAdmin bool) (*model.ChannelMember, *model.AppError) { member, err := a.GetChannelMember(rctx, channelID, userID) if err != nil { return nil, err } if member.SchemeGuest { return nil, model.NewAppError("UpdateChannelMemberSchemeRoles", "api.channel.update_channel_member_roles.guest.app_error", nil, "", http.StatusBadRequest) } if isSchemeGuest { return nil, model.NewAppError("UpdateChannelMemberSchemeRoles", "api.channel.update_channel_member_roles.user_and_guest.app_error", nil, "", http.StatusBadRequest) } if !isSchemeUser { return nil, model.NewAppError("UpdateChannelMemberSchemeRoles", "api.channel.update_channel_member_roles.unset_user_scheme.app_error", nil, "", http.StatusBadRequest) } member.SchemeAdmin = isSchemeAdmin member.SchemeUser = isSchemeUser member.SchemeGuest = isSchemeGuest // If the migration is not completed, we also need to check the default channel_admin/channel_user roles are not present in the roles field. if err = a.IsPhase2MigrationCompleted(); err != nil { member.ExplicitRoles = removeRoles([]string{model.ChannelGuestRoleId, model.ChannelUserRoleId, model.ChannelAdminRoleId}, member.ExplicitRoles) } return a.updateChannelMember(rctx, member) } func (a *App) UpdateChannelMemberNotifyProps(rctx request.CTX, data map[string]string, channelID string, userID string) (*model.ChannelMember, *model.AppError) { filteredProps := make(map[string]string) // update whichever notify properties have been provided, but don't change the others if markUnread, exists := data[model.MarkUnreadNotifyProp]; exists { filteredProps[model.MarkUnreadNotifyProp] = markUnread } if desktop, exists := data[model.DesktopNotifyProp]; exists { filteredProps[model.DesktopNotifyProp] = desktop } if desktop_sound, exists := data[model.DesktopSoundNotifyProp]; exists { filteredProps[model.DesktopSoundNotifyProp] = desktop_sound } if desktop_notification_sound, exists := data["desktop_notification_sound"]; exists { filteredProps["desktop_notification_sound"] = desktop_notification_sound } if desktop_threads, exists := data[model.DesktopThreadsNotifyProp]; exists { filteredProps[model.DesktopThreadsNotifyProp] = desktop_threads } if email, exists := data[model.EmailNotifyProp]; exists { filteredProps[model.EmailNotifyProp] = email } if push, exists := data[model.PushNotifyProp]; exists { filteredProps[model.PushNotifyProp] = push } if push_threads, exists := data[model.PushThreadsNotifyProp]; exists { filteredProps[model.PushThreadsNotifyProp] = push_threads } if ignoreChannelMentions, exists := data[model.IgnoreChannelMentionsNotifyProp]; exists { filteredProps[model.IgnoreChannelMentionsNotifyProp] = ignoreChannelMentions } if channelAutoFollowThreads, exists := data[model.ChannelAutoFollowThreads]; exists { filteredProps[model.ChannelAutoFollowThreads] = channelAutoFollowThreads } member, err := a.Srv().Store().Channel().UpdateMemberNotifyProps(channelID, userID, filteredProps) if err != nil { var appErr *model.AppError var invErr *store.ErrInvalidInput var nfErr *store.ErrNotFound switch { case errors.As(err, &appErr): return nil, appErr case errors.As(err, &invErr): return nil, model.NewAppError("updateMemberNotifyProps", "app.channel.update_member.notify_props_limit_exceeded.app_error", nil, "", http.StatusBadRequest).Wrap(err) case errors.As(err, &nfErr): return nil, model.NewAppError("updateMemberNotifyProps", MissingChannelMemberError, nil, "", http.StatusNotFound).Wrap(err) default: return nil, model.NewAppError("updateMemberNotifyProps", "app.channel.update_member.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } } a.Srv().Platform().InvalidateChannelCacheForUser(member.UserId) a.invalidateCacheForChannelMembersNotifyProps(member.ChannelId) // Notify the clients that the member notify props changed err = a.sendUpdateChannelMemberEvent(member) if err != nil { return nil, model.NewAppError("UpdateChannelMemberNotifyProps", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err) } return member, nil } func (a *App) UpdateChannelMemberAutotranslation(rctx request.CTX, channelID string, userID string, autoTranslationDisabled bool) (*model.ChannelMember, *model.AppError) { member, err := a.GetChannelMember(rctx, channelID, userID) if err != nil { return nil, err } member.AutoTranslationDisabled = autoTranslationDisabled member, err = a.updateChannelMember(rctx, member) if err != nil { return nil, err } a.Srv().Store().AutoTranslation().InvalidateUserAutoTranslation(userID, channelID) return member, nil } func (a *App) PatchChannelMembersNotifyProps(rctx request.CTX, members []*model.ChannelMemberIdentifier, notifyProps map[string]string) ([]*model.ChannelMember, *model.AppError) { if len(members) > UpdateMultipleMaximum { return nil, model.NewAppError("PatchChannelMembersNotifyProps", "app.channel.patch_channel_members_notify_props.too_many", map[string]any{"Max": UpdateMultipleMaximum}, "", http.StatusBadRequest) } updated, err := a.Srv().Store().Channel().PatchMultipleMembersNotifyProps(members, notifyProps) if err != nil { var appErr *model.AppError switch { case errors.As(err, &appErr): return nil, appErr default: return nil, model.NewAppError("UpdateMultipleMembersNotifyProps", "app.channel.patch_channel_members_notify_props.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } } // Invalidate caches for the users and channels that have changed userIds := make(map[string]bool) channelIds := make(map[string]bool) for _, member := range updated { userIds[member.UserId] = true channelIds[member.ChannelId] = true } for userId := range userIds { a.Srv().Platform().InvalidateChannelCacheForUser(userId) } for channelId := range channelIds { a.invalidateCacheForChannelMembersNotifyProps(channelId) } // Notify clients that their notify props have changed for _, member := range updated { err := a.sendUpdateChannelMemberEvent(member) if err != nil { rctx.Logger().Warn("Failed to send WebSocket event for updated channel member notify props", mlog.Err(err)) } } return updated, nil } func (a *App) sendUpdateChannelMemberEvent(member *model.ChannelMember) error { evt := model.NewWebSocketEvent(model.WebsocketEventChannelMemberUpdated, "", "", member.UserId, nil, "") memberJSON, jsonErr := json.Marshal(member) if jsonErr != nil { return jsonErr } evt.Add("channelMember", string(memberJSON)) a.Publish(evt) return nil } func (a *App) updateChannelMember(rctx request.CTX, member *model.ChannelMember) (*model.ChannelMember, *model.AppError) { member, err := a.Srv().Store().Channel().UpdateMember(rctx, member) if err != nil { var appErr *model.AppError var nfErr *store.ErrNotFound switch { case errors.As(err, &appErr): return nil, appErr case errors.As(err, &nfErr): return nil, model.NewAppError("updateChannelMember", MissingChannelMemberError, nil, "", http.StatusNotFound).Wrap(err) default: return nil, model.NewAppError("updateChannelMember", "app.channel.get_member.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } } a.Srv().Platform().InvalidateChannelCacheForUser(member.UserId) // Notify the clients that the member notify props changed evt := model.NewWebSocketEvent(model.WebsocketEventChannelMemberUpdated, "", "", member.UserId, nil, "") memberJSON, jsonErr := json.Marshal(member) if jsonErr != nil { return nil, model.NewAppError("updateChannelMember", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(jsonErr) } evt.Add("channelMember", string(memberJSON)) a.Publish(evt) return member, nil } func (a *App) DeleteChannel(rctx request.CTX, channel *model.Channel, userID string) *model.AppError { ihc := make(chan store.StoreResult[[]*model.IncomingWebhook], 1) ohc := make(chan store.StoreResult[[]*model.OutgoingWebhook], 1) go func() { webhooks, err := a.Srv().Store().Webhook().GetIncomingByChannel(channel.Id) ihc <- store.StoreResult[[]*model.IncomingWebhook]{Data: webhooks, NErr: err} close(ihc) }() go func() { outgoingHooks, err := a.Srv().Store().Webhook().GetOutgoingByChannel(channel.Id, -1, -1) ohc <- store.StoreResult[[]*model.OutgoingWebhook]{Data: outgoingHooks, NErr: err} close(ohc) }() var user *model.User if userID != "" { var nErr error user, nErr = a.Srv().Store().User().Get(rctx, userID) if nErr != nil { var nfErr *store.ErrNotFound switch { case errors.As(nErr, &nfErr): return model.NewAppError("DeleteChannel", MissingAccountError, nil, "", http.StatusNotFound).Wrap(nErr) default: return model.NewAppError("DeleteChannel", "app.user.get.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr) } } } ihcresult := <-ihc if ihcresult.NErr != nil { return model.NewAppError("DeleteChannel", "app.webhooks.get_incoming_by_channel.app_error", nil, "", http.StatusInternalServerError).Wrap(ihcresult.NErr) } ohcresult := <-ohc if ohcresult.NErr != nil { return model.NewAppError("DeleteChannel", "app.webhooks.get_outgoing_by_channel.app_error", nil, "", http.StatusInternalServerError).Wrap(ohcresult.NErr) } if channel.DeleteAt > 0 { err := model.NewAppError("deleteChannel", "api.channel.delete_channel.deleted.app_error", nil, "", http.StatusBadRequest) return err } if channel.Name == model.DefaultChannelName { err := model.NewAppError("deleteChannel", "api.channel.delete_channel.cannot.app_error", map[string]any{"Channel": model.DefaultChannelName}, "", http.StatusBadRequest) return err } // Space backing channels are internal; plugin ChannelWillBeArchived hooks are skipped. if !channel.IsSpace() { var archiveRejectionReason string pluginContext := pluginContext(rctx) a.ch.RunMultiHook(func(hooks plugin.Hooks, _ *model.Manifest) bool { archiveRejectionReason = hooks.ChannelWillBeArchived(pluginContext, channel) return archiveRejectionReason == "" }, plugin.ChannelWillBeArchivedID) if archiveRejectionReason != "" { return model.NewAppError("DeleteChannel", "app.channel.delete_channel.rejected_by_plugin", map[string]any{"Reason": archiveRejectionReason}, "", http.StatusBadRequest) } } deleteAt := model.GetMillis() if err := a.Srv().Store().Channel().Delete(channel.Id, deleteAt); err != nil { return model.NewAppError("DeleteChannel", "app.channel.delete.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } // Space backing channels are internal and skip the archive system post. if !channel.IsSpace() { if user != nil { T := i18n.GetUserTranslations(user.Locale) post := &model.Post{ ChannelId: channel.Id, Message: fmt.Sprintf(T("api.channel.delete_channel.archived"), user.Username), Type: model.PostTypeChannelDeleted, UserId: userID, Props: model.StringInterface{ "username": user.Username, }, } if _, _, err := a.CreatePost(rctx, post, channel, model.CreatePostFlags{SetOnline: true}); err != nil { rctx.Logger().Warn("Failed to post archive message", mlog.Err(err)) } } else { systemBot, err := a.GetSystemBot(rctx) if err != nil { rctx.Logger().Warn("Failed to post archive message", mlog.Err(err)) } else { post := &model.Post{ ChannelId: channel.Id, Message: fmt.Sprintf(i18n.T("api.channel.delete_channel.archived"), systemBot.Username), Type: model.PostTypeChannelDeleted, UserId: systemBot.UserId, Props: model.StringInterface{ "username": systemBot.Username, }, } if _, _, err := a.CreatePost(rctx, post, channel, model.CreatePostFlags{SetOnline: true}); err != nil { rctx.Logger().Warn("Failed to post archive message", mlog.Err(err)) } } } } now := model.GetMillis() for _, hook := range ihcresult.Data { if err := a.Srv().Store().Webhook().DeleteIncoming(hook.Id, now); err != nil { rctx.Logger().Warn("Encountered error deleting incoming webhook", mlog.String("hook_id", hook.Id), mlog.Err(err)) } a.Srv().Platform().InvalidateCacheForWebhook(hook.Id) } for _, hook := range ohcresult.Data { if err := a.Srv().Store().Webhook().DeleteOutgoing(hook.Id, now); err != nil { rctx.Logger().Warn("Encountered error deleting outgoing webhook", mlog.String("hook_id", hook.Id), mlog.Err(err)) } } if err := a.Srv().Store().PostPersistentNotification().DeleteByChannel([]string{channel.Id}); err != nil { return model.NewAppError("DeleteChannel", "app.post_persistent_notification.delete_by_channel.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } // Archiving a channel tears down the membership policy attached to it. // If the channel is later restored the admin can re-apply policies; // leaving the policy row behind would otherwise keep the sync job // processing an archived channel. a.cleanupChannelAccessControlPolicy(rctx, channel) a.Srv().Platform().InvalidateCacheForChannel(channel) if channel.IsSpace() { return nil } var message *model.WebSocketEvent if channel.Type == model.ChannelTypeOpen { message = model.NewWebSocketEvent(model.WebsocketEventChannelDeleted, channel.TeamId, "", "", nil, "") } else { message = model.NewWebSocketEvent(model.WebsocketEventChannelDeleted, "", channel.Id, "", nil, "") } message.Add("channel_id", channel.Id) message.Add("delete_at", deleteAt) a.Publish(message) return nil } func (a *App) addUserToChannel(rctx request.CTX, user *model.User, channel *model.Channel) (*model.ChannelMember, *model.AppError) { if channel.Type != model.ChannelTypeOpen && channel.Type != model.ChannelTypePrivate && !channel.IsSpace() { return nil, model.NewAppError("AddUserToChannel", "api.channel.add_user_to_channel.type.app_error", nil, "", http.StatusBadRequest) } channelMember, nErr := a.Srv().Store().Channel().GetMember(rctx, channel.Id, user.Id) if nErr != nil { var nfErr *store.ErrNotFound if !errors.As(nErr, &nfErr) { return nil, model.NewAppError("AddUserToChannel", "app.channel.get_member.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr) } } else { return channelMember, nil } if channel.IsGroupConstrained() { nonMembers, err := a.FilterNonGroupChannelMembers(rctx, []string{user.Id}, channel) if err != nil { return nil, model.NewAppError("addUserToChannel", "api.channel.add_user_to_channel.type.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } if len(nonMembers) > 0 { return nil, model.NewAppError("addUserToChannel", "api.channel.add_members.user_denied", map[string]any{"UserIDs": nonMembers}, "", http.StatusBadRequest) } } newMember := &model.ChannelMember{ ChannelId: channel.Id, UserId: user.Id, NotifyProps: model.GetDefaultChannelNotifyProps(), SchemeGuest: user.IsGuest(), SchemeUser: !user.IsGuest(), } if !user.IsGuest() { var userShouldBeAdmin bool userShouldBeAdmin, appErr := a.UserIsInAdminRoleGroup(user.Id, channel.Id, model.GroupSyncableTypeChannel) if appErr != nil { return nil, appErr } newMember.SchemeAdmin = userShouldBeAdmin } if channel.Type == model.ChannelTypePrivate { if ok, appErr := a.ChannelAccessControlled(rctx, channel.Id); ok { if acs := a.Srv().Channels().AccessControl; acs != nil { s, buildErr := a.BuildAccessControlSubject(rctx, user.Id, user.Roles, channel.Id) if buildErr != nil { return nil, model.NewAppError("AddUserToChannel", "api.channel.add_user.to.channel.abac_subject_build_failed.app_error", nil, fmt.Sprintf("failed to build subject: %v, user_id: %s, channel_id: %s", buildErr, user.Id, channel.Id), http.StatusInternalServerError) } decision, evalErr := acs.AccessEvaluation(rctx, model.AccessRequest{ Subject: *s, Resource: model.Resource{ Type: model.AccessControlPolicyTypeChannel, ID: channel.Id, }, Action: "membership", }) if evalErr != nil { return nil, evalErr } else if !decision.Decision { return nil, model.NewAppError("AddUserToChannel", "api.channel.add_user.to.channel.rejected", nil, "", http.StatusForbidden) } } } else if appErr != nil { return nil, appErr } } var channelMemberErr *model.AppError if !channel.IsSpace() { newMember, channelMemberErr = a.runGuardedChannelMemberWillBeAdded(rctx, channel.Id, newMember) if channelMemberErr != nil { return nil, channelMemberErr } } newMember, nErr = a.Srv().Store().Channel().SaveMember(rctx, newMember) if nErr != nil { return nil, model.NewAppError("AddUserToChannel", "api.channel.add_user.to.channel.failed.app_error", nil, fmt.Sprintf("failed to add member: %v, user_id: %s, channel_id: %s", nErr, user.Id, channel.Id), http.StatusInternalServerError) } if nErr := a.Srv().Store().ChannelMemberHistory().LogJoinEvent(user.Id, channel.Id, model.GetMillis()); nErr != nil { return nil, model.NewAppError("AddUserToChannel", "app.channel_member_history.log_join_event.internal_error", nil, "", http.StatusInternalServerError).Wrap(nErr) } a.Srv().Platform().InvalidateChannelCacheForUser(user.Id) a.invalidateCacheForChannelMembers(channel.Id) // Synchronize membership change for shared channels if channel.IsShared() { if scs := a.Srv().Platform().GetSharedChannelService(); scs != nil { scs.NotifyMembershipChanged(channel.Id, user.GetRemoteID()) } } return newMember, nil } // AddUserToChannel adds a user to a given channel. func (a *App) AddUserToChannel(rctx request.CTX, user *model.User, channel *model.Channel, skipTeamMemberIntegrityCheck bool) (*model.ChannelMember, *model.AppError) { if !skipTeamMemberIntegrityCheck { teamMember, nErr := a.Srv().Store().Team().GetMember(rctx, channel.TeamId, user.Id) if nErr != nil { var nfErr *store.ErrNotFound switch { case errors.As(nErr, &nfErr): return nil, model.NewAppError("AddUserToChannel", "app.team.get_member.missing.app_error", nil, "", http.StatusNotFound).Wrap(nErr) default: return nil, model.NewAppError("AddUserToChannel", "app.team.get_member.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr) } } if teamMember.DeleteAt > 0 { return nil, model.NewAppError("AddUserToChannel", "api.channel.add_user.to.channel.failed.deleted.app_error", nil, "", http.StatusBadRequest) } } newMember, err := a.addUserToChannel(rctx, user, channel) if err != nil { return nil, err } if channel.IsSpace() { return newMember, nil } a.addChannelToDefaultCategory(rctx, user.Id, channel) // We are sending separate websocket events to the user added and to the channel // This is to get around potential cluster syncing issues where other nodes may not receive the most up to date channel members message := model.NewWebSocketEvent(model.WebsocketEventUserAdded, "", channel.Id, "", map[string]bool{user.Id: true}, "") message.Add("user_id", user.Id) message.Add("team_id", channel.TeamId) a.Publish(message) userMessage := model.NewWebSocketEvent(model.WebsocketEventUserAdded, "", channel.Id, user.Id, nil, "") userMessage.Add("user_id", user.Id) userMessage.Add("team_id", channel.TeamId) a.Publish(userMessage) return newMember, nil } type ChannelMemberOpts struct { UserRequestorID string PostRootID string // SkipTeamMemberIntegrityCheck is used to indicate whether it should be checked // that a user has already been removed from that team or not. // This is useful to avoid in scenarios when we just added the team member, // and thereby know that there is no need to check this. SkipTeamMemberIntegrityCheck bool } // AddChannelMember adds a user to a channel. It is a wrapper over AddUserToChannel. func (a *App) AddChannelMember(rctx request.CTX, userID string, channel *model.Channel, opts ChannelMemberOpts) (*model.ChannelMember, *model.AppError) { if member, err := a.Srv().Store().Channel().GetMember(rctx, channel.Id, userID); err != nil { var nfErr *store.ErrNotFound if !errors.As(err, &nfErr) { return nil, model.NewAppError("AddChannelMember", "app.channel.get_member.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } } else { return member, nil } var user *model.User var err *model.AppError if user, err = a.GetUser(rctx, userID); err != nil { return nil, err } if user.DeleteAt > 0 { return nil, model.NewAppError("AddChannelMember", "app.channel.add_member.deleted_user.app_error", nil, "", http.StatusForbidden) } var userRequestor *model.User if opts.UserRequestorID != "" { if userRequestor, err = a.GetUser(rctx, opts.UserRequestorID); err != nil { return nil, err } } cm, err := a.AddUserToChannel(rctx, user, channel, opts.SkipTeamMemberIntegrityCheck) if err != nil { return nil, err } if channel.IsSpace() { return cm, nil } a.Srv().Go(func() { pluginContext := pluginContext(rctx) a.ch.RunMultiHook(func(hooks plugin.Hooks, _ *model.Manifest) bool { hooks.UserHasJoinedChannel(pluginContext, cm, userRequestor) return true }, plugin.UserHasJoinedChannelID) }) if opts.UserRequestorID == "" || userID == opts.UserRequestorID { if err := a.postJoinChannelMessage(rctx, user, channel); err != nil { return nil, err } } else { a.Srv().Go(func() { if err := a.PostAddToChannelMessage(rctx, userRequestor, user, channel, opts.PostRootID); err != nil { rctx.Logger().Error("Failed to post AddToChannel message", mlog.Err(err)) } }) } return cm, nil } func (a *App) AddDirectChannels(rctx request.CTX, teamID string, user *model.User) *model.AppError { var profiles []*model.User options := &model.UserGetOptions{InTeamId: teamID, Page: 0, PerPage: 100} profiles, err := a.Srv().Store().User().GetProfiles(options) if err != nil { return model.NewAppError("AddDirectChannels", "api.user.add_direct_channels_and_forget.failed.error", map[string]any{"UserId": user.Id, "TeamId": teamID, "Error": err.Error()}, "", http.StatusInternalServerError) } var preferences model.Preferences for _, profile := range profiles { if profile.Id == user.Id { continue } preference := model.Preference{ UserId: user.Id, Category: model.PreferenceCategoryDirectChannelShow, Name: profile.Id, Value: "true", } preferences = append(preferences, preference) if len(preferences) >= 10 { break } } if err := a.Srv().Store().Preference().Save(preferences); err != nil { return model.NewAppError("AddDirectChannels", "api.user.add_direct_channels_and_forget.failed.error", map[string]any{"UserId": user.Id, "TeamId": teamID, "Error": err.Error()}, "", http.StatusInternalServerError) } return nil } func (a *App) PostUpdateChannelHeaderMessage(rctx request.CTX, userID string, channel *model.Channel, oldChannelHeader, newChannelHeader string) *model.AppError { user, err := a.Srv().Store().User().Get(rctx, userID) if err != nil { return model.NewAppError("PostUpdateChannelHeaderMessage", "api.channel.post_update_channel_header_message_and_forget.retrieve_user.error", nil, "", http.StatusBadRequest).Wrap(err) } var message string if oldChannelHeader == "" { message = fmt.Sprintf(i18n.T("api.channel.post_update_channel_header_message_and_forget.updated_to"), user.Username, newChannelHeader) } else if newChannelHeader == "" { message = fmt.Sprintf(i18n.T("api.channel.post_update_channel_header_message_and_forget.removed"), user.Username, oldChannelHeader) } else { message = fmt.Sprintf(i18n.T("api.channel.post_update_channel_header_message_and_forget.updated_from"), user.Username, oldChannelHeader, newChannelHeader) } post := &model.Post{ ChannelId: channel.Id, Message: message, Type: model.PostTypeHeaderChange, UserId: userID, Props: model.StringInterface{ "username": user.Username, "old_header": oldChannelHeader, "new_header": newChannelHeader, }, } if _, _, err := a.CreatePost(rctx, post, channel, model.CreatePostFlags{SetOnline: true}); err != nil { return model.NewAppError("", "api.channel.post_update_channel_header_message_and_forget.post.error", nil, "", http.StatusInternalServerError).Wrap(err) } return nil } func (a *App) PostUpdateChannelPurposeMessage(rctx request.CTX, userID string, channel *model.Channel, oldChannelPurpose string, newChannelPurpose string) *model.AppError { user, err := a.Srv().Store().User().Get(rctx, userID) if err != nil { return model.NewAppError("PostUpdateChannelPurposeMessage", "app.channel.post_update_channel_purpose_message.retrieve_user.error", nil, "", http.StatusBadRequest).Wrap(err) } var message string if oldChannelPurpose == "" { message = fmt.Sprintf(i18n.T("app.channel.post_update_channel_purpose_message.updated_to"), user.Username, newChannelPurpose) } else if newChannelPurpose == "" { message = fmt.Sprintf(i18n.T("app.channel.post_update_channel_purpose_message.removed"), user.Username, oldChannelPurpose) } else { message = fmt.Sprintf(i18n.T("app.channel.post_update_channel_purpose_message.updated_from"), user.Username, oldChannelPurpose, newChannelPurpose) } post := &model.Post{ ChannelId: channel.Id, Message: message, Type: model.PostTypePurposeChange, UserId: userID, Props: model.StringInterface{ "username": user.Username, "old_purpose": oldChannelPurpose, "new_purpose": newChannelPurpose, }, } if _, _, err := a.CreatePost(rctx, post, channel, model.CreatePostFlags{SetOnline: true}); err != nil { return model.NewAppError("", "app.channel.post_update_channel_purpose_message.post.error", nil, "", http.StatusInternalServerError).Wrap(err) } return nil } func (a *App) postUpdateChannelAutotranslationMessage(rctx request.CTX, userID string, channel *model.Channel, newChannelAutotranslation bool) *model.AppError { user, err := a.Srv().Store().User().Get(rctx, userID) if err != nil { return model.NewAppError("PostUpdateChannelAutotranslationMessage", "api.channel.post_update_channel_autotranslation_message.retrieve_user.error", nil, "", http.StatusBadRequest).Wrap(err) } var message string if newChannelAutotranslation { message = fmt.Sprintf(i18n.T("api.channel.post_update_channel_autotranslation_message.enabled"), user.Username) } else { message = fmt.Sprintf(i18n.T("api.channel.post_update_channel_autotranslation_message.disabled"), user.Username) } post := &model.Post{ ChannelId: channel.Id, Message: message, Type: model.PostTypeAutotranslationChange, UserId: userID, Props: model.StringInterface{ "username": user.Username, "enabled": newChannelAutotranslation, }, } if _, _, err := a.CreatePost(rctx, post, channel, model.CreatePostFlags{SetOnline: true}); err != nil { return model.NewAppError("PostUpdateChannelAutotranslationMessage", "api.channel.post_update_channel_autotranslation_message.create_post.error", nil, "", http.StatusInternalServerError).Wrap(err) } return nil } func (a *App) PostUpdateChannelDisplayNameMessage(rctx request.CTX, userID string, channel *model.Channel, oldChannelDisplayName, newChannelDisplayName string) *model.AppError { user, err := a.Srv().Store().User().Get(rctx, userID) if err != nil { return model.NewAppError("PostUpdateChannelDisplayNameMessage", "api.channel.post_update_channel_displayname_message_and_forget.retrieve_user.error", nil, "", http.StatusBadRequest).Wrap(err) } message := fmt.Sprintf(i18n.T("api.channel.post_update_channel_displayname_message_and_forget.updated_from"), user.Username, oldChannelDisplayName, newChannelDisplayName) post := &model.Post{ ChannelId: channel.Id, Message: message, Type: model.PostTypeDisplaynameChange, UserId: userID, Props: model.StringInterface{ "username": user.Username, "old_displayname": oldChannelDisplayName, "new_displayname": newChannelDisplayName, }, } if _, _, err := a.CreatePost(rctx, post, channel, model.CreatePostFlags{SetOnline: true}); err != nil { return model.NewAppError("PostUpdateChannelDisplayNameMessage", "api.channel.post_update_channel_displayname_message_and_forget.create_post.error", nil, "", http.StatusInternalServerError).Wrap(err) } return nil } func (a *App) GetChannel(rctx request.CTX, channelID string) (*model.Channel, *model.AppError) { channel, appErr := a.Srv().getChannel(rctx, channelID) if appErr != nil { return nil, appErr } // Hydrate policy action set so consumers can distinguish a membership // policy from a permission-only policy without a second round-trip. // No-op on channels with PolicyEnforced=false, keeping the cost on the // common no-policy path at zero. if appErr := a.HydrateChannelPolicyActions(rctx, channel); appErr != nil { rctx.Logger().Warn("Failed to hydrate channel policy actions; returning channel without action map", mlog.String("channel_id", channelID), mlog.Err(appErr), ) } return channel, nil } func (a *App) GetBoardChannel(rctx request.CTX, channelID string) (*model.Channel, *model.AppError) { channel, err := a.Srv().Store().Channel().GetBoardChannel(channelID) if err != nil { var nfErr *store.ErrNotFound switch { case errors.As(err, &nfErr): return nil, model.NewAppError("GetBoardChannel", "app.channel.get.existing.app_error", map[string]any{"channel_id": channelID}, "", http.StatusNotFound).Wrap(err) default: return nil, model.NewAppError("GetBoardChannel", "app.channel.get.find.app_error", map[string]any{"channel_id": channelID}, "", http.StatusInternalServerError).Wrap(err) } } return channel, nil } // GetChannelOfType resolves a channel by ID, requiring it to be of the given type. Generic // channel access goes through GetChannel, which excludes opaque backing channel types (e.g. // space); callers that legitimately need such a channel ask for it by its exact type here. func (a *App) GetChannelOfType(rctx request.CTX, channelID string, channelType model.ChannelType) (*model.Channel, *model.AppError) { channel, err := a.Srv().Store().Channel().GetChannelOfType(rctx, channelID, channelType) if err != nil { var nfErr *store.ErrNotFound switch { case errors.As(err, &nfErr): return nil, model.NewAppError("GetChannelOfType", "app.channel.get.existing.app_error", map[string]any{"channel_id": channelID}, "", http.StatusNotFound).Wrap(err) default: return nil, model.NewAppError("GetChannelOfType", "app.channel.get.find.app_error", map[string]any{"channel_id": channelID}, "", http.StatusInternalServerError).Wrap(err) } } return channel, nil } func (s *Server) getChannel(rctx request.CTX, channelID string) (*model.Channel, *model.AppError) { channel, err := s.Store().Channel().Get(channelID, true) if err != nil { errCtx := map[string]any{"channel_id": channelID} var nfErr *store.ErrNotFound switch { case errors.As(err, &nfErr): return nil, model.NewAppError("GetChannel", "app.channel.get.existing.app_error", errCtx, "", http.StatusNotFound).Wrap(err) default: return nil, model.NewAppError("GetChannel", "app.channel.get.find.app_error", errCtx, "", http.StatusInternalServerError).Wrap(err) } } return channel, nil } func (a *App) GetChannels(rctx request.CTX, channelIDs []string) ([]*model.Channel, *model.AppError) { channels, err := a.Srv().Store().Channel().GetMany(channelIDs, true) if err != nil { errCtx := map[string]any{"channel_id": channelIDs} var nfErr *store.ErrNotFound switch { case errors.As(err, &nfErr): return nil, model.NewAppError("GetChannel", "app.channel.get.existing.app_error", errCtx, "", http.StatusNotFound).Wrap(err) default: return nil, model.NewAppError("GetChannel", "app.channel.get.find.app_error", errCtx, "", http.StatusInternalServerError).Wrap(err) } } // Batched hydration: a single round-trip aggregates the action union // for every PolicyEnforced=true channel in the slice. No-policy // channels skip the lookup entirely. if appErr := a.HydrateChannelsPolicyActions(rctx, channels); appErr != nil { rctx.Logger().Warn("Failed to hydrate channel policy actions in batch; returning channels without action map", mlog.Int("count", len(channels)), mlog.Err(appErr), ) } return channels, nil } func (a *App) GetChannelsMemberCount(rctx request.CTX, channelIDs []string) (map[string]int64, *model.AppError) { channelsCount, err := a.Srv().Store().Channel().GetChannelsMemberCount(channelIDs) if err != nil { var nfErr *store.ErrNotFound switch { case errors.As(err, &nfErr): return nil, model.NewAppError("GetChannelsMemberCount", "app.channel.get_channels_member_count.existing.app_error", nil, "", http.StatusNotFound).Wrap(err) default: return nil, model.NewAppError("GetChannelsMemberCount", "app.channel.get_channels_member_count.find.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } } return channelsCount, nil } func (a *App) GetChannelByName(rctx request.CTX, channelName, teamID string, includeDeleted bool) (*model.Channel, *model.AppError) { var channel *model.Channel var err error if includeDeleted { channel, err = a.Srv().Store().Channel().GetByNameIncludeDeleted(teamID, channelName, false) } else { channel, err = a.Srv().Store().Channel().GetByName(teamID, channelName, false) } if err != nil { var nfErr *store.ErrNotFound switch { case errors.As(err, &nfErr): return nil, model.NewAppError("GetChannelByName", "app.channel.get_by_name.missing.app_error", nil, "", http.StatusNotFound).Wrap(err) default: return nil, model.NewAppError("GetChannelByName", "app.channel.get_by_name.existing.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } } return channel, nil } func (a *App) GetChannelsByNames(rctx request.CTX, channelNames []string, teamID string) ([]*model.Channel, *model.AppError) { channels, err := a.Srv().Store().Channel().GetByNames(teamID, channelNames, true) if err != nil { return nil, model.NewAppError("GetChannelsByNames", "app.channel.get_by_name.existing.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } return channels, nil } func (a *App) GetChannelByNameForTeamName(rctx request.CTX, channelName, teamName string, includeDeleted bool) (*model.Channel, *model.AppError) { var team *model.Team team, err := a.Srv().Store().Team().GetByName(teamName) if err != nil { var nfErr *store.ErrNotFound switch { case errors.As(err, &nfErr): return nil, model.NewAppError("GetChannelByNameForTeamName", "app.team.get_by_name.missing.app_error", nil, "", http.StatusNotFound).Wrap(err) default: return nil, model.NewAppError("GetChannelByNameForTeamName", "app.team.get_by_name.app_error", nil, "", http.StatusNotFound).Wrap(err) } } var result *model.Channel var nErr error if includeDeleted { result, nErr = a.Srv().Store().Channel().GetByNameIncludeDeleted(team.Id, channelName, false) } else { result, nErr = a.Srv().Store().Channel().GetByName(team.Id, channelName, false) } if nErr != nil { var nfErr *store.ErrNotFound switch { case errors.As(nErr, &nfErr): return nil, model.NewAppError("GetChannelByNameForTeamName", "app.channel.get_by_name.missing.app_error", nil, "", http.StatusNotFound).Wrap(nErr) default: return nil, model.NewAppError("GetChannelByNameForTeamName", "app.channel.get_by_name.existing.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr) } } return result, nil } func (s *Server) getChannelsForTeamForUser(rctx request.CTX, teamID string, userID string, opts *model.ChannelSearchOpts) (model.ChannelList, *model.AppError) { list, err := s.Store().Channel().GetChannels(teamID, userID, opts) if err != nil { var nfErr *store.ErrNotFound switch { case errors.As(err, &nfErr): return nil, model.NewAppError("GetChannelsForUser", "app.channel.get_channels.not_found.app_error", nil, "", http.StatusNotFound).Wrap(err) default: return nil, model.NewAppError("GetChannelsForUser", "app.channel.get_channels.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } } return list, nil } func (a *App) GetChannelsForTeamForUser(rctx request.CTX, teamID string, userID string, opts *model.ChannelSearchOpts) (model.ChannelList, *model.AppError) { channels, appErr := a.Srv().getChannelsForTeamForUser(rctx, teamID, userID, opts) if appErr != nil { return nil, appErr } // Hydrate the policy action set so the frontend can distinguish a // membership policy from a permission-only one (e.g. file upload) without // a second round-trip. Without this the guest-invite picker reads the bare // PolicyEnforced flag and wrongly hides permission-only channels. No-op for // channels without an attached policy, keeping the no-policy path free. if appErr := a.HydrateChannelsPolicyActions(rctx, channels); appErr != nil { rctx.Logger().Warn("Failed to hydrate channel policy actions for team channels; returning channels without action map", mlog.String("team_id", teamID), mlog.String("user_id", userID), mlog.Err(appErr), ) } return channels, nil } func (a *App) GetChannelsForUser(rctx request.CTX, userID string, includeDeleted bool, lastDeleteAt, pageSize int, fromChannelID string) (model.ChannelList, *model.AppError) { list, err := a.Srv().Store().Channel().GetChannelsByUser(userID, includeDeleted, lastDeleteAt, pageSize, fromChannelID) if err != nil { var nfErr *store.ErrNotFound switch { case errors.As(err, &nfErr): return nil, model.NewAppError("GetChannelsForUser", "app.channel.get_channels.not_found.app_error", nil, "", http.StatusNotFound).Wrap(err) default: return nil, model.NewAppError("GetChannelsForUser", "app.channel.get_channels.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } } return list, nil } func (a *App) GetAllChannels(rctx request.CTX, page, perPage int, opts model.ChannelSearchOpts) (model.ChannelListWithTeamData, *model.AppError) { if opts.ExcludeDefaultChannels { opts.ExcludeChannelNames = a.DefaultChannelNames(rctx) } storeOpts := store.ChannelSearchOpts{ NotAssociatedToGroup: opts.NotAssociatedToGroup, IncludeDeleted: opts.IncludeDeleted, ExcludeChannelNames: opts.ExcludeChannelNames, GroupConstrained: opts.GroupConstrained, ExcludeGroupConstrained: opts.ExcludeGroupConstrained, ExcludePolicyConstrained: opts.ExcludePolicyConstrained, IncludePolicyID: opts.IncludePolicyID, AccessControlPolicyEnforced: opts.AccessControlPolicyEnforced, ExcludeAccessControlPolicyEnforced: opts.ExcludeAccessControlPolicyEnforced, } channels, err := a.Srv().Store().Channel().GetAllChannels(page*perPage, perPage, storeOpts) if err != nil { return nil, model.NewAppError("GetAllChannels", "app.channel.get_all_channels.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } return channels, nil } func (a *App) GetAllChannelsCount(rctx request.CTX, opts model.ChannelSearchOpts) (int64, *model.AppError) { if opts.ExcludeDefaultChannels { opts.ExcludeChannelNames = a.DefaultChannelNames(rctx) } storeOpts := store.ChannelSearchOpts{ NotAssociatedToGroup: opts.NotAssociatedToGroup, IncludeDeleted: opts.IncludeDeleted, ExcludeChannelNames: opts.ExcludeChannelNames, GroupConstrained: opts.GroupConstrained, ExcludeGroupConstrained: opts.ExcludeGroupConstrained, ExcludePolicyConstrained: opts.ExcludePolicyConstrained, IncludePolicyID: opts.IncludePolicyID, } count, err := a.Srv().Store().Channel().GetAllChannelsCount(storeOpts) if err != nil { return 0, model.NewAppError("GetAllChannelsCount", "app.channel.get_all_channels_count.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } return count, nil } func (a *App) GetDeletedChannels(rctx request.CTX, teamID string, offset int, limit int, userID string, skipTeamMembershipCheck bool) (model.ChannelList, *model.AppError) { list, err := a.Srv().Store().Channel().GetDeleted(teamID, offset, limit, userID, skipTeamMembershipCheck) if err != nil { var nfErr *store.ErrNotFound switch { case errors.As(err, &nfErr): return nil, model.NewAppError("GetDeletedChannels", "app.channel.get_deleted.missing.app_error", nil, "", http.StatusNotFound).Wrap(err) default: return nil, model.NewAppError("GetDeletedChannels", "app.channel.get_deleted.existing.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } } return list, nil } func (a *App) GetChannelsUserNotIn(rctx request.CTX, teamID string, userID string, offset int, limit int) (model.ChannelList, *model.AppError) { channels, err := a.Srv().Store().Channel().GetMoreChannels(teamID, userID, offset, limit) if err != nil { return nil, model.NewAppError("GetChannelsUserNotIn", "app.channel.get_more_channels.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } return channels, nil } func (a *App) GetPublicChannelsByIdsForTeam(rctx request.CTX, teamID string, channelIDs []string) (model.ChannelList, *model.AppError) { list, err := a.Srv().Store().Channel().GetPublicChannelsByIdsForTeam(teamID, channelIDs) if err != nil { var nfErr *store.ErrNotFound switch { case errors.As(err, &nfErr): return nil, model.NewAppError("GetPublicChannelsByIdsForTeam", "app.channel.get_channels_by_ids.not_found.app_error", nil, "", http.StatusNotFound).Wrap(err) default: return nil, model.NewAppError("GetPublicChannelsByIdsForTeam", "app.channel.get_channels_by_ids.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } } return list, nil } func (a *App) GetPublicChannelsForTeam(rctx request.CTX, teamID string, offset int, limit int) (model.ChannelList, *model.AppError) { list, err := a.Srv().Store().Channel().GetPublicChannelsForTeam(teamID, offset, limit) if err != nil { return nil, model.NewAppError("GetPublicChannelsForTeam", "app.channel.get_public_channels.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } return list, nil } func (a *App) GetPrivateChannelsForTeam(rctx request.CTX, teamID string, offset int, limit int) (model.ChannelList, *model.AppError) { list, err := a.Srv().Store().Channel().GetPrivateChannelsForTeam(teamID, offset, limit) if err != nil { return nil, model.NewAppError("GetPrivateChannelsForTeam", "app.channel.get_private_channels.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } return list, nil } func (a *App) GetChannelMember(rctx request.CTX, channelID string, userID string) (*model.ChannelMember, *model.AppError) { return a.Srv().getChannelMember(rctx, channelID, userID) } func (s *Server) getChannelMember(rctx request.CTX, channelID string, userID string) (*model.ChannelMember, *model.AppError) { channelMember, err := s.Store().Channel().GetMember(rctx, channelID, userID) if err != nil { var nfErr *store.ErrNotFound switch { case errors.As(err, &nfErr): return nil, model.NewAppError("GetChannelMember", MissingChannelMemberError, nil, "", http.StatusNotFound).Wrap(err) default: return nil, model.NewAppError("GetChannelMember", "app.channel.get_member.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } } return channelMember, nil } func (s *Server) getChannelMemberLastViewedAt(rctx request.CTX, channelID string, userID string) (int64, *model.AppError) { lastViewedAt, err := s.Store().Channel().GetMemberLastViewedAt(rctx, channelID, userID) if err != nil { var nfErr *store.ErrNotFound switch { case errors.As(err, &nfErr): return 0, model.NewAppError("getChannelMemberLastViewedAt", MissingChannelMemberError, nil, "", http.StatusNotFound).Wrap(err) default: return 0, model.NewAppError("getChannelMemberLastViewedAt", "app.channel.get_member.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } } return lastViewedAt, nil } func (a *App) GetChannelMembersPage(rctx request.CTX, channelID string, page, perPage int) (model.ChannelMembers, *model.AppError) { opts := model.ChannelMembersGetOptions{ ChannelID: channelID, Offset: page * perPage, Limit: perPage, } channelMembers, err := a.Srv().Store().Channel().GetMembers(opts) if err != nil { return nil, model.NewAppError("GetChannelMembersPage", "app.channel.get_members.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } return channelMembers, nil } func (a *App) GetChannelMembersTimezones(rctx request.CTX, channelID string) ([]string, *model.AppError) { membersTimezones, err := a.Srv().Store().Channel().GetChannelMembersTimezones(channelID) if err != nil { return nil, model.NewAppError("GetChannelMembersTimezones", "app.channel.get_members.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } var timezones []string for _, membersTimezone := range membersTimezones { if membersTimezone["automaticTimezone"] == "" && membersTimezone["manualTimezone"] == "" { continue } timezones = append(timezones, model.GetPreferredTimezone(membersTimezone)) } return model.RemoveDuplicateStrings(timezones), nil } func (a *App) GetChannelMembersByIds(rctx request.CTX, channelID string, userIDs []string) (model.ChannelMembers, *model.AppError) { members, err := a.Srv().Store().Channel().GetMembersByIds(channelID, userIDs) if err != nil { return nil, model.NewAppError("GetChannelMembersByIds", "app.channel.get_members_by_ids.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } return members, nil } func (a *App) GetChannelMembersForUser(rctx request.CTX, teamID string, userID string) (model.ChannelMembers, *model.AppError) { channelMembers, err := a.Srv().Store().Channel().GetMembersForUser(teamID, userID) if err != nil { return nil, model.NewAppError("GetChannelMembersForUser", "app.channel.get_members.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } return channelMembers, nil } func (a *App) GetChannelMembersForUserWithPagination(rctx request.CTX, userID string, page, perPage int) ([]*model.ChannelMember, *model.AppError) { m, err := a.Srv().Store().Channel().GetMembersForUserWithPagination(userID, page, perPage) if err != nil { return nil, model.NewAppError("GetChannelMembersForUserWithPagination", "app.channel.get_members.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } members := make([]*model.ChannelMember, 0, len(m)) for _, member := range m { members = append(members, &member.ChannelMember) } return members, nil } func (a *App) GetChannelMembersWithTeamDataForUserWithPagination(rctx request.CTX, userID string, cursor *model.ChannelMemberCursor) (model.ChannelMembersWithTeamData, *model.AppError) { var m model.ChannelMembersWithTeamData var err error var method string if cursor.Page == -1 { m, err = a.Srv().Store().Channel().GetMembersForUserWithCursorPagination(userID, cursor.PerPage, cursor.FromChannelID) method = "GetMembersForUserWithCursorPagination" } else { m, err = a.Srv().Store().Channel().GetMembersForUserWithPagination(userID, cursor.Page, cursor.PerPage) method = "GetMembersForUserWithPagination" } if err != nil { var nfErr *store.ErrNotFound switch { case errors.As(err, &nfErr): return nil, model.NewAppError(method, MissingChannelMemberError, nil, "", http.StatusNotFound).Wrap(err) default: return nil, model.NewAppError(method, "app.channel.get_members.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } } return m, nil } func (a *App) GetChannelMemberCount(rctx request.CTX, channelID string) (int64, *model.AppError) { count, err := a.Srv().Store().Channel().GetMemberCount(channelID, true) if err != nil { return 0, model.NewAppError("GetChannelMemberCount", "app.channel.get_member_count.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } return count, nil } func (a *App) GetChannelFileCount(rctx request.CTX, channelID string) (int64, *model.AppError) { count, err := a.Srv().Store().Channel().GetFileCount(channelID) if err != nil { return 0, model.NewAppError("SqlChannelStore.GetFileCount", "app.channel.get_file_count.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } return count, nil } func (a *App) GetChannelGuestCount(rctx request.CTX, channelID string) (int64, *model.AppError) { count, err := a.Srv().Store().Channel().GetGuestCount(channelID, true) if err != nil { return 0, model.NewAppError("SqlChannelStore.GetGuestCount", "app.channel.get_member_count.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } return count, nil } func (a *App) GetChannelPinnedPostCount(rctx request.CTX, channelID string) (int64, *model.AppError) { count, err := a.Srv().Store().Channel().GetPinnedPostCount(channelID, true) if err != nil { return 0, model.NewAppError("GetChannelPinnedPostCount", "app.channel.get_pinnedpost_count.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } return count, nil } func (a *App) GetChannelUnread(rctx request.CTX, channelID, userID string) (*model.ChannelUnread, *model.AppError) { channelUnread, err := a.Srv().Store().Channel().GetChannelUnread(channelID, userID) if err != nil { var nfErr *store.ErrNotFound switch { case errors.As(err, &nfErr): return nil, model.NewAppError("GetChannelUnread", "app.channel.get_unread.app_error", nil, "", http.StatusNotFound).Wrap(err) default: return nil, model.NewAppError("GetChannelUnread", "app.channel.get_unread.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } } if channelUnread.NotifyProps[model.MarkUnreadNotifyProp] == model.ChannelMarkUnreadMention { channelUnread.MsgCount = 0 channelUnread.MsgCountRoot = 0 } return channelUnread, nil } func (a *App) JoinChannel(rctx request.CTX, channel *model.Channel, userID string) *model.AppError { userChan := make(chan store.StoreResult[*model.User], 1) memberChan := make(chan store.StoreResult[*model.ChannelMember], 1) go func() { user, err := a.Srv().Store().User().Get(rctx, userID) userChan <- store.StoreResult[*model.User]{Data: user, NErr: err} close(userChan) }() go func() { member, err := a.Srv().Store().Channel().GetMember(rctx, channel.Id, userID) memberChan <- store.StoreResult[*model.ChannelMember]{Data: member, NErr: err} close(memberChan) }() uresult := <-userChan if uresult.NErr != nil { var nfErr *store.ErrNotFound switch { case errors.As(uresult.NErr, &nfErr): return model.NewAppError("CreateChannel", MissingAccountError, nil, "", http.StatusNotFound).Wrap(uresult.NErr) default: return model.NewAppError("CreateChannel", "app.user.get.app_error", nil, "", http.StatusInternalServerError).Wrap(uresult.NErr) } } mresult := <-memberChan if mresult.NErr == nil && mresult.Data != nil { // user is already in the channel return nil } user := uresult.Data if channel.Type != model.ChannelTypeOpen { return model.NewAppError("JoinChannel", "api.channel.join_channel.permissions.app_error", nil, "", http.StatusBadRequest) } cm, err := a.AddUserToChannel(rctx, user, channel, false) if err != nil { return err } a.Srv().Go(func() { pluginContext := pluginContext(rctx) a.ch.RunMultiHook(func(hooks plugin.Hooks, _ *model.Manifest) bool { hooks.UserHasJoinedChannel(pluginContext, cm, nil) return true }, plugin.UserHasJoinedChannelID) }) if err := a.postJoinChannelMessage(rctx, user, channel); err != nil { return err } return nil } func (a *App) postJoinChannelMessage(rctx request.CTX, user *model.User, channel *model.Channel) *model.AppError { message := fmt.Sprintf(i18n.T("api.channel.join_channel.post_and_forget"), user.Username) postType := model.PostTypeJoinChannel if user.IsGuest() { message = fmt.Sprintf(i18n.T("api.channel.guest_join_channel.post_and_forget"), user.Username) postType = model.PostTypeGuestJoinChannel } post := &model.Post{ ChannelId: channel.Id, Message: message, Type: postType, UserId: user.Id, Props: model.StringInterface{ "username": user.Username, }, } if _, _, err := a.CreatePost(rctx, post, channel, model.CreatePostFlags{SetOnline: true}); err != nil { return model.NewAppError("postJoinChannelMessage", "api.channel.post_user_add_remove_message_and_forget.error", nil, "", http.StatusInternalServerError).Wrap(err) } return nil } func (a *App) postJoinTeamMessage(rctx request.CTX, user *model.User, channel *model.Channel) *model.AppError { post := &model.Post{ ChannelId: channel.Id, Message: fmt.Sprintf(i18n.T("api.team.join_team.post_and_forget"), user.Username), Type: model.PostTypeJoinTeam, UserId: user.Id, Props: model.StringInterface{ "username": user.Username, }, } if _, _, err := a.CreatePost(rctx, post, channel, model.CreatePostFlags{SetOnline: true}); err != nil { return model.NewAppError("postJoinTeamMessage", "api.channel.post_user_add_remove_message_and_forget.error", nil, "", http.StatusInternalServerError).Wrap(err) } return nil } func (a *App) LeaveChannel(rctx request.CTX, channelID string, userID string) *model.AppError { sc := make(chan store.StoreResult[*model.Channel], 1) go func() { channel, err := a.Srv().Store().Channel().Get(channelID, true) sc <- store.StoreResult[*model.Channel]{Data: channel, NErr: err} close(sc) }() uc := make(chan store.StoreResult[*model.User], 1) go func() { user, err := a.Srv().Store().User().Get(rctx, userID) uc <- store.StoreResult[*model.User]{Data: user, NErr: err} close(uc) }() cresult := <-sc if cresult.NErr != nil { errCtx := map[string]any{"channel_id": channelID} var nfErr *store.ErrNotFound switch { case errors.As(cresult.NErr, &nfErr): return model.NewAppError("LeaveChannel", "app.channel.get.existing.app_error", errCtx, "", http.StatusNotFound).Wrap(cresult.NErr) default: return model.NewAppError("LeaveChannel", "app.channel.get.find.app_error", errCtx, "", http.StatusInternalServerError).Wrap(cresult.NErr) } } uresult := <-uc if uresult.NErr != nil { var nfErr *store.ErrNotFound switch { case errors.As(uresult.NErr, &nfErr): return model.NewAppError("LeaveChannel", MissingAccountError, nil, "", http.StatusNotFound).Wrap(uresult.NErr) default: return model.NewAppError("LeaveChannel", "app.user.get.app_error", nil, "", http.StatusInternalServerError).Wrap(uresult.NErr) } } channel := cresult.Data user := uresult.Data if channel.IsGroupOrDirect() { err := model.NewAppError("LeaveChannel", "api.channel.leave.direct.app_error", nil, "", http.StatusBadRequest) return err } if err := a.removeUserFromChannel(rctx, userID, userID, channel); err != nil { return err } if channel.Name == model.DefaultChannelName && !*a.Config().ServiceSettings.ExperimentalEnableDefaultChannelLeaveJoinMessages { return nil } a.Srv().Go(func() { if err := a.postLeaveChannelMessage(rctx, user, channel); err != nil { rctx.Logger().Error("Failed to post LeaveChannel message", mlog.Err(err)) } }) return nil } func (a *App) postLeaveChannelMessage(rctx request.CTX, user *model.User, channel *model.Channel) *model.AppError { post := &model.Post{ ChannelId: channel.Id, // Message here embeds `@username`, not just `username`, to ensure that mentions // treat this as a username mention even though the user has now left the channel. // The client renders its own system message, ignoring this value altogether. Message: fmt.Sprintf(i18n.T("api.channel.leave.left"), fmt.Sprintf("@%s", user.Username)), Type: model.PostTypeLeaveChannel, UserId: user.Id, Props: model.StringInterface{ "username": user.Username, }, } if _, _, err := a.CreatePost(rctx, post, channel, model.CreatePostFlags{SetOnline: true}); err != nil { return model.NewAppError("postLeaveChannelMessage", "api.channel.post_user_add_remove_message_and_forget.error", nil, "", http.StatusInternalServerError).Wrap(err) } return nil } func (a *App) PostAddToChannelMessage(rctx request.CTX, user *model.User, addedUser *model.User, channel *model.Channel, postRootId string) *model.AppError { message := fmt.Sprintf(i18n.T("api.channel.add_member.added"), addedUser.Username, user.Username) postType := model.PostTypeAddToChannel if addedUser.IsGuest() { message = fmt.Sprintf(i18n.T("api.channel.add_guest.added"), addedUser.Username, user.Username) postType = model.PostTypeAddGuestToChannel } post := &model.Post{ ChannelId: channel.Id, Message: message, Type: postType, UserId: user.Id, Props: model.StringInterface{ "userId": user.Id, "username": user.Username, model.PostPropsAddedUserId: addedUser.Id, "addedUsername": addedUser.Username, }, } if _, _, err := a.CreatePost(rctx, post, channel, model.CreatePostFlags{SetOnline: true}); err != nil { return model.NewAppError("postAddToChannelMessage", "api.channel.post_user_add_remove_message_and_forget.error", nil, "", http.StatusInternalServerError).Wrap(err) } return nil } func (a *App) postAddToTeamMessage(rctx request.CTX, user *model.User, addedUser *model.User, channel *model.Channel, postRootId string) *model.AppError { post := &model.Post{ ChannelId: channel.Id, Message: fmt.Sprintf(i18n.T("api.team.add_user_to_team.added"), addedUser.Username, user.Username), Type: model.PostTypeAddToTeam, UserId: user.Id, RootId: postRootId, Props: model.StringInterface{ "userId": user.Id, "username": user.Username, model.PostPropsAddedUserId: addedUser.Id, "addedUsername": addedUser.Username, }, } if _, _, err := a.CreatePost(rctx, post, channel, model.CreatePostFlags{SetOnline: true}); err != nil { return model.NewAppError("postAddToTeamMessage", "api.channel.post_user_add_remove_message_and_forget.error", nil, "", http.StatusInternalServerError).Wrap(err) } return nil } func (a *App) postRemoveFromChannelMessage(rctx request.CTX, removerUserId string, removedUser *model.User, channel *model.Channel) *model.AppError { messageUserId := removerUserId if messageUserId == "" { systemBot, err := a.GetSystemBot(rctx) if err != nil { return model.NewAppError("postRemoveFromChannelMessage", "api.channel.post_user_add_remove_message_and_forget.error", nil, "", http.StatusInternalServerError).Wrap(err) } messageUserId = systemBot.UserId } post := &model.Post{ ChannelId: channel.Id, // Message here embeds `@username`, not just `username`, to ensure that mentions // treat this as a username mention even though the user has now left the channel. // The client renders its own system message, ignoring this value altogether. Message: fmt.Sprintf(i18n.T("api.channel.remove_member.removed"), fmt.Sprintf("@%s", removedUser.Username)), Type: model.PostTypeRemoveFromChannel, UserId: messageUserId, Props: model.StringInterface{ "removedUserId": removedUser.Id, "removedUsername": removedUser.Username, }, } if _, _, err := a.CreatePost(rctx, post, channel, model.CreatePostFlags{SetOnline: true}); err != nil { return model.NewAppError("postRemoveFromChannelMessage", "api.channel.post_user_add_remove_message_and_forget.error", nil, "", http.StatusInternalServerError).Wrap(err) } return nil } // removeChannelMembership strips a user's channel membership and the associated // thread memberships. Keeping these together ensures channel access cannot be // revoked without also dropping the thread state that depends on it. func (a *App) removeChannelMembership(rctx request.CTX, userID, channelID, caller string) *model.AppError { if err := a.Srv().Store().Channel().RemoveMember(rctx, channelID, userID); err != nil { return model.NewAppError(caller, "app.channel.remove_member.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } if err := a.Srv().Store().Thread().DeleteMembershipsForChannel(userID, channelID); err != nil { return model.NewAppError(caller, model.NoTranslation, nil, "failed to delete threadmemberships upon leaving channel", http.StatusInternalServerError).Wrap(err) } return nil } func (a *App) removeUserFromChannel(rctx request.CTX, userIDToRemove string, removerUserId string, channel *model.Channel) *model.AppError { user, nErr := a.Srv().Store().User().Get(rctx, userIDToRemove) if nErr != nil { var nfErr *store.ErrNotFound switch { case errors.As(nErr, &nfErr): return model.NewAppError("removeUserFromChannel", MissingAccountError, nil, "", http.StatusNotFound).Wrap(nErr) default: return model.NewAppError("removeUserFromChannel", "app.user.get.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr) } } isGuest := user.IsGuest() if channel.Name == model.DefaultChannelName { if !isGuest { return model.NewAppError("RemoveUserFromChannel", "api.channel.remove.default.app_error", map[string]any{"Channel": model.DefaultChannelName}, "", http.StatusBadRequest) } } if channel.IsGroupConstrained() && userIDToRemove != removerUserId && !user.IsBot { nonMembers, err := a.FilterNonGroupChannelMembers(rctx, []string{userIDToRemove}, channel) if err != nil { return model.NewAppError("removeUserFromChannel", "api.channel.remove_user_from_channel.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } if len(nonMembers) == 0 { return model.NewAppError("removeUserFromChannel", "api.channel.remove_members.denied", map[string]any{"UserIDs": nonMembers}, "", http.StatusBadRequest) } } cm, err := a.GetChannelMember(rctx, channel.Id, userIDToRemove) if err != nil { return err } if appErr := a.removeChannelMembership(rctx, userIDToRemove, channel.Id, "removeUserFromChannel"); appErr != nil { return appErr } if err := a.Srv().Store().ChannelMemberHistory().LogLeaveEvent(rctx, userIDToRemove, channel.Id, model.GetMillis()); err != nil { return model.NewAppError("removeUserFromChannel", "app.channel_member_history.log_leave_event.internal_error", nil, "", http.StatusInternalServerError).Wrap(err) } if isGuest { currentMembers, err := a.GetChannelMembersForUser(rctx, channel.TeamId, userIDToRemove) if err != nil { return err } // GetChannelMembersForUser excludes space backing channels. A guest still in a space // belongs to the team, so count space memberships before evicting them from the team. if len(currentMembers) == 0 { spaceChannels, sErr := a.Srv().Store().Channel().GetTeamSpaceChannelsForUser(channel.TeamId, userIDToRemove) if sErr != nil { return model.NewAppError("removeUserFromChannel", "app.channel.get_channels.get.app_error", nil, "", http.StatusInternalServerError).Wrap(sErr) } if len(spaceChannels) == 0 { teamMember, err := a.GetTeamMember(rctx, channel.TeamId, userIDToRemove) if err != nil { return model.NewAppError("removeUserFromChannel", "api.team.remove_user_from_team.missing.app_error", nil, "", http.StatusBadRequest).Wrap(err) } if err := a.ch.srv.teamService.RemoveTeamMember(rctx, teamMember); err != nil { return model.NewAppError("removeUserFromChannel", "api.team.remove_user_from_team.missing.app_error", nil, "", http.StatusBadRequest).Wrap(err) } if err = a.postProcessTeamMemberLeave(rctx, teamMember, removerUserId); err != nil { return err } } } } a.Srv().Platform().InvalidateChannelCacheForUser(userIDToRemove) a.invalidateCacheForChannelMembers(channel.Id) a.Srv().Store().AutoTranslation().InvalidateUserAutoTranslation(userIDToRemove, channel.Id) a.Srv().Store().AutoTranslation().InvalidateUserLocaleCache(userIDToRemove) // Space backing channels are internal: skip the user_removed chat events and plugin hook. if channel.IsSpace() { return nil } var actorUser *model.User if removerUserId != "" { actorUser, _ = a.GetUser(rctx, removerUserId) } a.Srv().Go(func() { pluginContext := pluginContext(rctx) a.ch.RunMultiHook(func(hooks plugin.Hooks, _ *model.Manifest) bool { hooks.UserHasLeftChannel(pluginContext, cm, actorUser) return true }, plugin.UserHasLeftChannelID) }) message := model.NewWebSocketEvent(model.WebsocketEventUserRemoved, "", channel.Id, "", nil, "") message.Add("user_id", userIDToRemove) message.Add("remover_id", removerUserId) a.Publish(message) // because the removed user no longer belongs to the channel we need to send a separate websocket event userMsg := model.NewWebSocketEvent(model.WebsocketEventUserRemoved, "", "", userIDToRemove, nil, "") userMsg.Add("channel_id", channel.Id) userMsg.Add("remover_id", removerUserId) a.Publish(userMsg) // Synchronize membership change for shared channels if channel.IsShared() { if scs := a.Srv().Platform().GetSharedChannelService(); scs != nil { scs.NotifyMembershipChanged(channel.Id, "") } } return nil } func (a *App) RemoveUserFromChannel(rctx request.CTX, userIDToRemove string, removerUserId string, channel *model.Channel) *model.AppError { var err *model.AppError if err = a.removeUserFromChannel(rctx, userIDToRemove, removerUserId, channel); err != nil { return err } // Space backing channels are internal: skip the leave/remove system post. if channel.IsSpace() { return nil } var user *model.User if user, err = a.GetUser(rctx, userIDToRemove); err != nil { return err } if userIDToRemove == removerUserId { if err := a.postLeaveChannelMessage(rctx, user, channel); err != nil { return err } } else { if err := a.postRemoveFromChannelMessage(rctx, removerUserId, user, channel); err != nil { rctx.Logger().Error("Failed to post user removal message", mlog.Err(err)) } } return nil } func (a *App) GetNumberOfChannelsOnTeam(rctx request.CTX, teamID string) (int, *model.AppError) { // Get total number of channels on current team list, err := a.Srv().Store().Channel().GetTeamChannels(teamID) if err != nil { var nfErr *store.ErrNotFound switch { case errors.As(err, &nfErr): return 0, model.NewAppError("GetNumberOfChannelsOnTeam", "app.channel.get_channels.not_found.app_error", nil, "", http.StatusNotFound).Wrap(err) default: return 0, model.NewAppError("GetNumberOfChannelsOnTeam", "app.channel.get_channels.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } } return len(list), nil } func (a *App) SetActiveChannel(rctx request.CTX, userID string, channelID string) *model.AppError { status, err := a.Srv().Platform().GetStatus(userID) oldStatus := model.StatusOffline if err != nil { status = &model.Status{UserId: userID, Status: model.StatusOnline, Manual: false, LastActivityAt: model.GetMillis(), ActiveChannel: channelID} } else { oldStatus = status.Status status.ActiveChannel = channelID if !status.Manual && channelID != "" { status.Status = model.StatusOnline } status.LastActivityAt = model.GetMillis() } a.Srv().Platform().AddStatusCache(status) if status.Status != oldStatus { a.Srv().Platform().BroadcastStatus(status) } return nil } func (a *App) IsCRTEnabledForUser(rctx request.CTX, userID string) bool { appCRT := *a.Config().ServiceSettings.CollapsedThreads if appCRT == model.CollapsedThreadsDisabled { return false } if appCRT == model.CollapsedThreadsAlwaysOn { return true } threadsEnabled := appCRT == model.CollapsedThreadsDefaultOn // check if a participant has overridden collapsed threads settings if preference, err := a.Srv().Store().Preference().Get(userID, model.PreferenceCategoryDisplaySettings, model.PreferenceNameCollapsedThreadsEnabled); err == nil { threadsEnabled = preference.Value == "on" } return threadsEnabled } // ValidateUserPermissionsOnChannels filters channelIds based on whether userId is authorized to manage channel members. Unauthorized channels are removed from the returned list. func (a *App) ValidateUserPermissionsOnChannels(rctx request.CTX, userId string, channelIds []string) []string { var allowedChannelIds []string for _, channelId := range channelIds { channel, err := a.GetChannel(rctx, channelId) if err != nil { rctx.Logger().Info("Invite users to team - couldn't get channel " + channelId) continue } allowedPrivate := false if channel.Type == model.ChannelTypePrivate { allowedPrivate, _ = a.HasPermissionToChannel(rctx, userId, channelId, model.PermissionManagePrivateChannelMembers) } allowedPublic := false if channel.Type == model.ChannelTypeOpen { allowedPublic, _ = a.HasPermissionToChannel(rctx, userId, channelId, model.PermissionManagePublicChannelMembers) } if allowedPrivate || allowedPublic { allowedChannelIds = append(allowedChannelIds, channelId) } else { rctx.Logger().Info("Invite users to team - no permission to add members to that channel. UserId: " + userId + " ChannelId: " + channelId) } } return allowedChannelIds } // MarkChanelAsUnreadFromPost will take a post and set the channel as unread from that one. func (a *App) MarkChannelAsUnreadFromPost(rctx request.CTX, postID string, userID string, collapsedThreadsSupported bool) (*model.ChannelUnreadAt, *model.AppError) { if !collapsedThreadsSupported || !a.IsCRTEnabledForUser(rctx, userID) { return a.markChannelAsUnreadFromPostCRTUnsupported(rctx, postID, userID) } post, err := a.GetSinglePost(rctx, postID, false) if err != nil { return nil, err } user, err := a.GetUser(rctx, userID) if err != nil { return nil, err } unreadMentions, unreadMentionsRoot, urgentMentions, err := a.countMentionsFromPost(rctx, user, post) if err != nil { return nil, err } channelUnread, nErr := a.Srv().Store().Channel().UpdateLastViewedAtPost(post, userID, unreadMentions, unreadMentionsRoot, urgentMentions, true) if nErr != nil { return channelUnread, model.NewAppError("MarkChannelAsUnreadFromPost", "app.channel.update_last_viewed_at_post.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr) } a.sendWebSocketPostUnreadEvent(rctx, channelUnread, postID) a.UpdateMobileAppBadge(userID) return channelUnread, nil } func (a *App) markChannelAsUnreadFromPostCRTUnsupported(rctx request.CTX, postID string, userID string) (*model.ChannelUnreadAt, *model.AppError) { post, appErr := a.GetSinglePost(rctx, postID, false) if appErr != nil { return nil, appErr } user, appErr := a.GetUser(rctx, userID) if appErr != nil { return nil, appErr } threadId := post.RootId if post.RootId == "" { threadId = post.Id } unreadMentions, unreadMentionsRoot, urgentMentions, appErr := a.countMentionsFromPost(rctx, user, post) if appErr != nil { return nil, appErr } // if root post, // In CRT Supported Client: badge on channel only sums mentions in root posts including and below the post that was marked. // In CRT Unsupported Client: badge on channel sums mentions in all posts (root & replies) including and below the post that was marked unread. if post.RootId == "" { channelUnread, nErr := a.Srv().Store().Channel().UpdateLastViewedAtPost(post, userID, unreadMentions, unreadMentionsRoot, urgentMentions, true) if nErr != nil { return channelUnread, model.NewAppError("MarkChannelAsUnreadFromPost", "app.channel.update_last_viewed_at_post.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr) } a.sendWebSocketPostUnreadEvent(rctx, channelUnread, postID) a.UpdateMobileAppBadge(userID) return channelUnread, nil } // if reply post, autofollow thread and // In CRT Supported Client: Mark the specific thread as unread but not the channel where the thread exists. // If there are replies with mentions below the marked reply in the thread, then sum the mentions for the threads mention badge. // In CRT Unsupported Client: Channel is marked as unread and new messages line inserted above the marked post. // Badge on channel sums mentions in all posts (root & replies) including and below the post that was marked unread. rootPost, appErr := a.GetSinglePost(rctx, post.RootId, false) if appErr != nil { return nil, appErr } channel, nErr := a.Srv().Store().Channel().Get(post.ChannelId, true) if nErr != nil { return nil, model.NewAppError("MarkChannelAsUnreadFromPost", "app.channel.update_last_viewed_at_post.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr) } if *a.Config().ServiceSettings.ThreadAutoFollow { threadMembership, mErr := a.Srv().Store().Thread().GetMembershipForUser(user.Id, threadId) var errNotFound *store.ErrNotFound if mErr != nil && !errors.As(mErr, &errNotFound) { return nil, model.NewAppError("MarkChannelAsUnreadFromPost", "app.channel.update_last_viewed_at_post.app_error", nil, "", http.StatusInternalServerError).Wrap(mErr) } // Follow thread if we're not already following it if threadMembership == nil { opts := store.ThreadMembershipOpts{ Following: true, IncrementMentions: false, UpdateFollowing: true, UpdateViewedTimestamp: false, UpdateParticipants: false, } threadMembership, mErr = a.Srv().Store().Thread().MaintainMembership(user.Id, threadId, opts) if mErr != nil { return nil, model.NewAppError("MarkChannelAsUnreadFromPost", "app.channel.update_last_viewed_at_post.app_error", nil, "", http.StatusInternalServerError).Wrap(mErr) } } // If threadmembership already exists but user had previously unfollowed the thread, then follow the thread again. threadMembership.Following = true threadMembership.LastViewed = post.CreateAt - 1 threadMembership.UnreadMentions, appErr = a.countThreadMentions(rctx, user, rootPost, channel.TeamId, post.CreateAt-1) if appErr != nil { return nil, appErr } threadMembership, mErr = a.Srv().Store().Thread().UpdateMembership(threadMembership) if mErr != nil { return nil, model.NewAppError("MarkChannelAsUnreadFromPost", "app.channel.update_last_viewed_at_post.app_error", nil, "", http.StatusInternalServerError).Wrap(mErr) } thread, mErr := a.Srv().Store().Thread().GetThreadForUser(rctx, threadMembership, true, a.IsPostPriorityEnabled()) if mErr != nil { return nil, model.NewAppError("MarkChannelAsUnreadFromPost", "app.channel.update_last_viewed_at_post.app_error", nil, "", http.StatusInternalServerError).Wrap(mErr) } a.sanitizeProfiles(thread.Participants, false) thread.Post.SanitizeNonIdentityProps() thread.Post.StripActionIntegrations() if a.IsCRTEnabledForUser(rctx, userID) { payload, jsonErr := json.Marshal(thread) if jsonErr != nil { return nil, model.NewAppError("MarkChannelAsUnreadFromPost", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(jsonErr) } message := model.NewWebSocketEvent(model.WebsocketEventThreadUpdated, channel.TeamId, "", userID, nil, "") message.Add("thread", string(payload)) a.markPostDeliveryForBroadcast(rctx, message, thread.Post) a.Publish(message) } } channelUnread, nErr := a.Srv().Store().Channel().UpdateLastViewedAtPost(post, userID, unreadMentions, 0, 0, false) if nErr != nil { return channelUnread, model.NewAppError("MarkChannelAsUnreadFromPost", "app.channel.update_last_viewed_at_post.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr) } a.sendWebSocketPostUnreadEvent(rctx, channelUnread, postID) a.UpdateMobileAppBadge(userID) return channelUnread, nil } func (a *App) sendWebSocketPostUnreadEvent(rctx request.CTX, channelUnread *model.ChannelUnreadAt, postID string) { message := model.NewWebSocketEvent(model.WebsocketEventPostUnread, channelUnread.TeamId, channelUnread.ChannelId, channelUnread.UserId, nil, "") message.Add("msg_count", channelUnread.MsgCount) message.Add("msg_count_root", channelUnread.MsgCountRoot) message.Add("mention_count", channelUnread.MentionCount) message.Add("mention_count_root", channelUnread.MentionCountRoot) message.Add("urgent_mention_count", channelUnread.UrgentMentionCount) message.Add("last_viewed_at", channelUnread.LastViewedAt) message.Add("post_id", postID) a.Publish(message) } func (a *App) AutocompleteChannels(rctx request.CTX, userID, term string) (model.ChannelListWithTeamData, *model.AppError) { includeDeleted := true term = strings.TrimSpace(term) user, appErr := a.GetUser(rctx, userID) if appErr != nil { return nil, appErr } channelList, err := a.Srv().Store().Channel().Autocomplete(rctx, userID, term, includeDeleted, user.IsGuest()) if err != nil { return nil, model.NewAppError("AutocompleteChannels", "app.channel.search.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } channelList, _, appErr = a.FilterChannelListWithTeamDataForUserVisibility(rctx, channelList, userID) if appErr != nil { return nil, appErr } return channelList, nil } func (a *App) AutocompleteChannelsForTeam(rctx request.CTX, teamID, userID, term string) (model.ChannelList, *model.AppError) { includeDeleted := true term = strings.TrimSpace(term) user, appErr := a.GetUser(rctx, userID) if appErr != nil { return nil, appErr } channelList, err := a.Srv().Store().Channel().AutocompleteInTeam(rctx, teamID, userID, term, includeDeleted, user.IsGuest()) if err != nil { return nil, model.NewAppError("AutocompleteChannels", "app.channel.search.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } return a.FilterChannelListForUserVisibility(rctx, channelList, userID) } func (a *App) AutocompleteChannelsForTeamFiltered(rctx request.CTX, teamID, userID, term string, privateOnly, excludeGroupConstrained bool) (model.ChannelList, *model.AppError) { includeDeleted := true term = strings.TrimSpace(term) user, appErr := a.GetUser(rctx, userID) if appErr != nil { return nil, appErr } channelList, err := a.Srv().Store().Channel().AutocompleteInTeamFiltered(rctx, teamID, userID, term, includeDeleted, user.IsGuest(), privateOnly, excludeGroupConstrained) if err != nil { return nil, model.NewAppError("AutocompleteChannelsForTeamFiltered", "app.channel.search.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } return a.FilterChannelListForUserVisibility(rctx, channelList, userID) } func (a *App) AutocompleteChannelsForSearch(rctx request.CTX, teamID string, userID string, term string) (model.ChannelList, *model.AppError) { includeDeleted := true term = strings.TrimSpace(term) channelList, err := a.Srv().Store().Channel().AutocompleteInTeamForSearch(teamID, userID, term, includeDeleted) if err != nil { return nil, model.NewAppError("AutocompleteChannelsForSearch", "app.channel.search.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } return channelList, nil } // SearchAllChannels returns a list of channels, the total count of the results of the search (if the paginate search option is true), and an error. func (a *App) SearchAllChannels(rctx request.CTX, term string, opts model.ChannelSearchOpts) (model.ChannelListWithTeamData, int64, *model.AppError) { if opts.ExcludeDefaultChannels { opts.ExcludeChannelNames = a.DefaultChannelNames(rctx) } storeOpts := store.ChannelSearchOpts{ ExcludeChannelNames: opts.ExcludeChannelNames, NotAssociatedToGroup: opts.NotAssociatedToGroup, IncludeDeleted: opts.IncludeDeleted, Deleted: opts.Deleted, TeamIds: opts.TeamIds, GroupConstrained: opts.GroupConstrained, ExcludeGroupConstrained: opts.ExcludeGroupConstrained, PolicyID: opts.PolicyID, IncludePolicyID: opts.IncludePolicyID, IncludeSearchByID: opts.IncludeSearchById, ExcludeRemote: opts.ExcludeRemote, ExcludePolicyConstrained: opts.ExcludePolicyConstrained, Public: opts.Public, Private: opts.Private, Page: opts.Page, PerPage: opts.PerPage, AccessControlPolicyEnforced: opts.AccessControlPolicyEnforced, ExcludeAccessControlPolicyEnforced: opts.ExcludeAccessControlPolicyEnforced, ParentAccessControlPolicyId: opts.ParentAccessControlPolicyId, } term = strings.TrimSpace(term) channelList, totalCount, err := a.Srv().Store().Channel().SearchAllChannels(term, storeOpts) if err != nil { return nil, 0, model.NewAppError("SearchAllChannels", "app.channel.search.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } return channelList, totalCount, nil } func (a *App) SearchChannels(rctx request.CTX, teamID string, term string) (model.ChannelList, *model.AppError) { includeDeleted := true term = strings.TrimSpace(term) channelList, err := a.Srv().Store().Channel().SearchInTeam(teamID, term, includeDeleted) if err != nil { return nil, model.NewAppError("SearchChannels", "app.channel.search.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } // Hydrate policy actions so search results carry the same action map as the // bootstrap channel list; otherwise a search would overwrite a hydrated // channel in the frontend store with an unhydrated copy. No-op without a // policy attached. if appErr := a.HydrateChannelsPolicyActions(rctx, channelList); appErr != nil { rctx.Logger().Warn("Failed to hydrate channel policy actions for search results; returning channels without action map", mlog.String("team_id", teamID), mlog.Err(appErr), ) } return channelList, nil } func (a *App) SearchChannelsForUser(rctx request.CTX, userID, teamID, term string) (model.ChannelList, *model.AppError) { includeDeleted := true term = strings.TrimSpace(term) channelList, err := a.Srv().Store().Channel().SearchForUserInTeam(userID, teamID, term, includeDeleted) if err != nil { return nil, model.NewAppError("SearchChannelsForUser", "app.channel.search.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } // Hydrate policy actions so search results carry the same action map as the // bootstrap channel list; otherwise a search would overwrite a hydrated // channel in the frontend store with an unhydrated copy. No-op without a // policy attached. if appErr := a.HydrateChannelsPolicyActions(rctx, channelList); appErr != nil { rctx.Logger().Warn("Failed to hydrate channel policy actions for user search results; returning channels without action map", mlog.String("team_id", teamID), mlog.String("user_id", userID), mlog.Err(appErr), ) } return channelList, nil } func (a *App) SearchGroupChannels(rctx request.CTX, userID, term string) (model.ChannelList, *model.AppError) { if term == "" { return model.ChannelList{}, nil } channelList, err := a.Srv().Store().Channel().SearchGroupChannels(userID, term) if err != nil { return nil, model.NewAppError("SearchGroupChannels", "app.channel.search_group_channels.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } return channelList, nil } func (a *App) SearchChannelsUserNotIn(rctx request.CTX, teamID string, userID string, term string) (model.ChannelList, *model.AppError) { term = strings.TrimSpace(term) channelList, err := a.Srv().Store().Channel().SearchMore(userID, teamID, term) if err != nil { return nil, model.NewAppError("SearchChannelsUserNotIn", "app.channel.search.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } return channelList, nil } func (a *App) MarkTeamChannelsAndThreadsViewed(rctx request.CTX, teamID string, userID string, currentSessionID string, isCRTEnabled bool) (map[string]int64, *model.AppError) { user, err := a.Srv().Store().User().Get(rctx, userID) if err != nil { return nil, model.NewAppError("MarkTeamChannelsAndThreadsViewed", "app.user.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } channelsToView, channelsToClearPushNotifications, times, err := a.Srv().Store().Channel().GetTeamChannelsWithUnreadAndMentions(rctx, teamID, userID, user.NotifyProps) if err != nil { return nil, model.NewAppError("MarkTeamChannelsAndThreadsViewed", "app.channel.get_channels_by_team_with_unreads_and_with_mentions.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } // times already contains every channel the user belongs to in this team, including // fully-read ones. We pass the full set to the thread store because a CRT-enabled // user can have unread thread replies in a channel whose channel-level counters are // already up to date (thread replies don't bump TotalMsgCount). The thread store's // `LastReplyAt > LastViewed` clause keeps the actual UPDATE bounded to genuinely // stale thread memberships. allChannelIDs := make([]string, 0, len(times)) for channelID := range times { allChannelIDs = append(allChannelIDs, channelID) } if err = a.Srv().Store().Thread().MarkAllAsReadByChannels(userID, allChannelIDs); err != nil { return nil, model.NewAppError("MarkTeamChannelsAndThreadsViewed", "app.thread.mark_all_as_read_by_channels.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } if len(channelsToView) > 0 { _, err = a.Srv().Store().Channel().UpdateLastViewedAt(channelsToView, userID) if err != nil { var invErr *store.ErrInvalidInput switch { case errors.As(err, &invErr): return nil, model.NewAppError("MarkTeamChannelsAndThreadsViewed", "app.channel.update_last_viewed_at.app_error", nil, "", http.StatusBadRequest).Wrap(err) default: return nil, model.NewAppError("MarkTeamChannelsAndThreadsViewed", "app.channel.update_last_viewed_at.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } } if *a.Config().ServiceSettings.EnableChannelViewedMessages { message := model.NewWebSocketEvent(model.WebsocketEventMultipleChannelsViewed, "", "", userID, nil, "") message.Add("channel_times", times) a.Publish(message) } } for _, channelID := range channelsToClearPushNotifications { a.clearPushNotification(currentSessionID, userID, channelID, "") } if isCRTEnabled { // Threads can have been marked read across the entire team, so broadcast a // single team-scoped event. The client routes this to a single // ALL_TEAM_THREADS_READ Redux action — it does NOT trigger any API calls. message := model.NewWebSocketEvent(model.WebsocketEventThreadReadChanged, teamID, "", userID, nil, "") message.Add("timestamp", model.GetMillis()) a.Publish(message) } return times, nil } func (a *App) MarkAllDirectAndGroupMessagesViewed(rctx request.CTX, userID string, currentSessionID string, isCRTEnabled bool) (map[string]int64, *model.AppError) { user, err := a.Srv().Store().User().Get(rctx, userID) if err != nil { return nil, model.NewAppError("MarkAllDirectAndGroupMessagesViewed", "app.user.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } messagesToView, messagesToClearPushNotifications, times, err := a.Srv().Store().Channel().GetDirectMessagesWithUnreadAndMentions(rctx, userID, user.NotifyProps) if err != nil { return nil, model.NewAppError("MarkAllDirectAndGroupMessagesViewed", "app.channel.get_channels_by_team_with_unreads_and_with_mentions.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } // times already contains every DM/GM the user belongs to, including fully-read // ones. We pass the full set to the thread store because a CRT-enabled user can // have unread thread replies in a channel whose channel-level counters are // already up to date (thread replies don't bump TotalMsgCount). The thread // store's `LastReplyAt > LastViewed` clause keeps the actual UPDATE bounded to // genuinely stale thread memberships. allChannelIDs := make([]string, 0, len(times)) for channelID := range times { allChannelIDs = append(allChannelIDs, channelID) } if err = a.Srv().Store().Thread().MarkAllAsReadByChannels(userID, allChannelIDs); err != nil { return nil, model.NewAppError("MarkAllDirectAndGroupMessagesViewed", "app.thread.mark_all_as_read_by_channels.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } if len(messagesToView) > 0 { _, err = a.Srv().Store().Channel().UpdateLastViewedAt(messagesToView, userID) if err != nil { var invErr *store.ErrInvalidInput switch { case errors.As(err, &invErr): return nil, model.NewAppError("MarkAllDirectAndGroupMessagesViewed", "app.channel.update_last_viewed_at.app_error", nil, "", http.StatusBadRequest).Wrap(err) default: return nil, model.NewAppError("MarkAllDirectAndGroupMessagesViewed", "app.channel.update_last_viewed_at.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } } if *a.Config().ServiceSettings.EnableChannelViewedMessages { message := model.NewWebSocketEvent(model.WebsocketEventMultipleChannelsViewed, "", "", userID, nil, "") message.Add("channel_times", times) a.Publish(message) } } for _, channelID := range messagesToClearPushNotifications { a.clearPushNotification(currentSessionID, userID, channelID, "") } if isCRTEnabled { // Threads can have been marked read in any DM/GM. There's no team to // broadcast on, so emit one event per channel. The client routes each to a // single ALL_THREADS_IN_CHANNEL_READ Redux action — no API calls are made. timestamp := model.GetMillis() for _, channelID := range allChannelIDs { message := model.NewWebSocketEvent(model.WebsocketEventThreadReadChanged, "", channelID, userID, nil, "") message.Add("timestamp", timestamp) a.Publish(message) } } return times, nil } func (a *App) MarkChannelsAsViewed(rctx request.CTX, channelIDs []string, userID string, currentSessionID string, collapsedThreadsSupported, isCRTEnabled bool) (map[string]int64, *model.AppError) { var err error user, err := a.Srv().Store().User().Get(rctx, userID) if err != nil { return nil, model.NewAppError("MarkChannelsAsViewed", "app.user.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } // We use channelsToView to later only update those, or early return if no channel is to be read channelsToView, channelsToClearPushNotifications, times, err := a.Srv().Store().Channel().GetChannelsWithUnreadsAndWithMentions(rctx, channelIDs, userID, user.NotifyProps) if err != nil { return nil, model.NewAppError("MarkChannelsAsViewed", "app.channel.get_channels_with_unreads_and_with_mentions.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } if len(channelsToView) == 0 { return times, nil } updateThreads := *a.Config().ServiceSettings.ThreadAutoFollow && (!collapsedThreadsSupported || !isCRTEnabled) if updateThreads { err = a.Srv().Store().Thread().MarkAllAsReadByChannels(userID, channelsToView) if err != nil { return nil, model.NewAppError("MarkChannelsAsViewed", "app.thread.mark_all_as_read_by_channels.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } } _, err = a.Srv().Store().Channel().UpdateLastViewedAt(channelsToView, userID) if err != nil { var invErr *store.ErrInvalidInput switch { case errors.As(err, &invErr): return nil, model.NewAppError("MarkChannelsAsViewed", "app.channel.update_last_viewed_at.app_error", nil, "", http.StatusBadRequest).Wrap(err) default: return nil, model.NewAppError("MarkChannelsAsViewed", "app.channel.update_last_viewed_at.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } } if *a.Config().ServiceSettings.EnableChannelViewedMessages { message := model.NewWebSocketEvent(model.WebsocketEventMultipleChannelsViewed, "", "", userID, nil, "") message.Add("channel_times", times) a.Publish(message) } for _, channelID := range channelsToClearPushNotifications { a.clearPushNotification(currentSessionID, userID, channelID, "") } if updateThreads && isCRTEnabled { timestamp := model.GetMillis() for _, channelID := range channelsToView { message := model.NewWebSocketEvent(model.WebsocketEventThreadReadChanged, "", channelID, userID, nil, "") message.Add("timestamp", timestamp) a.Publish(message) } } return times, nil } func (a *App) ViewChannel(rctx request.CTX, view *model.ChannelView, userID string, currentSessionId string, collapsedThreadsSupported bool) (map[string]int64, *model.AppError) { if err := a.SetActiveChannel(rctx, userID, view.ChannelId); err != nil { return nil, err } channelIDs := []string{} if view.ChannelId != "" { channelIDs = append(channelIDs, view.ChannelId) } if view.PrevChannelId != "" { channelIDs = append(channelIDs, view.PrevChannelId) } if len(channelIDs) == 0 { return map[string]int64{}, nil } return a.MarkChannelsAsViewed(rctx, channelIDs, userID, currentSessionId, collapsedThreadsSupported, a.IsCRTEnabledForUser(rctx, userID)) } func (a *App) PermanentDeleteChannel(rctx request.CTX, channel *model.Channel) *model.AppError { if err := a.Srv().Store().Post().PermanentDeleteByChannel(rctx, channel.Id); err != nil { return model.NewAppError("PermanentDeleteChannel", "app.post.permanent_delete_by_channel.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } if err := a.Srv().Store().Channel().PermanentDeleteMembersByChannel(rctx, channel.Id); err != nil { return model.NewAppError("PermanentDeleteChannel", "app.channel.remove_member.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } if err := a.Srv().Store().Webhook().PermanentDeleteIncomingByChannel(channel.Id); err != nil { return model.NewAppError("PermanentDeleteChannel", "app.webhooks.permanent_delete_incoming_by_channel.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } if err := a.Srv().Store().Webhook().PermanentDeleteOutgoingByChannel(channel.Id); err != nil { return model.NewAppError("PermanentDeleteChannel", "app.webhooks.permanent_delete_outgoing_by_channel.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } if err := a.Srv().Store().PostPersistentNotification().DeleteByChannel([]string{channel.Id}); err != nil { return model.NewAppError("PermanentDeleteChannel", "app.post_persistent_notification.delete_by_channel.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } deleteAt := model.GetMillis() if nErr := a.Srv().Store().Channel().PermanentDelete(rctx, channel.Id); nErr != nil { return model.NewAppError("PermanentDeleteChannel", "app.channel.permanent_delete.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr) } // Remove any orphaned channel-scope ABAC policy tied to this channel ID. // cleanupChannelAccessControlPolicy intentionally does not gate on // PolicyEnforced (see its doc comment) — the underlying DeletePolicy is a // no-op when no row exists, so it's safe to call unconditionally. a.cleanupChannelAccessControlPolicy(rctx, channel) a.Srv().Platform().InvalidateCacheForChannel(channel) if channel.IsSpace() { return nil } var message *model.WebSocketEvent if channel.Type == model.ChannelTypeOpen { message = model.NewWebSocketEvent(model.WebsocketEventChannelDeleted, channel.TeamId, "", "", nil, "") } else { message = model.NewWebSocketEvent(model.WebsocketEventChannelDeleted, "", channel.Id, "", nil, "") } message.Add("channel_id", channel.Id) message.Add("delete_at", deleteAt) a.Publish(message) return nil } func (a *App) RemoveAllDeactivatedMembersFromChannel(rctx request.CTX, channel *model.Channel) *model.AppError { err := a.Srv().Store().Channel().RemoveAllDeactivatedMembers(rctx, channel.Id) if err != nil { return model.NewAppError("RemoveAllDeactivatedMembersFromChannel", "app.channel.remove_all_deactivated_members.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } return nil } // MoveChannel method is prone to data races if someone joins to channel during the move process. However this // function is only exposed to sysadmins and the possibility of this edge case is relatively small. func (a *App) MoveChannel(rctx request.CTX, team *model.Team, channel *model.Channel, user *model.User) *model.AppError { if channel.IsSpace() { return model.NewAppError("MoveChannel", "app.channel.move_channel.space.app_error", nil, "", http.StatusForbidden) } // Check that all channel members are in the destination team. channelMembers, err := a.GetChannelMembersPage(rctx, channel.Id, 0, 10000000) if err != nil { return err } channelMemberIds := []string{} for _, channelMember := range channelMembers { channelMemberIds = append(channelMemberIds, channelMember.UserId) } if len(channelMemberIds) > 0 { teamMembers, err2 := a.GetTeamMembersByIds(team.Id, channelMemberIds, nil) if err2 != nil { return err2 } if len(teamMembers) != len(channelMembers) { teamMembersMap := make(map[string]*model.TeamMember, len(teamMembers)) for _, teamMember := range teamMembers { teamMembersMap[teamMember.UserId] = teamMember } for _, channelMember := range channelMembers { if _, ok := teamMembersMap[channelMember.UserId]; !ok { rctx.Logger().Warn("Not member of the target team", mlog.String("user_id", channelMember.UserId)) } } return model.NewAppError("MoveChannel", "app.channel.move_channel.members_do_not_match.error", nil, "", http.StatusInternalServerError) } } // keep instance of the previous team previousTeam, nErr := a.Srv().Store().Team().Get(channel.TeamId) if nErr != nil { var nfErr *store.ErrNotFound switch { case errors.As(nErr, &nfErr): return model.NewAppError("MoveChannel", "app.team.get.find.app_error", nil, "", http.StatusNotFound).Wrap(nErr) default: return model.NewAppError("MoveChannel", "app.team.get.finding.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr) } } if nErr := a.Srv().Store().Channel().UpdateSidebarChannelCategoryOnMove(channel, team.Id); nErr != nil { return model.NewAppError("MoveChannel", "app.channel.sidebar_categories.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr) } channel.TeamId = team.Id if _, err := a.Srv().Store().Channel().Update(rctx, channel); err != nil { var appErr *model.AppError var uniqueConstraintErr *store.ErrUniqueConstraint var invErr *store.ErrInvalidInput switch { case errors.As(err, &invErr): return model.NewAppError("MoveChannel", "app.channel.update.bad_id", nil, "", http.StatusBadRequest).Wrap(err) case errors.As(err, &uniqueConstraintErr): return model.NewAppError("MoveChannel", store.ChannelExistsError, nil, "", http.StatusBadRequest).Wrap(err) case errors.As(err, &appErr): return appErr default: return model.NewAppError("MoveChannel", "app.channel.update_channel.internal_error", nil, "", http.StatusInternalServerError).Wrap(err) } } if incomingWebhooks, err := a.GetIncomingWebhooksForTeamPage(previousTeam.Id, 0, 10000000); err != nil { rctx.Logger().Warn("Failed to get incoming webhooks", mlog.Err(err)) } else { for _, webhook := range incomingWebhooks { if webhook.ChannelId == channel.Id { webhook.TeamId = team.Id if _, err := a.Srv().Store().Webhook().UpdateIncoming(webhook); err != nil { rctx.Logger().Warn("Failed to move incoming webhook to new team", mlog.String("webhook_id", webhook.Id)) } } } } if outgoingWebhooks, err := a.GetOutgoingWebhooksForTeamPage(previousTeam.Id, 0, 10000000); err != nil { rctx.Logger().Warn("Failed to get outgoing webhooks", mlog.Err(err)) } else { for _, webhook := range outgoingWebhooks { if webhook.ChannelId == channel.Id { webhook.TeamId = team.Id if _, err := a.Srv().Store().Webhook().UpdateOutgoing(webhook); err != nil { rctx.Logger().Warn("Failed to move outgoing webhook to new team.", mlog.String("webhook_id", webhook.Id)) } } } } // Update the threads within this channel to the new team if err := a.Srv().Store().Thread().UpdateTeamIdForChannelThreads(channel.Id, team.Id); err != nil { rctx.Logger().Warn("error while updating threads after channel move", mlog.Err(err)) } if err := a.RemoveUsersFromChannelNotMemberOfTeam(rctx, user, channel, team); err != nil { rctx.Logger().Warn("error while removing non-team member users", mlog.Err(err)) } if user != nil { if err := a.postChannelMoveMessage(rctx, user, channel, previousTeam); err != nil { rctx.Logger().Warn("error while posting move channel message", mlog.Err(err)) } } return nil } func (a *App) postChannelMoveMessage(rctx request.CTX, user *model.User, channel *model.Channel, previousTeam *model.Team) *model.AppError { post := &model.Post{ ChannelId: channel.Id, Message: fmt.Sprintf(i18n.T("api.team.move_channel.success"), previousTeam.Name), Type: model.PostTypeMoveChannel, UserId: user.Id, Props: model.StringInterface{ "username": user.Username, }, } if _, _, err := a.CreatePost(rctx, post, channel, model.CreatePostFlags{SetOnline: true}); err != nil { return model.NewAppError("postChannelMoveMessage", "api.team.move_channel.post.error", nil, "", http.StatusInternalServerError).Wrap(err) } return nil } func (a *App) RemoveUsersFromChannelNotMemberOfTeam(rctx request.CTX, remover *model.User, channel *model.Channel, team *model.Team) *model.AppError { channelMembers, err := a.GetChannelMembersPage(rctx, channel.Id, 0, 10000000) if err != nil { return err } channelMemberIds := []string{} channelMemberMap := make(map[string]struct{}) for _, channelMember := range channelMembers { channelMemberMap[channelMember.UserId] = struct{}{} channelMemberIds = append(channelMemberIds, channelMember.UserId) } if len(channelMemberIds) > 0 { teamMembers, err := a.GetTeamMembersByIds(team.Id, channelMemberIds, nil) if err != nil { return err } if len(teamMembers) != len(channelMembers) { for _, teamMember := range teamMembers { delete(channelMemberMap, teamMember.UserId) } var removerId string if remover != nil { removerId = remover.Id } for userID := range channelMemberMap { if err := a.removeUserFromChannel(rctx, userID, removerId, channel); err != nil { return err } } } } return nil } func (a *App) GetPinnedPosts(rctx request.CTX, channelID string) (*model.PostList, *model.AppError) { posts, err := a.Srv().Store().Channel().GetPinnedPosts(channelID) if err != nil { return nil, model.NewAppError("GetPinnedPosts", "app.channel.pinned_posts.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } if appErr := a.filterInaccessiblePosts(posts, filterPostOptions{assumeSortedCreatedAt: true}); appErr != nil { return nil, appErr } return posts, nil } func (a *App) ToggleMuteChannel(rctx request.CTX, channelID, userID string) (*model.ChannelMember, *model.AppError) { member, nErr := a.Srv().Store().Channel().GetMember(rctx, channelID, userID) if nErr != nil { var appErr *model.AppError var nfErr *store.ErrNotFound switch { case errors.As(nErr, &appErr): return nil, appErr case errors.As(nErr, &nfErr): return nil, model.NewAppError("ToggleMuteChannel", MissingChannelMemberError, nil, "", http.StatusNotFound).Wrap(nErr) default: return nil, model.NewAppError("ToggleMuteChannel", "app.channel.get_member.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr) } } member.SetChannelMuted(!member.IsChannelMuted()) member, err := a.updateChannelMember(rctx, member) if err != nil { return nil, err } a.invalidateCacheForChannelMembersNotifyProps(member.ChannelId) return member, nil } func (a *App) setChannelsMuted(rctx request.CTX, channelIDs []string, userID string, muted bool) ([]*model.ChannelMember, *model.AppError) { members, err := a.Srv().Store().Channel().GetMembersByChannelIds(channelIDs, userID) if err != nil { var appErr *model.AppError switch { case errors.As(err, &appErr): return nil, appErr default: return nil, model.NewAppError("setChannelsMuted", "app.channel.get_member.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } } var membersToUpdate []*model.ChannelMember for _, member := range members { if muted == member.IsChannelMuted() { continue } updatedMember := member updatedMember.SetChannelMuted(muted) membersToUpdate = append(membersToUpdate, &updatedMember) } if len(membersToUpdate) == 0 { return nil, nil } updated, err := a.Srv().Store().Channel().UpdateMultipleMembers(membersToUpdate) if err != nil { var appErr *model.AppError var nfErr *store.ErrNotFound switch { case errors.As(err, &appErr): return nil, appErr case errors.As(err, &nfErr): return nil, model.NewAppError("setChannelsMuted", MissingChannelMemberError, nil, "", http.StatusNotFound).Wrap(err) default: return nil, model.NewAppError("setChannelsMuted", "app.channel.get_member.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } } for _, member := range updated { a.invalidateCacheForChannelMembersNotifyProps(member.ChannelId) evt := model.NewWebSocketEvent(model.WebsocketEventChannelMemberUpdated, "", "", member.UserId, nil, "") memberJSON, jsonErr := json.Marshal(member) if jsonErr != nil { return nil, model.NewAppError("setChannelsMuted", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(jsonErr) } evt.Add("channelMember", string(memberJSON)) a.Publish(evt) } return updated, nil } func (a *App) FillInChannelProps(rctx request.CTX, channel *model.Channel) *model.AppError { return a.FillInChannelsProps(rctx, model.ChannelList{channel}) } func (a *App) FillInChannelsProps(rctx request.CTX, channelList model.ChannelList) *model.AppError { // Group the channels by team and call GetChannelsByNames just once per team. channelsByTeam := make(map[string]model.ChannelList) for _, channel := range channelList { channelsByTeam[channel.TeamId] = append(channelsByTeam[channel.TeamId], channel) } for teamID, channelList := range channelsByTeam { allChannelMentions := make(map[string]bool) channelMentions := make(map[*model.Channel][]string, len(channelList)) // Collect mentions across the channels so as to query just once for this team. for _, channel := range channelList { channelMentions[channel] = model.ChannelMentions(channel.Header) for _, channelMention := range channelMentions[channel] { allChannelMentions[channelMention] = true } } allChannelMentionNames := make([]string, 0, len(allChannelMentions)) for channelName := range allChannelMentions { allChannelMentionNames = append(allChannelMentionNames, channelName) } if len(allChannelMentionNames) > 0 { mentionedChannels, err := a.GetChannelsByNames(rctx, allChannelMentionNames, teamID) if err != nil { return err } mentionedChannelsByName := make(map[string]*model.Channel) for _, channel := range mentionedChannels { mentionedChannelsByName[channel.Name] = channel } for _, channel := range channelList { channelMentionsProp := make(map[string]any, len(channelMentions[channel])) for _, channelMention := range channelMentions[channel] { if mentioned, ok := mentionedChannelsByName[channelMention]; ok { if mentioned.Type == model.ChannelTypeOpen { channelMentionsProp[mentioned.Name] = map[string]any{ "display_name": mentioned.DisplayName, } } } } if len(channelMentionsProp) > 0 { channel.AddProp("channel_mentions", channelMentionsProp) } else if channel.Props != nil { delete(channel.Props, "channel_mentions") } } } } return nil } func (a *App) forEachChannelMember(rctx request.CTX, channelID string, f func(model.ChannelMember) error) error { perPage := 100 page := 0 for { opts := model.ChannelMembersGetOptions{ ChannelID: channelID, Offset: page * perPage, Limit: perPage, } channelMembers, err := a.Srv().Store().Channel().GetMembers(opts) if err != nil { return err } for _, channelMember := range channelMembers { if err = f(channelMember); err != nil { return err } } length := len(channelMembers) if length < perPage { break } page++ } return nil } func (a *App) ClearChannelMembersCache(rctx request.CTX, channelID string) error { clearSessionCache := func(channelMember model.ChannelMember) error { a.ClearSessionCacheForUser(channelMember.UserId) message := model.NewWebSocketEvent(model.WebsocketEventChannelMemberUpdated, "", "", channelMember.UserId, nil, "") memberJSON, jsonErr := json.Marshal(channelMember) if jsonErr != nil { return jsonErr } message.Add("channelMember", string(memberJSON)) a.Publish(message) return nil } if err := a.forEachChannelMember(rctx, channelID, clearSessionCache); err != nil { return fmt.Errorf("error clearing cache for channel members: channel_id: %s, error: %w", channelID, err) } return nil } func (a *App) GetMemberCountsByGroup(rctx request.CTX, channelID string, includeTimezones bool) ([]*model.ChannelMemberCountByGroup, *model.AppError) { channelMemberCounts, err := a.Srv().Store().Channel().GetMemberCountsByGroup(rctx, channelID, includeTimezones) if err != nil { return nil, model.NewAppError("GetMemberCountsByGroup", "app.channel.get_member_count.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } return channelMemberCounts, nil } func (a *App) getDirectChannel(rctx request.CTX, userID, otherUserID string) (*model.Channel, *model.AppError) { return a.Srv().getDirectChannel(rctx, userID, otherUserID) } // GetDirectOrGroupMessageMembersCommonTeamsAsUser is a variant of GetDirectOrGroupMessageMembersCommonTeams // that returns results relative to the requesting user from the session in the request context. func (a *App) GetDirectOrGroupMessageMembersCommonTeamsAsUser(rctx request.CTX, channelID string) ([]*model.Team, *model.AppError) { return a.getDirectOrGroupMessageMembersCommonTeams(rctx, rctx.Session().UserId, channelID) } // GetDirectOrGroupMessageMembersCommonTeams returns the set of teams in common for the members of the given DM/GM channel. // // Prefer GetDirectOrGroupMessageMembersCommonTeamsAsUser unless the request context is independent of any given user. func (a *App) GetDirectOrGroupMessageMembersCommonTeams(rctx request.CTX, channelID string) ([]*model.Team, *model.AppError) { return a.getDirectOrGroupMessageMembersCommonTeams(rctx, "", channelID) } // getDirectOrGroupMessageMembersCommonTeams returns the set teams common to the members of the given channel. // // If a requesting user id is specified, but the user isn't an active member of the channel, we return an empty // set of channels. We don't just exclude all inactive users to offer more flexibility to the remaining users // on where to create the replacement channel. func (a *App) getDirectOrGroupMessageMembersCommonTeams(rctx request.CTX, requestingUserID, channelID string) ([]*model.Team, *model.AppError) { channel, appErr := a.GetChannel(rctx, channelID) if appErr != nil { return nil, appErr } if channel.Type != model.ChannelTypeGroup && channel.Type != model.ChannelTypeDirect { return nil, model.NewAppError("GetDirectOrGroupMessageMembersCommonTeams", "app.channel.get_common_teams.incorrect_channel_type", nil, "", http.StatusBadRequest) } users, appErr := a.GetUsersInChannel(&model.UserGetOptions{ PerPage: model.ChannelGroupMaxUsers, Page: 0, InChannelId: channelID, Inactive: false, Active: true, }) if appErr != nil { return nil, appErr } userIDs := make([]string, 0, len(users)) for _, user := range users { if user.IsBot { exempt, err := a.IsBotExemptFromDMRestrictions(rctx, user.Id) if err != nil { return nil, err } if exempt { continue } } userIDs = append(userIDs, user.Id) } // If a requesting user is specified, but we don't find them above as an active member // of the channel, just short-circuit and return an empty set. We don't return an error // as this is a valid result for some callers. if requestingUserID != "" && !slices.Contains(userIDs, requestingUserID) { return nil, nil } commonTeamIDs, err := a.Srv().Store().Team().GetCommonTeamIDsForMultipleUsers(userIDs) if err != nil { return nil, model.NewAppError("GetDirectOrGroupMessageMembersCommonTeams", "app.channel.get_common_teams.store_get_common_teams_error", nil, "", http.StatusInternalServerError).Wrap(err) } teams := []*model.Team{} if len(commonTeamIDs) > 0 { teams, appErr = a.GetTeams(commonTeamIDs) } return teams, appErr } func (a *App) ConvertGroupMessageToChannel(rctx request.CTX, convertedByUserId string, gmConversionRequest *model.GroupMessageConversionRequestBody) (*model.Channel, *model.AppError) { originalChannel, appErr := a.GetChannel(rctx, gmConversionRequest.ChannelID) if appErr != nil { return nil, appErr } appErr = a.validateForConvertGroupMessageToChannel(rctx, convertedByUserId, originalChannel, gmConversionRequest) if appErr != nil { return nil, appErr } toUpdate := originalChannel.DeepCopy() toUpdate.Type = model.ChannelTypePrivate toUpdate.TeamId = gmConversionRequest.TeamID toUpdate.Name = gmConversionRequest.Name toUpdate.DisplayName = gmConversionRequest.DisplayName updatedChannel, appErr := a.UpdateChannel(rctx, toUpdate) if appErr != nil { return nil, appErr } a.Srv().Platform().InvalidateCacheForChannel(originalChannel) users, appErr := a.GetUsersInChannelPage(&model.UserGetOptions{ InChannelId: gmConversionRequest.ChannelID, Page: 0, PerPage: model.ChannelGroupMaxUsers, }, false) if appErr != nil { return nil, appErr } _ = a.setSidebarCategoriesForConvertedGroupMessage(rctx, gmConversionRequest, users) _ = a.postMessageForConvertGroupMessageToChannel(rctx, gmConversionRequest.ChannelID, convertedByUserId, users) // the user conversion the GM becomes the channel admin. _, appErr = a.UpdateChannelMemberSchemeRoles(rctx, gmConversionRequest.ChannelID, convertedByUserId, false, true, true) if appErr != nil { return nil, appErr } return updatedChannel, nil } func (a *App) setSidebarCategoriesForConvertedGroupMessage(rctx request.CTX, gmConversionRequest *model.GroupMessageConversionRequestBody, channelUsers []*model.User) *model.AppError { // First we'll delete channel from everyone's sidebar. Only the members of GM // can have it in sidebar, so we can delete the channel from all SidebarChannels entries. err := a.Srv().Store().Channel().DeleteAllSidebarChannelForChannel(gmConversionRequest.ChannelID) if err != nil { return model.NewAppError( "setSidebarCategoriesForConvertedGroupMessage", "app.channel.gm_conversion_set_categories.delete_all.error", nil, "", http.StatusInternalServerError, ).Wrap(err) } // Now that we've deleted existing entries, we can set the channel in default "Channels" category // for all GM members for _, user := range channelUsers { categories, appErr := a.GetSidebarCategories(rctx, user.Id, gmConversionRequest.TeamID) if appErr != nil { rctx.Logger().Error("Failed to search sidebar categories for user for adding converted GM") continue } if len(categories.Categories) < 1 { // It is normal for user to not have the default category. // The default "Channels" category is created when the user first logs in, // and all their channels are moved to this category at the same time. // So its perfectly okay for this condition to occur. continue } // when we fetch the default "Channels" category from store layer, // it auto-fills any channels the user has access to but aren't associated to a category in the database. // So what we do is fetch the category, so we get an auto-filled data, // then call update category to persist the data and send the websocket events. channelsCategory := categories.Categories[0] _, appErr = a.UpdateSidebarCategories(rctx, user.Id, gmConversionRequest.TeamID, []*model.SidebarCategoryWithChannels{channelsCategory}) if appErr != nil { rctx.Logger().Error("Failed to add converted GM to default sidebar category for user", mlog.String("user_id", user.Id), mlog.Err(appErr)) } } return nil } func (a *App) validateForConvertGroupMessageToChannel(rctx request.CTX, convertedByUserId string, originalChannel *model.Channel, gmConversionRequest *model.GroupMessageConversionRequestBody) *model.AppError { commonTeams, appErr := a.GetDirectOrGroupMessageMembersCommonTeams(rctx, originalChannel.Id) if appErr != nil { return appErr } teamFound := false for _, team := range commonTeams { if team.Id == gmConversionRequest.TeamID { teamFound = true break } } if !teamFound { return model.NewAppError( "validateForConvertGroupMessageToChannel", "app.channel.group_message_conversion.incorrect_team", nil, "", http.StatusBadRequest, ) } if originalChannel.Type != model.ChannelTypeGroup { return model.NewAppError( "ConvertGroupMessageToChannel", "app.channel.group_message_conversion.original_channel_not_gm", nil, "", http.StatusNotFound, ) } channelMember, appErr := a.GetChannelMember(rctx, gmConversionRequest.ChannelID, convertedByUserId) if appErr != nil { return appErr } if channelMember == nil { return model.NewAppError("ConvertGroupMessageToChannel", "app.channel.group_message_conversion.channel_member_missing", nil, "", http.StatusNotFound) } // apply dummy changes to check validity clone := originalChannel.DeepCopy() clone.Type = model.ChannelTypePrivate clone.Name = gmConversionRequest.Name clone.DisplayName = gmConversionRequest.DisplayName return clone.IsValid() } func (a *App) postMessageForConvertGroupMessageToChannel(rctx request.CTX, channelID, convertedByUserId string, channelUsers []*model.User) *model.AppError { convertedByUser, appErr := a.GetUser(rctx, convertedByUserId) if appErr != nil { return appErr } userIDs := make([]string, len(channelUsers)) usernames := make([]string, len(channelUsers)) for i, user := range channelUsers { userIDs[i] = user.Id usernames[i] = user.Username } message := i18n.T( "api.channel.group_message.converted.to_private_channel", map[string]any{ "ConvertedByUsername": convertedByUser.Username, "GMMembers": utils.JoinList(usernames), }) post := &model.Post{ ChannelId: channelID, Message: message, Type: model.PostTypeGMConvertedToChannel, UserId: convertedByUserId, } // these props are used for re-constructing a localized message on the client post.AddProp("convertedByUserId", convertedByUser.Id) post.AddProp("gmMembersDuringConversionIDs", userIDs) channel, appErr := a.GetChannel(rctx, channelID) if appErr != nil { return appErr } if _, _, appErr := a.CreatePost(rctx, post, channel, model.CreatePostFlags{SetOnline: true}); appErr != nil { rctx.Logger().Error("Failed to create post for notifying about GM converted to private channel", mlog.Err(appErr)) return model.NewAppError( "postMessageForConvertGroupMessageToChannel", "app.channel.group_message_conversion.post_message.error", nil, "", http.StatusInternalServerError, ).Wrap(appErr) } return nil } func (s *Server) getDirectChannel(rctx request.CTX, userID, otherUserID string) (*model.Channel, *model.AppError) { channel, nErr := s.Store().Channel().GetByName("", model.GetDMNameFromIds(userID, otherUserID), true) if nErr != nil { var nfErr *store.ErrNotFound if errors.As(nErr, &nfErr) { return nil, nil } return nil, model.NewAppError("GetOrCreateDirectChannel", "web.incoming_webhook.channel.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr) } return channel, nil } func (a *App) CheckIfChannelIsRestrictedDM(rctx request.CTX, channel *model.Channel) (bool, *model.AppError) { if *a.Config().TeamSettings.RestrictDirectMessage != model.DirectMessageTeam { return false, nil } if channel.Type != model.ChannelTypeDirect && channel.Type != model.ChannelTypeGroup { return false, nil } teams, err := a.GetDirectOrGroupMessageMembersCommonTeams(rctx, channel.Id) if err != nil { return false, model.NewAppError("CheckIfChannelIsRestrictedDM", "app.channel.get_common_teams.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } return len(teams) == 0, nil } // ChannelAccessControlled reports whether the given channel's membership is // gated by an ABAC policy. Channels carrying only a permission policy (e.g. // file upload restriction) return false — those policies do not control who // can be a member and so must not surface through this gate. Phase 1's // PolicyActions hydration is required for the answer to be correct; this // fetches the channel via the store directly (not App.GetChannel) and then // invokes the hydrator explicitly to avoid the recursive plumbing surface. func (a *App) ChannelAccessControlled(rctx request.CTX, channelID string) (bool, *model.AppError) { if l := a.License(); !model.MinimumEnterpriseAdvancedLicense(l) || !*a.Config().AccessControlSettings.EnableAttributeBasedAccessControl { return false, nil } channel, err := a.Srv().Store().Channel().Get(channelID, true) var nfErr *store.ErrNotFound if err != nil && !errors.As(err, &nfErr) { return false, model.NewAppError("ChannelIsAccessControlled", "app.channel.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } else if errors.As(err, &nfErr) { return false, nil } if appErr := a.HydrateChannelPolicyActions(rctx, channel); appErr != nil { // Fail-closed: a hydration error must not silently downgrade an // ABAC-controlled channel to "unrestricted" for callers that rely // on this gate (HasPermissionToChannel and friends). return false, appErr } return channel.HasMembershipPolicyAction(), nil } // cleanupChannelAccessControlPolicy removes the channel-scope ABAC policy row, // if any, for a channel being archived or permanently deleted. Orphan policy // rows left behind would still be picked up by the sync job and surface in // searches that filter by AccessControlPolicyEnforced. Failures are logged // but not returned — deleting/archiving a channel must not be blocked by an // ABAC cleanup error. // // We intentionally do NOT gate this on channel.PolicyEnforced: that flag is // computed from the AccessControlPolicies table via the channel store and can // be stale when read through the channel cache, which would cause us to skip // cleanup and leave an orphaned policy behind. DeletePolicy is a no-op when // no matching row exists, so calling it unconditionally is safe. // // When the enterprise access control service is unavailable (acs == nil) or // reports the operation as unsupported (NotImplemented / NotAcceptable — // e.g. running on Team Edition or under a license that gates the ABAC // engine), we still need to remove the underlying row to avoid leaving an // orphaned policy behind. In those cases we fall back to deleting directly // through the access control policy store. func (a *App) cleanupChannelAccessControlPolicy(rctx request.CTX, channel *model.Channel) { if channel == nil || channel.Id == "" { return } useStoreFallback := false acs := a.Srv().Channels().AccessControl if acs == nil { useStoreFallback = true } else if appErr := acs.DeletePolicy(rctx, channel.Id); appErr != nil { switch appErr.StatusCode { case http.StatusNotImplemented, http.StatusNotAcceptable: useStoreFallback = true default: rctx.Logger().Warn("Failed to delete channel ABAC policy during channel delete/archive", mlog.String("channel_id", channel.Id), mlog.Err(appErr), ) } } if useStoreFallback { if err := a.Srv().Store().AccessControlPolicy().Delete(rctx, channel.Id); err != nil { rctx.Logger().Warn("Failed to delete channel ABAC policy during channel delete/archive", mlog.String("channel_id", channel.Id), mlog.Err(err), ) } } // Drop the channel's cached render-ETag epoch: its policy row is gone, so a stale epoch // would otherwise linger until the cache TTL. Gated, unlike the delete above: nothing can be // cached for this channel if ABAC was off, and every archive would cost a cluster message. if a.attributeBasedAccessControlEnabled() { a.Srv().Store().AccessControlPolicy().InvalidateEtagForChannel(channel.Id) } } // recommendedPublicChannelsScanPageSize is the per-page size used while // paginating through policy-enforced public channels in // GetRecommendedPublicChannelsForUser. const recommendedPublicChannelsScanPageSize = 200 // recommendedPublicChannelsScanCap is a hard upper bound on the total number // of candidate channels we will evaluate per request. Evaluation is O(n) and // CEL programs are non-trivial; this guards against runaway scans on // pathological deployments. In practice teams have far fewer policy-enforced // public channels than this cap. const recommendedPublicChannelsScanCap = 2000 // GetRecommendedPublicChannelsForUser returns public channels in the given team // that have an ABAC policy assigned and whose membership rule the user // satisfies. ABAC policies on public channels are advisory — this list is // consumed by the "Recommended channels" feature in the Browse Channels UI. // // Channels the user is already a member of are intentionally NOT filtered out: // membership filtering is a presentation concern handled by the caller (the // Browse Channels UI has its own "Hide joined channels" preference and may // want to show membership state alongside each recommendation). // // Returns an empty list when the Enterprise Advanced license or ABAC feature // flag is not available, or when the access control service is not wired up. func (a *App) GetRecommendedPublicChannelsForUser(rctx request.CTX, userID, teamID string) (model.ChannelList, *model.AppError) { if l := a.License(); !model.MinimumEnterpriseAdvancedLicense(l) || !*a.Config().AccessControlSettings.EnableAttributeBasedAccessControl { return model.ChannelList{}, nil } acs := a.Srv().Channels().AccessControl if acs == nil { return model.ChannelList{}, nil } user, appErr := a.GetUser(rctx, userID) if appErr != nil { return nil, appErr } subject, appErr := a.BuildAccessControlSubject(rctx, user.Id, user.Roles, "") if appErr != nil { return nil, appErr } // Page through policy-enforced public channels until the team is fully // scanned or we hit the hard cap. SearchAllChannels signals "no more // pages" by returning fewer rows than the requested page size. candidates := make(model.ChannelListWithTeamData, 0) for page := 0; len(candidates) < recommendedPublicChannelsScanCap; page++ { batch, _, searchErr := a.SearchAllChannels(rctx, "", model.ChannelSearchOpts{ TeamIds: []string{teamID}, Public: true, AccessControlPolicyEnforced: true, Page: new(page), PerPage: new(recommendedPublicChannelsScanPageSize), }) if searchErr != nil { return nil, searchErr } if len(batch) == 0 { break } // Truncate to the remaining cap so the final candidate count never // exceeds the documented bound. The loop condition only gates entry // to a new iteration; without this, a final page worth of channels // could push len past the cap by up to (pageSize - 1). remaining := recommendedPublicChannelsScanCap - len(candidates) if len(batch) > remaining { candidates = append(candidates, batch[:remaining]...) break } candidates = append(candidates, batch...) if len(batch) < recommendedPublicChannelsScanPageSize { break } } // We intentionally do NOT filter out channels the user is already a member // of here — the Browse Channels UI has its own "Hide joined channels" // preference and callers may want to show membership state next to each // recommendation. Membership filtering is a presentation concern. recommended := make(model.ChannelList, 0, len(candidates)) for _, channel := range candidates { decision, evalErr := acs.AccessEvaluation(rctx, model.AccessRequest{ Subject: *subject, Resource: model.Resource{ Type: model.AccessControlPolicyTypeChannel, ID: channel.Id, }, Action: "membership", }) if evalErr != nil { rctx.Logger().Debug("ABAC evaluation failed when computing recommended channels", mlog.String("user_id", userID), mlog.String("channel_id", channel.Id), mlog.Err(evalErr), ) continue } if !decision.Decision { continue } recommended = append(recommended, &channel.Channel) } return recommended, nil } func (a *App) addChannelToDefaultCategory(rctx request.CTX, userID string, channel *model.Channel) { // Add channel to default category if specified if channel.DefaultCategoryName != "" && *a.Config().TeamSettings.EnableChannelCategorySorting { // Get user's categories for this team categories, err := a.GetSidebarCategoriesForTeamForUser(rctx, userID, channel.TeamId) if err != nil { mlog.Error("Failed to get sidebar categories", mlog.String("user_id", userID), mlog.String("team_id", channel.TeamId), mlog.Err(err)) return } // Find or create the category var targetCategory *model.SidebarCategoryWithChannels for _, category := range categories.Categories { if category.Type == model.SidebarCategoryCustom && strings.EqualFold(category.DisplayName, channel.DefaultCategoryName) { targetCategory = category break } } // Find the original category if the channel is already in a category var originalCategory *model.SidebarCategoryWithChannels for _, category := range categories.Categories { if category.Type != model.SidebarCategoryDirectMessages && slices.Contains(category.Channels, channel.Id) { originalCategory = category break } } // The channel is already in the target category, so there's nothing to move. if originalCategory != nil && originalCategory == targetCategory { return } var categoriesToUpdate []*model.SidebarCategoryWithChannels if originalCategory != nil { idx := slices.Index(originalCategory.Channels, channel.Id) originalCategory.Channels = slices.Delete(originalCategory.Channels, idx, idx+1) categoriesToUpdate = append(categoriesToUpdate, originalCategory) } if targetCategory == nil { // Create new category if it doesn't exist targetCategory = &model.SidebarCategoryWithChannels{ SidebarCategory: model.SidebarCategory{ UserId: userID, TeamId: channel.TeamId, Type: model.SidebarCategoryCustom, DisplayName: channel.DefaultCategoryName, Sorting: model.SidebarCategorySortDefault, }, Channels: []string{channel.Id}, } _, err = a.CreateSidebarCategory(rctx, userID, channel.TeamId, targetCategory) if err != nil { mlog.Error("Failed to create default category", mlog.String("user_id", userID), mlog.String("team_id", channel.TeamId), mlog.String("category_name", channel.DefaultCategoryName), mlog.Err(err)) } } else { targetCategory.Channels = append([]string{channel.Id}, targetCategory.Channels...) categoriesToUpdate = append(categoriesToUpdate, targetCategory) } // Add channel to existing category _, err = a.UpdateSidebarCategories(rctx, userID, channel.TeamId, categoriesToUpdate) if err != nil { mlog.Error("Failed to update default category", mlog.String("user_id", userID), mlog.String("team_id", channel.TeamId), mlog.String("category_name", channel.DefaultCategoryName), mlog.Err(err)) } } } // SetChannelMembers reconciles the membership of a channel to match the desired list of user IDs. // Users already in the channel are left untouched (no-op). Users not in the desired list are removed. // Users in the desired list but not in the channel are added. // // When adminSet is non-nil, channel admin roles are set declaratively: users in adminSet become // channel admins, all other members lose the admin role. When adminSet is nil, existing admin // roles are preserved for members who remain in the channel. // // Operations are processed in batches with a configurable delay between batches. // The onBatch callback is invoked after each batch with the results for that batch. func (a *App) SetChannelMembers(rctx request.CTX, channel *model.Channel, desiredUserIDs []string, adminSet map[string]struct{}, requestorUserID string, batchSize int, batchDelayMs int, onBatch func(*model.SetChannelMembersResponse)) *model.AppError { currentIDs, err := a.Srv().Store().Channel().GetAllChannelMemberIdsByChannelId(channel.Id) if err != nil { return model.NewAppError("SetChannelMembers", "app.channel.set_members.get_current.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } currentSet := make(map[string]struct{}, len(currentIDs)) for _, id := range currentIDs { currentSet[id] = struct{}{} } desiredSet := make(map[string]struct{}, len(desiredUserIDs)) for _, id := range desiredUserIDs { desiredSet[id] = struct{}{} } var toAdd, toRemove []string for id := range desiredSet { if _, exists := currentSet[id]; !exists { toAdd = append(toAdd, id) } } for id := range currentSet { if _, exists := desiredSet[id]; !exists { toRemove = append(toRemove, id) } } // Prevent emptying private channels, they become orphaned since non-admins can't rejoin. if channel.Type == model.ChannelTypePrivate && len(desiredUserIDs) == 0 { return model.NewAppError("SetChannelMembers", "app.channel.set_members.empty_private.app_error", nil, "", http.StatusBadRequest) } if len(toAdd) == 0 && len(toRemove) == 0 && adminSet == nil { onBatch(&model.SetChannelMembersResponse{}) return nil } if batchSize <= 0 { batchSize = 1 } batchDelay := time.Duration(batchDelayMs) * time.Millisecond // Process removals first for i := 0; i < len(toRemove); i += batchSize { end := min(i+batchSize, len(toRemove)) batch := toRemove[i:end] result := &model.SetChannelMembersResponse{} for _, userID := range batch { if appErr := a.removeUserFromChannel(rctx, userID, requestorUserID, channel); appErr != nil { result.Errors = append(result.Errors, model.SetChannelMembersError{ UserID: userID, ID: appErr.Id, Error: appErr.Error(), }) } else { result.Removed = append(result.Removed, userID) } } onBatch(result) if end < len(toRemove) { select { case <-rctx.Context().Done(): return model.NewAppError("SetChannelMembers", "app.channel.set_members.context_cancelled.app_error", nil, "", http.StatusInternalServerError).Wrap(rctx.Context().Err()) case <-time.After(batchDelay): } } } // Check context between removal and addition phases if rctx.Context().Err() != nil { return model.NewAppError("SetChannelMembers", "app.channel.set_members.context_cancelled.app_error", nil, "", http.StatusInternalServerError).Wrap(rctx.Context().Err()) } // Process additions for i := 0; i < len(toAdd); i += batchSize { end := min(i+batchSize, len(toAdd)) batch := toAdd[i:end] result := &model.SetChannelMembersResponse{} for _, userID := range batch { if _, appErr := a.AddChannelMember(rctx, userID, channel, ChannelMemberOpts{UserRequestorID: requestorUserID}); appErr != nil { result.Errors = append(result.Errors, model.SetChannelMembersError{ UserID: userID, ID: appErr.Id, Error: appErr.Error(), }) } else { result.Added = append(result.Added, userID) } } onBatch(result) if end < len(toAdd) { select { case <-rctx.Context().Done(): return model.NewAppError("SetChannelMembers", "app.channel.set_members.context_cancelled.app_error", nil, "", http.StatusInternalServerError).Wrap(rctx.Context().Err()) case <-time.After(batchDelay): } } } // Reconcile channel admin roles when adminSet is provided. if adminSet != nil { if rctx.Context().Err() != nil { return model.NewAppError("SetChannelMembers", "app.channel.set_members.context_cancelled.app_error", nil, "", http.StatusInternalServerError).Wrap(rctx.Context().Err()) } // Build the list of members whose admin status needs to change. // We need the current SchemeAdmin state for all remaining members. members, storeErr := a.Srv().Store().Channel().GetMembers(model.ChannelMembersGetOptions{ChannelID: channel.Id, Offset: 0, Limit: 100000}) if storeErr != nil { return model.NewAppError("SetChannelMembers", "app.channel.set_members.get_members.app_error", nil, "", http.StatusInternalServerError).Wrap(storeErr) } var toPromote, toDemote []string for _, m := range members { _, wantAdmin := adminSet[m.UserId] if wantAdmin && !m.SchemeAdmin { toPromote = append(toPromote, m.UserId) } else if !wantAdmin && m.SchemeAdmin { toDemote = append(toDemote, m.UserId) } } // Process role changes in batches for i := 0; i < len(toPromote); i += batchSize { end := min(i+batchSize, len(toPromote)) batch := toPromote[i:end] result := &model.SetChannelMembersResponse{} for _, userID := range batch { if _, appErr := a.UpdateChannelMemberSchemeRoles(rctx, channel.Id, userID, false, true, true); appErr != nil { result.Errors = append(result.Errors, model.SetChannelMembersError{ UserID: userID, ID: appErr.Id, Error: appErr.Error(), }) } else { result.Promoted = append(result.Promoted, userID) } } onBatch(result) if end < len(toPromote) { select { case <-rctx.Context().Done(): return model.NewAppError("SetChannelMembers", "app.channel.set_members.context_cancelled.app_error", nil, "", http.StatusInternalServerError).Wrap(rctx.Context().Err()) case <-time.After(batchDelay): } } } if len(toDemote) > 0 && rctx.Context().Err() != nil { return model.NewAppError("SetChannelMembers", "app.channel.set_members.context_cancelled.app_error", nil, "", http.StatusInternalServerError).Wrap(rctx.Context().Err()) } for i := 0; i < len(toDemote); i += batchSize { end := min(i+batchSize, len(toDemote)) batch := toDemote[i:end] result := &model.SetChannelMembersResponse{} for _, userID := range batch { if _, appErr := a.UpdateChannelMemberSchemeRoles(rctx, channel.Id, userID, false, true, false); appErr != nil { result.Errors = append(result.Errors, model.SetChannelMembersError{ UserID: userID, ID: appErr.Id, Error: appErr.Error(), }) } else { result.Demoted = append(result.Demoted, userID) } } onBatch(result) if end < len(toDemote) { select { case <-rctx.Context().Done(): return model.NewAppError("SetChannelMembers", "app.channel.set_members.context_cancelled.app_error", nil, "", http.StatusInternalServerError).Wrap(rctx.Context().Err()) case <-time.After(batchDelay): } } } } // Warn if a private channel was left with no members (known limitation) if channel.Type == model.ChannelTypePrivate { remainingIDs, err := a.Srv().Store().Channel().GetAllChannelMemberIdsByChannelId(channel.Id) if err == nil && len(remainingIDs) == 0 { rctx.Logger().Error("SetChannelMembers left private channel with no members", mlog.String("channel_id", channel.Id), mlog.String("requestor_user_id", requestorUserID), ) } } return nil }