// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. package app import ( "context" "encoding/json" "errors" "fmt" "net/http" "slices" "strings" "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(context.Background(), 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("channelName", channelName), mlog.String("teamID", 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) { if channel.IsGroupOrDirect() { return nil, model.NewAppError("CreateChannelWithUser", "api.channel.create_channel.direct_channel.app_error", nil, "", 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) if err != nil { return nil, err } a.addChannelToDefaultCategory(rctx, userID, channel) var user *model.User if user, err = a.GetUser(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) (*model.Channel, *model.AppError) { a.handleChannelCategoryName(channel) channel.DisplayName = strings.TrimSpace(channel.DisplayName) 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(context.Background(), 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) } 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 } 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 isPluginOwnedBot bool for _, user := range users { if user.IsBot { isOwnedByCurrentUserOrPlugin, err := a.IsBotOwnedByCurrentUserOrPlugin(rctx, user.Id) if err != nil { return nil, err } if isOwnedByCurrentUserOrPlugin { isPluginOwnedBot = true break } } } // if one of the users is a bot, don't restrict to team members if !isPluginOwnedBot { 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: model.NewPointer(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) { ok, appErr := a.ChannelAccessControlled(rctx, channel.Id) if appErr != nil { return nil, appErr } if ok && channel.Type != model.ChannelTypePrivate { return nil, model.NewAppError("UpdateChannel", "api.channel.update_channel.not_allowed.app_error", nil, "", http.StatusForbidden) } _, 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) 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) { 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 } 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 } a.Srv().Platform().InvalidateCacheForChannel(channel) messageWs := model.NewWebSocketEvent(model.WebsocketEventChannelConverted, channel.TeamId, "", "", nil, "") messageWs.Add("channel_id", channel.Id) 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) } 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) 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(context.Background(), 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 channel.Patch(patch) a.handleChannelCategoryName(channel) 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()) } } 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 } func (a *App) UpdateChannelMemberRoles(rctx request.CTX, channelID string, userID string, newRoles string) (*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 { // The role is not scheme-managed, so it's OK to apply it to the explicit roles field. 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 prevSchemeGuestValue != member.SchemeGuest { return nil, model.NewAppError("UpdateChannelMemberRoles", "api.channel.update_channel_member_roles.changing_guest_role.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) 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(context.Background(), 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 } 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) } 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) } a.Srv().Platform().InvalidateCacheForChannel(channel) 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 { 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 { groupID, err := a.CpaGroupID() if err != nil { return nil, model.NewAppError("AddUserToChannel", "api.channel.add_user.to.channel.failed.app_error", nil, fmt.Sprintf("failed to get group: %v, user_id: %s, channel_id: %s", err, user.Id, channel.Id), http.StatusInternalServerError) } s, err := a.Srv().Store().Attributes().GetSubject(rctx, user.Id, groupID) if err != nil { return nil, model.NewAppError("AddUserToChannel", "api.channel.add_user.to.channel.failed.app_error", nil, fmt.Sprintf("failed to get subject: %v, user_id: %s, channel_id: %s", err, user.Id, channel.Id), http.StatusForbidden) } decision, evalErr := acs.AccessEvaluation(rctx, model.AccessRequest{ Subject: *s, Resource: model.Resource{ Type: model.AccessControlPolicyTypeChannel, ID: channel.Id, }, Action: "join_channel", }) 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 } } 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.HandleMembershipChange(channel.Id, user.Id, true, 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 } 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(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(opts.UserRequestorID); err != nil { return nil, err } } cm, err := a.AddUserToChannel(rctx, user, channel, opts.SkipTeamMemberIntegrityCheck) if err != nil { return nil, err } 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(context.Background(), 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(context.Background(), 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) PostUpdateChannelDisplayNameMessage(rctx request.CTX, userID string, channel *model.Channel, oldChannelDisplayName, newChannelDisplayName string) *model.AppError { user, err := a.Srv().Store().User().Get(context.Background(), 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) { return a.Srv().getChannel(rctx, channelID) } 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) } } 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) { return a.Srv().getChannelsForTeamForUser(rctx, teamID, userID, opts) } 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) GetChannelCounts(rctx request.CTX, teamID string, userID string) (*model.ChannelCounts, *model.AppError) { counts, err := a.Srv().Store().Channel().GetChannelCounts(teamID, userID) if err != nil { return nil, model.NewAppError("SqlChannelStore.GetChannelCounts", "app.channel.get_channel_counts.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } return counts, 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(context.Background(), 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(context.Background(), 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 } func (a *App) removeUserFromChannel(rctx request.CTX, userIDToRemove string, removerUserId string, channel *model.Channel) *model.AppError { user, nErr := a.Srv().Store().User().Get(context.Background(), 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 err := a.Srv().Store().Channel().RemoveMember(rctx, channel.Id, userIDToRemove); err != nil { return model.NewAppError("removeUserFromChannel", "app.channel.remove_member.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } if err := a.Srv().Store().ChannelMemberHistory().LogLeaveEvent(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 err := a.Srv().Store().Thread().DeleteMembershipsForChannel(userIDToRemove, channel.Id); err != nil { return model.NewAppError("removeUserFromChannel", model.NoTranslation, nil, "failed to delete threadmemberships upon leaving channel", http.StatusInternalServerError).Wrap(err) } if isGuest { currentMembers, err := a.GetChannelMembersForUser(rctx, channel.TeamId, userIDToRemove) if err != nil { return err } if len(currentMembers) == 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) var actorUser *model.User if removerUserId != "" { actorUser, _ = a.GetUser(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() { // isAdd=false, empty remoteId means locally initiated if scs := a.Srv().Platform().GetSharedChannelService(); scs != nil { scs.HandleMembershipChange(channel.Id, userIDToRemove, false, "") } } 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 } var user *model.User if user, err = a.GetUser(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 } if channel.Type == model.ChannelTypePrivate && a.HasPermissionToChannel(rctx, userId, channelId, model.PermissionManagePrivateChannelMembers) { allowedChannelIds = append(allowedChannelIds, channelId) } else if channel.Type == model.ChannelTypeOpen && a.HasPermissionToChannel(rctx, userId, channelId, model.PermissionManagePublicChannelMembers) { 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(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(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.SanitizeProps() 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.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(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) } 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(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 channelList, nil } 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) } 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) } 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) 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.Context(), 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) } a.Srv().Platform().InvalidateCacheForChannel(channel) 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 { // 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("userId", 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) } func (a *App) GetDirectOrGroupMessageMembersCommonTeams(rctx request.CTX, 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, }) userIDs := make([]string, 0, len(users)) for _, user := range users { if user.IsBot { isOwnedByCurrentUserOrPlugin, err := a.IsBotOwnedByCurrentUserOrPlugin(rctx, user.Id) if err != nil { return nil, err } if isOwnedByCurrentUserOrPlugin { continue } } userIDs = append(userIDs, user.Id) } 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(err)) } } 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(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 } 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 } return channel.PolicyEnforced, nil } func (a *App) handleChannelCategoryName(channel *model.Channel) { if *a.Config().ExperimentalSettings.ExperimentalChannelCategorySorting && strings.Contains(channel.DisplayName, "/") { parts := strings.Split(channel.DisplayName, "/") channel.DisplayName = strings.TrimSpace(strings.Join(parts[1:], "/")) channel.DefaultCategoryName = strings.TrimSpace(parts[0]) } } func (a *App) addChannelToDefaultCategory(rctx request.CTX, userID string, channel *model.Channel) { // Add channel to default category if specified if channel.DefaultCategoryName != "" && *a.Config().ExperimentalSettings.ExperimentalChannelCategorySorting { // 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 } } 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 { // Add channel to existing category targetCategory.Channels = append([]string{channel.Id}, targetCategory.Channels...) _, err = a.UpdateSidebarCategories(rctx, userID, channel.TeamId, []*model.SidebarCategoryWithChannels{targetCategory}) 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)) } } } }