* 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>
4084 строки
155 KiB
Go
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, <Err):
|
|
return nil, model.NewAppError("CreateChannel", "store.sql_channel.save_channel.limit.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
|
|
case errors.As(nErr, &appErr): // in case we haven't converted to plain error.
|
|
return nil, appErr
|
|
default: // last fallback in case it doesn't map to an existing app error.
|
|
return nil, model.NewAppError("CreateChannel", "app.channel.create_channel.internal_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
|
|
}
|
|
}
|
|
|
|
if addMember {
|
|
user, nErr := a.Srv().Store().User().Get(context.Background(), channel.CreatorId)
|
|
if nErr != nil {
|
|
var nfErr *store.ErrNotFound
|
|
switch {
|
|
case errors.As(nErr, &nfErr):
|
|
return nil, model.NewAppError("CreateChannel", MissingAccountError, nil, "", http.StatusNotFound).Wrap(nErr)
|
|
default:
|
|
return nil, model.NewAppError("CreateChannel", "app.user.get.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
|
|
}
|
|
}
|
|
|
|
cm := &model.ChannelMember{
|
|
ChannelId: sc.Id,
|
|
UserId: user.Id,
|
|
SchemeGuest: user.IsGuest(),
|
|
SchemeUser: !user.IsGuest(),
|
|
SchemeAdmin: true,
|
|
NotifyProps: model.GetDefaultChannelNotifyProps(),
|
|
}
|
|
|
|
if _, nErr := a.Srv().Store().Channel().SaveMember(c, cm); nErr != nil {
|
|
var appErr *model.AppError
|
|
var cErr *store.ErrConflict
|
|
switch {
|
|
case errors.As(nErr, &cErr):
|
|
switch cErr.Resource {
|
|
case "ChannelMembers":
|
|
return nil, model.NewAppError("CreateChannel", "app.channel.save_member.exists.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
|
|
}
|
|
case errors.As(nErr, &appErr):
|
|
return nil, appErr
|
|
default:
|
|
return nil, model.NewAppError("CreateChannel", "app.channel.create_direct_channel.internal_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
|
|
}
|
|
}
|
|
|
|
if err := a.Srv().Store().ChannelMemberHistory().LogJoinEvent(channel.CreatorId, sc.Id, model.GetMillis()); err != nil {
|
|
return nil, model.NewAppError("CreateChannel", "app.channel_member_history.log_join_event.internal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
a.Srv().Platform().InvalidateChannelCacheForUser(channel.CreatorId)
|
|
}
|
|
|
|
a.Srv().Go(func() {
|
|
pluginContext := pluginContext(c)
|
|
a.ch.RunMultiHook(func(hooks plugin.Hooks, _ *model.Manifest) bool {
|
|
hooks.ChannelHasBeenCreated(pluginContext, sc)
|
|
return true
|
|
}, plugin.ChannelHasBeenCreatedID)
|
|
})
|
|
|
|
return sc, nil
|
|
}
|
|
|
|
func (a *App) GetOrCreateDirectChannel(c request.CTX, userID, otherUserID string, channelOptions ...model.ChannelOption) (*model.Channel, *model.AppError) {
|
|
channel, nErr := a.getDirectChannel(c, userID, otherUserID)
|
|
if nErr != nil {
|
|
return nil, nErr
|
|
}
|
|
|
|
if channel != nil {
|
|
return channel, nil
|
|
}
|
|
|
|
if *a.Config().TeamSettings.RestrictDirectMessage == model.DirectMessageTeam &&
|
|
!a.SessionHasPermissionTo(*c.Session(), model.PermissionManageSystem) {
|
|
users, err := a.GetUsersByIds([]string{userID, otherUserID}, &store.UserGetByIdsOpts{})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var isBot bool
|
|
for _, user := range users {
|
|
if user.IsBot {
|
|
isBot = true
|
|
break
|
|
}
|
|
}
|
|
// if one of the users is a bot, don't restrict to team members
|
|
if !isBot {
|
|
commonTeamIDs, err := a.GetCommonTeamIDsForTwoUsers(userID, otherUserID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(commonTeamIDs) == 0 {
|
|
return nil, model.NewAppError("createDirectChannel", "api.channel.create_channel.direct_channel.team_restricted_error", nil, "", http.StatusForbidden)
|
|
}
|
|
}
|
|
}
|
|
|
|
channel, err := a.createDirectChannel(c, userID, otherUserID, channelOptions...)
|
|
if err != nil {
|
|
if err.Id == store.ChannelExistsError {
|
|
return channel, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
a.handleCreationEvent(c, userID, otherUserID, channel)
|
|
return channel, nil
|
|
}
|
|
|
|
func (a *App) getOrCreateDirectChannelWithUser(c request.CTX, user, otherUser *model.User) (*model.Channel, *model.AppError) {
|
|
channel, nErr := a.getDirectChannel(c, user.Id, otherUser.Id)
|
|
if nErr != nil {
|
|
return nil, nErr
|
|
}
|
|
|
|
if channel != nil {
|
|
return channel, nil
|
|
}
|
|
|
|
channel, err := a.createDirectChannelWithUser(c, user, otherUser)
|
|
if err != nil {
|
|
if err.Id == store.ChannelExistsError {
|
|
return channel, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
a.handleCreationEvent(c, user.Id, otherUser.Id, channel)
|
|
return channel, nil
|
|
}
|
|
|
|
func (a *App) handleCreationEvent(c request.CTX, userID, otherUserID string, channel *model.Channel) {
|
|
a.Srv().Platform().InvalidateChannelCacheForUser(userID)
|
|
a.Srv().Platform().InvalidateChannelCacheForUser(otherUserID)
|
|
|
|
a.Srv().Go(func() {
|
|
pluginContext := pluginContext(c)
|
|
a.ch.RunMultiHook(func(hooks plugin.Hooks, _ *model.Manifest) bool {
|
|
hooks.ChannelHasBeenCreated(pluginContext, channel)
|
|
return true
|
|
}, plugin.ChannelHasBeenCreatedID)
|
|
})
|
|
|
|
message := model.NewWebSocketEvent(model.WebsocketEventDirectAdded, "", channel.Id, "", nil, "")
|
|
message.Add("creator_id", userID)
|
|
message.Add("teammate_id", otherUserID)
|
|
a.Publish(message)
|
|
}
|
|
|
|
func (a *App) createDirectChannel(c request.CTX, userID string, otherUserID string, channelOptions ...model.ChannelOption) (*model.Channel, *model.AppError) {
|
|
users, err := a.Srv().Store().User().GetMany(context.Background(), []string{userID, otherUserID})
|
|
if err != nil {
|
|
return nil, model.NewAppError("CreateDirectChannel", "api.channel.create_direct_channel.invalid_user.app_error", nil, "", http.StatusBadRequest).Wrap(err)
|
|
}
|
|
|
|
if len(users) == 0 {
|
|
return nil, model.NewAppError("CreateDirectChannel", "api.channel.create_direct_channel.invalid_user.app_error", nil, fmt.Sprintf("No users found for ids: %s. %s", userID, otherUserID), http.StatusBadRequest)
|
|
}
|
|
|
|
// We are doing this because we allow a user to create a direct channel with themselves
|
|
if userID == otherUserID {
|
|
users = append(users, users[0])
|
|
}
|
|
|
|
// After we counted for direct channels with the same user, if we do not have two users then we failed to find one
|
|
if len(users) != 2 {
|
|
return nil, model.NewAppError("CreateDirectChannel", "api.channel.create_direct_channel.invalid_user.app_error", nil, fmt.Sprintf("No users found for ids: %s. %s", userID, otherUserID), http.StatusBadRequest)
|
|
}
|
|
|
|
// The potential swap dance below is necessary in order to guarantee determinism when creating a direct channel.
|
|
// When we query the database for some given user ids, the database result is not deterministic, meaning we can get
|
|
// the same results but in different order. In order to conform the contract of Channel.CreateDirectChannel method
|
|
// below we need to identify which user is who.
|
|
user := users[0]
|
|
otherUser := users[1]
|
|
if user.Id != userID {
|
|
user = users[1]
|
|
otherUser = users[0]
|
|
}
|
|
return a.createDirectChannelWithUser(c, user, otherUser, channelOptions...)
|
|
}
|
|
|
|
func (a *App) createDirectChannelWithUser(c request.CTX, user, otherUser *model.User, channelOptions ...model.ChannelOption) (*model.Channel, *model.AppError) {
|
|
if !a.Config().FeatureFlags.EnableSharedChannelsDMs && (user.IsRemote() || otherUser.IsRemote()) {
|
|
return nil, model.NewAppError("createDirectChannelWithUser", "api.channel.create_channel.direct_channel.remote_restricted.app_error", nil, "", http.StatusForbidden)
|
|
}
|
|
|
|
channel, nErr := a.Srv().Store().Channel().CreateDirectChannel(c, user, otherUser, channelOptions...)
|
|
if nErr != nil {
|
|
var invErr *store.ErrInvalidInput
|
|
var cErr *store.ErrConflict
|
|
var ltErr *store.ErrLimitExceeded
|
|
var appErr *model.AppError
|
|
switch {
|
|
case errors.As(nErr, &invErr):
|
|
switch {
|
|
case invErr.Entity == "Channel" && invErr.Field == "DeleteAt":
|
|
return nil, model.NewAppError("createDirectChannelWithUser", "store.sql_channel.save.archived_channel.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
|
|
case invErr.Entity == "Channel" && invErr.Field == "Type":
|
|
return nil, model.NewAppError("createDirectChannelWithUser", "store.sql_channel.save_direct_channel.not_direct.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
|
|
case invErr.Entity == "Channel" && invErr.Field == "Id":
|
|
return nil, model.NewAppError("SqlChannelStore.Save", "store.sql_channel.save_channel.existing.app_error", nil, "id="+invErr.Value.(string), http.StatusBadRequest).Wrap(nErr)
|
|
}
|
|
case errors.As(nErr, &cErr):
|
|
switch cErr.Resource {
|
|
case "Channel":
|
|
return channel, model.NewAppError("createDirectChannelWithUser", store.ChannelExistsError, nil, "", http.StatusBadRequest).Wrap(nErr)
|
|
case "ChannelMembers":
|
|
return nil, model.NewAppError("createDirectChannelWithUser", "app.channel.save_member.exists.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
|
|
}
|
|
case errors.As(nErr, <Err):
|
|
return nil, model.NewAppError("createDirectChannelWithUser", "store.sql_channel.save_channel.limit.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
|
|
case errors.As(nErr, &appErr): // in case we haven't converted to plain error.
|
|
return nil, appErr
|
|
default: // last fallback in case it doesn't map to an existing app error.
|
|
return nil, model.NewAppError("createDirectChannelWithUser", "app.channel.create_direct_channel.internal_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
|
|
}
|
|
}
|
|
|
|
if err := a.Srv().Store().ChannelMemberHistory().LogJoinEvent(user.Id, channel.Id, model.GetMillis()); err != nil {
|
|
return nil, model.NewAppError("createDirectChannelWithUser", "app.channel_member_history.log_join_event.internal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
if user.Id != otherUser.Id {
|
|
if err := a.Srv().Store().ChannelMemberHistory().LogJoinEvent(otherUser.Id, channel.Id, model.GetMillis()); err != nil {
|
|
return nil, model.NewAppError("createDirectChannelWithUser", "app.channel_member_history.log_join_event.internal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
}
|
|
|
|
// When the newly created channel is shared and the creator is local
|
|
// create a local shared channel record
|
|
if channel.IsShared() && !user.IsRemote() {
|
|
sc := &model.SharedChannel{
|
|
ChannelId: channel.Id,
|
|
TeamId: channel.TeamId,
|
|
Home: true,
|
|
ReadOnly: false,
|
|
ShareName: channel.Name,
|
|
ShareDisplayName: channel.DisplayName,
|
|
SharePurpose: channel.Purpose,
|
|
ShareHeader: channel.Header,
|
|
CreatorId: user.Id,
|
|
Type: channel.Type,
|
|
}
|
|
|
|
if _, err := a.ShareChannel(c, sc); err != nil {
|
|
c.Logger().Error("Failed to share newly created direct channel", mlog.String("channel_id", channel.Id), mlog.Err(err))
|
|
}
|
|
}
|
|
|
|
return channel, nil
|
|
}
|
|
|
|
func (a *App) CreateGroupChannel(c request.CTX, userIDs []string, creatorId string, 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, <Err):
|
|
return nil, model.NewAppError("CreateChannel", "store.sql_channel.save_channel.limit.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
|
|
case errors.As(nErr, &appErr): // in case we haven't converted to plain error.
|
|
return nil, appErr
|
|
default: // last fallback in case it doesn't map to an existing app error.
|
|
return nil, model.NewAppError("CreateChannel", "app.channel.create_channel.internal_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
|
|
}
|
|
}
|
|
|
|
for _, user := range users {
|
|
cm := &model.ChannelMember{
|
|
UserId: user.Id,
|
|
ChannelId: channel.Id,
|
|
NotifyProps: model.GetDefaultChannelNotifyProps(),
|
|
SchemeGuest: user.IsGuest(),
|
|
SchemeUser: !user.IsGuest(),
|
|
}
|
|
|
|
if _, nErr = a.Srv().Store().Channel().SaveMember(c, cm); nErr != nil {
|
|
var appErr *model.AppError
|
|
var cErr *store.ErrConflict
|
|
switch {
|
|
case errors.As(nErr, &cErr):
|
|
switch cErr.Resource {
|
|
case "ChannelMembers":
|
|
return nil, model.NewAppError("createGroupChannel", "app.channel.save_member.exists.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
|
|
}
|
|
case errors.As(nErr, &appErr):
|
|
return nil, appErr
|
|
default:
|
|
return nil, model.NewAppError("createGroupChannel", "app.channel.create_direct_channel.internal_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
|
|
}
|
|
}
|
|
if err := a.Srv().Store().ChannelMemberHistory().LogJoinEvent(user.Id, channel.Id, model.GetMillis()); err != nil {
|
|
return nil, model.NewAppError("createGroupChannel", "app.channel_member_history.log_join_event.internal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
}
|
|
|
|
// 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))
|
|
}
|
|
}
|
|
}
|
|
}
|