Files
mostlymatter/server/channels/app/channel.go
Maria A Nunez 2ff29e375b [release-10.11] Tighten thread membership cleanup on team membership changes (#37081)
* Tighten thread membership cleanup on team membership changes (#36764)

* Tighten thread membership cleanup on team membership changes

Ensure ThreadMembership rows are cleaned up when a user is removed
from or leaves a team, add a defense-in-depth filter on the thread
retrieval path so memberships referencing channels the user is no
longer a member of are excluded, and add a one-time migration to
clean up stale records on existing deployments.

https://mattermost.atlassian.net/browse/MM-69008

* Centralize per-channel membership removal and tighten tests

Extract the combined channel-member and thread-membership removal into
a shared helper used by both removeUserFromChannel and LeaveTeam, so
future code paths cannot revoke channel access without dropping the
dependent thread state. The channel-leave event is now logged after
the combined removal completes.

Also drop verbose test header comments and rename a test to a
behavior-focused name.

* Backfill channel members in thread storetest setups

The new ChannelMembers predicate on thread read queries filters out
ThreadMembership rows whose user has no ChannelMembers row for the
thread's channel. Several existing storetest setups bypassed the
normal write path and inserted threads/memberships without channel
members. Add the missing channel-member rows so the test data matches
the real-world invariant.

* Retrigger enterprise CI

Pick up enterprise merge e6953d4 (master into
MM-69008-thread-membership-team-leave) in the combined Enterprise
CI/tests lane, which pins the enterprise SHA at mattermost-side
dispatch time.

Co-authored-by: Maria A Nunez <maria.nunez@mattermost.com>

---------

Co-authored-by: Mattermost Build <build@mattermost.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>

* Add MySQL migration for 000195_threadmemberships_cleanup_v2

Co-authored-by: Cursor <cursoragent@cursor.com>

* Apply pre-commit lint fixes

Co-authored-by: Cursor <cursoragent@cursor.com>

* Retrigger CI

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Mattermost Build <build@mattermost.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-06-22 13:52:05 -04:00

4084 строки
155 KiB
Go

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"slices"
"strings"
"github.com/mattermost/mattermost/server/v8/channels/utils"
"github.com/mattermost/mattermost/server/v8/platform/services/sharedchannel"
"github.com/mattermost/mattermost/server/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
}
a.addChannelToDefaultCategory(c, userID, channel)
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)
}
// Clean up the channel name and display name
newChannelName = strings.TrimSpace(newChannelName)
newDisplayName = strings.TrimSpace(newDisplayName)
channel.Name = newChannelName
if newDisplayName != "" {
channel.DisplayName = newDisplayName
}
newChannel, err := a.UpdateChannel(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) {
a.handleChannelCategoryName(channel)
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, &ltErr):
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, &ltErr):
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, channelOptions ...model.ChannelOption) (*model.Channel, *model.AppError) {
channel, err := a.createGroupChannel(c, userIDs, creatorId, channelOptions...)
if err != nil {
if err.Id == store.ChannelExistsError {
return channel, nil
}
return nil, err
}
jsonIDs := model.ArrayToJSON(userIDs)
for _, userID := range userIDs {
a.Srv().Platform().InvalidateChannelCacheForUser(userID)
message := model.NewWebSocketEvent(model.WebsocketEventGroupAdded, "", channel.Id, userID, nil, "")
message.Add("teammate_ids", jsonIDs)
a.Publish(message)
}
return channel, nil
}
// creatorId is used to determine if the group channel should have a
// shared channel record attached. It can be empty if the caller
// doesn't know who the creator is (e.g. the import process) and the
// resulting group channel will not be shared
func (a *App) createGroupChannel(c request.CTX, userIDs []string, creatorID string, channelOptions ...model.ChannelOption) (*model.Channel, *model.AppError) {
if len(userIDs) > model.ChannelGroupMaxUsers || len(userIDs) < model.ChannelGroupMinUsers {
return nil, model.NewAppError("CreateGroupChannel", "api.channel.create_group.bad_size.app_error", nil, "", http.StatusBadRequest)
}
// we skip cache and use master when fetching profiles to avoid
// issues in shared channels and HA, when users are created from a
// shared channels GM invite right before creating the GM
ctx := sqlstore.RequestContextWithMaster(c).Context()
users, err := a.Srv().Store().User().GetProfileByIds(ctx, userIDs, nil, false)
if err != nil {
return nil, model.NewAppError("createGroupChannel", "app.user.get_profiles.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if len(users) != len(userIDs) {
return nil, model.NewAppError("CreateGroupChannel", "api.channel.create_group.bad_user.app_error", nil, "user_ids="+model.ArrayToJSON(userIDs), http.StatusBadRequest)
}
// extracts the creator and the remotes involved in the GM to
// decide how to handle the shared part of the creation
var creator *model.User
remoteIDs := map[string]bool{}
for _, user := range users {
if user.Id == creatorID {
creator = user
}
if user.IsRemote() {
remoteIDs[*user.RemoteId] = true
}
}
channelIsShared := len(remoteIDs) > 0
if channelIsShared && !a.Config().FeatureFlags.EnableSharedChannelsDMs {
for _, user := range users {
if user.IsRemote() {
return nil, model.NewAppError("createGroupChannel", "api.channel.create_group.remote_restricted.app_error", nil, "", http.StatusForbidden)
}
}
}
group := &model.Channel{
Name: model.GetGroupNameFromUserIds(userIDs),
DisplayName: model.GetGroupDisplayNameFromUsers(users, true),
Type: model.ChannelTypeGroup,
Shared: model.NewPointer(channelIsShared),
}
channel, nErr := a.Srv().Store().Channel().Save(c, group, *a.Config().TeamSettings.MaxChannelsPerTeam, channelOptions...)
if nErr != nil {
var invErr *store.ErrInvalidInput
var cErr *store.ErrConflict
var ltErr *store.ErrLimitExceeded
var appErr *model.AppError
switch {
case errors.As(nErr, &invErr):
switch {
case invErr.Entity == "Channel" && invErr.Field == "DeleteAt":
return nil, model.NewAppError("CreateChannel", "store.sql_channel.save.archived_channel.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
case invErr.Entity == "Channel" && invErr.Field == "Type":
return nil, model.NewAppError("CreateChannel", "store.sql_channel.save.direct_channel.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
case invErr.Entity == "Channel" && invErr.Field == "Id":
return nil, model.NewAppError("CreateChannel", "store.sql_channel.save_channel.existing.app_error", nil, "id="+invErr.Value.(string), http.StatusBadRequest).Wrap(nErr)
}
case errors.As(nErr, &cErr):
return channel, model.NewAppError("CreateChannel", store.ChannelExistsError, nil, "", http.StatusBadRequest).Wrap(nErr)
case errors.As(nErr, &ltErr):
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)
}
}
// When the newly created channel is shared, the creator is local
// and one of the participants is remote create a local shared
// channel record
if channel.IsShared() && creator != nil && !creator.IsRemote() {
sc := &model.SharedChannel{
ChannelId: channel.Id,
TeamId: channel.TeamId,
Home: true,
ReadOnly: false,
ShareName: channel.Name,
ShareDisplayName: channel.DisplayName,
SharePurpose: channel.Purpose,
ShareHeader: channel.Header,
CreatorId: creatorID,
Type: channel.Type,
}
if _, err := a.ShareChannel(c, sc); err != nil {
c.Logger().Error("Failed to share newly created group channel", mlog.String("channel_id", channel.Id), mlog.Err(err))
} else {
// if we could successfully share the channel, we invite
// the remotes involved to it
if sc, _ := a.getSharedChannelsService(false); sc != nil {
for remoteID := range remoteIDs {
rc, err := a.Srv().Store().RemoteCluster().Get(remoteID, false)
if err != nil {
c.Logger().Error("Failed to send invite to group message channel, can't retrieve remote cluster", mlog.String("channel_id", channel.Id), mlog.String("remote_id", remoteID), mlog.Err(err))
continue
}
opts := []sharedchannel.InviteOption{sharedchannel.WithCreator(creatorID)}
for _, user := range users {
opts = append(opts, sharedchannel.WithDirectParticipant(user, remoteID))
}
if err := sc.SendChannelInvite(channel, creatorID, rc, opts...); err != nil {
c.Logger().Error("Failed to send invite to group message channel, error sending the invite", mlog.String("channel_id", channel.Id), mlog.String("remote_id", remoteID), mlog.Err(err))
}
}
}
}
}
a.Srv().Go(func() {
pluginContext := pluginContext(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) {
ok, appErr := a.ChannelAccessControlled(c, channel.Id)
if appErr != nil {
return nil, appErr
}
if ok && channel.Type != model.ChannelTypePrivate {
return nil, model.NewAppError("UpdateChannel", "api.channel.update_channel.not_allowed.app_error", nil, "", http.StatusForbidden)
}
_, err := a.Srv().Store().Channel().Update(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)
a.handleChannelCategoryName(channel)
channel, err := a.UpdateChannel(c, channel)
if err != nil {
return nil, err
}
a.addChannelToDefaultCategory(c, userID, channel)
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)
}
if !isSchemeUser {
return nil, model.NewAppError("UpdateChannelMemberSchemeRoles", "api.channel.update_channel_member_roles.unset_user_scheme.app_error", nil, "", http.StatusBadRequest)
}
member.SchemeAdmin = isSchemeAdmin
member.SchemeUser = isSchemeUser
member.SchemeGuest = isSchemeGuest
// If the migration is not completed, we also need to check the default channel_admin/channel_user roles are not present in the roles field.
if err = a.IsPhase2MigrationCompleted(); err != nil {
member.ExplicitRoles = removeRoles([]string{model.ChannelGuestRoleId, model.ChannelUserRoleId, model.ChannelAdminRoleId}, member.ExplicitRoles)
}
return a.updateChannelMember(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
}
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)
}
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)
}
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).Wrap(err)
}
if len(nonMembers) > 0 {
return nil, model.NewAppError("addUserToChannel", "api.channel.add_members.user_denied", map[string]any{"UserIDs": nonMembers}, "", http.StatusBadRequest)
}
}
newMember := &model.ChannelMember{
ChannelId: channel.Id,
UserId: user.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
SchemeGuest: user.IsGuest(),
SchemeUser: !user.IsGuest(),
}
if !user.IsGuest() {
var userShouldBeAdmin bool
userShouldBeAdmin, appErr := a.UserIsInAdminRoleGroup(user.Id, channel.Id, model.GroupSyncableTypeChannel)
if appErr != nil {
return nil, appErr
}
newMember.SchemeAdmin = userShouldBeAdmin
}
if channel.Type == model.ChannelTypePrivate {
if ok, appErr := a.ChannelAccessControlled(c, channel.Id); ok {
if acs := a.Srv().Channels().AccessControl; acs != nil {
groupID, err := a.CpaGroupID()
if err != nil {
return nil, model.NewAppError("AddUserToChannel", "api.channel.add_user.to.channel.failed.app_error", nil,
fmt.Sprintf("failed to get group: %v, user_id: %s, channel_id: %s", err, user.Id, channel.Id), http.StatusInternalServerError)
}
s, err := a.Srv().Store().Attributes().GetSubject(c, user.Id, groupID)
if err != nil {
return nil, model.NewAppError("AddUserToChannel", "api.channel.add_user.to.channel.failed.app_error", nil,
fmt.Sprintf("failed to get subject: %v, user_id: %s, channel_id: %s", err, user.Id, channel.Id), http.StatusNotFound)
}
decision, evalErr := acs.AccessEvaluation(c, model.AccessRequest{
Subject: *s,
Resource: model.Resource{
Type: model.AccessControlPolicyTypeChannel,
ID: channel.Id,
},
Action: "join_channel",
})
if evalErr != nil {
return nil, evalErr
} else if !decision.Decision {
return nil, model.NewAppError("AddUserToChannel", "api.channel.add_user.to.channel.rejected", nil, "", http.StatusForbidden)
}
}
} else if appErr != nil {
return nil, appErr
}
}
newMember, nErr = a.Srv().Store().Channel().SaveMember(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)
// Synchronize membership change for shared channels
if channel.IsShared() {
if scs := a.Srv().Platform().GetSharedChannelService(); scs != nil {
scs.HandleMembershipChange(channel.Id, user.Id, true, user.GetRemoteID())
}
}
return newMember, nil
}
// AddUserToChannel adds a user to a given channel.
func (a *App) AddUserToChannel(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
}
a.addChannelToDefaultCategory(c, user.Id, channel)
// We are sending separate websocket events to the user added and to the channel
// This is to get around potential cluster syncing issues where other nodes may not receive the most up to date channel members
message := model.NewWebSocketEvent(model.WebsocketEventUserAdded, "", channel.Id, "", map[string]bool{user.Id: true}, "")
message.Add("user_id", user.Id)
message.Add("team_id", channel.TeamId)
a.Publish(message)
userMessage := model.NewWebSocketEvent(model.WebsocketEventUserAdded, "", channel.Id, user.Id, nil, "")
userMessage.Add("user_id", user.Id)
userMessage.Add("team_id", channel.TeamId)
a.Publish(userMessage)
return newMember, nil
}
type ChannelMemberOpts struct {
UserRequestorID string
PostRootID string
// SkipTeamMemberIntegrityCheck is used to indicate whether it should be checked
// that a user has already been removed from that team or not.
// This is useful to avoid in scenarios when we just added the team member,
// and thereby know that there is no need to check this.
SkipTeamMemberIntegrityCheck bool
}
// AddChannelMember adds a user to a channel. It is a wrapper over AddUserToChannel.
func (a *App) AddChannelMember(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,
AccessControlPolicyEnforced: opts.AccessControlPolicyEnforced,
ExcludeAccessControlPolicyEnforced: opts.ExcludeAccessControlPolicyEnforced,
}
channels, err := a.Srv().Store().Channel().GetAllChannels(page*perPage, perPage, storeOpts)
if err != nil {
return nil, model.NewAppError("GetAllChannels", "app.channel.get_all_channels.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return channels, nil
}
func (a *App) GetAllChannelsCount(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) {
opts := model.ChannelMembersGetOptions{
ChannelID: channelID,
Offset: page * perPage,
Limit: perPage,
}
channelMembers, err := a.Srv().Store().Channel().GetMembers(opts)
if err != nil {
return nil, model.NewAppError("GetChannelMembersPage", "app.channel.get_members.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return channelMembers, nil
}
func (a *App) GetChannelMembersTimezones(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, cursor *model.ChannelMemberCursor) (model.ChannelMembersWithTeamData, *model.AppError) {
var m model.ChannelMembersWithTeamData
var err error
var method string
if cursor.Page == -1 {
m, err = a.Srv().Store().Channel().GetMembersForUserWithCursorPagination(userID, cursor.PerPage, cursor.FromChannelID)
method = "GetMembersForUserWithCursorPagination"
} else {
m, err = a.Srv().Store().Channel().GetMembersForUserWithPagination(userID, cursor.Page, cursor.PerPage)
method = "GetMembersForUserWithPagination"
}
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return nil, model.NewAppError(method, MissingChannelMemberError, nil, "", http.StatusNotFound).Wrap(err)
default:
return nil, model.NewAppError(method, "app.channel.get_members.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
return m, nil
}
func (a *App) GetChannelMemberCount(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)
}()
cresult := <-sc
if cresult.NErr != nil {
errCtx := map[string]any{"channel_id": channelID}
var nfErr *store.ErrNotFound
switch {
case errors.As(cresult.NErr, &nfErr):
return model.NewAppError("LeaveChannel", "app.channel.get.existing.app_error", errCtx, "", http.StatusNotFound).Wrap(cresult.NErr)
default:
return model.NewAppError("LeaveChannel", "app.channel.get.find.app_error", errCtx, "", http.StatusInternalServerError).Wrap(cresult.NErr)
}
}
uresult := <-uc
if uresult.NErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(uresult.NErr, &nfErr):
return model.NewAppError("LeaveChannel", MissingAccountError, nil, "", http.StatusNotFound).Wrap(uresult.NErr)
default:
return model.NewAppError("LeaveChannel", "app.user.get.app_error", nil, "", http.StatusInternalServerError).Wrap(uresult.NErr)
}
}
channel := cresult.Data
user := uresult.Data
if channel.IsGroupOrDirect() {
err := model.NewAppError("LeaveChannel", "api.channel.leave.direct.app_error", nil, "", http.StatusBadRequest)
return err
}
if err := a.removeUserFromChannel(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
}
// removeChannelMembership strips a user's channel membership and the associated
// thread memberships. Keeping these together ensures channel access cannot be
// revoked without also dropping the thread state that depends on it.
func (a *App) removeChannelMembership(rctx request.CTX, userID, channelID, caller string) *model.AppError {
if err := a.Srv().Store().Channel().RemoveMember(rctx, channelID, userID); err != nil {
return model.NewAppError(caller, "app.channel.remove_member.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if err := a.Srv().Store().Thread().DeleteMembershipsForChannel(userID, channelID); err != nil {
return model.NewAppError(caller, model.NoTranslation, nil, "failed to delete threadmemberships upon leaving channel", http.StatusInternalServerError).Wrap(err)
}
return nil
}
func (a *App) removeUserFromChannel(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 appErr := a.removeChannelMembership(c, userIDToRemove, channel.Id, "removeUserFromChannel"); appErr != nil {
return appErr
}
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 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)
// Synchronize membership change for shared channels
if channel.IsShared() {
// isAdd=false, empty remoteId means locally initiated
if scs := a.Srv().Platform().GetSharedChannelService(); scs != nil {
scs.HandleMembershipChange(channel.Id, userIDToRemove, false, "")
}
}
return nil
}
func (a *App) RemoveUserFromChannel(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
}
allowedPrivate := false
if channel.Type == model.ChannelTypePrivate {
allowedPrivate, _ = a.HasPermissionToChannel(c, userId, channelId, model.PermissionManagePrivateChannelMembers)
}
allowedPublic := false
if channel.Type == model.ChannelTypeOpen {
allowedPublic, _ = a.HasPermissionToChannel(c, userId, channelId, model.PermissionManagePublicChannelMembers)
}
if allowedPrivate || allowedPublic {
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,
AccessControlPolicyEnforced: opts.AccessControlPolicyEnforced,
ExcludeAccessControlPolicyEnforced: opts.ExcludeAccessControlPolicyEnforced,
ParentAccessControlPolicyId: opts.ParentAccessControlPolicyId,
}
term = strings.TrimSpace(term)
channelList, totalCount, err := a.Srv().Store().Channel().SearchAllChannels(term, storeOpts)
if err != nil {
return nil, 0, model.NewAppError("SearchAllChannels", "app.channel.search.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return channelList, totalCount, nil
}
func (a *App) SearchChannels(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 {
opts := model.ChannelMembersGetOptions{
ChannelID: channelID,
Offset: page * perPage,
Limit: perPage,
}
channelMembers, err := a.Srv().Store().Channel().GetMembers(opts)
if err != nil {
return err
}
for _, channelMember := range channelMembers {
if err = f(channelMember); err != nil {
return err
}
}
length := len(channelMembers)
if length < perPage {
break
}
page++
}
return nil
}
func (a *App) ClearChannelMembersCache(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)
}
// GetGroupMessageMembersCommonTeamsAsUser is a variant of GetGroupMessageMembersCommonTeams
// that returns results relative to the requesting user from the session in the request context.
func (a *App) GetGroupMessageMembersCommonTeamsAsUser(rctx request.CTX, channelID string) ([]*model.Team, *model.AppError) {
return a.getGroupMessageMembersCommonTeams(rctx, rctx.Session().UserId, channelID)
}
// GetGroupMessageMembersCommonTeams returns the set of teams in common for the members of the given GM channel.
//
// Prefer GetGroupMessageMembersCommonTeamsAsUser unless the request context is independent of any given user.
func (a *App) GetGroupMessageMembersCommonTeams(rctx request.CTX, channelID string) ([]*model.Team, *model.AppError) {
return a.getGroupMessageMembersCommonTeams(rctx, "", channelID)
}
// getGroupMessageMembersCommonTeams returns the set of teams common to the members of the given GM channel.
//
// If a requesting user id is specified, but the user isn't an active member of the channel, we return an empty
// set of channels. We don't just exclude all inactive users to offer more flexibility to the remaining users
// on where to create the replacement channel.
func (a *App) getGroupMessageMembersCommonTeams(rctx request.CTX, requestingUserID, channelID string) ([]*model.Team, *model.AppError) {
channel, appErr := a.GetChannel(rctx, channelID)
if appErr != nil {
return nil, appErr
}
if channel.Type != model.ChannelTypeGroup {
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,
})
if appErr != nil {
return nil, appErr
}
userIDs := make([]string, len(users))
for i := 0; i < len(users); i++ {
userIDs[i] = users[i].Id
}
// If a requesting user is specified, but we don't find them above as an active member
// of the channel, just short-circuit and return an empty set. We don't return an error
// as this is a valid result for some callers.
if requestingUserID != "" && !slices.Contains(userIDs, requestingUserID) {
return nil, nil
}
commonTeamIDs, err := a.Srv().Store().Team().GetCommonTeamIDsForMultipleUsers(userIDs)
if err != nil {
return nil, model.NewAppError("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, gmConversionRequest.TeamID)
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
}
func (a *App) ChannelAccessControlled(c request.CTX, channelID string) (bool, *model.AppError) {
if l := a.License(); !model.MinimumEnterpriseAdvancedLicense(l) || !*a.Config().AccessControlSettings.EnableAttributeBasedAccessControl {
return false, nil
}
channel, err := a.Srv().Store().Channel().Get(channelID, true)
var nfErr *store.ErrNotFound
if err != nil && !errors.As(err, &nfErr) {
return false, model.NewAppError("ChannelIsAccessControlled", "app.channel.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
} else if errors.As(err, &nfErr) {
return false, nil
}
return channel.PolicyEnforced, nil
}
func (a *App) handleChannelCategoryName(channel *model.Channel) {
if *a.Config().ExperimentalSettings.ExperimentalChannelCategorySorting && strings.Contains(channel.DisplayName, "/") {
parts := strings.Split(channel.DisplayName, "/")
channel.DisplayName = strings.TrimSpace(strings.Join(parts[1:], "/"))
channel.DefaultCategoryName = strings.TrimSpace(parts[0])
}
}
func (a *App) addChannelToDefaultCategory(c request.CTX, userID string, channel *model.Channel) {
// Add channel to default category if specified
if channel.DefaultCategoryName != "" && *a.Config().ExperimentalSettings.ExperimentalChannelCategorySorting {
// Get user's categories for this team
categories, err := a.GetSidebarCategoriesForTeamForUser(c, userID, channel.TeamId)
if err != nil {
mlog.Error("Failed to get sidebar categories", mlog.String("user_id", userID), mlog.String("team_id", channel.TeamId), mlog.Err(err))
return
}
// Find or create the category
var targetCategory *model.SidebarCategoryWithChannels
for _, category := range categories.Categories {
if category.Type == model.SidebarCategoryCustom && strings.EqualFold(category.DisplayName, channel.DefaultCategoryName) {
targetCategory = category
break
}
}
if targetCategory == nil {
// Create new category if it doesn't exist
targetCategory = &model.SidebarCategoryWithChannels{
SidebarCategory: model.SidebarCategory{
UserId: userID,
TeamId: channel.TeamId,
Type: model.SidebarCategoryCustom,
DisplayName: channel.DefaultCategoryName,
Sorting: model.SidebarCategorySortDefault,
},
Channels: []string{channel.Id},
}
_, err = a.CreateSidebarCategory(c, userID, channel.TeamId, targetCategory)
if err != nil {
mlog.Error("Failed to create default category", mlog.String("user_id", userID), mlog.String("team_id", channel.TeamId), mlog.String("category_name", channel.DefaultCategoryName), mlog.Err(err))
}
} else {
// Add channel to existing category
targetCategory.Channels = append([]string{channel.Id}, targetCategory.Channels...)
_, err = a.UpdateSidebarCategories(c, userID, channel.TeamId, []*model.SidebarCategoryWithChannels{targetCategory})
if err != nil {
mlog.Error("Failed to update default category", mlog.String("user_id", userID), mlog.String("team_id", channel.TeamId), mlog.String("category_name", channel.DefaultCategoryName), mlog.Err(err))
}
}
}
}