// 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/telemetry" "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(c 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(c 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(c) { channel, channelErr := a.Srv().Store().Channel().GetByName(teamID, channelName, true) if channelErr != nil { c.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(c, 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(c, user, requestor, channel); aErr != nil { c.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(c 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(c, user, channel); err != nil { return err } } else { if err := a.postAddToTeamMessage(c, requestor, user, channel, ""); err != nil { return err } } } else { if requestor == nil { if err := a.postJoinChannelMessage(c, user, channel); err != nil { return err } } else { if err := a.PostAddToChannelMessage(c, requestor, user, channel, ""); err != nil { return err } } } return nil } func (a *App) CreateChannelWithUser(c 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(c, 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(c, channel, true) if err != nil { return nil, err } var user *model.User if user, err = a.GetUser(userID); err != nil { return nil, err } if err = a.postJoinChannelMessage(c, 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(c 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) } channel.Name = newChannelName if newDisplayName != "" { channel.DisplayName = newDisplayName } newChannel, err := a.UpdateChannel(c, channel) if err != nil { return nil, err } return newChannel, nil } func (a *App) CreateChannel(c request.CTX, channel *model.Channel, addMember bool) (*model.Channel, *model.AppError) { channel.DisplayName = strings.TrimSpace(channel.DisplayName) sc, nErr := a.Srv().Store().Channel().Save(c, 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(c, 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(c) 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(c request.CTX, userID, otherUserID string, channelOptions ...model.ChannelOption) (*model.Channel, *model.AppError) { channel, nErr := a.getDirectChannel(c, userID, otherUserID) if nErr != nil { return nil, nErr } if channel != nil { return channel, nil } if *a.Config().TeamSettings.RestrictDirectMessage == model.DirectMessageTeam && !a.SessionHasPermissionTo(*c.Session(), model.PermissionManageSystem) { users, err := a.GetUsersByIds([]string{userID, otherUserID}, &store.UserGetByIdsOpts{}) if err != nil { return nil, err } var isBot bool for _, user := range users { if user.IsBot { isBot = true break } } // if one of the users is a bot, don't restrict to team members if !isBot { 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(c, userID, otherUserID, channelOptions...) if err != nil { if err.Id == store.ChannelExistsError { return channel, nil } return nil, err } a.handleCreationEvent(c, userID, otherUserID, channel) return channel, nil } func (a *App) getOrCreateDirectChannelWithUser(c request.CTX, user, otherUser *model.User) (*model.Channel, *model.AppError) { channel, nErr := a.getDirectChannel(c, user.Id, otherUser.Id) if nErr != nil { return nil, nErr } if channel != nil { return channel, nil } channel, err := a.createDirectChannelWithUser(c, user, otherUser) if err != nil { if err.Id == store.ChannelExistsError { return channel, nil } return nil, err } a.handleCreationEvent(c, user.Id, otherUser.Id, channel) return channel, nil } func (a *App) handleCreationEvent(c request.CTX, userID, otherUserID string, channel *model.Channel) { a.Srv().Platform().InvalidateChannelCacheForUser(userID) a.Srv().Platform().InvalidateChannelCacheForUser(otherUserID) a.Srv().Go(func() { pluginContext := pluginContext(c) 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(c request.CTX, userID string, otherUserID string, channelOptions ...model.ChannelOption) (*model.Channel, *model.AppError) { users, err := a.Srv().Store().User().GetMany(context.Background(), []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(c, user, otherUser, channelOptions...) } func (a *App) createDirectChannelWithUser(c 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(c, 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(c, sc); err != nil { c.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(c request.CTX, userIDs []string, creatorId string) (*model.Channel, *model.AppError) { channel, err := a.createGroupChannel(c, userIDs) 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 } func (a *App) createGroupChannel(c request.CTX, userIDs []string) (*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) } users, err := a.Srv().Store().User().GetProfileByIds(context.Background(), userIDs, nil, true) 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) } if !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, } channel, nErr := a.Srv().Store().Channel().Save(c, group, *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 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(c, 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) } } a.Srv().Go(func() { pluginContext := pluginContext(c) 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(c 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(context.Background(), 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(c, 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(c request.CTX, channel *model.Channel) (*model.Channel, *model.AppError) { _, err := a.Srv().Store().Channel().Update(c, 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(c 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(c, channel); err != nil { return nil, err } return scheme, nil } // DeleteChannelScheme deletes a channels scheme and sets its SchemeId to nil. func (a *App) DeleteChannelScheme(c 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(c, channel) } // UpdateChannelScheme saves the new SchemeId of the channel passed. func (a *App) UpdateChannelScheme(c request.CTX, channel *model.Channel) (*model.Channel, *model.AppError) { var oldChannel *model.Channel var err *model.AppError if oldChannel, err = a.GetChannel(c, channel.Id); err != nil { return nil, err } oldChannel.SchemeId = channel.SchemeId return a.UpdateChannel(c, oldChannel) } func (a *App) UpdateChannelPrivacy(c request.CTX, oldChannel *model.Channel, user *model.User) (*model.Channel, *model.AppError) { channel, err := a.UpdateChannel(c, oldChannel) if err != nil { return channel, err } postErr := a.postChannelPrivacyMessage(c, 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(c, 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(c 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(c) 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(c, 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(c 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(c, post, channel, model.CreatePostFlags{SetOnline: true}); err != nil { c.Logger().Warn("Failed to post unarchive message", mlog.Err(err)) } } else { a.Srv().Go(func() { systemBot, err := a.GetSystemBot(c) if err != nil { c.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(c, post, channel, model.CreatePostFlags{SetOnline: true}); err != nil { c.Logger().Error("Failed to post unarchive message", mlog.Err(err)) } }) } return channel, nil } func (a *App) PatchChannel(c request.CTX, channel *model.Channel, patch *model.ChannelPatch, userID string) (*model.Channel, *model.AppError) { oldChannelDisplayName := channel.DisplayName oldChannelHeader := channel.Header oldChannelPurpose := channel.Purpose channel.Patch(patch) channel, err := a.UpdateChannel(c, channel) if err != nil { return nil, err } if oldChannelDisplayName != channel.DisplayName { if err = a.PostUpdateChannelDisplayNameMessage(c, userID, channel, oldChannelDisplayName, channel.DisplayName); err != nil { c.Logger().Warn(err.Error()) } } if channel.Header != oldChannelHeader { if err = a.PostUpdateChannelHeaderMessage(c, userID, channel, oldChannelHeader, channel.Header); err != nil { c.Logger().Warn(err.Error()) } } if channel.Purpose != oldChannelPurpose { if err = a.PostUpdateChannelPurposeMessage(c, userID, channel, oldChannelPurpose, channel.Purpose); err != nil { c.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(c request.CTX, channelID string) (guestRoleName, userRoleName, adminRoleName string, err *model.AppError) { channel, err := a.GetChannel(c, 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(c, 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(c 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(c request.CTX, channel *model.Channel) ([]*model.ChannelModeration, *model.AppError) { guestRoleName, memberRoleName, _, err := a.GetSchemeRolesForChannel(c, channel.Id) if err != nil { return nil, err } memberRole, err := a.GetRoleByName(context.Background(), memberRoleName) if err != nil { return nil, err } var guestRole *model.Role if guestRoleName != "" { guestRole, err = a.GetRoleByName(context.Background(), guestRoleName) if err != nil { return nil, err } } higherScopedGuestRoleName, higherScopedMemberRoleName, _, err := a.GetTeamSchemeChannelRoles(c, channel.TeamId) if err != nil { return nil, err } higherScopedMemberRole, err := a.GetRoleByName(context.Background(), higherScopedMemberRoleName) if err != nil { return nil, err } var higherScopedGuestRole *model.Role if higherScopedGuestRoleName != "" { higherScopedGuestRole, err = a.GetRoleByName(context.Background(), higherScopedGuestRoleName) if err != nil { return nil, err } } return buildChannelModerations(c, 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(c request.CTX, channel *model.Channel, channelModerationsPatch []*model.ChannelModerationPatch) ([]*model.ChannelModeration, *model.AppError) { higherScopedGuestRoleName, higherScopedMemberRoleName, _, err := a.GetTeamSchemeChannelRoles(c, channel.TeamId) if err != nil { return nil, err } ctx := sqlstore.WithMaster(context.Background()) higherScopedMemberRole, err := a.GetRoleByName(ctx, higherScopedMemberRoleName) if err != nil { return nil, err } var higherScopedGuestRole *model.Role if higherScopedGuestRoleName != "" { higherScopedGuestRole, err = a.GetRoleByName(ctx, 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.AppError{Message: "Cannot add a permission that is restricted by the team or system permission scheme"} } if moderationPatch.Roles.Guests != nil && *moderationPatch.Roles.Guests && !higherScopedGuestPermissions[*moderationPatch.Name] { return nil, &model.AppError{Message: "Cannot add a permission that is restricted by the team or system permission scheme"} } } var scheme *model.Scheme // Channel has no scheme so create one if channel.SchemeId == nil || *channel.SchemeId == "" { scheme, err = a.CreateChannelScheme(c, 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(ctx, 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) c.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(ctx, memberRoleName) if err != nil { return nil, err } var guestRole *model.Role if guestRoleName != "" { guestRole, err = a.GetRoleByName(ctx, 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 { c.Logger().Info("Permission enabled for guests.", mlog.String("permission", permissionModified), mlog.String("channel_id", channel.Id), mlog.String("channel_name", channel.Name)) } else { c.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 { c.Logger().Info("Permission enabled for members.", mlog.String("permission", permissionModified), mlog.String("channel_id", channel.Id), mlog.String("channel_name", channel.Name)) } else { c.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(c, channel); err != nil { return nil, err } message := model.NewWebSocketEvent(model.WebsocketEventChannelSchemeUpdated, "", channel.Id, "", nil, "") a.Publish(message) memberRole = higherScopedMemberRole guestRole = higherScopedGuestRole c.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(c, 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(c, channel.Type, memberRole, guestRole, higherScopedMemberRole, higherScopedGuestRole), nil } func buildChannelModerations(c 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(c 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(c, channelID, userID); err != nil { return nil, err } schemeGuestRole, schemeUserRole, schemeAdminRole, err := a.GetSchemeRolesForChannel(c, 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.Fields(newRoles) { var role *model.Role role, err = a.GetRoleByName(context.Background(), 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(c, member) } func (a *App) UpdateChannelMemberSchemeRoles(c request.CTX, channelID string, userID string, isSchemeGuest bool, isSchemeUser bool, isSchemeAdmin bool) (*model.ChannelMember, *model.AppError) { member, err := a.GetChannelMember(c, 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) } 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(c, member) } func (a *App) UpdateChannelMemberNotifyProps(c 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(c 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 { c.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(c request.CTX, member *model.ChannelMember) (*model.ChannelMember, *model.AppError) { member, err := a.Srv().Store().Channel().UpdateMember(c, 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(c 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(c, post, channel, model.CreatePostFlags{SetOnline: true}); err != nil { c.Logger().Warn("Failed to post archive message", mlog.Err(err)) } } else { systemBot, err := a.GetSystemBot(c) if err != nil { c.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(c, post, channel, model.CreatePostFlags{SetOnline: true}); err != nil { c.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 { c.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 { c.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(c 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(context.Background(), 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([]string{user.Id}, channel) if err != nil { return nil, model.NewAppError("addUserToChannel", "api.channel.add_user_to_channel.type.app_error", nil, "", http.StatusInternalServerError) } 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 } newMember, nErr = a.Srv().Store().Channel().SaveMember(c, 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) } if user.IsGuest() { a.Srv().telemetryService.SendTelemetryForFeature( telemetry.TrackGuestFeature, "add_guest_to_channel", map[string]any{telemetry.TrackPropertyUser: user.Id}) } a.Srv().Platform().InvalidateChannelCacheForUser(user.Id) a.invalidateCacheForChannelMembers(channel.Id) return newMember, nil } // AddUserToChannel adds a user to a given channel. func (a *App) AddUserToChannel(c request.CTX, user *model.User, channel *model.Channel, skipTeamMemberIntegrityCheck bool) (*model.ChannelMember, *model.AppError) { if !skipTeamMemberIntegrityCheck { teamMember, nErr := a.Srv().Store().Team().GetMember(c, 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(c, user, channel) if err != nil { return nil, err } // 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(c request.CTX, userID string, channel *model.Channel, opts ChannelMemberOpts) (*model.ChannelMember, *model.AppError) { if member, err := a.Srv().Store().Channel().GetMember(context.Background(), 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(c, user, channel, opts.SkipTeamMemberIntegrityCheck) if err != nil { return nil, err } a.Srv().Go(func() { pluginContext := pluginContext(c) 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(c, user, channel); err != nil { return nil, err } } else { a.Srv().Go(func() { if err := a.PostAddToChannelMessage(c, userRequestor, user, channel, opts.PostRootID); err != nil { c.Logger().Error("Failed to post AddToChannel message", mlog.Err(err)) } }) } return cm, nil } func (a *App) AddDirectChannels(c 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(c 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(c, 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(c 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(c, 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(c 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(c, 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(c request.CTX, channelID string) (*model.Channel, *model.AppError) { return a.Srv().getChannel(c, channelID) } func (s *Server) getChannel(c 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(c 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(c 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(c 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(c 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(c 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(c 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(c request.CTX, teamID string, userID string, opts *model.ChannelSearchOpts) (model.ChannelList, *model.AppError) { return a.Srv().getChannelsForTeamForUser(c, teamID, userID, opts) } func (a *App) GetChannelsForUser(c 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(c request.CTX, page, perPage int, opts model.ChannelSearchOpts) (model.ChannelListWithTeamData, *model.AppError) { if opts.ExcludeDefaultChannels { opts.ExcludeChannelNames = a.DefaultChannelNames(c) } storeOpts := store.ChannelSearchOpts{ NotAssociatedToGroup: opts.NotAssociatedToGroup, IncludeDeleted: opts.IncludeDeleted, ExcludeChannelNames: opts.ExcludeChannelNames, GroupConstrained: opts.GroupConstrained, ExcludeGroupConstrained: opts.ExcludeGroupConstrained, ExcludePolicyConstrained: opts.ExcludePolicyConstrained, IncludePolicyID: opts.IncludePolicyID, } 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(c request.CTX, opts model.ChannelSearchOpts) (int64, *model.AppError) { if opts.ExcludeDefaultChannels { opts.ExcludeChannelNames = a.DefaultChannelNames(c) } 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(c 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(c 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(c 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(c 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(c 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(c request.CTX, channelID string, userID string) (*model.ChannelMember, *model.AppError) { return a.Srv().getChannelMember(c, channelID, userID) } func (s *Server) getChannelMember(c request.CTX, channelID string, userID string) (*model.ChannelMember, *model.AppError) { channelMember, err := s.Store().Channel().GetMember(c.Context(), 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(c request.CTX, channelID string, userID string) (int64, *model.AppError) { lastViewedAt, err := s.Store().Channel().GetMemberLastViewedAt(c.Context(), 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(c request.CTX, channelID string, page, perPage int) (model.ChannelMembers, *model.AppError) { channelMembers, err := a.Srv().Store().Channel().GetMembers(channelID, page*perPage, perPage) 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(c 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(c 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(c 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(c 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 { member := member members = append(members, &member.ChannelMember) } return members, nil } func (a *App) GetChannelMembersWithTeamDataForUserWithPagination(c request.CTX, userID string, page, perPage int) (model.ChannelMembersWithTeamData, *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) } return m, nil } func (a *App) GetChannelMemberCount(c 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(c 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(c 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(c 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(c 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(c 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(c 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(context.Background(), 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(c, user, channel, false) if err != nil { return err } a.Srv().Go(func() { pluginContext := pluginContext(c) a.ch.RunMultiHook(func(hooks plugin.Hooks, _ *model.Manifest) bool { hooks.UserHasJoinedChannel(pluginContext, cm, nil) return true }, plugin.UserHasJoinedChannelID) }) if err := a.postJoinChannelMessage(c, user, channel); err != nil { return err } return nil } func (a *App) postJoinChannelMessage(c 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(c, 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(c 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(c, 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(c 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) }() mcc := make(chan store.StoreResult[int64], 1) go func() { count, err := a.Srv().Store().Channel().GetMemberCount(channelID, false) mcc <- store.StoreResult[int64]{Data: count, NErr: err} close(mcc) }() 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) } } ccresult := <-mcc if ccresult.NErr != nil { return model.NewAppError("LeaveChannel", "app.channel.get_member_count.app_error", nil, "", http.StatusInternalServerError).Wrap(ccresult.NErr) } channel := cresult.Data user := uresult.Data membersCount := ccresult.Data if channel.IsGroupOrDirect() { err := model.NewAppError("LeaveChannel", "api.channel.leave.direct.app_error", nil, "", http.StatusBadRequest) return err } if channel.Type == model.ChannelTypePrivate && membersCount == 1 { err := model.NewAppError("LeaveChannel", "api.channel.leave.last_member.app_error", nil, "userId="+user.Id, http.StatusBadRequest) return err } if err := a.removeUserFromChannel(c, 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(c, user, channel); err != nil { c.Logger().Error("Failed to post LeaveChannel message", mlog.Err(err)) } }) return nil } func (a *App) postLeaveChannelMessage(c 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(c, 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(c 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(c, 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(c 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(c, 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(c request.CTX, removerUserId string, removedUser *model.User, channel *model.Channel) *model.AppError { messageUserId := removerUserId if messageUserId == "" { systemBot, err := a.GetSystemBot(c) 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(c, 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(c 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([]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(c, channel.Id, userIDToRemove) if err != nil { return err } if err := a.Srv().Store().Channel().RemoveMember(c, 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(c, channel.TeamId, userIDToRemove) if err != nil { return err } if len(currentMembers) == 0 { teamMember, err := a.GetTeamMember(c, 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(c, 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(c, 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(c) 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) return nil } func (a *App) RemoveUserFromChannel(c request.CTX, userIDToRemove string, removerUserId string, channel *model.Channel) *model.AppError { var err *model.AppError if err = a.removeUserFromChannel(c, 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(c, user, channel); err != nil { return err } } else { if err := a.postRemoveFromChannelMessage(c, removerUserId, user, channel); err != nil { c.Logger().Error("Failed to post user removal message", mlog.Err(err)) } } return nil } func (a *App) GetNumberOfChannelsOnTeam(c 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(c request.CTX, userID string, channelID string) *model.AppError { status, err := a.Srv().Platform().GetStatus(userID) oldStatus := model.StatusOffline oldChannelID := "" if err != nil { status = &model.Status{UserId: userID, Status: model.StatusOnline, Manual: false, LastActivityAt: model.GetMillis(), ActiveChannel: channelID} } else { oldStatus = status.Status oldChannelID = status.ActiveChannel 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) } if channelID != "" && oldChannelID != channelID { // is this a read-only channel? isReadOnly, ircErr := a.Srv().Store().Channel().IsReadOnlyChannel(channelID) if ircErr != nil { mlog.Warn("Error trying to check if it is a readonly channel", mlog.Err(ircErr)) } if isReadOnly { a.Srv().telemetryService.SendTelemetryForFeature( telemetry.TrackReadOnlyFeature, "read_only_channel_viewed", map[string]any{ telemetry.TrackPropertyUser: userID, telemetry.TrackPropertyChannel: channelID, }, ) } isShared, shErr := a.Srv().Store().SharedChannel().HasChannel(channelID) if shErr == nil && isShared { a.Srv().telemetryService.SendTelemetryForFeature( telemetry.TrackSharedChannelsFeature, "shared_channel_viewed", map[string]any{ telemetry.TrackPropertyUser: userID, telemetry.TrackPropertyChannel: channelID, }, ) } } return nil } func (a *App) IsCRTEnabledForUser(c 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(c request.CTX, userId string, channelIds []string) []string { var allowedChannelIds []string for _, channelId := range channelIds { channel, err := a.GetChannel(c, channelId) if err != nil { c.Logger().Info("Invite users to team - couldn't get channel " + channelId) continue } if channel.Type == model.ChannelTypePrivate && a.HasPermissionToChannel(c, userId, channelId, model.PermissionManagePrivateChannelMembers) { allowedChannelIds = append(allowedChannelIds, channelId) } else if channel.Type == model.ChannelTypeOpen && a.HasPermissionToChannel(c, userId, channelId, model.PermissionManagePublicChannelMembers) { allowedChannelIds = append(allowedChannelIds, channelId) } else { c.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(c request.CTX, postID string, userID string, collapsedThreadsSupported bool) (*model.ChannelUnreadAt, *model.AppError) { if !collapsedThreadsSupported || !a.IsCRTEnabledForUser(c, userID) { return a.markChannelAsUnreadFromPostCRTUnsupported(c, postID, userID) } post, err := a.GetSinglePost(c, 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(c, 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(c, channelUnread, postID) a.UpdateMobileAppBadge(userID) return channelUnread, nil } func (a *App) markChannelAsUnreadFromPostCRTUnsupported(c request.CTX, postID string, userID string) (*model.ChannelUnreadAt, *model.AppError) { post, appErr := a.GetSinglePost(c, 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(c, 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(c, 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(c, 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(c, 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(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(c, 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(c, channelUnread, postID) a.UpdateMobileAppBadge(userID) return channelUnread, nil } func (a *App) sendWebSocketPostUnreadEvent(c 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(c request.CTX, userID, term string) (model.ChannelListWithTeamData, *model.AppError) { includeDeleted := *a.Config().TeamSettings.ExperimentalViewArchivedChannels term = strings.TrimSpace(term) user, appErr := a.GetUser(userID) if appErr != nil { return nil, appErr } channelList, err := a.Srv().Store().Channel().Autocomplete(c, 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(c request.CTX, teamID, userID, term string) (model.ChannelList, *model.AppError) { includeDeleted := *a.Config().TeamSettings.ExperimentalViewArchivedChannels term = strings.TrimSpace(term) user, appErr := a.GetUser(userID) if appErr != nil { return nil, appErr } channelList, err := a.Srv().Store().Channel().AutocompleteInTeam(c, 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(c request.CTX, teamID string, userID string, term string) (model.ChannelList, *model.AppError) { includeDeleted := *a.Config().TeamSettings.ExperimentalViewArchivedChannels 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(c request.CTX, term string, opts model.ChannelSearchOpts) (model.ChannelListWithTeamData, int64, *model.AppError) { if opts.ExcludeDefaultChannels { opts.ExcludeChannelNames = a.DefaultChannelNames(c) } 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, } 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(c request.CTX, teamID string, term string) (model.ChannelList, *model.AppError) { includeDeleted := *a.Config().TeamSettings.ExperimentalViewArchivedChannels 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) SearchArchivedChannels(c request.CTX, teamID string, term string, userID string) (model.ChannelList, *model.AppError) { term = strings.TrimSpace(term) channelList, err := a.Srv().Store().Channel().SearchArchivedInTeam(teamID, term, userID) if err != nil { return nil, model.NewAppError("SearchArchivedChannels", "app.channel.search.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } return channelList, nil } func (a *App) SearchChannelsForUser(c request.CTX, userID, teamID, term string) (model.ChannelList, *model.AppError) { includeDeleted := *a.Config().TeamSettings.ExperimentalViewArchivedChannels 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(c 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(c 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(c 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(c.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(c.Context(), 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(c request.CTX, view *model.ChannelView, userID string, currentSessionId string, collapsedThreadsSupported bool) (map[string]int64, *model.AppError) { if err := a.SetActiveChannel(c, 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(c, channelIDs, userID, currentSessionId, collapsedThreadsSupported, a.IsCRTEnabledForUser(c, userID)) } func (a *App) PermanentDeleteChannel(c request.CTX, channel *model.Channel) *model.AppError { if err := a.Srv().Store().Post().PermanentDeleteByChannel(c, 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(c, 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(c, 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(c request.CTX, channel *model.Channel) *model.AppError { err := a.Srv().Store().Channel().RemoveAllDeactivatedMembers(c, 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(c 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(c, 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 { c.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(c, 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 { c.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 { c.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 { c.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 { c.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 { c.Logger().Warn("error while updating threads after channel move", mlog.Err(err)) } if err := a.RemoveUsersFromChannelNotMemberOfTeam(c, user, channel, team); err != nil { c.Logger().Warn("error while removing non-team member users", mlog.Err(err)) } if user != nil { if err := a.postChannelMoveMessage(c, user, channel, previousTeam); err != nil { c.Logger().Warn("error while posting move channel message", mlog.Err(err)) } } return nil } func (a *App) postChannelMoveMessage(c 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(c, 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(c request.CTX, remover *model.User, channel *model.Channel, team *model.Team) *model.AppError { channelMembers, err := a.GetChannelMembersPage(c, 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(c, userID, removerId, channel); err != nil { return err } } } } return nil } func (a *App) GetPinnedPosts(c 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(c request.CTX, channelID, userID string) (*model.ChannelMember, *model.AppError) { member, nErr := a.Srv().Store().Channel().GetMember(context.Background(), 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(c, member) if err != nil { return nil, err } a.invalidateCacheForChannelMembersNotifyProps(member.ChannelId) return member, nil } func (a *App) setChannelsMuted(c 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(c request.CTX, channel *model.Channel) *model.AppError { return a.FillInChannelsProps(c, model.ChannelList{channel}) } func (a *App) FillInChannelsProps(c 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(c, 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(c request.CTX, channelID string, f func(model.ChannelMember) error) error { perPage := 100 page := 0 for { channelMembers, err := a.Srv().Store().Channel().GetMembers(channelID, page*perPage, perPage) 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(c 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(c, 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.Context(), 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(c request.CTX, userID, otherUserID string) (*model.Channel, *model.AppError) { return a.Srv().getDirectChannel(c, userID, otherUserID) } func (a *App) GetGroupMessageMembersCommonTeams(c request.CTX, channelID string) ([]*model.Team, *model.AppError) { channel, appErr := a.GetChannel(c, channelID) if appErr != nil { return nil, appErr } if channel.Type != model.ChannelTypeGroup { return nil, model.NewAppError("GetGroupMessageMembersCommonTeams", "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, len(users)) for i := 0; i < len(users); i++ { userIDs[i] = users[i].Id } commonTeamIDs, err := a.Srv().Store().Team().GetCommonTeamIDsForMultipleUsers(userIDs) if err != nil { return nil, model.NewAppError("GetGroupMessageMembersCommonTeams", "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(c request.CTX, convertedByUserId string, gmConversionRequest *model.GroupMessageConversionRequestBody) (*model.Channel, *model.AppError) { originalChannel, appErr := a.GetChannel(c, gmConversionRequest.ChannelID) if appErr != nil { return nil, appErr } appErr = a.validateForConvertGroupMessageToChannel(c, 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(c, 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(c, gmConversionRequest, users) _ = a.postMessageForConvertGroupMessageToChannel(c, gmConversionRequest.ChannelID, convertedByUserId, users) // the user conversion the GM becomes the channel admin. _, appErr = a.UpdateChannelMemberSchemeRoles(c, gmConversionRequest.ChannelID, convertedByUserId, false, true, true) if appErr != nil { return nil, appErr } return updatedChannel, nil } func (a *App) setSidebarCategoriesForConvertedGroupMessage(c 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(c, user.Id, &store.SidebarCategorySearchOpts{ TeamID: gmConversionRequest.TeamID, Type: model.SidebarCategoryChannels, }) if appErr != nil { c.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(c, user.Id, gmConversionRequest.TeamID, []*model.SidebarCategoryWithChannels{channelsCategory}) if appErr != nil { c.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(c request.CTX, convertedByUserId string, originalChannel *model.Channel, gmConversionRequest *model.GroupMessageConversionRequestBody) *model.AppError { commonTeams, appErr := a.GetGroupMessageMembersCommonTeams(c, 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(c, 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(c 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(c, channelID) if appErr != nil { return appErr } if _, appErr := a.CreatePost(c, post, channel, model.CreatePostFlags{SetOnline: true}); appErr != nil { c.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(c 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 }