Full Mattermost server source with integrated Community Enterprise features. Includes vendor directory for offline/air-gapped builds. Structure: - enterprise-impl/: Enterprise feature implementations - enterprise-community/: Init files that register implementations - enterprise/: Bridge imports (community_imports.go) - vendor/: All dependencies for offline builds Build (online): go build ./cmd/mattermost Build (offline/air-gapped): go build -mod=vendor ./cmd/mattermost 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
4026 lines
153 KiB
Go
4026 lines
153 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/public/model"
|
|
"github.com/mattermost/mattermost/server/public/plugin"
|
|
"github.com/mattermost/mattermost/server/public/shared/i18n"
|
|
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
|
"github.com/mattermost/mattermost/server/public/shared/request"
|
|
"github.com/mattermost/mattermost/server/v8/channels/store"
|
|
"github.com/mattermost/mattermost/server/v8/channels/store/sqlstore"
|
|
)
|
|
|
|
const (
|
|
UpdateMultipleMaximum = 200
|
|
)
|
|
|
|
// DefaultChannelNames returns the list of system-wide default channel names.
|
|
//
|
|
// By default the list will be (not necessarily in this order):
|
|
//
|
|
// ['town-square', 'off-topic']
|
|
//
|
|
// However, if TeamSettings.ExperimentalDefaultChannels contains a list of channels then that list will replace
|
|
// 'off-topic' and be included in the return results in addition to 'town-square'. For example:
|
|
//
|
|
// ['town-square', 'game-of-thrones', 'wow']
|
|
func (a *App) DefaultChannelNames(rctx request.CTX) []string {
|
|
names := []string{"town-square"}
|
|
|
|
if len(a.Config().TeamSettings.ExperimentalDefaultChannels) == 0 {
|
|
names = append(names, "off-topic")
|
|
} else {
|
|
seenChannels := map[string]bool{"town-square": true}
|
|
for _, channelName := range a.Config().TeamSettings.ExperimentalDefaultChannels {
|
|
if !seenChannels[channelName] {
|
|
names = append(names, channelName)
|
|
seenChannels[channelName] = true
|
|
}
|
|
}
|
|
}
|
|
|
|
return names
|
|
}
|
|
|
|
func (a *App) JoinDefaultChannels(rctx request.CTX, teamID string, user *model.User, shouldBeAdmin bool, userRequestorId string) *model.AppError {
|
|
var requestor *model.User
|
|
var nErr error
|
|
if userRequestorId != "" {
|
|
requestor, nErr = a.Srv().Store().User().Get(context.Background(), userRequestorId)
|
|
if nErr != nil {
|
|
var nfErr *store.ErrNotFound
|
|
switch {
|
|
case errors.As(nErr, &nfErr):
|
|
return model.NewAppError("JoinDefaultChannels", MissingAccountError, nil, "", http.StatusNotFound).Wrap(nErr)
|
|
default:
|
|
return model.NewAppError("JoinDefaultChannels", "app.user.get.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
|
|
}
|
|
}
|
|
}
|
|
|
|
for _, channelName := range a.DefaultChannelNames(rctx) {
|
|
channel, channelErr := a.Srv().Store().Channel().GetByName(teamID, channelName, true)
|
|
if channelErr != nil {
|
|
rctx.Logger().Warn("No default channel with this name", mlog.String("channelName", channelName), mlog.String("teamID", teamID), mlog.Err(channelErr))
|
|
continue
|
|
}
|
|
|
|
if channel.Type != model.ChannelTypeOpen {
|
|
continue
|
|
}
|
|
|
|
cm := &model.ChannelMember{
|
|
ChannelId: channel.Id,
|
|
UserId: user.Id,
|
|
SchemeGuest: user.IsGuest(),
|
|
SchemeUser: !user.IsGuest(),
|
|
SchemeAdmin: shouldBeAdmin,
|
|
NotifyProps: model.GetDefaultChannelNotifyProps(),
|
|
}
|
|
|
|
_, nErr = a.Srv().Store().Channel().SaveMember(rctx, cm)
|
|
if histErr := a.Srv().Store().ChannelMemberHistory().LogJoinEvent(user.Id, channel.Id, model.GetMillis()); histErr != nil {
|
|
return model.NewAppError("JoinDefaultChannels", "app.channel_member_history.log_join_event.internal_error", nil, "", http.StatusInternalServerError).Wrap(histErr)
|
|
}
|
|
|
|
if *a.Config().ServiceSettings.ExperimentalEnableDefaultChannelLeaveJoinMessages {
|
|
if aErr := a.postJoinMessageForDefaultChannel(rctx, user, requestor, channel); aErr != nil {
|
|
rctx.Logger().Warn("Failed to post join/leave message", mlog.Err(aErr))
|
|
}
|
|
}
|
|
|
|
a.invalidateCacheForChannelMembers(channel.Id)
|
|
|
|
message := model.NewWebSocketEvent(model.WebsocketEventUserAdded, "", channel.Id, "", nil, "")
|
|
message.Add("user_id", user.Id)
|
|
message.Add("team_id", channel.TeamId)
|
|
a.Publish(message)
|
|
}
|
|
|
|
if nErr != nil {
|
|
var appErr *model.AppError
|
|
var cErr *store.ErrConflict
|
|
switch {
|
|
case errors.As(nErr, &cErr):
|
|
if cErr.Resource == "ChannelMembers" {
|
|
return model.NewAppError("JoinDefaultChannels", "app.channel.save_member.exists.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
|
|
}
|
|
case errors.As(nErr, &appErr):
|
|
return appErr
|
|
default:
|
|
return model.NewAppError("JoinDefaultChannels", "app.channel.create_direct_channel.internal_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) postJoinMessageForDefaultChannel(rctx request.CTX, user *model.User, requestor *model.User, channel *model.Channel) *model.AppError {
|
|
if channel.Name == model.DefaultChannelName {
|
|
if requestor == nil {
|
|
if err := a.postJoinTeamMessage(rctx, user, channel); err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
if err := a.postAddToTeamMessage(rctx, requestor, user, channel, ""); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
} else {
|
|
if requestor == nil {
|
|
if err := a.postJoinChannelMessage(rctx, user, channel); err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
if err := a.PostAddToChannelMessage(rctx, requestor, user, channel, ""); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) CreateChannelWithUser(rctx request.CTX, channel *model.Channel, userID string) (*model.Channel, *model.AppError) {
|
|
if channel.IsGroupOrDirect() {
|
|
return nil, model.NewAppError("CreateChannelWithUser", "api.channel.create_channel.direct_channel.app_error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
if channel.TeamId == "" {
|
|
return nil, model.NewAppError("CreateChannelWithUser", "app.channel.create_channel.no_team_id.app_error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
// Get total number of channels on current team
|
|
count, err := a.GetNumberOfChannelsOnTeam(rctx, channel.TeamId)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if int64(count+1) > *a.Config().TeamSettings.MaxChannelsPerTeam {
|
|
return nil, model.NewAppError("CreateChannelWithUser", "api.channel.create_channel.max_channel_limit.app_error", map[string]any{"MaxChannelsPerTeam": *a.Config().TeamSettings.MaxChannelsPerTeam}, "", http.StatusBadRequest)
|
|
}
|
|
|
|
channel.CreatorId = userID
|
|
|
|
rchannel, err := a.CreateChannel(rctx, channel, true)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
a.addChannelToDefaultCategory(rctx, userID, channel)
|
|
|
|
var user *model.User
|
|
if user, err = a.GetUser(userID); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err = a.postJoinChannelMessage(rctx, user, channel); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
message := model.NewWebSocketEvent(model.WebsocketEventChannelCreated, "", "", userID, nil, "")
|
|
message.Add("channel_id", channel.Id)
|
|
message.Add("team_id", channel.TeamId)
|
|
a.Publish(message)
|
|
|
|
return rchannel, nil
|
|
}
|
|
|
|
// RenameChannel is used to rename the channel Name and the DisplayName fields
|
|
func (a *App) RenameChannel(rctx request.CTX, channel *model.Channel, newChannelName string, newDisplayName string) (*model.Channel, *model.AppError) {
|
|
if channel.Type == model.ChannelTypeDirect {
|
|
return nil, model.NewAppError("RenameChannel", "api.channel.rename_channel.cant_rename_direct_messages.app_error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
if channel.Type == model.ChannelTypeGroup {
|
|
return nil, model.NewAppError("RenameChannel", "api.channel.rename_channel.cant_rename_group_messages.app_error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
// Clean up the channel name and display name
|
|
newChannelName = strings.TrimSpace(newChannelName)
|
|
newDisplayName = strings.TrimSpace(newDisplayName)
|
|
|
|
channel.Name = newChannelName
|
|
if newDisplayName != "" {
|
|
channel.DisplayName = newDisplayName
|
|
}
|
|
|
|
newChannel, err := a.UpdateChannel(rctx, channel)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return newChannel, nil
|
|
}
|
|
|
|
func (a *App) CreateChannel(rctx request.CTX, channel *model.Channel, addMember bool) (*model.Channel, *model.AppError) {
|
|
a.handleChannelCategoryName(channel)
|
|
|
|
channel.DisplayName = strings.TrimSpace(channel.DisplayName)
|
|
sc, nErr := a.Srv().Store().Channel().Save(rctx, channel, *a.Config().TeamSettings.MaxChannelsPerTeam)
|
|
if nErr != nil {
|
|
var invErr *store.ErrInvalidInput
|
|
var cErr *store.ErrConflict
|
|
var ltErr *store.ErrLimitExceeded
|
|
var appErr *model.AppError
|
|
switch {
|
|
case errors.As(nErr, &invErr):
|
|
switch {
|
|
case invErr.Entity == "Channel" && invErr.Field == "DeleteAt":
|
|
return nil, model.NewAppError("CreateChannel", "store.sql_channel.save.archived_channel.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
|
|
case invErr.Entity == "Channel" && invErr.Field == "Type":
|
|
return nil, model.NewAppError("CreateChannel", "store.sql_channel.save.direct_channel.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
|
|
case invErr.Entity == "Channel" && invErr.Field == "Id":
|
|
return nil, model.NewAppError("CreateChannel", "store.sql_channel.save_channel.existing.app_error", nil, "id="+invErr.Value.(string), http.StatusBadRequest).Wrap(nErr)
|
|
}
|
|
case errors.As(nErr, &cErr):
|
|
return sc, model.NewAppError("CreateChannel", store.ChannelExistsError, nil, "", http.StatusBadRequest).Wrap(nErr)
|
|
case errors.As(nErr, <Err):
|
|
return nil, model.NewAppError("CreateChannel", "store.sql_channel.save_channel.limit.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
|
|
case errors.As(nErr, &appErr): // in case we haven't converted to plain error.
|
|
return nil, appErr
|
|
default: // last fallback in case it doesn't map to an existing app error.
|
|
return nil, model.NewAppError("CreateChannel", "app.channel.create_channel.internal_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
|
|
}
|
|
}
|
|
|
|
if addMember {
|
|
user, nErr := a.Srv().Store().User().Get(context.Background(), channel.CreatorId)
|
|
if nErr != nil {
|
|
var nfErr *store.ErrNotFound
|
|
switch {
|
|
case errors.As(nErr, &nfErr):
|
|
return nil, model.NewAppError("CreateChannel", MissingAccountError, nil, "", http.StatusNotFound).Wrap(nErr)
|
|
default:
|
|
return nil, model.NewAppError("CreateChannel", "app.user.get.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
|
|
}
|
|
}
|
|
|
|
cm := &model.ChannelMember{
|
|
ChannelId: sc.Id,
|
|
UserId: user.Id,
|
|
SchemeGuest: user.IsGuest(),
|
|
SchemeUser: !user.IsGuest(),
|
|
SchemeAdmin: true,
|
|
NotifyProps: model.GetDefaultChannelNotifyProps(),
|
|
}
|
|
|
|
if _, nErr := a.Srv().Store().Channel().SaveMember(rctx, cm); nErr != nil {
|
|
var appErr *model.AppError
|
|
var cErr *store.ErrConflict
|
|
switch {
|
|
case errors.As(nErr, &cErr):
|
|
switch cErr.Resource {
|
|
case "ChannelMembers":
|
|
return nil, model.NewAppError("CreateChannel", "app.channel.save_member.exists.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
|
|
}
|
|
case errors.As(nErr, &appErr):
|
|
return nil, appErr
|
|
default:
|
|
return nil, model.NewAppError("CreateChannel", "app.channel.create_direct_channel.internal_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
|
|
}
|
|
}
|
|
|
|
if err := a.Srv().Store().ChannelMemberHistory().LogJoinEvent(channel.CreatorId, sc.Id, model.GetMillis()); err != nil {
|
|
return nil, model.NewAppError("CreateChannel", "app.channel_member_history.log_join_event.internal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
a.Srv().Platform().InvalidateChannelCacheForUser(channel.CreatorId)
|
|
}
|
|
|
|
a.Srv().Go(func() {
|
|
pluginContext := pluginContext(rctx)
|
|
a.ch.RunMultiHook(func(hooks plugin.Hooks, _ *model.Manifest) bool {
|
|
hooks.ChannelHasBeenCreated(pluginContext, sc)
|
|
return true
|
|
}, plugin.ChannelHasBeenCreatedID)
|
|
})
|
|
|
|
return sc, nil
|
|
}
|
|
|
|
func (a *App) GetOrCreateDirectChannel(rctx request.CTX, userID, otherUserID string, channelOptions ...model.ChannelOption) (*model.Channel, *model.AppError) {
|
|
channel, nErr := a.getDirectChannel(rctx, userID, otherUserID)
|
|
if nErr != nil {
|
|
return nil, nErr
|
|
}
|
|
|
|
if channel != nil {
|
|
return channel, nil
|
|
}
|
|
|
|
if *a.Config().TeamSettings.RestrictDirectMessage == model.DirectMessageTeam &&
|
|
!a.SessionHasPermissionTo(*rctx.Session(), model.PermissionManageSystem) {
|
|
users, err := a.GetUsersByIds(rctx, []string{userID, otherUserID}, &store.UserGetByIdsOpts{})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var isPluginOwnedBot bool
|
|
for _, user := range users {
|
|
if user.IsBot {
|
|
isOwnedByCurrentUserOrPlugin, err := a.IsBotOwnedByCurrentUserOrPlugin(rctx, user.Id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if isOwnedByCurrentUserOrPlugin {
|
|
isPluginOwnedBot = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
// if one of the users is a bot, don't restrict to team members
|
|
if !isPluginOwnedBot {
|
|
commonTeamIDs, err := a.GetCommonTeamIDsForTwoUsers(userID, otherUserID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(commonTeamIDs) == 0 {
|
|
return nil, model.NewAppError("createDirectChannel", "api.channel.create_channel.direct_channel.team_restricted_error", nil, "", http.StatusForbidden)
|
|
}
|
|
}
|
|
}
|
|
|
|
channel, err := a.createDirectChannel(rctx, userID, otherUserID, channelOptions...)
|
|
if err != nil {
|
|
if err.Id == store.ChannelExistsError {
|
|
return channel, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
a.handleCreationEvent(rctx, userID, otherUserID, channel)
|
|
return channel, nil
|
|
}
|
|
|
|
func (a *App) getOrCreateDirectChannelWithUser(rctx request.CTX, user, otherUser *model.User) (*model.Channel, *model.AppError) {
|
|
channel, nErr := a.getDirectChannel(rctx, user.Id, otherUser.Id)
|
|
if nErr != nil {
|
|
return nil, nErr
|
|
}
|
|
|
|
if channel != nil {
|
|
return channel, nil
|
|
}
|
|
|
|
channel, err := a.createDirectChannelWithUser(rctx, user, otherUser)
|
|
if err != nil {
|
|
if err.Id == store.ChannelExistsError {
|
|
return channel, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
a.handleCreationEvent(rctx, user.Id, otherUser.Id, channel)
|
|
return channel, nil
|
|
}
|
|
|
|
func (a *App) handleCreationEvent(rctx request.CTX, userID, otherUserID string, channel *model.Channel) {
|
|
a.Srv().Platform().InvalidateChannelCacheForUser(userID)
|
|
a.Srv().Platform().InvalidateChannelCacheForUser(otherUserID)
|
|
|
|
a.Srv().Go(func() {
|
|
pluginContext := pluginContext(rctx)
|
|
a.ch.RunMultiHook(func(hooks plugin.Hooks, _ *model.Manifest) bool {
|
|
hooks.ChannelHasBeenCreated(pluginContext, channel)
|
|
return true
|
|
}, plugin.ChannelHasBeenCreatedID)
|
|
})
|
|
|
|
message := model.NewWebSocketEvent(model.WebsocketEventDirectAdded, "", channel.Id, "", nil, "")
|
|
message.Add("creator_id", userID)
|
|
message.Add("teammate_id", otherUserID)
|
|
a.Publish(message)
|
|
}
|
|
|
|
func (a *App) createDirectChannel(rctx request.CTX, userID string, otherUserID string, channelOptions ...model.ChannelOption) (*model.Channel, *model.AppError) {
|
|
users, err := a.Srv().Store().User().GetMany(rctx, []string{userID, otherUserID})
|
|
if err != nil {
|
|
return nil, model.NewAppError("CreateDirectChannel", "api.channel.create_direct_channel.invalid_user.app_error", nil, "", http.StatusBadRequest).Wrap(err)
|
|
}
|
|
|
|
if len(users) == 0 {
|
|
return nil, model.NewAppError("CreateDirectChannel", "api.channel.create_direct_channel.invalid_user.app_error", nil, fmt.Sprintf("No users found for ids: %s. %s", userID, otherUserID), http.StatusBadRequest)
|
|
}
|
|
|
|
// We are doing this because we allow a user to create a direct channel with themselves
|
|
if userID == otherUserID {
|
|
users = append(users, users[0])
|
|
}
|
|
|
|
// After we counted for direct channels with the same user, if we do not have two users then we failed to find one
|
|
if len(users) != 2 {
|
|
return nil, model.NewAppError("CreateDirectChannel", "api.channel.create_direct_channel.invalid_user.app_error", nil, fmt.Sprintf("No users found for ids: %s. %s", userID, otherUserID), http.StatusBadRequest)
|
|
}
|
|
|
|
// The potential swap dance below is necessary in order to guarantee determinism when creating a direct channel.
|
|
// When we query the database for some given user ids, the database result is not deterministic, meaning we can get
|
|
// the same results but in different order. In order to conform the contract of Channel.CreateDirectChannel method
|
|
// below we need to identify which user is who.
|
|
user := users[0]
|
|
otherUser := users[1]
|
|
if user.Id != userID {
|
|
user = users[1]
|
|
otherUser = users[0]
|
|
}
|
|
return a.createDirectChannelWithUser(rctx, user, otherUser, channelOptions...)
|
|
}
|
|
|
|
func (a *App) createDirectChannelWithUser(rctx request.CTX, user, otherUser *model.User, channelOptions ...model.ChannelOption) (*model.Channel, *model.AppError) {
|
|
if !a.Config().FeatureFlags.EnableSharedChannelsDMs && (user.IsRemote() || otherUser.IsRemote()) {
|
|
return nil, model.NewAppError("createDirectChannelWithUser", "api.channel.create_channel.direct_channel.remote_restricted.app_error", nil, "", http.StatusForbidden)
|
|
}
|
|
|
|
channel, nErr := a.Srv().Store().Channel().CreateDirectChannel(rctx, user, otherUser, channelOptions...)
|
|
if nErr != nil {
|
|
var invErr *store.ErrInvalidInput
|
|
var cErr *store.ErrConflict
|
|
var ltErr *store.ErrLimitExceeded
|
|
var appErr *model.AppError
|
|
switch {
|
|
case errors.As(nErr, &invErr):
|
|
switch {
|
|
case invErr.Entity == "Channel" && invErr.Field == "DeleteAt":
|
|
return nil, model.NewAppError("createDirectChannelWithUser", "store.sql_channel.save.archived_channel.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
|
|
case invErr.Entity == "Channel" && invErr.Field == "Type":
|
|
return nil, model.NewAppError("createDirectChannelWithUser", "store.sql_channel.save_direct_channel.not_direct.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
|
|
case invErr.Entity == "Channel" && invErr.Field == "Id":
|
|
return nil, model.NewAppError("SqlChannelStore.Save", "store.sql_channel.save_channel.existing.app_error", nil, "id="+invErr.Value.(string), http.StatusBadRequest).Wrap(nErr)
|
|
}
|
|
case errors.As(nErr, &cErr):
|
|
switch cErr.Resource {
|
|
case "Channel":
|
|
return channel, model.NewAppError("createDirectChannelWithUser", store.ChannelExistsError, nil, "", http.StatusBadRequest).Wrap(nErr)
|
|
case "ChannelMembers":
|
|
return nil, model.NewAppError("createDirectChannelWithUser", "app.channel.save_member.exists.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
|
|
}
|
|
case errors.As(nErr, <Err):
|
|
return nil, model.NewAppError("createDirectChannelWithUser", "store.sql_channel.save_channel.limit.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
|
|
case errors.As(nErr, &appErr): // in case we haven't converted to plain error.
|
|
return nil, appErr
|
|
default: // last fallback in case it doesn't map to an existing app error.
|
|
return nil, model.NewAppError("createDirectChannelWithUser", "app.channel.create_direct_channel.internal_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
|
|
}
|
|
}
|
|
|
|
if err := a.Srv().Store().ChannelMemberHistory().LogJoinEvent(user.Id, channel.Id, model.GetMillis()); err != nil {
|
|
return nil, model.NewAppError("createDirectChannelWithUser", "app.channel_member_history.log_join_event.internal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
if user.Id != otherUser.Id {
|
|
if err := a.Srv().Store().ChannelMemberHistory().LogJoinEvent(otherUser.Id, channel.Id, model.GetMillis()); err != nil {
|
|
return nil, model.NewAppError("createDirectChannelWithUser", "app.channel_member_history.log_join_event.internal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
}
|
|
|
|
// When the newly created channel is shared and the creator is local
|
|
// create a local shared channel record
|
|
if channel.IsShared() && !user.IsRemote() {
|
|
sc := &model.SharedChannel{
|
|
ChannelId: channel.Id,
|
|
TeamId: channel.TeamId,
|
|
Home: true,
|
|
ReadOnly: false,
|
|
ShareName: channel.Name,
|
|
ShareDisplayName: channel.DisplayName,
|
|
SharePurpose: channel.Purpose,
|
|
ShareHeader: channel.Header,
|
|
CreatorId: user.Id,
|
|
Type: channel.Type,
|
|
}
|
|
|
|
if _, err := a.ShareChannel(rctx, sc); err != nil {
|
|
rctx.Logger().Error("Failed to share newly created direct channel", mlog.String("channel_id", channel.Id), mlog.Err(err))
|
|
}
|
|
}
|
|
|
|
return channel, nil
|
|
}
|
|
|
|
func (a *App) CreateGroupChannel(rctx request.CTX, userIDs []string, creatorId string, channelOptions ...model.ChannelOption) (*model.Channel, *model.AppError) {
|
|
channel, err := a.createGroupChannel(rctx, userIDs, creatorId, channelOptions...)
|
|
if err != nil {
|
|
if err.Id == store.ChannelExistsError {
|
|
return channel, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
jsonIDs := model.ArrayToJSON(userIDs)
|
|
for _, userID := range userIDs {
|
|
a.Srv().Platform().InvalidateChannelCacheForUser(userID)
|
|
|
|
message := model.NewWebSocketEvent(model.WebsocketEventGroupAdded, "", channel.Id, userID, nil, "")
|
|
message.Add("teammate_ids", jsonIDs)
|
|
a.Publish(message)
|
|
}
|
|
|
|
return channel, nil
|
|
}
|
|
|
|
// creatorId is used to determine if the group channel should have a
|
|
// shared channel record attached. It can be empty if the caller
|
|
// doesn't know who the creator is (e.g. the import process) and the
|
|
// resulting group channel will not be shared
|
|
func (a *App) createGroupChannel(rctx request.CTX, userIDs []string, creatorID string, channelOptions ...model.ChannelOption) (*model.Channel, *model.AppError) {
|
|
if len(userIDs) > model.ChannelGroupMaxUsers || len(userIDs) < model.ChannelGroupMinUsers {
|
|
return nil, model.NewAppError("CreateGroupChannel", "api.channel.create_group.bad_size.app_error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
// we skip cache and use master when fetching profiles to avoid
|
|
// issues in shared channels and HA, when users are created from a
|
|
// shared channels GM invite right before creating the GM
|
|
users, err := a.Srv().Store().User().GetProfileByIds(sqlstore.RequestContextWithMaster(rctx), userIDs, nil, false)
|
|
if err != nil {
|
|
return nil, model.NewAppError("createGroupChannel", "app.user.get_profiles.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
if len(users) != len(userIDs) {
|
|
return nil, model.NewAppError("CreateGroupChannel", "api.channel.create_group.bad_user.app_error", nil, "user_ids="+model.ArrayToJSON(userIDs), http.StatusBadRequest)
|
|
}
|
|
|
|
// extracts the creator and the remotes involved in the GM to
|
|
// decide how to handle the shared part of the creation
|
|
var creator *model.User
|
|
remoteIDs := map[string]bool{}
|
|
for _, user := range users {
|
|
if user.Id == creatorID {
|
|
creator = user
|
|
}
|
|
if user.IsRemote() {
|
|
remoteIDs[*user.RemoteId] = true
|
|
}
|
|
}
|
|
channelIsShared := len(remoteIDs) > 0
|
|
|
|
if channelIsShared && !a.Config().FeatureFlags.EnableSharedChannelsDMs {
|
|
for _, user := range users {
|
|
if user.IsRemote() {
|
|
return nil, model.NewAppError("createGroupChannel", "api.channel.create_group.remote_restricted.app_error", nil, "", http.StatusForbidden)
|
|
}
|
|
}
|
|
}
|
|
|
|
group := &model.Channel{
|
|
Name: model.GetGroupNameFromUserIds(userIDs),
|
|
DisplayName: model.GetGroupDisplayNameFromUsers(users, true),
|
|
Type: model.ChannelTypeGroup,
|
|
Shared: model.NewPointer(channelIsShared),
|
|
}
|
|
|
|
channel, nErr := a.Srv().Store().Channel().Save(rctx, group, *a.Config().TeamSettings.MaxChannelsPerTeam, channelOptions...)
|
|
if nErr != nil {
|
|
var invErr *store.ErrInvalidInput
|
|
var cErr *store.ErrConflict
|
|
var ltErr *store.ErrLimitExceeded
|
|
var appErr *model.AppError
|
|
switch {
|
|
case errors.As(nErr, &invErr):
|
|
switch {
|
|
case invErr.Entity == "Channel" && invErr.Field == "DeleteAt":
|
|
return nil, model.NewAppError("CreateChannel", "store.sql_channel.save.archived_channel.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
|
|
case invErr.Entity == "Channel" && invErr.Field == "Type":
|
|
return nil, model.NewAppError("CreateChannel", "store.sql_channel.save.direct_channel.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
|
|
case invErr.Entity == "Channel" && invErr.Field == "Id":
|
|
return nil, model.NewAppError("CreateChannel", "store.sql_channel.save_channel.existing.app_error", nil, "id="+invErr.Value.(string), http.StatusBadRequest).Wrap(nErr)
|
|
}
|
|
case errors.As(nErr, &cErr):
|
|
return channel, model.NewAppError("CreateChannel", store.ChannelExistsError, nil, "", http.StatusBadRequest).Wrap(nErr)
|
|
case errors.As(nErr, <Err):
|
|
return nil, model.NewAppError("CreateChannel", "store.sql_channel.save_channel.limit.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
|
|
case errors.As(nErr, &appErr): // in case we haven't converted to plain error.
|
|
return nil, appErr
|
|
default: // last fallback in case it doesn't map to an existing app error.
|
|
return nil, model.NewAppError("CreateChannel", "app.channel.create_channel.internal_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
|
|
}
|
|
}
|
|
|
|
for _, user := range users {
|
|
cm := &model.ChannelMember{
|
|
UserId: user.Id,
|
|
ChannelId: channel.Id,
|
|
NotifyProps: model.GetDefaultChannelNotifyProps(),
|
|
SchemeGuest: user.IsGuest(),
|
|
SchemeUser: !user.IsGuest(),
|
|
}
|
|
|
|
if _, nErr = a.Srv().Store().Channel().SaveMember(rctx, cm); nErr != nil {
|
|
var appErr *model.AppError
|
|
var cErr *store.ErrConflict
|
|
switch {
|
|
case errors.As(nErr, &cErr):
|
|
switch cErr.Resource {
|
|
case "ChannelMembers":
|
|
return nil, model.NewAppError("createGroupChannel", "app.channel.save_member.exists.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
|
|
}
|
|
case errors.As(nErr, &appErr):
|
|
return nil, appErr
|
|
default:
|
|
return nil, model.NewAppError("createGroupChannel", "app.channel.create_direct_channel.internal_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
|
|
}
|
|
}
|
|
if err := a.Srv().Store().ChannelMemberHistory().LogJoinEvent(user.Id, channel.Id, model.GetMillis()); err != nil {
|
|
return nil, model.NewAppError("createGroupChannel", "app.channel_member_history.log_join_event.internal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
}
|
|
|
|
// When the newly created channel is shared, the creator is local
|
|
// and one of the participants is remote create a local shared
|
|
// channel record
|
|
if channel.IsShared() && creator != nil && !creator.IsRemote() {
|
|
sc := &model.SharedChannel{
|
|
ChannelId: channel.Id,
|
|
TeamId: channel.TeamId,
|
|
Home: true,
|
|
ReadOnly: false,
|
|
ShareName: channel.Name,
|
|
ShareDisplayName: channel.DisplayName,
|
|
SharePurpose: channel.Purpose,
|
|
ShareHeader: channel.Header,
|
|
CreatorId: creatorID,
|
|
Type: channel.Type,
|
|
}
|
|
|
|
if _, err := a.ShareChannel(rctx, sc); err != nil {
|
|
rctx.Logger().Error("Failed to share newly created group channel", mlog.String("channel_id", channel.Id), mlog.Err(err))
|
|
} else {
|
|
// if we could successfully share the channel, we invite
|
|
// the remotes involved to it
|
|
if sc, _ := a.getSharedChannelsService(false); sc != nil {
|
|
for remoteID := range remoteIDs {
|
|
rc, err := a.Srv().Store().RemoteCluster().Get(remoteID, false)
|
|
if err != nil {
|
|
rctx.Logger().Error("Failed to send invite to group message channel, can't retrieve remote cluster", mlog.String("channel_id", channel.Id), mlog.String("remote_id", remoteID), mlog.Err(err))
|
|
continue
|
|
}
|
|
|
|
opts := []sharedchannel.InviteOption{sharedchannel.WithCreator(creatorID)}
|
|
for _, user := range users {
|
|
opts = append(opts, sharedchannel.WithDirectParticipant(user, remoteID))
|
|
}
|
|
if err := sc.SendChannelInvite(channel, creatorID, rc, opts...); err != nil {
|
|
rctx.Logger().Error("Failed to send invite to group message channel, error sending the invite", mlog.String("channel_id", channel.Id), mlog.String("remote_id", remoteID), mlog.Err(err))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
a.Srv().Go(func() {
|
|
pluginContext := pluginContext(rctx)
|
|
a.ch.RunMultiHook(func(hooks plugin.Hooks, _ *model.Manifest) bool {
|
|
hooks.ChannelHasBeenCreated(pluginContext, channel)
|
|
return true
|
|
}, plugin.ChannelHasBeenCreatedID)
|
|
})
|
|
|
|
return channel, nil
|
|
}
|
|
|
|
func (a *App) GetGroupChannel(rctx request.CTX, userIDs []string) (*model.Channel, *model.AppError) {
|
|
if len(userIDs) > model.ChannelGroupMaxUsers || len(userIDs) < model.ChannelGroupMinUsers {
|
|
return nil, model.NewAppError("GetGroupChannel", "api.channel.create_group.bad_size.app_error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
users, err := a.Srv().Store().User().GetProfileByIds(rctx, userIDs, nil, true)
|
|
if err != nil {
|
|
return nil, model.NewAppError("GetGroupChannel", "app.user.get_profiles.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
if len(users) != len(userIDs) {
|
|
return nil, model.NewAppError("GetGroupChannel", "api.channel.create_group.bad_user.app_error", nil, "user_ids="+model.ArrayToJSON(userIDs), http.StatusBadRequest)
|
|
}
|
|
|
|
channel, appErr := a.GetChannelByName(rctx, model.GetGroupNameFromUserIds(userIDs), "", true)
|
|
if appErr != nil {
|
|
return nil, appErr
|
|
}
|
|
|
|
return channel, nil
|
|
}
|
|
|
|
// UpdateChannel updates a given channel by its Id. It also publishes the CHANNEL_UPDATED event.
|
|
func (a *App) UpdateChannel(rctx request.CTX, channel *model.Channel) (*model.Channel, *model.AppError) {
|
|
ok, appErr := a.ChannelAccessControlled(rctx, channel.Id)
|
|
if appErr != nil {
|
|
return nil, appErr
|
|
}
|
|
if ok && channel.Type != model.ChannelTypePrivate {
|
|
return nil, model.NewAppError("UpdateChannel", "api.channel.update_channel.not_allowed.app_error", nil, "", http.StatusForbidden)
|
|
}
|
|
|
|
_, err := a.Srv().Store().Channel().Update(rctx, channel)
|
|
if err != nil {
|
|
var appErr *model.AppError
|
|
var uniqueConstraintErr *store.ErrUniqueConstraint
|
|
var invErr *store.ErrInvalidInput
|
|
switch {
|
|
case errors.As(err, &uniqueConstraintErr):
|
|
return nil, model.NewAppError("UpdateChannel", store.ChannelExistsError, nil, "", http.StatusBadRequest).Wrap(err)
|
|
case errors.As(err, &invErr):
|
|
return nil, model.NewAppError("UpdateChannel", "app.channel.update.bad_id", nil, "", http.StatusBadRequest).Wrap(err)
|
|
case errors.As(err, &appErr):
|
|
return nil, appErr
|
|
default:
|
|
return nil, model.NewAppError("UpdateChannel", "app.channel.update_channel.internal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
}
|
|
|
|
a.Srv().Platform().InvalidateCacheForChannel(channel)
|
|
|
|
messageWs := model.NewWebSocketEvent(model.WebsocketEventChannelUpdated, "", channel.Id, "", nil, "")
|
|
channelJSON, jsonErr := json.Marshal(channel)
|
|
if jsonErr != nil {
|
|
return nil, model.NewAppError("UpdateChannel", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(jsonErr)
|
|
}
|
|
messageWs.Add("channel", string(channelJSON))
|
|
a.Publish(messageWs)
|
|
|
|
return channel, nil
|
|
}
|
|
|
|
// CreateChannelScheme creates a new Scheme of scope channel and assigns it to the channel.
|
|
func (a *App) CreateChannelScheme(rctx request.CTX, channel *model.Channel) (*model.Scheme, *model.AppError) {
|
|
scheme, err := a.CreateScheme(&model.Scheme{
|
|
Name: model.NewId(),
|
|
DisplayName: model.NewId(),
|
|
Scope: model.SchemeScopeChannel,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
channel.SchemeId = &scheme.Id
|
|
if _, err := a.UpdateChannelScheme(rctx, channel); err != nil {
|
|
return nil, err
|
|
}
|
|
return scheme, nil
|
|
}
|
|
|
|
// DeleteChannelScheme deletes a channels scheme and sets its SchemeId to nil.
|
|
func (a *App) DeleteChannelScheme(rctx request.CTX, channel *model.Channel) (*model.Channel, *model.AppError) {
|
|
if channel.SchemeId != nil && *channel.SchemeId != "" {
|
|
if _, err := a.DeleteScheme(*channel.SchemeId); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
channel.SchemeId = nil
|
|
return a.UpdateChannelScheme(rctx, channel)
|
|
}
|
|
|
|
// UpdateChannelScheme saves the new SchemeId of the channel passed.
|
|
func (a *App) UpdateChannelScheme(rctx request.CTX, channel *model.Channel) (*model.Channel, *model.AppError) {
|
|
var oldChannel *model.Channel
|
|
var err *model.AppError
|
|
if oldChannel, err = a.GetChannel(rctx, channel.Id); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
oldChannel.SchemeId = channel.SchemeId
|
|
return a.UpdateChannel(rctx, oldChannel)
|
|
}
|
|
|
|
func (a *App) UpdateChannelPrivacy(rctx request.CTX, oldChannel *model.Channel, user *model.User) (*model.Channel, *model.AppError) {
|
|
channel, err := a.UpdateChannel(rctx, oldChannel)
|
|
if err != nil {
|
|
return channel, err
|
|
}
|
|
|
|
postErr := a.postChannelPrivacyMessage(rctx, user, channel)
|
|
if postErr != nil {
|
|
if channel.Type == model.ChannelTypeOpen {
|
|
channel.Type = model.ChannelTypePrivate
|
|
} else {
|
|
channel.Type = model.ChannelTypeOpen
|
|
}
|
|
// revert to previous channel privacy
|
|
if _, err = a.UpdateChannel(rctx, channel); err != nil {
|
|
a.Log().Error("Failed to revert channel privacy after posting an update message failed", mlog.Err(err))
|
|
}
|
|
return channel, postErr
|
|
}
|
|
|
|
a.Srv().Platform().InvalidateCacheForChannel(channel)
|
|
|
|
messageWs := model.NewWebSocketEvent(model.WebsocketEventChannelConverted, channel.TeamId, "", "", nil, "")
|
|
messageWs.Add("channel_id", channel.Id)
|
|
a.Publish(messageWs)
|
|
|
|
return channel, nil
|
|
}
|
|
|
|
func (a *App) postChannelPrivacyMessage(rctx request.CTX, user *model.User, channel *model.Channel) *model.AppError {
|
|
var authorId string
|
|
var authorUsername string
|
|
if user != nil {
|
|
authorId = user.Id
|
|
authorUsername = user.Username
|
|
} else {
|
|
systemBot, err := a.GetSystemBot(rctx)
|
|
if err != nil {
|
|
return model.NewAppError("postChannelPrivacyMessage", "api.channel.post_channel_privacy_message.error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
authorId = systemBot.UserId
|
|
authorUsername = systemBot.Username
|
|
}
|
|
|
|
message := (map[model.ChannelType]string{
|
|
model.ChannelTypeOpen: i18n.T("api.channel.change_channel_privacy.private_to_public"),
|
|
model.ChannelTypePrivate: i18n.T("api.channel.change_channel_privacy.public_to_private"),
|
|
})[channel.Type]
|
|
post := &model.Post{
|
|
ChannelId: channel.Id,
|
|
Message: message,
|
|
Type: model.PostTypeChangeChannelPrivacy,
|
|
UserId: authorId,
|
|
Props: model.StringInterface{
|
|
"username": authorUsername,
|
|
},
|
|
}
|
|
|
|
if _, err := a.CreatePost(rctx, post, channel, model.CreatePostFlags{SetOnline: true}); err != nil {
|
|
return model.NewAppError("postChannelPrivacyMessage", "api.channel.post_channel_privacy_message.error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) RestoreChannel(rctx request.CTX, channel *model.Channel, userID string) (*model.Channel, *model.AppError) {
|
|
if channel.DeleteAt == 0 {
|
|
return nil, model.NewAppError("restoreChannel", "api.channel.restore_channel.restored.app_error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
if err := a.Srv().Store().Channel().Restore(channel.Id, model.GetMillis()); err != nil {
|
|
return nil, model.NewAppError("RestoreChannel", "app.channel.restore.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
channel.DeleteAt = 0
|
|
a.Srv().Platform().InvalidateCacheForChannel(channel)
|
|
|
|
var message *model.WebSocketEvent
|
|
if channel.Type == model.ChannelTypeOpen {
|
|
message = model.NewWebSocketEvent(model.WebsocketEventChannelRestored, channel.TeamId, "", "", nil, "")
|
|
} else {
|
|
message = model.NewWebSocketEvent(model.WebsocketEventChannelRestored, "", channel.Id, "", nil, "")
|
|
}
|
|
message.Add("channel_id", channel.Id)
|
|
a.Publish(message)
|
|
|
|
var user *model.User
|
|
if userID != "" {
|
|
var nErr error
|
|
user, nErr = a.Srv().Store().User().Get(context.Background(), userID)
|
|
if nErr != nil {
|
|
var nfErr *store.ErrNotFound
|
|
switch {
|
|
case errors.As(nErr, &nfErr):
|
|
return nil, model.NewAppError("RestoreChannel", MissingAccountError, nil, "", http.StatusNotFound).Wrap(nErr)
|
|
default:
|
|
return nil, model.NewAppError("RestoreChannel", "app.user.get.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
|
|
}
|
|
}
|
|
}
|
|
|
|
if user != nil {
|
|
T := i18n.GetUserTranslations(user.Locale)
|
|
|
|
post := &model.Post{
|
|
ChannelId: channel.Id,
|
|
Message: T("api.channel.restore_channel.unarchived", map[string]any{"Username": user.Username}),
|
|
Type: model.PostTypeChannelRestored,
|
|
UserId: userID,
|
|
Props: model.StringInterface{
|
|
"username": user.Username,
|
|
},
|
|
}
|
|
|
|
if _, err := a.CreatePost(rctx, post, channel, model.CreatePostFlags{SetOnline: true}); err != nil {
|
|
rctx.Logger().Warn("Failed to post unarchive message", mlog.Err(err))
|
|
}
|
|
} else {
|
|
a.Srv().Go(func() {
|
|
systemBot, err := a.GetSystemBot(rctx)
|
|
if err != nil {
|
|
rctx.Logger().Error("Failed to post unarchive message", mlog.Err(err))
|
|
return
|
|
}
|
|
|
|
post := &model.Post{
|
|
ChannelId: channel.Id,
|
|
Message: i18n.T("api.channel.restore_channel.unarchived", map[string]any{"Username": systemBot.Username}),
|
|
Type: model.PostTypeChannelRestored,
|
|
UserId: systemBot.UserId,
|
|
Props: model.StringInterface{
|
|
"username": systemBot.Username,
|
|
},
|
|
}
|
|
|
|
if _, err := a.CreatePost(rctx, post, channel, model.CreatePostFlags{SetOnline: true}); err != nil {
|
|
rctx.Logger().Error("Failed to post unarchive message", mlog.Err(err))
|
|
}
|
|
})
|
|
}
|
|
|
|
return channel, nil
|
|
}
|
|
|
|
func (a *App) PatchChannel(rctx request.CTX, channel *model.Channel, patch *model.ChannelPatch, userID string) (*model.Channel, *model.AppError) {
|
|
restrictDM, err := a.CheckIfChannelIsRestrictedDM(rctx, channel)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if restrictDM {
|
|
return nil, model.NewAppError("PatchChannel", "api.channel.patch_update_channel.restricted_dm.app_error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
oldChannelDisplayName := channel.DisplayName
|
|
oldChannelHeader := channel.Header
|
|
oldChannelPurpose := channel.Purpose
|
|
|
|
channel.Patch(patch)
|
|
a.handleChannelCategoryName(channel)
|
|
channel, err = a.UpdateChannel(rctx, channel)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
a.addChannelToDefaultCategory(rctx, userID, channel)
|
|
|
|
if oldChannelDisplayName != channel.DisplayName {
|
|
if err = a.PostUpdateChannelDisplayNameMessage(rctx, userID, channel, oldChannelDisplayName, channel.DisplayName); err != nil {
|
|
rctx.Logger().Warn(err.Error())
|
|
}
|
|
}
|
|
|
|
if channel.Header != oldChannelHeader {
|
|
if err = a.PostUpdateChannelHeaderMessage(rctx, userID, channel, oldChannelHeader, channel.Header); err != nil {
|
|
rctx.Logger().Warn(err.Error())
|
|
}
|
|
}
|
|
|
|
if channel.Purpose != oldChannelPurpose {
|
|
if err = a.PostUpdateChannelPurposeMessage(rctx, userID, channel, oldChannelPurpose, channel.Purpose); err != nil {
|
|
rctx.Logger().Warn(err.Error())
|
|
}
|
|
}
|
|
|
|
return channel, nil
|
|
}
|
|
|
|
// GetSchemeRolesForChannel Checks if a channel or its team has an override scheme for channel roles and returns the scheme roles or default channel roles.
|
|
func (a *App) GetSchemeRolesForChannel(rctx request.CTX, channelID string) (guestRoleName, userRoleName, adminRoleName string, err *model.AppError) {
|
|
channel, err := a.GetChannel(rctx, channelID)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
if channel.SchemeId != nil && *channel.SchemeId != "" {
|
|
var scheme *model.Scheme
|
|
scheme, err = a.GetScheme(*channel.SchemeId)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
guestRoleName = scheme.DefaultChannelGuestRole
|
|
userRoleName = scheme.DefaultChannelUserRole
|
|
adminRoleName = scheme.DefaultChannelAdminRole
|
|
|
|
return
|
|
}
|
|
|
|
return a.GetTeamSchemeChannelRoles(rctx, channel.TeamId)
|
|
}
|
|
|
|
// GetTeamSchemeChannelRoles Checks if a team has an override scheme and returns the scheme channel role names or default channel role names.
|
|
func (a *App) GetTeamSchemeChannelRoles(rctx request.CTX, teamID string) (guestRoleName, userRoleName, adminRoleName string, err *model.AppError) {
|
|
team, err := a.GetTeam(teamID)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
if team.SchemeId != nil && *team.SchemeId != "" {
|
|
var scheme *model.Scheme
|
|
scheme, err = a.GetScheme(*team.SchemeId)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
guestRoleName = scheme.DefaultChannelGuestRole
|
|
userRoleName = scheme.DefaultChannelUserRole
|
|
adminRoleName = scheme.DefaultChannelAdminRole
|
|
} else {
|
|
guestRoleName = model.ChannelGuestRoleId
|
|
userRoleName = model.ChannelUserRoleId
|
|
adminRoleName = model.ChannelAdminRoleId
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// GetChannelModerationsForChannel Gets a channels ChannelModerations from either the higherScoped roles or from the channel scheme roles.
|
|
func (a *App) GetChannelModerationsForChannel(rctx request.CTX, channel *model.Channel) ([]*model.ChannelModeration, *model.AppError) {
|
|
guestRoleName, memberRoleName, _, err := a.GetSchemeRolesForChannel(rctx, channel.Id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
memberRole, err := a.GetRoleByName(rctx, memberRoleName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var guestRole *model.Role
|
|
if guestRoleName != "" {
|
|
guestRole, err = a.GetRoleByName(rctx, guestRoleName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
higherScopedGuestRoleName, higherScopedMemberRoleName, _, err := a.GetTeamSchemeChannelRoles(rctx, channel.TeamId)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
higherScopedMemberRole, err := a.GetRoleByName(rctx, higherScopedMemberRoleName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var higherScopedGuestRole *model.Role
|
|
if higherScopedGuestRoleName != "" {
|
|
higherScopedGuestRole, err = a.GetRoleByName(rctx, higherScopedGuestRoleName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return buildChannelModerations(rctx, channel.Type, memberRole, guestRole, higherScopedMemberRole, higherScopedGuestRole), nil
|
|
}
|
|
|
|
// PatchChannelModerationsForChannel Updates a channels scheme roles based on a given ChannelModerationPatch, if the permissions match the higher scoped role the scheme is deleted.
|
|
func (a *App) PatchChannelModerationsForChannel(rctx request.CTX, channel *model.Channel, channelModerationsPatch []*model.ChannelModerationPatch) ([]*model.ChannelModeration, *model.AppError) {
|
|
higherScopedGuestRoleName, higherScopedMemberRoleName, _, err := a.GetTeamSchemeChannelRoles(rctx, channel.TeamId)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
higherScopedMemberRole, err := a.GetRoleByName(RequestContextWithMaster(rctx), higherScopedMemberRoleName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var higherScopedGuestRole *model.Role
|
|
if higherScopedGuestRoleName != "" {
|
|
higherScopedGuestRole, err = a.GetRoleByName(RequestContextWithMaster(rctx), higherScopedGuestRoleName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
higherScopedMemberPermissions := higherScopedMemberRole.GetChannelModeratedPermissions(channel.Type)
|
|
|
|
var higherScopedGuestPermissions map[string]bool
|
|
if higherScopedGuestRole != nil {
|
|
higherScopedGuestPermissions = higherScopedGuestRole.GetChannelModeratedPermissions(channel.Type)
|
|
}
|
|
|
|
for _, moderationPatch := range channelModerationsPatch {
|
|
if moderationPatch.Roles.Members != nil && *moderationPatch.Roles.Members && !higherScopedMemberPermissions[*moderationPatch.Name] {
|
|
return nil, model.NewAppError("PatchChannelModerationsForChannel", "api.channel.patch_channel_moderations_for_channel.restricted_permission.app_error", nil, "", http.StatusForbidden)
|
|
}
|
|
if moderationPatch.Roles.Guests != nil && *moderationPatch.Roles.Guests && !higherScopedGuestPermissions[*moderationPatch.Name] {
|
|
return nil, model.NewAppError("PatchChannelModerationsForChannel", "api.channel.patch_channel_moderations_for_channel.restricted_permission.app_error", nil, "", http.StatusForbidden)
|
|
}
|
|
}
|
|
|
|
var scheme *model.Scheme
|
|
// Channel has no scheme so create one
|
|
if channel.SchemeId == nil || *channel.SchemeId == "" {
|
|
scheme, err = a.CreateChannelScheme(rctx, channel)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Send a websocket event about this new role. The other new roles—member and guest—get emitted when they're updated.
|
|
var adminRole *model.Role
|
|
adminRole, err = a.GetRoleByName(rctx, scheme.DefaultChannelAdminRole)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if appErr := a.sendUpdatedRoleEvent(adminRole); appErr != nil {
|
|
return nil, appErr
|
|
}
|
|
|
|
message := model.NewWebSocketEvent(model.WebsocketEventChannelSchemeUpdated, "", channel.Id, "", nil, "")
|
|
a.Publish(message)
|
|
rctx.Logger().Info("Permission scheme created.", mlog.String("channel_id", channel.Id), mlog.String("channel_name", channel.Name))
|
|
} else {
|
|
scheme, err = a.GetScheme(*channel.SchemeId)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
guestRoleName := scheme.DefaultChannelGuestRole
|
|
memberRoleName := scheme.DefaultChannelUserRole
|
|
memberRole, err := a.GetRoleByName(rctx, memberRoleName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var guestRole *model.Role
|
|
if guestRoleName != "" {
|
|
guestRole, err = a.GetRoleByName(rctx, guestRoleName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
memberRolePatch := memberRole.RolePatchFromChannelModerationsPatch(channelModerationsPatch, "members")
|
|
var guestRolePatch *model.RolePatch
|
|
if guestRole != nil {
|
|
guestRolePatch = guestRole.RolePatchFromChannelModerationsPatch(channelModerationsPatch, "guests")
|
|
}
|
|
|
|
for _, channelModerationPatch := range channelModerationsPatch {
|
|
permissionModified := *channelModerationPatch.Name
|
|
if channelModerationPatch.Roles.Guests != nil && slices.Contains(model.ChannelModeratedPermissionsChangedByPatch(guestRole, guestRolePatch), permissionModified) {
|
|
if *channelModerationPatch.Roles.Guests {
|
|
rctx.Logger().Info("Permission enabled for guests.", mlog.String("permission", permissionModified), mlog.String("channel_id", channel.Id), mlog.String("channel_name", channel.Name))
|
|
} else {
|
|
rctx.Logger().Info("Permission disabled for guests.", mlog.String("permission", permissionModified), mlog.String("channel_id", channel.Id), mlog.String("channel_name", channel.Name))
|
|
}
|
|
}
|
|
|
|
if channelModerationPatch.Roles.Members != nil && slices.Contains(model.ChannelModeratedPermissionsChangedByPatch(memberRole, memberRolePatch), permissionModified) {
|
|
if *channelModerationPatch.Roles.Members {
|
|
rctx.Logger().Info("Permission enabled for members.", mlog.String("permission", permissionModified), mlog.String("channel_id", channel.Id), mlog.String("channel_name", channel.Name))
|
|
} else {
|
|
rctx.Logger().Info("Permission disabled for members.", mlog.String("permission", permissionModified), mlog.String("channel_id", channel.Id), mlog.String("channel_name", channel.Name))
|
|
}
|
|
}
|
|
}
|
|
|
|
memberRolePermissionsUnmodified := len(model.ChannelModeratedPermissionsChangedByPatch(higherScopedMemberRole, memberRolePatch)) == 0
|
|
guestRolePermissionsUnmodified := len(model.ChannelModeratedPermissionsChangedByPatch(higherScopedGuestRole, guestRolePatch)) == 0
|
|
if memberRolePermissionsUnmodified && guestRolePermissionsUnmodified {
|
|
// The channel scheme matches the permissions of its higherScoped scheme so delete the scheme
|
|
if _, err = a.DeleteChannelScheme(rctx, channel); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
message := model.NewWebSocketEvent(model.WebsocketEventChannelSchemeUpdated, "", channel.Id, "", nil, "")
|
|
a.Publish(message)
|
|
|
|
memberRole = higherScopedMemberRole
|
|
guestRole = higherScopedGuestRole
|
|
rctx.Logger().Info("Permission scheme deleted.", mlog.String("channel_id", channel.Id), mlog.String("channel_name", channel.Name))
|
|
} else {
|
|
memberRole, err = a.PatchRole(memberRole, memberRolePatch)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
guestRole, err = a.PatchRole(guestRole, guestRolePatch)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
cErr := a.forEachChannelMember(rctx, channel.Id, func(channelMember model.ChannelMember) error {
|
|
a.Srv().Store().Channel().InvalidateAllChannelMembersForUser(channelMember.UserId)
|
|
|
|
evt := model.NewWebSocketEvent(model.WebsocketEventChannelMemberUpdated, "", "", channelMember.UserId, nil, "")
|
|
memberJSON, jsonErr := json.Marshal(channelMember)
|
|
if jsonErr != nil {
|
|
return jsonErr
|
|
}
|
|
evt.Add("channelMember", string(memberJSON))
|
|
a.Publish(evt)
|
|
|
|
return nil
|
|
})
|
|
if cErr != nil {
|
|
return nil, model.NewAppError("PatchChannelModerationsForChannel", "api.channel.patch_channel_moderations.cache_invalidation.error", nil, "", http.StatusInternalServerError).Wrap(cErr)
|
|
}
|
|
|
|
return buildChannelModerations(rctx, channel.Type, memberRole, guestRole, higherScopedMemberRole, higherScopedGuestRole), nil
|
|
}
|
|
|
|
func buildChannelModerations(rctx request.CTX, channelType model.ChannelType, memberRole *model.Role, guestRole *model.Role, higherScopedMemberRole *model.Role, higherScopedGuestRole *model.Role) []*model.ChannelModeration {
|
|
var memberPermissions, guestPermissions, higherScopedMemberPermissions, higherScopedGuestPermissions map[string]bool
|
|
if memberRole != nil {
|
|
memberPermissions = memberRole.GetChannelModeratedPermissions(channelType)
|
|
}
|
|
if guestRole != nil {
|
|
guestPermissions = guestRole.GetChannelModeratedPermissions(channelType)
|
|
}
|
|
if higherScopedMemberRole != nil {
|
|
higherScopedMemberPermissions = higherScopedMemberRole.GetChannelModeratedPermissions(channelType)
|
|
}
|
|
if higherScopedGuestRole != nil {
|
|
higherScopedGuestPermissions = higherScopedGuestRole.GetChannelModeratedPermissions(channelType)
|
|
}
|
|
|
|
var channelModerations []*model.ChannelModeration
|
|
for _, permissionKey := range model.ChannelModeratedPermissions {
|
|
roles := &model.ChannelModeratedRoles{}
|
|
|
|
roles.Members = &model.ChannelModeratedRole{
|
|
Value: memberPermissions[permissionKey],
|
|
Enabled: higherScopedMemberPermissions[permissionKey],
|
|
}
|
|
|
|
if permissionKey == "manage_members" || permissionKey == "manage_bookmarks" {
|
|
roles.Guests = nil
|
|
} else {
|
|
roles.Guests = &model.ChannelModeratedRole{
|
|
Value: guestPermissions[permissionKey],
|
|
Enabled: higherScopedGuestPermissions[permissionKey],
|
|
}
|
|
}
|
|
|
|
moderation := &model.ChannelModeration{
|
|
Name: permissionKey,
|
|
Roles: roles,
|
|
}
|
|
|
|
channelModerations = append(channelModerations, moderation)
|
|
}
|
|
|
|
return channelModerations
|
|
}
|
|
|
|
func (a *App) UpdateChannelMemberRoles(rctx request.CTX, channelID string, userID string, newRoles string) (*model.ChannelMember, *model.AppError) {
|
|
var member *model.ChannelMember
|
|
var err *model.AppError
|
|
if member, err = a.GetChannelMember(rctx, channelID, userID); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
schemeGuestRole, schemeUserRole, schemeAdminRole, err := a.GetSchemeRolesForChannel(rctx, channelID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
prevSchemeGuestValue := member.SchemeGuest
|
|
|
|
var newExplicitRoles []string
|
|
member.SchemeGuest = false
|
|
member.SchemeUser = false
|
|
member.SchemeAdmin = false
|
|
|
|
for roleName := range strings.FieldsSeq(newRoles) {
|
|
var role *model.Role
|
|
role, err = a.GetRoleByName(rctx, roleName)
|
|
if err != nil {
|
|
err.StatusCode = http.StatusBadRequest
|
|
return nil, err
|
|
}
|
|
|
|
if !role.SchemeManaged {
|
|
// The role is not scheme-managed, so it's OK to apply it to the explicit roles field.
|
|
newExplicitRoles = append(newExplicitRoles, roleName)
|
|
} else {
|
|
// The role is scheme-managed, so need to check if it is part of the scheme for this channel or not.
|
|
switch roleName {
|
|
case schemeAdminRole:
|
|
member.SchemeAdmin = true
|
|
case schemeUserRole:
|
|
member.SchemeUser = true
|
|
case schemeGuestRole:
|
|
member.SchemeGuest = true
|
|
default:
|
|
// If not part of the scheme for this channel, then it is not allowed to apply it as an explicit role.
|
|
return nil, model.NewAppError("UpdateChannelMemberRoles", "api.channel.update_channel_member_roles.scheme_role.app_error", nil, "role_name="+roleName, http.StatusBadRequest)
|
|
}
|
|
}
|
|
}
|
|
|
|
if member.SchemeUser && member.SchemeGuest {
|
|
return nil, model.NewAppError("UpdateChannelMemberRoles", "api.channel.update_channel_member_roles.guest_and_user.app_error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
if prevSchemeGuestValue != member.SchemeGuest {
|
|
return nil, model.NewAppError("UpdateChannelMemberRoles", "api.channel.update_channel_member_roles.changing_guest_role.app_error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
member.ExplicitRoles = strings.Join(newExplicitRoles, " ")
|
|
|
|
return a.updateChannelMember(rctx, member)
|
|
}
|
|
|
|
func (a *App) UpdateChannelMemberSchemeRoles(rctx request.CTX, channelID string, userID string, isSchemeGuest bool, isSchemeUser bool, isSchemeAdmin bool) (*model.ChannelMember, *model.AppError) {
|
|
member, err := a.GetChannelMember(rctx, channelID, userID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if member.SchemeGuest {
|
|
return nil, model.NewAppError("UpdateChannelMemberSchemeRoles", "api.channel.update_channel_member_roles.guest.app_error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
if isSchemeGuest {
|
|
return nil, model.NewAppError("UpdateChannelMemberSchemeRoles", "api.channel.update_channel_member_roles.user_and_guest.app_error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
if !isSchemeUser {
|
|
return nil, model.NewAppError("UpdateChannelMemberSchemeRoles", "api.channel.update_channel_member_roles.unset_user_scheme.app_error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
member.SchemeAdmin = isSchemeAdmin
|
|
member.SchemeUser = isSchemeUser
|
|
member.SchemeGuest = isSchemeGuest
|
|
|
|
// If the migration is not completed, we also need to check the default channel_admin/channel_user roles are not present in the roles field.
|
|
if err = a.IsPhase2MigrationCompleted(); err != nil {
|
|
member.ExplicitRoles = removeRoles([]string{model.ChannelGuestRoleId, model.ChannelUserRoleId, model.ChannelAdminRoleId}, member.ExplicitRoles)
|
|
}
|
|
|
|
return a.updateChannelMember(rctx, member)
|
|
}
|
|
|
|
func (a *App) UpdateChannelMemberNotifyProps(rctx request.CTX, data map[string]string, channelID string, userID string) (*model.ChannelMember, *model.AppError) {
|
|
filteredProps := make(map[string]string)
|
|
|
|
// update whichever notify properties have been provided, but don't change the others
|
|
if markUnread, exists := data[model.MarkUnreadNotifyProp]; exists {
|
|
filteredProps[model.MarkUnreadNotifyProp] = markUnread
|
|
}
|
|
|
|
if desktop, exists := data[model.DesktopNotifyProp]; exists {
|
|
filteredProps[model.DesktopNotifyProp] = desktop
|
|
}
|
|
|
|
if desktop_sound, exists := data[model.DesktopSoundNotifyProp]; exists {
|
|
filteredProps[model.DesktopSoundNotifyProp] = desktop_sound
|
|
}
|
|
|
|
if desktop_notification_sound, exists := data["desktop_notification_sound"]; exists {
|
|
filteredProps["desktop_notification_sound"] = desktop_notification_sound
|
|
}
|
|
|
|
if desktop_threads, exists := data[model.DesktopThreadsNotifyProp]; exists {
|
|
filteredProps[model.DesktopThreadsNotifyProp] = desktop_threads
|
|
}
|
|
|
|
if email, exists := data[model.EmailNotifyProp]; exists {
|
|
filteredProps[model.EmailNotifyProp] = email
|
|
}
|
|
|
|
if push, exists := data[model.PushNotifyProp]; exists {
|
|
filteredProps[model.PushNotifyProp] = push
|
|
}
|
|
|
|
if push_threads, exists := data[model.PushThreadsNotifyProp]; exists {
|
|
filteredProps[model.PushThreadsNotifyProp] = push_threads
|
|
}
|
|
|
|
if ignoreChannelMentions, exists := data[model.IgnoreChannelMentionsNotifyProp]; exists {
|
|
filteredProps[model.IgnoreChannelMentionsNotifyProp] = ignoreChannelMentions
|
|
}
|
|
|
|
if channelAutoFollowThreads, exists := data[model.ChannelAutoFollowThreads]; exists {
|
|
filteredProps[model.ChannelAutoFollowThreads] = channelAutoFollowThreads
|
|
}
|
|
|
|
member, err := a.Srv().Store().Channel().UpdateMemberNotifyProps(channelID, userID, filteredProps)
|
|
if err != nil {
|
|
var appErr *model.AppError
|
|
var invErr *store.ErrInvalidInput
|
|
var nfErr *store.ErrNotFound
|
|
switch {
|
|
case errors.As(err, &appErr):
|
|
return nil, appErr
|
|
case errors.As(err, &invErr):
|
|
return nil, model.NewAppError("updateMemberNotifyProps", "app.channel.update_member.notify_props_limit_exceeded.app_error", nil, "", http.StatusBadRequest).Wrap(err)
|
|
case errors.As(err, &nfErr):
|
|
return nil, model.NewAppError("updateMemberNotifyProps", MissingChannelMemberError, nil, "", http.StatusNotFound).Wrap(err)
|
|
default:
|
|
return nil, model.NewAppError("updateMemberNotifyProps", "app.channel.update_member.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
}
|
|
|
|
a.Srv().Platform().InvalidateChannelCacheForUser(member.UserId)
|
|
a.invalidateCacheForChannelMembersNotifyProps(member.ChannelId)
|
|
|
|
// Notify the clients that the member notify props changed
|
|
err = a.sendUpdateChannelMemberEvent(member)
|
|
if err != nil {
|
|
return nil, model.NewAppError("UpdateChannelMemberNotifyProps", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
return member, nil
|
|
}
|
|
|
|
func (a *App) PatchChannelMembersNotifyProps(rctx request.CTX, members []*model.ChannelMemberIdentifier, notifyProps map[string]string) ([]*model.ChannelMember, *model.AppError) {
|
|
if len(members) > UpdateMultipleMaximum {
|
|
return nil, model.NewAppError("PatchChannelMembersNotifyProps", "app.channel.patch_channel_members_notify_props.too_many", map[string]any{"Max": UpdateMultipleMaximum}, "", http.StatusBadRequest)
|
|
}
|
|
|
|
updated, err := a.Srv().Store().Channel().PatchMultipleMembersNotifyProps(members, notifyProps)
|
|
if err != nil {
|
|
var appErr *model.AppError
|
|
switch {
|
|
case errors.As(err, &appErr):
|
|
return nil, appErr
|
|
default:
|
|
return nil, model.NewAppError("UpdateMultipleMembersNotifyProps", "app.channel.patch_channel_members_notify_props.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
}
|
|
|
|
// Invalidate caches for the users and channels that have changed
|
|
userIds := make(map[string]bool)
|
|
channelIds := make(map[string]bool)
|
|
for _, member := range updated {
|
|
userIds[member.UserId] = true
|
|
channelIds[member.ChannelId] = true
|
|
}
|
|
|
|
for userId := range userIds {
|
|
a.Srv().Platform().InvalidateChannelCacheForUser(userId)
|
|
}
|
|
for channelId := range channelIds {
|
|
a.invalidateCacheForChannelMembersNotifyProps(channelId)
|
|
}
|
|
|
|
// Notify clients that their notify props have changed
|
|
for _, member := range updated {
|
|
err := a.sendUpdateChannelMemberEvent(member)
|
|
if err != nil {
|
|
rctx.Logger().Warn("Failed to send WebSocket event for updated channel member notify props", mlog.Err(err))
|
|
}
|
|
}
|
|
|
|
return updated, nil
|
|
}
|
|
|
|
func (a *App) sendUpdateChannelMemberEvent(member *model.ChannelMember) error {
|
|
evt := model.NewWebSocketEvent(model.WebsocketEventChannelMemberUpdated, "", "", member.UserId, nil, "")
|
|
memberJSON, jsonErr := json.Marshal(member)
|
|
if jsonErr != nil {
|
|
return jsonErr
|
|
}
|
|
evt.Add("channelMember", string(memberJSON))
|
|
a.Publish(evt)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) updateChannelMember(rctx request.CTX, member *model.ChannelMember) (*model.ChannelMember, *model.AppError) {
|
|
member, err := a.Srv().Store().Channel().UpdateMember(rctx, member)
|
|
if err != nil {
|
|
var appErr *model.AppError
|
|
var nfErr *store.ErrNotFound
|
|
switch {
|
|
case errors.As(err, &appErr):
|
|
return nil, appErr
|
|
case errors.As(err, &nfErr):
|
|
return nil, model.NewAppError("updateChannelMember", MissingChannelMemberError, nil, "", http.StatusNotFound).Wrap(err)
|
|
default:
|
|
return nil, model.NewAppError("updateChannelMember", "app.channel.get_member.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
}
|
|
|
|
a.Srv().Platform().InvalidateChannelCacheForUser(member.UserId)
|
|
|
|
// Notify the clients that the member notify props changed
|
|
evt := model.NewWebSocketEvent(model.WebsocketEventChannelMemberUpdated, "", "", member.UserId, nil, "")
|
|
memberJSON, jsonErr := json.Marshal(member)
|
|
if jsonErr != nil {
|
|
return nil, model.NewAppError("updateChannelMember", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(jsonErr)
|
|
}
|
|
evt.Add("channelMember", string(memberJSON))
|
|
a.Publish(evt)
|
|
|
|
return member, nil
|
|
}
|
|
|
|
func (a *App) DeleteChannel(rctx request.CTX, channel *model.Channel, userID string) *model.AppError {
|
|
ihc := make(chan store.StoreResult[[]*model.IncomingWebhook], 1)
|
|
ohc := make(chan store.StoreResult[[]*model.OutgoingWebhook], 1)
|
|
|
|
go func() {
|
|
webhooks, err := a.Srv().Store().Webhook().GetIncomingByChannel(channel.Id)
|
|
ihc <- store.StoreResult[[]*model.IncomingWebhook]{Data: webhooks, NErr: err}
|
|
close(ihc)
|
|
}()
|
|
|
|
go func() {
|
|
outgoingHooks, err := a.Srv().Store().Webhook().GetOutgoingByChannel(channel.Id, -1, -1)
|
|
ohc <- store.StoreResult[[]*model.OutgoingWebhook]{Data: outgoingHooks, NErr: err}
|
|
close(ohc)
|
|
}()
|
|
|
|
var user *model.User
|
|
if userID != "" {
|
|
var nErr error
|
|
user, nErr = a.Srv().Store().User().Get(context.Background(), userID)
|
|
if nErr != nil {
|
|
var nfErr *store.ErrNotFound
|
|
switch {
|
|
case errors.As(nErr, &nfErr):
|
|
return model.NewAppError("DeleteChannel", MissingAccountError, nil, "", http.StatusNotFound).Wrap(nErr)
|
|
default:
|
|
return model.NewAppError("DeleteChannel", "app.user.get.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
|
|
}
|
|
}
|
|
}
|
|
|
|
ihcresult := <-ihc
|
|
if ihcresult.NErr != nil {
|
|
return model.NewAppError("DeleteChannel", "app.webhooks.get_incoming_by_channel.app_error", nil, "", http.StatusInternalServerError).Wrap(ihcresult.NErr)
|
|
}
|
|
|
|
ohcresult := <-ohc
|
|
if ohcresult.NErr != nil {
|
|
return model.NewAppError("DeleteChannel", "app.webhooks.get_outgoing_by_channel.app_error", nil, "", http.StatusInternalServerError).Wrap(ohcresult.NErr)
|
|
}
|
|
|
|
if channel.DeleteAt > 0 {
|
|
err := model.NewAppError("deleteChannel", "api.channel.delete_channel.deleted.app_error", nil, "", http.StatusBadRequest)
|
|
return err
|
|
}
|
|
|
|
if channel.Name == model.DefaultChannelName {
|
|
err := model.NewAppError("deleteChannel", "api.channel.delete_channel.cannot.app_error", map[string]any{"Channel": model.DefaultChannelName}, "", http.StatusBadRequest)
|
|
return err
|
|
}
|
|
|
|
if user != nil {
|
|
T := i18n.GetUserTranslations(user.Locale)
|
|
|
|
post := &model.Post{
|
|
ChannelId: channel.Id,
|
|
Message: fmt.Sprintf(T("api.channel.delete_channel.archived"), user.Username),
|
|
Type: model.PostTypeChannelDeleted,
|
|
UserId: userID,
|
|
Props: model.StringInterface{
|
|
"username": user.Username,
|
|
},
|
|
}
|
|
|
|
if _, err := a.CreatePost(rctx, post, channel, model.CreatePostFlags{SetOnline: true}); err != nil {
|
|
rctx.Logger().Warn("Failed to post archive message", mlog.Err(err))
|
|
}
|
|
} else {
|
|
systemBot, err := a.GetSystemBot(rctx)
|
|
if err != nil {
|
|
rctx.Logger().Warn("Failed to post archive message", mlog.Err(err))
|
|
} else {
|
|
post := &model.Post{
|
|
ChannelId: channel.Id,
|
|
Message: fmt.Sprintf(i18n.T("api.channel.delete_channel.archived"), systemBot.Username),
|
|
Type: model.PostTypeChannelDeleted,
|
|
UserId: systemBot.UserId,
|
|
Props: model.StringInterface{
|
|
"username": systemBot.Username,
|
|
},
|
|
}
|
|
|
|
if _, err := a.CreatePost(rctx, post, channel, model.CreatePostFlags{SetOnline: true}); err != nil {
|
|
rctx.Logger().Warn("Failed to post archive message", mlog.Err(err))
|
|
}
|
|
}
|
|
}
|
|
|
|
now := model.GetMillis()
|
|
for _, hook := range ihcresult.Data {
|
|
if err := a.Srv().Store().Webhook().DeleteIncoming(hook.Id, now); err != nil {
|
|
rctx.Logger().Warn("Encountered error deleting incoming webhook", mlog.String("hook_id", hook.Id), mlog.Err(err))
|
|
}
|
|
a.Srv().Platform().InvalidateCacheForWebhook(hook.Id)
|
|
}
|
|
|
|
for _, hook := range ohcresult.Data {
|
|
if err := a.Srv().Store().Webhook().DeleteOutgoing(hook.Id, now); err != nil {
|
|
rctx.Logger().Warn("Encountered error deleting outgoing webhook", mlog.String("hook_id", hook.Id), mlog.Err(err))
|
|
}
|
|
}
|
|
|
|
if err := a.Srv().Store().PostPersistentNotification().DeleteByChannel([]string{channel.Id}); err != nil {
|
|
return model.NewAppError("DeleteChannel", "app.post_persistent_notification.delete_by_channel.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
deleteAt := model.GetMillis()
|
|
|
|
if err := a.Srv().Store().Channel().Delete(channel.Id, deleteAt); err != nil {
|
|
return model.NewAppError("DeleteChannel", "app.channel.delete.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
a.Srv().Platform().InvalidateCacheForChannel(channel)
|
|
|
|
var message *model.WebSocketEvent
|
|
if channel.Type == model.ChannelTypeOpen {
|
|
message = model.NewWebSocketEvent(model.WebsocketEventChannelDeleted, channel.TeamId, "", "", nil, "")
|
|
} else {
|
|
message = model.NewWebSocketEvent(model.WebsocketEventChannelDeleted, "", channel.Id, "", nil, "")
|
|
}
|
|
message.Add("channel_id", channel.Id)
|
|
message.Add("delete_at", deleteAt)
|
|
a.Publish(message)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) addUserToChannel(rctx request.CTX, user *model.User, channel *model.Channel) (*model.ChannelMember, *model.AppError) {
|
|
if channel.Type != model.ChannelTypeOpen && channel.Type != model.ChannelTypePrivate {
|
|
return nil, model.NewAppError("AddUserToChannel", "api.channel.add_user_to_channel.type.app_error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
channelMember, nErr := a.Srv().Store().Channel().GetMember(rctx, channel.Id, user.Id)
|
|
if nErr != nil {
|
|
var nfErr *store.ErrNotFound
|
|
if !errors.As(nErr, &nfErr) {
|
|
return nil, model.NewAppError("AddUserToChannel", "app.channel.get_member.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
|
|
}
|
|
} else {
|
|
return channelMember, nil
|
|
}
|
|
|
|
if channel.IsGroupConstrained() {
|
|
nonMembers, err := a.FilterNonGroupChannelMembers(rctx, []string{user.Id}, channel)
|
|
if err != nil {
|
|
return nil, model.NewAppError("addUserToChannel", "api.channel.add_user_to_channel.type.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
if len(nonMembers) > 0 {
|
|
return nil, model.NewAppError("addUserToChannel", "api.channel.add_members.user_denied", map[string]any{"UserIDs": nonMembers}, "", http.StatusBadRequest)
|
|
}
|
|
}
|
|
|
|
newMember := &model.ChannelMember{
|
|
ChannelId: channel.Id,
|
|
UserId: user.Id,
|
|
NotifyProps: model.GetDefaultChannelNotifyProps(),
|
|
SchemeGuest: user.IsGuest(),
|
|
SchemeUser: !user.IsGuest(),
|
|
}
|
|
|
|
if !user.IsGuest() {
|
|
var userShouldBeAdmin bool
|
|
userShouldBeAdmin, appErr := a.UserIsInAdminRoleGroup(user.Id, channel.Id, model.GroupSyncableTypeChannel)
|
|
if appErr != nil {
|
|
return nil, appErr
|
|
}
|
|
newMember.SchemeAdmin = userShouldBeAdmin
|
|
}
|
|
|
|
if channel.Type == model.ChannelTypePrivate {
|
|
if ok, appErr := a.ChannelAccessControlled(rctx, channel.Id); ok {
|
|
if acs := a.Srv().Channels().AccessControl; acs != nil {
|
|
groupID, err := a.CpaGroupID()
|
|
if err != nil {
|
|
return nil, model.NewAppError("AddUserToChannel", "api.channel.add_user.to.channel.failed.app_error", nil,
|
|
fmt.Sprintf("failed to get group: %v, user_id: %s, channel_id: %s", err, user.Id, channel.Id), http.StatusInternalServerError)
|
|
}
|
|
|
|
s, err := a.Srv().Store().Attributes().GetSubject(rctx, user.Id, groupID)
|
|
if err != nil {
|
|
return nil, model.NewAppError("AddUserToChannel", "api.channel.add_user.to.channel.failed.app_error", nil,
|
|
fmt.Sprintf("failed to get subject: %v, user_id: %s, channel_id: %s", err, user.Id, channel.Id), http.StatusForbidden)
|
|
}
|
|
|
|
decision, evalErr := acs.AccessEvaluation(rctx, model.AccessRequest{
|
|
Subject: *s,
|
|
Resource: model.Resource{
|
|
Type: model.AccessControlPolicyTypeChannel,
|
|
ID: channel.Id,
|
|
},
|
|
Action: "join_channel",
|
|
})
|
|
if evalErr != nil {
|
|
return nil, evalErr
|
|
} else if !decision.Decision {
|
|
return nil, model.NewAppError("AddUserToChannel", "api.channel.add_user.to.channel.rejected", nil, "", http.StatusForbidden)
|
|
}
|
|
}
|
|
} else if appErr != nil {
|
|
return nil, appErr
|
|
}
|
|
}
|
|
|
|
newMember, nErr = a.Srv().Store().Channel().SaveMember(rctx, newMember)
|
|
if nErr != nil {
|
|
return nil, model.NewAppError("AddUserToChannel", "api.channel.add_user.to.channel.failed.app_error", nil,
|
|
fmt.Sprintf("failed to add member: %v, user_id: %s, channel_id: %s", nErr, user.Id, channel.Id), http.StatusInternalServerError)
|
|
}
|
|
|
|
if nErr := a.Srv().Store().ChannelMemberHistory().LogJoinEvent(user.Id, channel.Id, model.GetMillis()); nErr != nil {
|
|
return nil, model.NewAppError("AddUserToChannel", "app.channel_member_history.log_join_event.internal_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
|
|
}
|
|
|
|
a.Srv().Platform().InvalidateChannelCacheForUser(user.Id)
|
|
a.invalidateCacheForChannelMembers(channel.Id)
|
|
|
|
// Synchronize membership change for shared channels
|
|
if channel.IsShared() {
|
|
if scs := a.Srv().Platform().GetSharedChannelService(); scs != nil {
|
|
scs.HandleMembershipChange(channel.Id, user.Id, true, user.GetRemoteID())
|
|
}
|
|
}
|
|
|
|
return newMember, nil
|
|
}
|
|
|
|
// AddUserToChannel adds a user to a given channel.
|
|
func (a *App) AddUserToChannel(rctx request.CTX, user *model.User, channel *model.Channel, skipTeamMemberIntegrityCheck bool) (*model.ChannelMember, *model.AppError) {
|
|
if !skipTeamMemberIntegrityCheck {
|
|
teamMember, nErr := a.Srv().Store().Team().GetMember(rctx, channel.TeamId, user.Id)
|
|
if nErr != nil {
|
|
var nfErr *store.ErrNotFound
|
|
switch {
|
|
case errors.As(nErr, &nfErr):
|
|
return nil, model.NewAppError("AddUserToChannel", "app.team.get_member.missing.app_error", nil, "", http.StatusNotFound).Wrap(nErr)
|
|
default:
|
|
return nil, model.NewAppError("AddUserToChannel", "app.team.get_member.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
|
|
}
|
|
}
|
|
|
|
if teamMember.DeleteAt > 0 {
|
|
return nil, model.NewAppError("AddUserToChannel", "api.channel.add_user.to.channel.failed.deleted.app_error", nil, "", http.StatusBadRequest)
|
|
}
|
|
}
|
|
|
|
newMember, err := a.addUserToChannel(rctx, user, channel)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
a.addChannelToDefaultCategory(rctx, user.Id, channel)
|
|
|
|
// We are sending separate websocket events to the user added and to the channel
|
|
// This is to get around potential cluster syncing issues where other nodes may not receive the most up to date channel members
|
|
message := model.NewWebSocketEvent(model.WebsocketEventUserAdded, "", channel.Id, "", map[string]bool{user.Id: true}, "")
|
|
message.Add("user_id", user.Id)
|
|
message.Add("team_id", channel.TeamId)
|
|
a.Publish(message)
|
|
|
|
userMessage := model.NewWebSocketEvent(model.WebsocketEventUserAdded, "", channel.Id, user.Id, nil, "")
|
|
userMessage.Add("user_id", user.Id)
|
|
userMessage.Add("team_id", channel.TeamId)
|
|
a.Publish(userMessage)
|
|
|
|
return newMember, nil
|
|
}
|
|
|
|
type ChannelMemberOpts struct {
|
|
UserRequestorID string
|
|
PostRootID string
|
|
// SkipTeamMemberIntegrityCheck is used to indicate whether it should be checked
|
|
// that a user has already been removed from that team or not.
|
|
// This is useful to avoid in scenarios when we just added the team member,
|
|
// and thereby know that there is no need to check this.
|
|
SkipTeamMemberIntegrityCheck bool
|
|
}
|
|
|
|
// AddChannelMember adds a user to a channel. It is a wrapper over AddUserToChannel.
|
|
func (a *App) AddChannelMember(rctx request.CTX, userID string, channel *model.Channel, opts ChannelMemberOpts) (*model.ChannelMember, *model.AppError) {
|
|
if member, err := a.Srv().Store().Channel().GetMember(rctx, channel.Id, userID); err != nil {
|
|
var nfErr *store.ErrNotFound
|
|
if !errors.As(err, &nfErr) {
|
|
return nil, model.NewAppError("AddChannelMember", "app.channel.get_member.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
} else {
|
|
return member, nil
|
|
}
|
|
|
|
var user *model.User
|
|
var err *model.AppError
|
|
|
|
if user, err = a.GetUser(userID); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if user.DeleteAt > 0 {
|
|
return nil, model.NewAppError("AddChannelMember", "app.channel.add_member.deleted_user.app_error", nil, "", http.StatusForbidden)
|
|
}
|
|
|
|
var userRequestor *model.User
|
|
if opts.UserRequestorID != "" {
|
|
if userRequestor, err = a.GetUser(opts.UserRequestorID); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
cm, err := a.AddUserToChannel(rctx, user, channel, opts.SkipTeamMemberIntegrityCheck)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
a.Srv().Go(func() {
|
|
pluginContext := pluginContext(rctx)
|
|
a.ch.RunMultiHook(func(hooks plugin.Hooks, _ *model.Manifest) bool {
|
|
hooks.UserHasJoinedChannel(pluginContext, cm, userRequestor)
|
|
return true
|
|
}, plugin.UserHasJoinedChannelID)
|
|
})
|
|
|
|
if opts.UserRequestorID == "" || userID == opts.UserRequestorID {
|
|
if err := a.postJoinChannelMessage(rctx, user, channel); err != nil {
|
|
return nil, err
|
|
}
|
|
} else {
|
|
a.Srv().Go(func() {
|
|
if err := a.PostAddToChannelMessage(rctx, userRequestor, user, channel, opts.PostRootID); err != nil {
|
|
rctx.Logger().Error("Failed to post AddToChannel message", mlog.Err(err))
|
|
}
|
|
})
|
|
}
|
|
|
|
return cm, nil
|
|
}
|
|
|
|
func (a *App) AddDirectChannels(rctx request.CTX, teamID string, user *model.User) *model.AppError {
|
|
var profiles []*model.User
|
|
options := &model.UserGetOptions{InTeamId: teamID, Page: 0, PerPage: 100}
|
|
profiles, err := a.Srv().Store().User().GetProfiles(options)
|
|
if err != nil {
|
|
return model.NewAppError("AddDirectChannels", "api.user.add_direct_channels_and_forget.failed.error", map[string]any{"UserId": user.Id, "TeamId": teamID, "Error": err.Error()}, "", http.StatusInternalServerError)
|
|
}
|
|
|
|
var preferences model.Preferences
|
|
|
|
for _, profile := range profiles {
|
|
if profile.Id == user.Id {
|
|
continue
|
|
}
|
|
|
|
preference := model.Preference{
|
|
UserId: user.Id,
|
|
Category: model.PreferenceCategoryDirectChannelShow,
|
|
Name: profile.Id,
|
|
Value: "true",
|
|
}
|
|
|
|
preferences = append(preferences, preference)
|
|
|
|
if len(preferences) >= 10 {
|
|
break
|
|
}
|
|
}
|
|
|
|
if err := a.Srv().Store().Preference().Save(preferences); err != nil {
|
|
return model.NewAppError("AddDirectChannels", "api.user.add_direct_channels_and_forget.failed.error", map[string]any{"UserId": user.Id, "TeamId": teamID, "Error": err.Error()}, "", http.StatusInternalServerError)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) PostUpdateChannelHeaderMessage(rctx request.CTX, userID string, channel *model.Channel, oldChannelHeader, newChannelHeader string) *model.AppError {
|
|
user, err := a.Srv().Store().User().Get(context.Background(), userID)
|
|
if err != nil {
|
|
return model.NewAppError("PostUpdateChannelHeaderMessage", "api.channel.post_update_channel_header_message_and_forget.retrieve_user.error", nil, "", http.StatusBadRequest).Wrap(err)
|
|
}
|
|
|
|
var message string
|
|
if oldChannelHeader == "" {
|
|
message = fmt.Sprintf(i18n.T("api.channel.post_update_channel_header_message_and_forget.updated_to"), user.Username, newChannelHeader)
|
|
} else if newChannelHeader == "" {
|
|
message = fmt.Sprintf(i18n.T("api.channel.post_update_channel_header_message_and_forget.removed"), user.Username, oldChannelHeader)
|
|
} else {
|
|
message = fmt.Sprintf(i18n.T("api.channel.post_update_channel_header_message_and_forget.updated_from"), user.Username, oldChannelHeader, newChannelHeader)
|
|
}
|
|
|
|
post := &model.Post{
|
|
ChannelId: channel.Id,
|
|
Message: message,
|
|
Type: model.PostTypeHeaderChange,
|
|
UserId: userID,
|
|
Props: model.StringInterface{
|
|
"username": user.Username,
|
|
"old_header": oldChannelHeader,
|
|
"new_header": newChannelHeader,
|
|
},
|
|
}
|
|
|
|
if _, err := a.CreatePost(rctx, post, channel, model.CreatePostFlags{SetOnline: true}); err != nil {
|
|
return model.NewAppError("", "api.channel.post_update_channel_header_message_and_forget.post.error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) PostUpdateChannelPurposeMessage(rctx request.CTX, userID string, channel *model.Channel, oldChannelPurpose string, newChannelPurpose string) *model.AppError {
|
|
user, err := a.Srv().Store().User().Get(context.Background(), userID)
|
|
if err != nil {
|
|
return model.NewAppError("PostUpdateChannelPurposeMessage", "app.channel.post_update_channel_purpose_message.retrieve_user.error", nil, "", http.StatusBadRequest).Wrap(err)
|
|
}
|
|
|
|
var message string
|
|
if oldChannelPurpose == "" {
|
|
message = fmt.Sprintf(i18n.T("app.channel.post_update_channel_purpose_message.updated_to"), user.Username, newChannelPurpose)
|
|
} else if newChannelPurpose == "" {
|
|
message = fmt.Sprintf(i18n.T("app.channel.post_update_channel_purpose_message.removed"), user.Username, oldChannelPurpose)
|
|
} else {
|
|
message = fmt.Sprintf(i18n.T("app.channel.post_update_channel_purpose_message.updated_from"), user.Username, oldChannelPurpose, newChannelPurpose)
|
|
}
|
|
|
|
post := &model.Post{
|
|
ChannelId: channel.Id,
|
|
Message: message,
|
|
Type: model.PostTypePurposeChange,
|
|
UserId: userID,
|
|
Props: model.StringInterface{
|
|
"username": user.Username,
|
|
"old_purpose": oldChannelPurpose,
|
|
"new_purpose": newChannelPurpose,
|
|
},
|
|
}
|
|
if _, err := a.CreatePost(rctx, post, channel, model.CreatePostFlags{SetOnline: true}); err != nil {
|
|
return model.NewAppError("", "app.channel.post_update_channel_purpose_message.post.error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) PostUpdateChannelDisplayNameMessage(rctx request.CTX, userID string, channel *model.Channel, oldChannelDisplayName, newChannelDisplayName string) *model.AppError {
|
|
user, err := a.Srv().Store().User().Get(context.Background(), userID)
|
|
if err != nil {
|
|
return model.NewAppError("PostUpdateChannelDisplayNameMessage", "api.channel.post_update_channel_displayname_message_and_forget.retrieve_user.error", nil, "", http.StatusBadRequest).Wrap(err)
|
|
}
|
|
|
|
message := fmt.Sprintf(i18n.T("api.channel.post_update_channel_displayname_message_and_forget.updated_from"), user.Username, oldChannelDisplayName, newChannelDisplayName)
|
|
|
|
post := &model.Post{
|
|
ChannelId: channel.Id,
|
|
Message: message,
|
|
Type: model.PostTypeDisplaynameChange,
|
|
UserId: userID,
|
|
Props: model.StringInterface{
|
|
"username": user.Username,
|
|
"old_displayname": oldChannelDisplayName,
|
|
"new_displayname": newChannelDisplayName,
|
|
},
|
|
}
|
|
|
|
if _, err := a.CreatePost(rctx, post, channel, model.CreatePostFlags{SetOnline: true}); err != nil {
|
|
return model.NewAppError("PostUpdateChannelDisplayNameMessage", "api.channel.post_update_channel_displayname_message_and_forget.create_post.error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) GetChannel(rctx request.CTX, channelID string) (*model.Channel, *model.AppError) {
|
|
return a.Srv().getChannel(rctx, channelID)
|
|
}
|
|
|
|
func (s *Server) getChannel(rctx request.CTX, channelID string) (*model.Channel, *model.AppError) {
|
|
channel, err := s.Store().Channel().Get(channelID, true)
|
|
if err != nil {
|
|
errCtx := map[string]any{"channel_id": channelID}
|
|
var nfErr *store.ErrNotFound
|
|
switch {
|
|
case errors.As(err, &nfErr):
|
|
return nil, model.NewAppError("GetChannel", "app.channel.get.existing.app_error", errCtx, "", http.StatusNotFound).Wrap(err)
|
|
default:
|
|
return nil, model.NewAppError("GetChannel", "app.channel.get.find.app_error", errCtx, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
}
|
|
return channel, nil
|
|
}
|
|
|
|
func (a *App) GetChannels(rctx request.CTX, channelIDs []string) ([]*model.Channel, *model.AppError) {
|
|
channels, err := a.Srv().Store().Channel().GetMany(channelIDs, true)
|
|
if err != nil {
|
|
errCtx := map[string]any{"channel_id": channelIDs}
|
|
var nfErr *store.ErrNotFound
|
|
switch {
|
|
case errors.As(err, &nfErr):
|
|
return nil, model.NewAppError("GetChannel", "app.channel.get.existing.app_error", errCtx, "", http.StatusNotFound).Wrap(err)
|
|
default:
|
|
return nil, model.NewAppError("GetChannel", "app.channel.get.find.app_error", errCtx, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
}
|
|
return channels, nil
|
|
}
|
|
|
|
func (a *App) GetChannelsMemberCount(rctx request.CTX, channelIDs []string) (map[string]int64, *model.AppError) {
|
|
channelsCount, err := a.Srv().Store().Channel().GetChannelsMemberCount(channelIDs)
|
|
if err != nil {
|
|
var nfErr *store.ErrNotFound
|
|
switch {
|
|
case errors.As(err, &nfErr):
|
|
return nil, model.NewAppError("GetChannelsMemberCount", "app.channel.get_channels_member_count.existing.app_error", nil, "", http.StatusNotFound).Wrap(err)
|
|
default:
|
|
return nil, model.NewAppError("GetChannelsMemberCount", "app.channel.get_channels_member_count.find.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
}
|
|
return channelsCount, nil
|
|
}
|
|
|
|
func (a *App) GetChannelByName(rctx request.CTX, channelName, teamID string, includeDeleted bool) (*model.Channel, *model.AppError) {
|
|
var channel *model.Channel
|
|
var err error
|
|
|
|
if includeDeleted {
|
|
channel, err = a.Srv().Store().Channel().GetByNameIncludeDeleted(teamID, channelName, false)
|
|
} else {
|
|
channel, err = a.Srv().Store().Channel().GetByName(teamID, channelName, false)
|
|
}
|
|
|
|
if err != nil {
|
|
var nfErr *store.ErrNotFound
|
|
switch {
|
|
case errors.As(err, &nfErr):
|
|
return nil, model.NewAppError("GetChannelByName", "app.channel.get_by_name.missing.app_error", nil, "", http.StatusNotFound).Wrap(err)
|
|
default:
|
|
return nil, model.NewAppError("GetChannelByName", "app.channel.get_by_name.existing.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
}
|
|
|
|
return channel, nil
|
|
}
|
|
|
|
func (a *App) GetChannelsByNames(rctx request.CTX, channelNames []string, teamID string) ([]*model.Channel, *model.AppError) {
|
|
channels, err := a.Srv().Store().Channel().GetByNames(teamID, channelNames, true)
|
|
if err != nil {
|
|
return nil, model.NewAppError("GetChannelsByNames", "app.channel.get_by_name.existing.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
return channels, nil
|
|
}
|
|
|
|
func (a *App) GetChannelByNameForTeamName(rctx request.CTX, channelName, teamName string, includeDeleted bool) (*model.Channel, *model.AppError) {
|
|
var team *model.Team
|
|
|
|
team, err := a.Srv().Store().Team().GetByName(teamName)
|
|
if err != nil {
|
|
var nfErr *store.ErrNotFound
|
|
switch {
|
|
case errors.As(err, &nfErr):
|
|
return nil, model.NewAppError("GetChannelByNameForTeamName", "app.team.get_by_name.missing.app_error", nil, "", http.StatusNotFound).Wrap(err)
|
|
default:
|
|
return nil, model.NewAppError("GetChannelByNameForTeamName", "app.team.get_by_name.app_error", nil, "", http.StatusNotFound).Wrap(err)
|
|
}
|
|
}
|
|
|
|
var result *model.Channel
|
|
|
|
var nErr error
|
|
if includeDeleted {
|
|
result, nErr = a.Srv().Store().Channel().GetByNameIncludeDeleted(team.Id, channelName, false)
|
|
} else {
|
|
result, nErr = a.Srv().Store().Channel().GetByName(team.Id, channelName, false)
|
|
}
|
|
|
|
if nErr != nil {
|
|
var nfErr *store.ErrNotFound
|
|
switch {
|
|
case errors.As(nErr, &nfErr):
|
|
return nil, model.NewAppError("GetChannelByNameForTeamName", "app.channel.get_by_name.missing.app_error", nil, "", http.StatusNotFound).Wrap(nErr)
|
|
default:
|
|
return nil, model.NewAppError("GetChannelByNameForTeamName", "app.channel.get_by_name.existing.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
|
|
}
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
func (s *Server) getChannelsForTeamForUser(rctx request.CTX, teamID string, userID string, opts *model.ChannelSearchOpts) (model.ChannelList, *model.AppError) {
|
|
list, err := s.Store().Channel().GetChannels(teamID, userID, opts)
|
|
if err != nil {
|
|
var nfErr *store.ErrNotFound
|
|
switch {
|
|
case errors.As(err, &nfErr):
|
|
return nil, model.NewAppError("GetChannelsForUser", "app.channel.get_channels.not_found.app_error", nil, "", http.StatusNotFound).Wrap(err)
|
|
default:
|
|
return nil, model.NewAppError("GetChannelsForUser", "app.channel.get_channels.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
}
|
|
|
|
return list, nil
|
|
}
|
|
|
|
func (a *App) GetChannelsForTeamForUser(rctx request.CTX, teamID string, userID string, opts *model.ChannelSearchOpts) (model.ChannelList, *model.AppError) {
|
|
return a.Srv().getChannelsForTeamForUser(rctx, teamID, userID, opts)
|
|
}
|
|
|
|
func (a *App) GetChannelsForUser(rctx request.CTX, userID string, includeDeleted bool, lastDeleteAt, pageSize int, fromChannelID string) (model.ChannelList, *model.AppError) {
|
|
list, err := a.Srv().Store().Channel().GetChannelsByUser(userID, includeDeleted, lastDeleteAt, pageSize, fromChannelID)
|
|
if err != nil {
|
|
var nfErr *store.ErrNotFound
|
|
switch {
|
|
case errors.As(err, &nfErr):
|
|
return nil, model.NewAppError("GetChannelsForUser", "app.channel.get_channels.not_found.app_error", nil, "", http.StatusNotFound).Wrap(err)
|
|
default:
|
|
return nil, model.NewAppError("GetChannelsForUser", "app.channel.get_channels.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
}
|
|
|
|
return list, nil
|
|
}
|
|
|
|
func (a *App) GetAllChannels(rctx request.CTX, page, perPage int, opts model.ChannelSearchOpts) (model.ChannelListWithTeamData, *model.AppError) {
|
|
if opts.ExcludeDefaultChannels {
|
|
opts.ExcludeChannelNames = a.DefaultChannelNames(rctx)
|
|
}
|
|
storeOpts := store.ChannelSearchOpts{
|
|
NotAssociatedToGroup: opts.NotAssociatedToGroup,
|
|
IncludeDeleted: opts.IncludeDeleted,
|
|
ExcludeChannelNames: opts.ExcludeChannelNames,
|
|
GroupConstrained: opts.GroupConstrained,
|
|
ExcludeGroupConstrained: opts.ExcludeGroupConstrained,
|
|
ExcludePolicyConstrained: opts.ExcludePolicyConstrained,
|
|
IncludePolicyID: opts.IncludePolicyID,
|
|
AccessControlPolicyEnforced: opts.AccessControlPolicyEnforced,
|
|
ExcludeAccessControlPolicyEnforced: opts.ExcludeAccessControlPolicyEnforced,
|
|
}
|
|
channels, err := a.Srv().Store().Channel().GetAllChannels(page*perPage, perPage, storeOpts)
|
|
if err != nil {
|
|
return nil, model.NewAppError("GetAllChannels", "app.channel.get_all_channels.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
return channels, nil
|
|
}
|
|
|
|
func (a *App) GetAllChannelsCount(rctx request.CTX, opts model.ChannelSearchOpts) (int64, *model.AppError) {
|
|
if opts.ExcludeDefaultChannels {
|
|
opts.ExcludeChannelNames = a.DefaultChannelNames(rctx)
|
|
}
|
|
storeOpts := store.ChannelSearchOpts{
|
|
NotAssociatedToGroup: opts.NotAssociatedToGroup,
|
|
IncludeDeleted: opts.IncludeDeleted,
|
|
ExcludeChannelNames: opts.ExcludeChannelNames,
|
|
GroupConstrained: opts.GroupConstrained,
|
|
ExcludeGroupConstrained: opts.ExcludeGroupConstrained,
|
|
ExcludePolicyConstrained: opts.ExcludePolicyConstrained,
|
|
IncludePolicyID: opts.IncludePolicyID,
|
|
}
|
|
count, err := a.Srv().Store().Channel().GetAllChannelsCount(storeOpts)
|
|
if err != nil {
|
|
return 0, model.NewAppError("GetAllChannelsCount", "app.channel.get_all_channels_count.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
return count, nil
|
|
}
|
|
|
|
func (a *App) GetDeletedChannels(rctx request.CTX, teamID string, offset int, limit int, userID string, skipTeamMembershipCheck bool) (model.ChannelList, *model.AppError) {
|
|
list, err := a.Srv().Store().Channel().GetDeleted(teamID, offset, limit, userID, skipTeamMembershipCheck)
|
|
if err != nil {
|
|
var nfErr *store.ErrNotFound
|
|
switch {
|
|
case errors.As(err, &nfErr):
|
|
return nil, model.NewAppError("GetDeletedChannels", "app.channel.get_deleted.missing.app_error", nil, "", http.StatusNotFound).Wrap(err)
|
|
default:
|
|
return nil, model.NewAppError("GetDeletedChannels", "app.channel.get_deleted.existing.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
}
|
|
|
|
return list, nil
|
|
}
|
|
|
|
func (a *App) GetChannelsUserNotIn(rctx request.CTX, teamID string, userID string, offset int, limit int) (model.ChannelList, *model.AppError) {
|
|
channels, err := a.Srv().Store().Channel().GetMoreChannels(teamID, userID, offset, limit)
|
|
if err != nil {
|
|
return nil, model.NewAppError("GetChannelsUserNotIn", "app.channel.get_more_channels.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
return channels, nil
|
|
}
|
|
|
|
func (a *App) GetPublicChannelsByIdsForTeam(rctx request.CTX, teamID string, channelIDs []string) (model.ChannelList, *model.AppError) {
|
|
list, err := a.Srv().Store().Channel().GetPublicChannelsByIdsForTeam(teamID, channelIDs)
|
|
if err != nil {
|
|
var nfErr *store.ErrNotFound
|
|
switch {
|
|
case errors.As(err, &nfErr):
|
|
return nil, model.NewAppError("GetPublicChannelsByIdsForTeam", "app.channel.get_channels_by_ids.not_found.app_error", nil, "", http.StatusNotFound).Wrap(err)
|
|
default:
|
|
return nil, model.NewAppError("GetPublicChannelsByIdsForTeam", "app.channel.get_channels_by_ids.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
}
|
|
|
|
return list, nil
|
|
}
|
|
|
|
func (a *App) GetPublicChannelsForTeam(rctx request.CTX, teamID string, offset int, limit int) (model.ChannelList, *model.AppError) {
|
|
list, err := a.Srv().Store().Channel().GetPublicChannelsForTeam(teamID, offset, limit)
|
|
if err != nil {
|
|
return nil, model.NewAppError("GetPublicChannelsForTeam", "app.channel.get_public_channels.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
return list, nil
|
|
}
|
|
|
|
func (a *App) GetPrivateChannelsForTeam(rctx request.CTX, teamID string, offset int, limit int) (model.ChannelList, *model.AppError) {
|
|
list, err := a.Srv().Store().Channel().GetPrivateChannelsForTeam(teamID, offset, limit)
|
|
if err != nil {
|
|
return nil, model.NewAppError("GetPrivateChannelsForTeam", "app.channel.get_private_channels.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
return list, nil
|
|
}
|
|
|
|
func (a *App) GetChannelMember(rctx request.CTX, channelID string, userID string) (*model.ChannelMember, *model.AppError) {
|
|
return a.Srv().getChannelMember(rctx, channelID, userID)
|
|
}
|
|
|
|
func (s *Server) getChannelMember(rctx request.CTX, channelID string, userID string) (*model.ChannelMember, *model.AppError) {
|
|
channelMember, err := s.Store().Channel().GetMember(rctx, channelID, userID)
|
|
if err != nil {
|
|
var nfErr *store.ErrNotFound
|
|
switch {
|
|
case errors.As(err, &nfErr):
|
|
return nil, model.NewAppError("GetChannelMember", MissingChannelMemberError, nil, "", http.StatusNotFound).Wrap(err)
|
|
default:
|
|
return nil, model.NewAppError("GetChannelMember", "app.channel.get_member.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
}
|
|
|
|
return channelMember, nil
|
|
}
|
|
|
|
func (s *Server) getChannelMemberLastViewedAt(rctx request.CTX, channelID string, userID string) (int64, *model.AppError) {
|
|
lastViewedAt, err := s.Store().Channel().GetMemberLastViewedAt(rctx, channelID, userID)
|
|
if err != nil {
|
|
var nfErr *store.ErrNotFound
|
|
switch {
|
|
case errors.As(err, &nfErr):
|
|
return 0, model.NewAppError("getChannelMemberLastViewedAt", MissingChannelMemberError, nil, "", http.StatusNotFound).Wrap(err)
|
|
default:
|
|
return 0, model.NewAppError("getChannelMemberLastViewedAt", "app.channel.get_member.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
}
|
|
|
|
return lastViewedAt, nil
|
|
}
|
|
|
|
func (a *App) GetChannelMembersPage(rctx request.CTX, channelID string, page, perPage int) (model.ChannelMembers, *model.AppError) {
|
|
opts := model.ChannelMembersGetOptions{
|
|
ChannelID: channelID,
|
|
Offset: page * perPage,
|
|
Limit: perPage,
|
|
}
|
|
channelMembers, err := a.Srv().Store().Channel().GetMembers(opts)
|
|
if err != nil {
|
|
return nil, model.NewAppError("GetChannelMembersPage", "app.channel.get_members.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
return channelMembers, nil
|
|
}
|
|
|
|
func (a *App) GetChannelMembersTimezones(rctx request.CTX, channelID string) ([]string, *model.AppError) {
|
|
membersTimezones, err := a.Srv().Store().Channel().GetChannelMembersTimezones(channelID)
|
|
if err != nil {
|
|
return nil, model.NewAppError("GetChannelMembersTimezones", "app.channel.get_members.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
var timezones []string
|
|
for _, membersTimezone := range membersTimezones {
|
|
if membersTimezone["automaticTimezone"] == "" && membersTimezone["manualTimezone"] == "" {
|
|
continue
|
|
}
|
|
timezones = append(timezones, model.GetPreferredTimezone(membersTimezone))
|
|
}
|
|
|
|
return model.RemoveDuplicateStrings(timezones), nil
|
|
}
|
|
|
|
func (a *App) GetChannelMembersByIds(rctx request.CTX, channelID string, userIDs []string) (model.ChannelMembers, *model.AppError) {
|
|
members, err := a.Srv().Store().Channel().GetMembersByIds(channelID, userIDs)
|
|
if err != nil {
|
|
return nil, model.NewAppError("GetChannelMembersByIds", "app.channel.get_members_by_ids.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
return members, nil
|
|
}
|
|
|
|
func (a *App) GetChannelMembersForUser(rctx request.CTX, teamID string, userID string) (model.ChannelMembers, *model.AppError) {
|
|
channelMembers, err := a.Srv().Store().Channel().GetMembersForUser(teamID, userID)
|
|
if err != nil {
|
|
return nil, model.NewAppError("GetChannelMembersForUser", "app.channel.get_members.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
return channelMembers, nil
|
|
}
|
|
|
|
func (a *App) GetChannelMembersForUserWithPagination(rctx request.CTX, userID string, page, perPage int) ([]*model.ChannelMember, *model.AppError) {
|
|
m, err := a.Srv().Store().Channel().GetMembersForUserWithPagination(userID, page, perPage)
|
|
if err != nil {
|
|
return nil, model.NewAppError("GetChannelMembersForUserWithPagination", "app.channel.get_members.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
members := make([]*model.ChannelMember, 0, len(m))
|
|
for _, member := range m {
|
|
members = append(members, &member.ChannelMember)
|
|
}
|
|
return members, nil
|
|
}
|
|
|
|
func (a *App) GetChannelMembersWithTeamDataForUserWithPagination(rctx request.CTX, userID string, cursor *model.ChannelMemberCursor) (model.ChannelMembersWithTeamData, *model.AppError) {
|
|
var m model.ChannelMembersWithTeamData
|
|
var err error
|
|
var method string
|
|
if cursor.Page == -1 {
|
|
m, err = a.Srv().Store().Channel().GetMembersForUserWithCursorPagination(userID, cursor.PerPage, cursor.FromChannelID)
|
|
method = "GetMembersForUserWithCursorPagination"
|
|
} else {
|
|
m, err = a.Srv().Store().Channel().GetMembersForUserWithPagination(userID, cursor.Page, cursor.PerPage)
|
|
method = "GetMembersForUserWithPagination"
|
|
}
|
|
if err != nil {
|
|
var nfErr *store.ErrNotFound
|
|
switch {
|
|
case errors.As(err, &nfErr):
|
|
return nil, model.NewAppError(method, MissingChannelMemberError, nil, "", http.StatusNotFound).Wrap(err)
|
|
default:
|
|
return nil, model.NewAppError(method, "app.channel.get_members.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
}
|
|
|
|
return m, nil
|
|
}
|
|
|
|
func (a *App) GetChannelMemberCount(rctx request.CTX, channelID string) (int64, *model.AppError) {
|
|
count, err := a.Srv().Store().Channel().GetMemberCount(channelID, true)
|
|
if err != nil {
|
|
return 0, model.NewAppError("GetChannelMemberCount", "app.channel.get_member_count.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
return count, nil
|
|
}
|
|
|
|
func (a *App) GetChannelFileCount(rctx request.CTX, channelID string) (int64, *model.AppError) {
|
|
count, err := a.Srv().Store().Channel().GetFileCount(channelID)
|
|
if err != nil {
|
|
return 0, model.NewAppError("SqlChannelStore.GetFileCount", "app.channel.get_file_count.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
return count, nil
|
|
}
|
|
|
|
func (a *App) GetChannelGuestCount(rctx request.CTX, channelID string) (int64, *model.AppError) {
|
|
count, err := a.Srv().Store().Channel().GetGuestCount(channelID, true)
|
|
if err != nil {
|
|
return 0, model.NewAppError("SqlChannelStore.GetGuestCount", "app.channel.get_member_count.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
return count, nil
|
|
}
|
|
|
|
func (a *App) GetChannelPinnedPostCount(rctx request.CTX, channelID string) (int64, *model.AppError) {
|
|
count, err := a.Srv().Store().Channel().GetPinnedPostCount(channelID, true)
|
|
if err != nil {
|
|
return 0, model.NewAppError("GetChannelPinnedPostCount", "app.channel.get_pinnedpost_count.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
return count, nil
|
|
}
|
|
|
|
func (a *App) GetChannelCounts(rctx request.CTX, teamID string, userID string) (*model.ChannelCounts, *model.AppError) {
|
|
counts, err := a.Srv().Store().Channel().GetChannelCounts(teamID, userID)
|
|
if err != nil {
|
|
return nil, model.NewAppError("SqlChannelStore.GetChannelCounts", "app.channel.get_channel_counts.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
return counts, nil
|
|
}
|
|
|
|
func (a *App) GetChannelUnread(rctx request.CTX, channelID, userID string) (*model.ChannelUnread, *model.AppError) {
|
|
channelUnread, err := a.Srv().Store().Channel().GetChannelUnread(channelID, userID)
|
|
if err != nil {
|
|
var nfErr *store.ErrNotFound
|
|
switch {
|
|
case errors.As(err, &nfErr):
|
|
return nil, model.NewAppError("GetChannelUnread", "app.channel.get_unread.app_error", nil, "", http.StatusNotFound).Wrap(err)
|
|
default:
|
|
return nil, model.NewAppError("GetChannelUnread", "app.channel.get_unread.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
}
|
|
|
|
if channelUnread.NotifyProps[model.MarkUnreadNotifyProp] == model.ChannelMarkUnreadMention {
|
|
channelUnread.MsgCount = 0
|
|
channelUnread.MsgCountRoot = 0
|
|
}
|
|
return channelUnread, nil
|
|
}
|
|
|
|
func (a *App) JoinChannel(rctx request.CTX, channel *model.Channel, userID string) *model.AppError {
|
|
userChan := make(chan store.StoreResult[*model.User], 1)
|
|
memberChan := make(chan store.StoreResult[*model.ChannelMember], 1)
|
|
go func() {
|
|
user, err := a.Srv().Store().User().Get(context.Background(), userID)
|
|
userChan <- store.StoreResult[*model.User]{Data: user, NErr: err}
|
|
close(userChan)
|
|
}()
|
|
go func() {
|
|
member, err := a.Srv().Store().Channel().GetMember(rctx, channel.Id, userID)
|
|
memberChan <- store.StoreResult[*model.ChannelMember]{Data: member, NErr: err}
|
|
close(memberChan)
|
|
}()
|
|
|
|
uresult := <-userChan
|
|
if uresult.NErr != nil {
|
|
var nfErr *store.ErrNotFound
|
|
switch {
|
|
case errors.As(uresult.NErr, &nfErr):
|
|
return model.NewAppError("CreateChannel", MissingAccountError, nil, "", http.StatusNotFound).Wrap(uresult.NErr)
|
|
default:
|
|
return model.NewAppError("CreateChannel", "app.user.get.app_error", nil, "", http.StatusInternalServerError).Wrap(uresult.NErr)
|
|
}
|
|
}
|
|
|
|
mresult := <-memberChan
|
|
if mresult.NErr == nil && mresult.Data != nil {
|
|
// user is already in the channel
|
|
return nil
|
|
}
|
|
|
|
user := uresult.Data
|
|
|
|
if channel.Type != model.ChannelTypeOpen {
|
|
return model.NewAppError("JoinChannel", "api.channel.join_channel.permissions.app_error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
cm, err := a.AddUserToChannel(rctx, user, channel, false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
a.Srv().Go(func() {
|
|
pluginContext := pluginContext(rctx)
|
|
a.ch.RunMultiHook(func(hooks plugin.Hooks, _ *model.Manifest) bool {
|
|
hooks.UserHasJoinedChannel(pluginContext, cm, nil)
|
|
return true
|
|
}, plugin.UserHasJoinedChannelID)
|
|
})
|
|
|
|
if err := a.postJoinChannelMessage(rctx, user, channel); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) postJoinChannelMessage(rctx request.CTX, user *model.User, channel *model.Channel) *model.AppError {
|
|
message := fmt.Sprintf(i18n.T("api.channel.join_channel.post_and_forget"), user.Username)
|
|
postType := model.PostTypeJoinChannel
|
|
|
|
if user.IsGuest() {
|
|
message = fmt.Sprintf(i18n.T("api.channel.guest_join_channel.post_and_forget"), user.Username)
|
|
postType = model.PostTypeGuestJoinChannel
|
|
}
|
|
|
|
post := &model.Post{
|
|
ChannelId: channel.Id,
|
|
Message: message,
|
|
Type: postType,
|
|
UserId: user.Id,
|
|
Props: model.StringInterface{
|
|
"username": user.Username,
|
|
},
|
|
}
|
|
|
|
if _, err := a.CreatePost(rctx, post, channel, model.CreatePostFlags{SetOnline: true}); err != nil {
|
|
return model.NewAppError("postJoinChannelMessage", "api.channel.post_user_add_remove_message_and_forget.error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) postJoinTeamMessage(rctx request.CTX, user *model.User, channel *model.Channel) *model.AppError {
|
|
post := &model.Post{
|
|
ChannelId: channel.Id,
|
|
Message: fmt.Sprintf(i18n.T("api.team.join_team.post_and_forget"), user.Username),
|
|
Type: model.PostTypeJoinTeam,
|
|
UserId: user.Id,
|
|
Props: model.StringInterface{
|
|
"username": user.Username,
|
|
},
|
|
}
|
|
|
|
if _, err := a.CreatePost(rctx, post, channel, model.CreatePostFlags{SetOnline: true}); err != nil {
|
|
return model.NewAppError("postJoinTeamMessage", "api.channel.post_user_add_remove_message_and_forget.error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) LeaveChannel(rctx request.CTX, channelID string, userID string) *model.AppError {
|
|
sc := make(chan store.StoreResult[*model.Channel], 1)
|
|
go func() {
|
|
channel, err := a.Srv().Store().Channel().Get(channelID, true)
|
|
sc <- store.StoreResult[*model.Channel]{Data: channel, NErr: err}
|
|
close(sc)
|
|
}()
|
|
|
|
uc := make(chan store.StoreResult[*model.User], 1)
|
|
go func() {
|
|
user, err := a.Srv().Store().User().Get(context.Background(), userID)
|
|
uc <- store.StoreResult[*model.User]{Data: user, NErr: err}
|
|
close(uc)
|
|
}()
|
|
|
|
cresult := <-sc
|
|
if cresult.NErr != nil {
|
|
errCtx := map[string]any{"channel_id": channelID}
|
|
var nfErr *store.ErrNotFound
|
|
switch {
|
|
case errors.As(cresult.NErr, &nfErr):
|
|
return model.NewAppError("LeaveChannel", "app.channel.get.existing.app_error", errCtx, "", http.StatusNotFound).Wrap(cresult.NErr)
|
|
default:
|
|
return model.NewAppError("LeaveChannel", "app.channel.get.find.app_error", errCtx, "", http.StatusInternalServerError).Wrap(cresult.NErr)
|
|
}
|
|
}
|
|
uresult := <-uc
|
|
if uresult.NErr != nil {
|
|
var nfErr *store.ErrNotFound
|
|
switch {
|
|
case errors.As(uresult.NErr, &nfErr):
|
|
return model.NewAppError("LeaveChannel", MissingAccountError, nil, "", http.StatusNotFound).Wrap(uresult.NErr)
|
|
default:
|
|
return model.NewAppError("LeaveChannel", "app.user.get.app_error", nil, "", http.StatusInternalServerError).Wrap(uresult.NErr)
|
|
}
|
|
}
|
|
|
|
channel := cresult.Data
|
|
user := uresult.Data
|
|
|
|
if channel.IsGroupOrDirect() {
|
|
err := model.NewAppError("LeaveChannel", "api.channel.leave.direct.app_error", nil, "", http.StatusBadRequest)
|
|
return err
|
|
}
|
|
|
|
if err := a.removeUserFromChannel(rctx, userID, userID, channel); err != nil {
|
|
return err
|
|
}
|
|
|
|
if channel.Name == model.DefaultChannelName && !*a.Config().ServiceSettings.ExperimentalEnableDefaultChannelLeaveJoinMessages {
|
|
return nil
|
|
}
|
|
|
|
a.Srv().Go(func() {
|
|
if err := a.postLeaveChannelMessage(rctx, user, channel); err != nil {
|
|
rctx.Logger().Error("Failed to post LeaveChannel message", mlog.Err(err))
|
|
}
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) postLeaveChannelMessage(rctx request.CTX, user *model.User, channel *model.Channel) *model.AppError {
|
|
post := &model.Post{
|
|
ChannelId: channel.Id,
|
|
// Message here embeds `@username`, not just `username`, to ensure that mentions
|
|
// treat this as a username mention even though the user has now left the channel.
|
|
// The client renders its own system message, ignoring this value altogether.
|
|
Message: fmt.Sprintf(i18n.T("api.channel.leave.left"), fmt.Sprintf("@%s", user.Username)),
|
|
Type: model.PostTypeLeaveChannel,
|
|
UserId: user.Id,
|
|
Props: model.StringInterface{
|
|
"username": user.Username,
|
|
},
|
|
}
|
|
|
|
if _, err := a.CreatePost(rctx, post, channel, model.CreatePostFlags{SetOnline: true}); err != nil {
|
|
return model.NewAppError("postLeaveChannelMessage", "api.channel.post_user_add_remove_message_and_forget.error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) PostAddToChannelMessage(rctx request.CTX, user *model.User, addedUser *model.User, channel *model.Channel, postRootId string) *model.AppError {
|
|
message := fmt.Sprintf(i18n.T("api.channel.add_member.added"), addedUser.Username, user.Username)
|
|
postType := model.PostTypeAddToChannel
|
|
|
|
if addedUser.IsGuest() {
|
|
message = fmt.Sprintf(i18n.T("api.channel.add_guest.added"), addedUser.Username, user.Username)
|
|
postType = model.PostTypeAddGuestToChannel
|
|
}
|
|
|
|
post := &model.Post{
|
|
ChannelId: channel.Id,
|
|
Message: message,
|
|
Type: postType,
|
|
UserId: user.Id,
|
|
Props: model.StringInterface{
|
|
"userId": user.Id,
|
|
"username": user.Username,
|
|
model.PostPropsAddedUserId: addedUser.Id,
|
|
"addedUsername": addedUser.Username,
|
|
},
|
|
}
|
|
|
|
if _, err := a.CreatePost(rctx, post, channel, model.CreatePostFlags{SetOnline: true}); err != nil {
|
|
return model.NewAppError("postAddToChannelMessage", "api.channel.post_user_add_remove_message_and_forget.error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) postAddToTeamMessage(rctx request.CTX, user *model.User, addedUser *model.User, channel *model.Channel, postRootId string) *model.AppError {
|
|
post := &model.Post{
|
|
ChannelId: channel.Id,
|
|
Message: fmt.Sprintf(i18n.T("api.team.add_user_to_team.added"), addedUser.Username, user.Username),
|
|
Type: model.PostTypeAddToTeam,
|
|
UserId: user.Id,
|
|
RootId: postRootId,
|
|
Props: model.StringInterface{
|
|
"userId": user.Id,
|
|
"username": user.Username,
|
|
model.PostPropsAddedUserId: addedUser.Id,
|
|
"addedUsername": addedUser.Username,
|
|
},
|
|
}
|
|
|
|
if _, err := a.CreatePost(rctx, post, channel, model.CreatePostFlags{SetOnline: true}); err != nil {
|
|
return model.NewAppError("postAddToTeamMessage", "api.channel.post_user_add_remove_message_and_forget.error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) postRemoveFromChannelMessage(rctx request.CTX, removerUserId string, removedUser *model.User, channel *model.Channel) *model.AppError {
|
|
messageUserId := removerUserId
|
|
if messageUserId == "" {
|
|
systemBot, err := a.GetSystemBot(rctx)
|
|
if err != nil {
|
|
return model.NewAppError("postRemoveFromChannelMessage", "api.channel.post_user_add_remove_message_and_forget.error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
messageUserId = systemBot.UserId
|
|
}
|
|
|
|
post := &model.Post{
|
|
ChannelId: channel.Id,
|
|
// Message here embeds `@username`, not just `username`, to ensure that mentions
|
|
// treat this as a username mention even though the user has now left the channel.
|
|
// The client renders its own system message, ignoring this value altogether.
|
|
Message: fmt.Sprintf(i18n.T("api.channel.remove_member.removed"), fmt.Sprintf("@%s", removedUser.Username)),
|
|
Type: model.PostTypeRemoveFromChannel,
|
|
UserId: messageUserId,
|
|
Props: model.StringInterface{
|
|
"removedUserId": removedUser.Id,
|
|
"removedUsername": removedUser.Username,
|
|
},
|
|
}
|
|
|
|
if _, err := a.CreatePost(rctx, post, channel, model.CreatePostFlags{SetOnline: true}); err != nil {
|
|
return model.NewAppError("postRemoveFromChannelMessage", "api.channel.post_user_add_remove_message_and_forget.error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) removeUserFromChannel(rctx request.CTX, userIDToRemove string, removerUserId string, channel *model.Channel) *model.AppError {
|
|
user, nErr := a.Srv().Store().User().Get(context.Background(), userIDToRemove)
|
|
if nErr != nil {
|
|
var nfErr *store.ErrNotFound
|
|
switch {
|
|
case errors.As(nErr, &nfErr):
|
|
return model.NewAppError("removeUserFromChannel", MissingAccountError, nil, "", http.StatusNotFound).Wrap(nErr)
|
|
default:
|
|
return model.NewAppError("removeUserFromChannel", "app.user.get.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
|
|
}
|
|
}
|
|
isGuest := user.IsGuest()
|
|
|
|
if channel.Name == model.DefaultChannelName {
|
|
if !isGuest {
|
|
return model.NewAppError("RemoveUserFromChannel", "api.channel.remove.default.app_error", map[string]any{"Channel": model.DefaultChannelName}, "", http.StatusBadRequest)
|
|
}
|
|
}
|
|
|
|
if channel.IsGroupConstrained() && userIDToRemove != removerUserId && !user.IsBot {
|
|
nonMembers, err := a.FilterNonGroupChannelMembers(rctx, []string{userIDToRemove}, channel)
|
|
if err != nil {
|
|
return model.NewAppError("removeUserFromChannel", "api.channel.remove_user_from_channel.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
if len(nonMembers) == 0 {
|
|
return model.NewAppError("removeUserFromChannel", "api.channel.remove_members.denied", map[string]any{"UserIDs": nonMembers}, "", http.StatusBadRequest)
|
|
}
|
|
}
|
|
|
|
cm, err := a.GetChannelMember(rctx, channel.Id, userIDToRemove)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := a.Srv().Store().Channel().RemoveMember(rctx, channel.Id, userIDToRemove); err != nil {
|
|
return model.NewAppError("removeUserFromChannel", "app.channel.remove_member.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
if err := a.Srv().Store().ChannelMemberHistory().LogLeaveEvent(userIDToRemove, channel.Id, model.GetMillis()); err != nil {
|
|
return model.NewAppError("removeUserFromChannel", "app.channel_member_history.log_leave_event.internal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
if err := a.Srv().Store().Thread().DeleteMembershipsForChannel(userIDToRemove, channel.Id); err != nil {
|
|
return model.NewAppError("removeUserFromChannel", model.NoTranslation, nil, "failed to delete threadmemberships upon leaving channel", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
if isGuest {
|
|
currentMembers, err := a.GetChannelMembersForUser(rctx, channel.TeamId, userIDToRemove)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(currentMembers) == 0 {
|
|
teamMember, err := a.GetTeamMember(rctx, channel.TeamId, userIDToRemove)
|
|
if err != nil {
|
|
return model.NewAppError("removeUserFromChannel", "api.team.remove_user_from_team.missing.app_error", nil, "", http.StatusBadRequest).Wrap(err)
|
|
}
|
|
|
|
if err := a.ch.srv.teamService.RemoveTeamMember(rctx, teamMember); err != nil {
|
|
return model.NewAppError("removeUserFromChannel", "api.team.remove_user_from_team.missing.app_error", nil, "", http.StatusBadRequest).Wrap(err)
|
|
}
|
|
|
|
if err = a.postProcessTeamMemberLeave(rctx, teamMember, removerUserId); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
a.Srv().Platform().InvalidateChannelCacheForUser(userIDToRemove)
|
|
a.invalidateCacheForChannelMembers(channel.Id)
|
|
|
|
var actorUser *model.User
|
|
if removerUserId != "" {
|
|
actorUser, _ = a.GetUser(removerUserId)
|
|
}
|
|
|
|
a.Srv().Go(func() {
|
|
pluginContext := pluginContext(rctx)
|
|
a.ch.RunMultiHook(func(hooks plugin.Hooks, _ *model.Manifest) bool {
|
|
hooks.UserHasLeftChannel(pluginContext, cm, actorUser)
|
|
return true
|
|
}, plugin.UserHasLeftChannelID)
|
|
})
|
|
|
|
message := model.NewWebSocketEvent(model.WebsocketEventUserRemoved, "", channel.Id, "", nil, "")
|
|
message.Add("user_id", userIDToRemove)
|
|
message.Add("remover_id", removerUserId)
|
|
a.Publish(message)
|
|
|
|
// because the removed user no longer belongs to the channel we need to send a separate websocket event
|
|
userMsg := model.NewWebSocketEvent(model.WebsocketEventUserRemoved, "", "", userIDToRemove, nil, "")
|
|
userMsg.Add("channel_id", channel.Id)
|
|
userMsg.Add("remover_id", removerUserId)
|
|
a.Publish(userMsg)
|
|
|
|
// Synchronize membership change for shared channels
|
|
if channel.IsShared() {
|
|
// isAdd=false, empty remoteId means locally initiated
|
|
if scs := a.Srv().Platform().GetSharedChannelService(); scs != nil {
|
|
scs.HandleMembershipChange(channel.Id, userIDToRemove, false, "")
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) RemoveUserFromChannel(rctx request.CTX, userIDToRemove string, removerUserId string, channel *model.Channel) *model.AppError {
|
|
var err *model.AppError
|
|
|
|
if err = a.removeUserFromChannel(rctx, userIDToRemove, removerUserId, channel); err != nil {
|
|
return err
|
|
}
|
|
|
|
var user *model.User
|
|
if user, err = a.GetUser(userIDToRemove); err != nil {
|
|
return err
|
|
}
|
|
|
|
if userIDToRemove == removerUserId {
|
|
if err := a.postLeaveChannelMessage(rctx, user, channel); err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
if err := a.postRemoveFromChannelMessage(rctx, removerUserId, user, channel); err != nil {
|
|
rctx.Logger().Error("Failed to post user removal message", mlog.Err(err))
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) GetNumberOfChannelsOnTeam(rctx request.CTX, teamID string) (int, *model.AppError) {
|
|
// Get total number of channels on current team
|
|
list, err := a.Srv().Store().Channel().GetTeamChannels(teamID)
|
|
if err != nil {
|
|
var nfErr *store.ErrNotFound
|
|
switch {
|
|
case errors.As(err, &nfErr):
|
|
return 0, model.NewAppError("GetNumberOfChannelsOnTeam", "app.channel.get_channels.not_found.app_error", nil, "", http.StatusNotFound).Wrap(err)
|
|
default:
|
|
return 0, model.NewAppError("GetNumberOfChannelsOnTeam", "app.channel.get_channels.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
}
|
|
return len(list), nil
|
|
}
|
|
|
|
func (a *App) SetActiveChannel(rctx request.CTX, userID string, channelID string) *model.AppError {
|
|
status, err := a.Srv().Platform().GetStatus(userID)
|
|
|
|
oldStatus := model.StatusOffline
|
|
|
|
if err != nil {
|
|
status = &model.Status{UserId: userID, Status: model.StatusOnline, Manual: false, LastActivityAt: model.GetMillis(), ActiveChannel: channelID}
|
|
} else {
|
|
oldStatus = status.Status
|
|
status.ActiveChannel = channelID
|
|
if !status.Manual && channelID != "" {
|
|
status.Status = model.StatusOnline
|
|
}
|
|
status.LastActivityAt = model.GetMillis()
|
|
}
|
|
|
|
a.Srv().Platform().AddStatusCache(status)
|
|
|
|
if status.Status != oldStatus {
|
|
a.Srv().Platform().BroadcastStatus(status)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) IsCRTEnabledForUser(rctx request.CTX, userID string) bool {
|
|
appCRT := *a.Config().ServiceSettings.CollapsedThreads
|
|
if appCRT == model.CollapsedThreadsDisabled {
|
|
return false
|
|
}
|
|
if appCRT == model.CollapsedThreadsAlwaysOn {
|
|
return true
|
|
}
|
|
threadsEnabled := appCRT == model.CollapsedThreadsDefaultOn
|
|
// check if a participant has overridden collapsed threads settings
|
|
if preference, err := a.Srv().Store().Preference().Get(userID, model.PreferenceCategoryDisplaySettings, model.PreferenceNameCollapsedThreadsEnabled); err == nil {
|
|
threadsEnabled = preference.Value == "on"
|
|
}
|
|
return threadsEnabled
|
|
}
|
|
|
|
// ValidateUserPermissionsOnChannels filters channelIds based on whether userId is authorized to manage channel members. Unauthorized channels are removed from the returned list.
|
|
func (a *App) ValidateUserPermissionsOnChannels(rctx request.CTX, userId string, channelIds []string) []string {
|
|
var allowedChannelIds []string
|
|
|
|
for _, channelId := range channelIds {
|
|
channel, err := a.GetChannel(rctx, channelId)
|
|
if err != nil {
|
|
rctx.Logger().Info("Invite users to team - couldn't get channel " + channelId)
|
|
continue
|
|
}
|
|
|
|
if channel.Type == model.ChannelTypePrivate && a.HasPermissionToChannel(rctx, userId, channelId, model.PermissionManagePrivateChannelMembers) {
|
|
allowedChannelIds = append(allowedChannelIds, channelId)
|
|
} else if channel.Type == model.ChannelTypeOpen && a.HasPermissionToChannel(rctx, userId, channelId, model.PermissionManagePublicChannelMembers) {
|
|
allowedChannelIds = append(allowedChannelIds, channelId)
|
|
} else {
|
|
rctx.Logger().Info("Invite users to team - no permission to add members to that channel. UserId: " + userId + " ChannelId: " + channelId)
|
|
}
|
|
}
|
|
return allowedChannelIds
|
|
}
|
|
|
|
// MarkChanelAsUnreadFromPost will take a post and set the channel as unread from that one.
|
|
func (a *App) MarkChannelAsUnreadFromPost(rctx request.CTX, postID string, userID string, collapsedThreadsSupported bool) (*model.ChannelUnreadAt, *model.AppError) {
|
|
if !collapsedThreadsSupported || !a.IsCRTEnabledForUser(rctx, userID) {
|
|
return a.markChannelAsUnreadFromPostCRTUnsupported(rctx, postID, userID)
|
|
}
|
|
post, err := a.GetSinglePost(rctx, postID, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
user, err := a.GetUser(userID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
unreadMentions, unreadMentionsRoot, urgentMentions, err := a.countMentionsFromPost(rctx, user, post)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
channelUnread, nErr := a.Srv().Store().Channel().UpdateLastViewedAtPost(post, userID, unreadMentions, unreadMentionsRoot, urgentMentions, true)
|
|
if nErr != nil {
|
|
return channelUnread, model.NewAppError("MarkChannelAsUnreadFromPost", "app.channel.update_last_viewed_at_post.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
|
|
}
|
|
|
|
a.sendWebSocketPostUnreadEvent(rctx, channelUnread, postID)
|
|
a.UpdateMobileAppBadge(userID)
|
|
|
|
return channelUnread, nil
|
|
}
|
|
|
|
func (a *App) markChannelAsUnreadFromPostCRTUnsupported(rctx request.CTX, postID string, userID string) (*model.ChannelUnreadAt, *model.AppError) {
|
|
post, appErr := a.GetSinglePost(rctx, postID, false)
|
|
if appErr != nil {
|
|
return nil, appErr
|
|
}
|
|
|
|
user, appErr := a.GetUser(userID)
|
|
if appErr != nil {
|
|
return nil, appErr
|
|
}
|
|
|
|
threadId := post.RootId
|
|
if post.RootId == "" {
|
|
threadId = post.Id
|
|
}
|
|
|
|
unreadMentions, unreadMentionsRoot, urgentMentions, appErr := a.countMentionsFromPost(rctx, user, post)
|
|
if appErr != nil {
|
|
return nil, appErr
|
|
}
|
|
|
|
// if root post,
|
|
// In CRT Supported Client: badge on channel only sums mentions in root posts including and below the post that was marked.
|
|
// In CRT Unsupported Client: badge on channel sums mentions in all posts (root & replies) including and below the post that was marked unread.
|
|
if post.RootId == "" {
|
|
channelUnread, nErr := a.Srv().Store().Channel().UpdateLastViewedAtPost(post, userID, unreadMentions, unreadMentionsRoot, urgentMentions, true)
|
|
if nErr != nil {
|
|
return channelUnread, model.NewAppError("MarkChannelAsUnreadFromPost", "app.channel.update_last_viewed_at_post.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
|
|
}
|
|
|
|
a.sendWebSocketPostUnreadEvent(rctx, channelUnread, postID)
|
|
a.UpdateMobileAppBadge(userID)
|
|
return channelUnread, nil
|
|
}
|
|
|
|
// if reply post, autofollow thread and
|
|
// In CRT Supported Client: Mark the specific thread as unread but not the channel where the thread exists.
|
|
// If there are replies with mentions below the marked reply in the thread, then sum the mentions for the threads mention badge.
|
|
// In CRT Unsupported Client: Channel is marked as unread and new messages line inserted above the marked post.
|
|
// Badge on channel sums mentions in all posts (root & replies) including and below the post that was marked unread.
|
|
rootPost, appErr := a.GetSinglePost(rctx, post.RootId, false)
|
|
if appErr != nil {
|
|
return nil, appErr
|
|
}
|
|
|
|
channel, nErr := a.Srv().Store().Channel().Get(post.ChannelId, true)
|
|
if nErr != nil {
|
|
return nil, model.NewAppError("MarkChannelAsUnreadFromPost", "app.channel.update_last_viewed_at_post.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
|
|
}
|
|
|
|
if *a.Config().ServiceSettings.ThreadAutoFollow {
|
|
threadMembership, mErr := a.Srv().Store().Thread().GetMembershipForUser(user.Id, threadId)
|
|
var errNotFound *store.ErrNotFound
|
|
if mErr != nil && !errors.As(mErr, &errNotFound) {
|
|
return nil, model.NewAppError("MarkChannelAsUnreadFromPost", "app.channel.update_last_viewed_at_post.app_error", nil, "", http.StatusInternalServerError).Wrap(mErr)
|
|
}
|
|
// Follow thread if we're not already following it
|
|
if threadMembership == nil {
|
|
opts := store.ThreadMembershipOpts{
|
|
Following: true,
|
|
IncrementMentions: false,
|
|
UpdateFollowing: true,
|
|
UpdateViewedTimestamp: false,
|
|
UpdateParticipants: false,
|
|
}
|
|
threadMembership, mErr = a.Srv().Store().Thread().MaintainMembership(user.Id, threadId, opts)
|
|
if mErr != nil {
|
|
return nil, model.NewAppError("MarkChannelAsUnreadFromPost", "app.channel.update_last_viewed_at_post.app_error", nil, "", http.StatusInternalServerError).Wrap(mErr)
|
|
}
|
|
}
|
|
// If threadmembership already exists but user had previously unfollowed the thread, then follow the thread again.
|
|
threadMembership.Following = true
|
|
threadMembership.LastViewed = post.CreateAt - 1
|
|
threadMembership.UnreadMentions, appErr = a.countThreadMentions(rctx, user, rootPost, channel.TeamId, post.CreateAt-1)
|
|
if appErr != nil {
|
|
return nil, appErr
|
|
}
|
|
threadMembership, mErr = a.Srv().Store().Thread().UpdateMembership(threadMembership)
|
|
if mErr != nil {
|
|
return nil, model.NewAppError("MarkChannelAsUnreadFromPost", "app.channel.update_last_viewed_at_post.app_error", nil, "", http.StatusInternalServerError).Wrap(mErr)
|
|
}
|
|
thread, mErr := a.Srv().Store().Thread().GetThreadForUser(rctx, threadMembership, true, a.IsPostPriorityEnabled())
|
|
if mErr != nil {
|
|
return nil, model.NewAppError("MarkChannelAsUnreadFromPost", "app.channel.update_last_viewed_at_post.app_error", nil, "", http.StatusInternalServerError).Wrap(mErr)
|
|
}
|
|
a.sanitizeProfiles(thread.Participants, false)
|
|
thread.Post.SanitizeProps()
|
|
|
|
if a.IsCRTEnabledForUser(rctx, userID) {
|
|
payload, jsonErr := json.Marshal(thread)
|
|
if jsonErr != nil {
|
|
return nil, model.NewAppError("MarkChannelAsUnreadFromPost", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(jsonErr)
|
|
}
|
|
message := model.NewWebSocketEvent(model.WebsocketEventThreadUpdated, channel.TeamId, "", userID, nil, "")
|
|
message.Add("thread", string(payload))
|
|
a.Publish(message)
|
|
}
|
|
}
|
|
|
|
channelUnread, nErr := a.Srv().Store().Channel().UpdateLastViewedAtPost(post, userID, unreadMentions, 0, 0, false)
|
|
if nErr != nil {
|
|
return channelUnread, model.NewAppError("MarkChannelAsUnreadFromPost", "app.channel.update_last_viewed_at_post.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
|
|
}
|
|
a.sendWebSocketPostUnreadEvent(rctx, channelUnread, postID)
|
|
a.UpdateMobileAppBadge(userID)
|
|
return channelUnread, nil
|
|
}
|
|
|
|
func (a *App) sendWebSocketPostUnreadEvent(rctx request.CTX, channelUnread *model.ChannelUnreadAt, postID string) {
|
|
message := model.NewWebSocketEvent(model.WebsocketEventPostUnread, channelUnread.TeamId, channelUnread.ChannelId, channelUnread.UserId, nil, "")
|
|
message.Add("msg_count", channelUnread.MsgCount)
|
|
message.Add("msg_count_root", channelUnread.MsgCountRoot)
|
|
message.Add("mention_count", channelUnread.MentionCount)
|
|
message.Add("mention_count_root", channelUnread.MentionCountRoot)
|
|
message.Add("urgent_mention_count", channelUnread.UrgentMentionCount)
|
|
message.Add("last_viewed_at", channelUnread.LastViewedAt)
|
|
message.Add("post_id", postID)
|
|
a.Publish(message)
|
|
}
|
|
|
|
func (a *App) AutocompleteChannels(rctx request.CTX, userID, term string) (model.ChannelListWithTeamData, *model.AppError) {
|
|
includeDeleted := true
|
|
term = strings.TrimSpace(term)
|
|
|
|
user, appErr := a.GetUser(userID)
|
|
if appErr != nil {
|
|
return nil, appErr
|
|
}
|
|
|
|
channelList, err := a.Srv().Store().Channel().Autocomplete(rctx, userID, term, includeDeleted, user.IsGuest())
|
|
if err != nil {
|
|
return nil, model.NewAppError("AutocompleteChannels", "app.channel.search.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
return channelList, nil
|
|
}
|
|
|
|
func (a *App) AutocompleteChannelsForTeam(rctx request.CTX, teamID, userID, term string) (model.ChannelList, *model.AppError) {
|
|
includeDeleted := true
|
|
term = strings.TrimSpace(term)
|
|
|
|
user, appErr := a.GetUser(userID)
|
|
if appErr != nil {
|
|
return nil, appErr
|
|
}
|
|
|
|
channelList, err := a.Srv().Store().Channel().AutocompleteInTeam(rctx, teamID, userID, term, includeDeleted, user.IsGuest())
|
|
if err != nil {
|
|
return nil, model.NewAppError("AutocompleteChannels", "app.channel.search.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
return channelList, nil
|
|
}
|
|
|
|
func (a *App) AutocompleteChannelsForSearch(rctx request.CTX, teamID string, userID string, term string) (model.ChannelList, *model.AppError) {
|
|
includeDeleted := true
|
|
|
|
term = strings.TrimSpace(term)
|
|
|
|
channelList, err := a.Srv().Store().Channel().AutocompleteInTeamForSearch(teamID, userID, term, includeDeleted)
|
|
if err != nil {
|
|
return nil, model.NewAppError("AutocompleteChannelsForSearch", "app.channel.search.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
return channelList, nil
|
|
}
|
|
|
|
// SearchAllChannels returns a list of channels, the total count of the results of the search (if the paginate search option is true), and an error.
|
|
func (a *App) SearchAllChannels(rctx request.CTX, term string, opts model.ChannelSearchOpts) (model.ChannelListWithTeamData, int64, *model.AppError) {
|
|
if opts.ExcludeDefaultChannels {
|
|
opts.ExcludeChannelNames = a.DefaultChannelNames(rctx)
|
|
}
|
|
storeOpts := store.ChannelSearchOpts{
|
|
ExcludeChannelNames: opts.ExcludeChannelNames,
|
|
NotAssociatedToGroup: opts.NotAssociatedToGroup,
|
|
IncludeDeleted: opts.IncludeDeleted,
|
|
Deleted: opts.Deleted,
|
|
TeamIds: opts.TeamIds,
|
|
GroupConstrained: opts.GroupConstrained,
|
|
ExcludeGroupConstrained: opts.ExcludeGroupConstrained,
|
|
PolicyID: opts.PolicyID,
|
|
IncludePolicyID: opts.IncludePolicyID,
|
|
IncludeSearchByID: opts.IncludeSearchById,
|
|
ExcludeRemote: opts.ExcludeRemote,
|
|
ExcludePolicyConstrained: opts.ExcludePolicyConstrained,
|
|
Public: opts.Public,
|
|
Private: opts.Private,
|
|
Page: opts.Page,
|
|
PerPage: opts.PerPage,
|
|
AccessControlPolicyEnforced: opts.AccessControlPolicyEnforced,
|
|
ExcludeAccessControlPolicyEnforced: opts.ExcludeAccessControlPolicyEnforced,
|
|
ParentAccessControlPolicyId: opts.ParentAccessControlPolicyId,
|
|
}
|
|
|
|
term = strings.TrimSpace(term)
|
|
|
|
channelList, totalCount, err := a.Srv().Store().Channel().SearchAllChannels(term, storeOpts)
|
|
if err != nil {
|
|
return nil, 0, model.NewAppError("SearchAllChannels", "app.channel.search.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
return channelList, totalCount, nil
|
|
}
|
|
|
|
func (a *App) SearchChannels(rctx request.CTX, teamID string, term string) (model.ChannelList, *model.AppError) {
|
|
includeDeleted := true
|
|
|
|
term = strings.TrimSpace(term)
|
|
|
|
channelList, err := a.Srv().Store().Channel().SearchInTeam(teamID, term, includeDeleted)
|
|
if err != nil {
|
|
return nil, model.NewAppError("SearchChannels", "app.channel.search.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
return channelList, nil
|
|
}
|
|
|
|
func (a *App) SearchChannelsForUser(rctx request.CTX, userID, teamID, term string) (model.ChannelList, *model.AppError) {
|
|
includeDeleted := true
|
|
|
|
term = strings.TrimSpace(term)
|
|
|
|
channelList, err := a.Srv().Store().Channel().SearchForUserInTeam(userID, teamID, term, includeDeleted)
|
|
if err != nil {
|
|
return nil, model.NewAppError("SearchChannelsForUser", "app.channel.search.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
return channelList, nil
|
|
}
|
|
|
|
func (a *App) SearchGroupChannels(rctx request.CTX, userID, term string) (model.ChannelList, *model.AppError) {
|
|
if term == "" {
|
|
return model.ChannelList{}, nil
|
|
}
|
|
|
|
channelList, err := a.Srv().Store().Channel().SearchGroupChannels(userID, term)
|
|
if err != nil {
|
|
return nil, model.NewAppError("SearchGroupChannels", "app.channel.search_group_channels.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
return channelList, nil
|
|
}
|
|
|
|
func (a *App) SearchChannelsUserNotIn(rctx request.CTX, teamID string, userID string, term string) (model.ChannelList, *model.AppError) {
|
|
term = strings.TrimSpace(term)
|
|
channelList, err := a.Srv().Store().Channel().SearchMore(userID, teamID, term)
|
|
if err != nil {
|
|
return nil, model.NewAppError("SearchChannelsUserNotIn", "app.channel.search.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
return channelList, nil
|
|
}
|
|
|
|
func (a *App) MarkChannelsAsViewed(rctx request.CTX, channelIDs []string, userID string, currentSessionId string, collapsedThreadsSupported, isCRTEnabled bool) (map[string]int64, *model.AppError) {
|
|
var err error
|
|
|
|
user, err := a.Srv().Store().User().Get(rctx.Context(), userID)
|
|
if err != nil {
|
|
return nil, model.NewAppError("MarkChannelsAsViewed", "app.user.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
// We use channelsToView to later only update those, or early return if no channel is to be read
|
|
channelsToView, channelsToClearPushNotifications, times, err := a.Srv().Store().Channel().GetChannelsWithUnreadsAndWithMentions(rctx, channelIDs, userID, user.NotifyProps)
|
|
if err != nil {
|
|
return nil, model.NewAppError("MarkChannelsAsViewed", "app.channel.get_channels_with_unreads_and_with_mentions.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
if len(channelsToView) == 0 {
|
|
return times, nil
|
|
}
|
|
|
|
updateThreads := *a.Config().ServiceSettings.ThreadAutoFollow && (!collapsedThreadsSupported || !isCRTEnabled)
|
|
if updateThreads {
|
|
err = a.Srv().Store().Thread().MarkAllAsReadByChannels(userID, channelsToView)
|
|
if err != nil {
|
|
return nil, model.NewAppError("MarkChannelsAsViewed", "app.thread.mark_all_as_read_by_channels.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
}
|
|
|
|
_, err = a.Srv().Store().Channel().UpdateLastViewedAt(channelsToView, userID)
|
|
if err != nil {
|
|
var invErr *store.ErrInvalidInput
|
|
switch {
|
|
case errors.As(err, &invErr):
|
|
return nil, model.NewAppError("MarkChannelsAsViewed", "app.channel.update_last_viewed_at.app_error", nil, "", http.StatusBadRequest).Wrap(err)
|
|
default:
|
|
return nil, model.NewAppError("MarkChannelsAsViewed", "app.channel.update_last_viewed_at.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
}
|
|
|
|
if *a.Config().ServiceSettings.EnableChannelViewedMessages {
|
|
message := model.NewWebSocketEvent(model.WebsocketEventMultipleChannelsViewed, "", "", userID, nil, "")
|
|
message.Add("channel_times", times)
|
|
a.Publish(message)
|
|
}
|
|
|
|
for _, channelID := range channelsToClearPushNotifications {
|
|
a.clearPushNotification(currentSessionId, userID, channelID, "")
|
|
}
|
|
|
|
if updateThreads && isCRTEnabled {
|
|
timestamp := model.GetMillis()
|
|
for _, channelID := range channelsToView {
|
|
message := model.NewWebSocketEvent(model.WebsocketEventThreadReadChanged, "", channelID, userID, nil, "")
|
|
message.Add("timestamp", timestamp)
|
|
a.Publish(message)
|
|
}
|
|
}
|
|
|
|
return times, nil
|
|
}
|
|
|
|
func (a *App) ViewChannel(rctx request.CTX, view *model.ChannelView, userID string, currentSessionId string, collapsedThreadsSupported bool) (map[string]int64, *model.AppError) {
|
|
if err := a.SetActiveChannel(rctx, userID, view.ChannelId); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
channelIDs := []string{}
|
|
|
|
if view.ChannelId != "" {
|
|
channelIDs = append(channelIDs, view.ChannelId)
|
|
}
|
|
|
|
if view.PrevChannelId != "" {
|
|
channelIDs = append(channelIDs, view.PrevChannelId)
|
|
}
|
|
|
|
if len(channelIDs) == 0 {
|
|
return map[string]int64{}, nil
|
|
}
|
|
|
|
return a.MarkChannelsAsViewed(rctx, channelIDs, userID, currentSessionId, collapsedThreadsSupported, a.IsCRTEnabledForUser(rctx, userID))
|
|
}
|
|
|
|
func (a *App) PermanentDeleteChannel(rctx request.CTX, channel *model.Channel) *model.AppError {
|
|
if err := a.Srv().Store().Post().PermanentDeleteByChannel(rctx, channel.Id); err != nil {
|
|
return model.NewAppError("PermanentDeleteChannel", "app.post.permanent_delete_by_channel.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
if err := a.Srv().Store().Channel().PermanentDeleteMembersByChannel(rctx, channel.Id); err != nil {
|
|
return model.NewAppError("PermanentDeleteChannel", "app.channel.remove_member.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
if err := a.Srv().Store().Webhook().PermanentDeleteIncomingByChannel(channel.Id); err != nil {
|
|
return model.NewAppError("PermanentDeleteChannel", "app.webhooks.permanent_delete_incoming_by_channel.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
if err := a.Srv().Store().Webhook().PermanentDeleteOutgoingByChannel(channel.Id); err != nil {
|
|
return model.NewAppError("PermanentDeleteChannel", "app.webhooks.permanent_delete_outgoing_by_channel.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
if err := a.Srv().Store().PostPersistentNotification().DeleteByChannel([]string{channel.Id}); err != nil {
|
|
return model.NewAppError("PermanentDeleteChannel", "app.post_persistent_notification.delete_by_channel.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
deleteAt := model.GetMillis()
|
|
|
|
if nErr := a.Srv().Store().Channel().PermanentDelete(rctx, channel.Id); nErr != nil {
|
|
return model.NewAppError("PermanentDeleteChannel", "app.channel.permanent_delete.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
|
|
}
|
|
|
|
a.Srv().Platform().InvalidateCacheForChannel(channel)
|
|
|
|
var message *model.WebSocketEvent
|
|
if channel.Type == model.ChannelTypeOpen {
|
|
message = model.NewWebSocketEvent(model.WebsocketEventChannelDeleted, channel.TeamId, "", "", nil, "")
|
|
} else {
|
|
message = model.NewWebSocketEvent(model.WebsocketEventChannelDeleted, "", channel.Id, "", nil, "")
|
|
}
|
|
message.Add("channel_id", channel.Id)
|
|
message.Add("delete_at", deleteAt)
|
|
a.Publish(message)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) RemoveAllDeactivatedMembersFromChannel(rctx request.CTX, channel *model.Channel) *model.AppError {
|
|
err := a.Srv().Store().Channel().RemoveAllDeactivatedMembers(rctx, channel.Id)
|
|
if err != nil {
|
|
return model.NewAppError("RemoveAllDeactivatedMembersFromChannel", "app.channel.remove_all_deactivated_members.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// MoveChannel method is prone to data races if someone joins to channel during the move process. However this
|
|
// function is only exposed to sysadmins and the possibility of this edge case is relatively small.
|
|
func (a *App) MoveChannel(rctx request.CTX, team *model.Team, channel *model.Channel, user *model.User) *model.AppError {
|
|
// Check that all channel members are in the destination team.
|
|
channelMembers, err := a.GetChannelMembersPage(rctx, channel.Id, 0, 10000000)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
channelMemberIds := []string{}
|
|
for _, channelMember := range channelMembers {
|
|
channelMemberIds = append(channelMemberIds, channelMember.UserId)
|
|
}
|
|
|
|
if len(channelMemberIds) > 0 {
|
|
teamMembers, err2 := a.GetTeamMembersByIds(team.Id, channelMemberIds, nil)
|
|
if err2 != nil {
|
|
return err2
|
|
}
|
|
|
|
if len(teamMembers) != len(channelMembers) {
|
|
teamMembersMap := make(map[string]*model.TeamMember, len(teamMembers))
|
|
for _, teamMember := range teamMembers {
|
|
teamMembersMap[teamMember.UserId] = teamMember
|
|
}
|
|
for _, channelMember := range channelMembers {
|
|
if _, ok := teamMembersMap[channelMember.UserId]; !ok {
|
|
rctx.Logger().Warn("Not member of the target team", mlog.String("userId", channelMember.UserId))
|
|
}
|
|
}
|
|
return model.NewAppError("MoveChannel", "app.channel.move_channel.members_do_not_match.error", nil, "", http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
// keep instance of the previous team
|
|
previousTeam, nErr := a.Srv().Store().Team().Get(channel.TeamId)
|
|
if nErr != nil {
|
|
var nfErr *store.ErrNotFound
|
|
switch {
|
|
case errors.As(nErr, &nfErr):
|
|
return model.NewAppError("MoveChannel", "app.team.get.find.app_error", nil, "", http.StatusNotFound).Wrap(nErr)
|
|
default:
|
|
return model.NewAppError("MoveChannel", "app.team.get.finding.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
|
|
}
|
|
}
|
|
|
|
if nErr := a.Srv().Store().Channel().UpdateSidebarChannelCategoryOnMove(channel, team.Id); nErr != nil {
|
|
return model.NewAppError("MoveChannel", "app.channel.sidebar_categories.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
|
|
}
|
|
|
|
channel.TeamId = team.Id
|
|
if _, err := a.Srv().Store().Channel().Update(rctx, channel); err != nil {
|
|
var appErr *model.AppError
|
|
var uniqueConstraintErr *store.ErrUniqueConstraint
|
|
var invErr *store.ErrInvalidInput
|
|
switch {
|
|
case errors.As(err, &invErr):
|
|
return model.NewAppError("MoveChannel", "app.channel.update.bad_id", nil, "", http.StatusBadRequest).Wrap(err)
|
|
case errors.As(err, &uniqueConstraintErr):
|
|
return model.NewAppError("MoveChannel", store.ChannelExistsError, nil, "", http.StatusBadRequest).Wrap(err)
|
|
case errors.As(err, &appErr):
|
|
return appErr
|
|
default:
|
|
return model.NewAppError("MoveChannel", "app.channel.update_channel.internal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
}
|
|
|
|
if incomingWebhooks, err := a.GetIncomingWebhooksForTeamPage(previousTeam.Id, 0, 10000000); err != nil {
|
|
rctx.Logger().Warn("Failed to get incoming webhooks", mlog.Err(err))
|
|
} else {
|
|
for _, webhook := range incomingWebhooks {
|
|
if webhook.ChannelId == channel.Id {
|
|
webhook.TeamId = team.Id
|
|
if _, err := a.Srv().Store().Webhook().UpdateIncoming(webhook); err != nil {
|
|
rctx.Logger().Warn("Failed to move incoming webhook to new team", mlog.String("webhook id", webhook.Id))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if outgoingWebhooks, err := a.GetOutgoingWebhooksForTeamPage(previousTeam.Id, 0, 10000000); err != nil {
|
|
rctx.Logger().Warn("Failed to get outgoing webhooks", mlog.Err(err))
|
|
} else {
|
|
for _, webhook := range outgoingWebhooks {
|
|
if webhook.ChannelId == channel.Id {
|
|
webhook.TeamId = team.Id
|
|
if _, err := a.Srv().Store().Webhook().UpdateOutgoing(webhook); err != nil {
|
|
rctx.Logger().Warn("Failed to move outgoing webhook to new team.", mlog.String("webhook id", webhook.Id))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Update the threads within this channel to the new team
|
|
if err := a.Srv().Store().Thread().UpdateTeamIdForChannelThreads(channel.Id, team.Id); err != nil {
|
|
rctx.Logger().Warn("error while updating threads after channel move", mlog.Err(err))
|
|
}
|
|
|
|
if err := a.RemoveUsersFromChannelNotMemberOfTeam(rctx, user, channel, team); err != nil {
|
|
rctx.Logger().Warn("error while removing non-team member users", mlog.Err(err))
|
|
}
|
|
|
|
if user != nil {
|
|
if err := a.postChannelMoveMessage(rctx, user, channel, previousTeam); err != nil {
|
|
rctx.Logger().Warn("error while posting move channel message", mlog.Err(err))
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) postChannelMoveMessage(rctx request.CTX, user *model.User, channel *model.Channel, previousTeam *model.Team) *model.AppError {
|
|
post := &model.Post{
|
|
ChannelId: channel.Id,
|
|
Message: fmt.Sprintf(i18n.T("api.team.move_channel.success"), previousTeam.Name),
|
|
Type: model.PostTypeMoveChannel,
|
|
UserId: user.Id,
|
|
Props: model.StringInterface{
|
|
"username": user.Username,
|
|
},
|
|
}
|
|
|
|
if _, err := a.CreatePost(rctx, post, channel, model.CreatePostFlags{SetOnline: true}); err != nil {
|
|
return model.NewAppError("postChannelMoveMessage", "api.team.move_channel.post.error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) RemoveUsersFromChannelNotMemberOfTeam(rctx request.CTX, remover *model.User, channel *model.Channel, team *model.Team) *model.AppError {
|
|
channelMembers, err := a.GetChannelMembersPage(rctx, channel.Id, 0, 10000000)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
channelMemberIds := []string{}
|
|
channelMemberMap := make(map[string]struct{})
|
|
for _, channelMember := range channelMembers {
|
|
channelMemberMap[channelMember.UserId] = struct{}{}
|
|
channelMemberIds = append(channelMemberIds, channelMember.UserId)
|
|
}
|
|
|
|
if len(channelMemberIds) > 0 {
|
|
teamMembers, err := a.GetTeamMembersByIds(team.Id, channelMemberIds, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(teamMembers) != len(channelMembers) {
|
|
for _, teamMember := range teamMembers {
|
|
delete(channelMemberMap, teamMember.UserId)
|
|
}
|
|
|
|
var removerId string
|
|
if remover != nil {
|
|
removerId = remover.Id
|
|
}
|
|
for userID := range channelMemberMap {
|
|
if err := a.removeUserFromChannel(rctx, userID, removerId, channel); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) GetPinnedPosts(rctx request.CTX, channelID string) (*model.PostList, *model.AppError) {
|
|
posts, err := a.Srv().Store().Channel().GetPinnedPosts(channelID)
|
|
if err != nil {
|
|
return nil, model.NewAppError("GetPinnedPosts", "app.channel.pinned_posts.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
if appErr := a.filterInaccessiblePosts(posts, filterPostOptions{assumeSortedCreatedAt: true}); appErr != nil {
|
|
return nil, appErr
|
|
}
|
|
|
|
return posts, nil
|
|
}
|
|
|
|
func (a *App) ToggleMuteChannel(rctx request.CTX, channelID, userID string) (*model.ChannelMember, *model.AppError) {
|
|
member, nErr := a.Srv().Store().Channel().GetMember(rctx, channelID, userID)
|
|
if nErr != nil {
|
|
var appErr *model.AppError
|
|
var nfErr *store.ErrNotFound
|
|
switch {
|
|
case errors.As(nErr, &appErr):
|
|
return nil, appErr
|
|
case errors.As(nErr, &nfErr):
|
|
return nil, model.NewAppError("ToggleMuteChannel", MissingChannelMemberError, nil, "", http.StatusNotFound).Wrap(nErr)
|
|
default:
|
|
return nil, model.NewAppError("ToggleMuteChannel", "app.channel.get_member.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
|
|
}
|
|
}
|
|
|
|
member.SetChannelMuted(!member.IsChannelMuted())
|
|
|
|
member, err := a.updateChannelMember(rctx, member)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
a.invalidateCacheForChannelMembersNotifyProps(member.ChannelId)
|
|
|
|
return member, nil
|
|
}
|
|
|
|
func (a *App) setChannelsMuted(rctx request.CTX, channelIDs []string, userID string, muted bool) ([]*model.ChannelMember, *model.AppError) {
|
|
members, err := a.Srv().Store().Channel().GetMembersByChannelIds(channelIDs, userID)
|
|
if err != nil {
|
|
var appErr *model.AppError
|
|
switch {
|
|
case errors.As(err, &appErr):
|
|
return nil, appErr
|
|
default:
|
|
return nil, model.NewAppError("setChannelsMuted", "app.channel.get_member.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
}
|
|
|
|
var membersToUpdate []*model.ChannelMember
|
|
for _, member := range members {
|
|
if muted == member.IsChannelMuted() {
|
|
continue
|
|
}
|
|
|
|
updatedMember := member
|
|
updatedMember.SetChannelMuted(muted)
|
|
|
|
membersToUpdate = append(membersToUpdate, &updatedMember)
|
|
}
|
|
|
|
if len(membersToUpdate) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
updated, err := a.Srv().Store().Channel().UpdateMultipleMembers(membersToUpdate)
|
|
if err != nil {
|
|
var appErr *model.AppError
|
|
var nfErr *store.ErrNotFound
|
|
switch {
|
|
case errors.As(err, &appErr):
|
|
return nil, appErr
|
|
case errors.As(err, &nfErr):
|
|
return nil, model.NewAppError("setChannelsMuted", MissingChannelMemberError, nil, "", http.StatusNotFound).Wrap(err)
|
|
default:
|
|
return nil, model.NewAppError("setChannelsMuted", "app.channel.get_member.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
}
|
|
|
|
for _, member := range updated {
|
|
a.invalidateCacheForChannelMembersNotifyProps(member.ChannelId)
|
|
|
|
evt := model.NewWebSocketEvent(model.WebsocketEventChannelMemberUpdated, "", "", member.UserId, nil, "")
|
|
|
|
memberJSON, jsonErr := json.Marshal(member)
|
|
if jsonErr != nil {
|
|
return nil, model.NewAppError("setChannelsMuted", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(jsonErr)
|
|
}
|
|
|
|
evt.Add("channelMember", string(memberJSON))
|
|
a.Publish(evt)
|
|
}
|
|
|
|
return updated, nil
|
|
}
|
|
|
|
func (a *App) FillInChannelProps(rctx request.CTX, channel *model.Channel) *model.AppError {
|
|
return a.FillInChannelsProps(rctx, model.ChannelList{channel})
|
|
}
|
|
|
|
func (a *App) FillInChannelsProps(rctx request.CTX, channelList model.ChannelList) *model.AppError {
|
|
// Group the channels by team and call GetChannelsByNames just once per team.
|
|
channelsByTeam := make(map[string]model.ChannelList)
|
|
for _, channel := range channelList {
|
|
channelsByTeam[channel.TeamId] = append(channelsByTeam[channel.TeamId], channel)
|
|
}
|
|
|
|
for teamID, channelList := range channelsByTeam {
|
|
allChannelMentions := make(map[string]bool)
|
|
channelMentions := make(map[*model.Channel][]string, len(channelList))
|
|
|
|
// Collect mentions across the channels so as to query just once for this team.
|
|
for _, channel := range channelList {
|
|
channelMentions[channel] = model.ChannelMentions(channel.Header)
|
|
|
|
for _, channelMention := range channelMentions[channel] {
|
|
allChannelMentions[channelMention] = true
|
|
}
|
|
}
|
|
|
|
allChannelMentionNames := make([]string, 0, len(allChannelMentions))
|
|
for channelName := range allChannelMentions {
|
|
allChannelMentionNames = append(allChannelMentionNames, channelName)
|
|
}
|
|
|
|
if len(allChannelMentionNames) > 0 {
|
|
mentionedChannels, err := a.GetChannelsByNames(rctx, allChannelMentionNames, teamID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
mentionedChannelsByName := make(map[string]*model.Channel)
|
|
for _, channel := range mentionedChannels {
|
|
mentionedChannelsByName[channel.Name] = channel
|
|
}
|
|
|
|
for _, channel := range channelList {
|
|
channelMentionsProp := make(map[string]any, len(channelMentions[channel]))
|
|
for _, channelMention := range channelMentions[channel] {
|
|
if mentioned, ok := mentionedChannelsByName[channelMention]; ok {
|
|
if mentioned.Type == model.ChannelTypeOpen {
|
|
channelMentionsProp[mentioned.Name] = map[string]any{
|
|
"display_name": mentioned.DisplayName,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(channelMentionsProp) > 0 {
|
|
channel.AddProp("channel_mentions", channelMentionsProp)
|
|
} else if channel.Props != nil {
|
|
delete(channel.Props, "channel_mentions")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) forEachChannelMember(rctx request.CTX, channelID string, f func(model.ChannelMember) error) error {
|
|
perPage := 100
|
|
page := 0
|
|
|
|
for {
|
|
opts := model.ChannelMembersGetOptions{
|
|
ChannelID: channelID,
|
|
Offset: page * perPage,
|
|
Limit: perPage,
|
|
}
|
|
channelMembers, err := a.Srv().Store().Channel().GetMembers(opts)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, channelMember := range channelMembers {
|
|
if err = f(channelMember); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
length := len(channelMembers)
|
|
if length < perPage {
|
|
break
|
|
}
|
|
|
|
page++
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) ClearChannelMembersCache(rctx request.CTX, channelID string) error {
|
|
clearSessionCache := func(channelMember model.ChannelMember) error {
|
|
a.ClearSessionCacheForUser(channelMember.UserId)
|
|
message := model.NewWebSocketEvent(model.WebsocketEventChannelMemberUpdated, "", "", channelMember.UserId, nil, "")
|
|
memberJSON, jsonErr := json.Marshal(channelMember)
|
|
if jsonErr != nil {
|
|
return jsonErr
|
|
}
|
|
message.Add("channelMember", string(memberJSON))
|
|
a.Publish(message)
|
|
return nil
|
|
}
|
|
if err := a.forEachChannelMember(rctx, channelID, clearSessionCache); err != nil {
|
|
return fmt.Errorf("error clearing cache for channel members: channel_id: %s, error: %w", channelID, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (a *App) GetMemberCountsByGroup(rctx request.CTX, channelID string, includeTimezones bool) ([]*model.ChannelMemberCountByGroup, *model.AppError) {
|
|
channelMemberCounts, err := a.Srv().Store().Channel().GetMemberCountsByGroup(rctx, channelID, includeTimezones)
|
|
if err != nil {
|
|
return nil, model.NewAppError("GetMemberCountsByGroup", "app.channel.get_member_count.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
return channelMemberCounts, nil
|
|
}
|
|
|
|
func (a *App) getDirectChannel(rctx request.CTX, userID, otherUserID string) (*model.Channel, *model.AppError) {
|
|
return a.Srv().getDirectChannel(rctx, userID, otherUserID)
|
|
}
|
|
|
|
func (a *App) GetDirectOrGroupMessageMembersCommonTeams(rctx request.CTX, channelID string) ([]*model.Team, *model.AppError) {
|
|
channel, appErr := a.GetChannel(rctx, channelID)
|
|
if appErr != nil {
|
|
return nil, appErr
|
|
}
|
|
|
|
if channel.Type != model.ChannelTypeGroup && channel.Type != model.ChannelTypeDirect {
|
|
return nil, model.NewAppError("GetDirectOrGroupMessageMembersCommonTeams", "app.channel.get_common_teams.incorrect_channel_type", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
users, appErr := a.GetUsersInChannel(&model.UserGetOptions{
|
|
PerPage: model.ChannelGroupMaxUsers,
|
|
Page: 0,
|
|
InChannelId: channelID,
|
|
Inactive: false,
|
|
Active: true,
|
|
})
|
|
|
|
userIDs := make([]string, 0, len(users))
|
|
for _, user := range users {
|
|
if user.IsBot {
|
|
isOwnedByCurrentUserOrPlugin, err := a.IsBotOwnedByCurrentUserOrPlugin(rctx, user.Id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if isOwnedByCurrentUserOrPlugin {
|
|
continue
|
|
}
|
|
}
|
|
userIDs = append(userIDs, user.Id)
|
|
}
|
|
|
|
commonTeamIDs, err := a.Srv().Store().Team().GetCommonTeamIDsForMultipleUsers(userIDs)
|
|
if err != nil {
|
|
return nil, model.NewAppError("GetDirectOrGroupMessageMembersCommonTeams", "app.channel.get_common_teams.store_get_common_teams_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
teams := []*model.Team{}
|
|
if len(commonTeamIDs) > 0 {
|
|
teams, appErr = a.GetTeams(commonTeamIDs)
|
|
}
|
|
|
|
return teams, appErr
|
|
}
|
|
|
|
func (a *App) ConvertGroupMessageToChannel(rctx request.CTX, convertedByUserId string, gmConversionRequest *model.GroupMessageConversionRequestBody) (*model.Channel, *model.AppError) {
|
|
originalChannel, appErr := a.GetChannel(rctx, gmConversionRequest.ChannelID)
|
|
if appErr != nil {
|
|
return nil, appErr
|
|
}
|
|
|
|
appErr = a.validateForConvertGroupMessageToChannel(rctx, convertedByUserId, originalChannel, gmConversionRequest)
|
|
if appErr != nil {
|
|
return nil, appErr
|
|
}
|
|
|
|
toUpdate := originalChannel.DeepCopy()
|
|
toUpdate.Type = model.ChannelTypePrivate
|
|
toUpdate.TeamId = gmConversionRequest.TeamID
|
|
toUpdate.Name = gmConversionRequest.Name
|
|
toUpdate.DisplayName = gmConversionRequest.DisplayName
|
|
|
|
updatedChannel, appErr := a.UpdateChannel(rctx, toUpdate)
|
|
if appErr != nil {
|
|
return nil, appErr
|
|
}
|
|
|
|
a.Srv().Platform().InvalidateCacheForChannel(originalChannel)
|
|
|
|
users, appErr := a.GetUsersInChannelPage(&model.UserGetOptions{
|
|
InChannelId: gmConversionRequest.ChannelID,
|
|
Page: 0,
|
|
PerPage: model.ChannelGroupMaxUsers,
|
|
}, false)
|
|
if appErr != nil {
|
|
return nil, appErr
|
|
}
|
|
|
|
_ = a.setSidebarCategoriesForConvertedGroupMessage(rctx, gmConversionRequest, users)
|
|
_ = a.postMessageForConvertGroupMessageToChannel(rctx, gmConversionRequest.ChannelID, convertedByUserId, users)
|
|
|
|
// the user conversion the GM becomes the channel admin.
|
|
_, appErr = a.UpdateChannelMemberSchemeRoles(rctx, gmConversionRequest.ChannelID, convertedByUserId, false, true, true)
|
|
if appErr != nil {
|
|
return nil, appErr
|
|
}
|
|
|
|
return updatedChannel, nil
|
|
}
|
|
|
|
func (a *App) setSidebarCategoriesForConvertedGroupMessage(rctx request.CTX, gmConversionRequest *model.GroupMessageConversionRequestBody, channelUsers []*model.User) *model.AppError {
|
|
// First we'll delete channel from everyone's sidebar. Only the members of GM
|
|
// can have it in sidebar, so we can delete the channel from all SidebarChannels entries.
|
|
err := a.Srv().Store().Channel().DeleteAllSidebarChannelForChannel(gmConversionRequest.ChannelID)
|
|
if err != nil {
|
|
return model.NewAppError(
|
|
"setSidebarCategoriesForConvertedGroupMessage",
|
|
"app.channel.gm_conversion_set_categories.delete_all.error",
|
|
nil,
|
|
"",
|
|
http.StatusInternalServerError,
|
|
).Wrap(err)
|
|
}
|
|
|
|
// Now that we've deleted existing entries, we can set the channel in default "Channels" category
|
|
// for all GM members
|
|
for _, user := range channelUsers {
|
|
categories, appErr := a.GetSidebarCategories(rctx, user.Id, gmConversionRequest.TeamID)
|
|
|
|
if appErr != nil {
|
|
rctx.Logger().Error("Failed to search sidebar categories for user for adding converted GM")
|
|
continue
|
|
}
|
|
|
|
if len(categories.Categories) < 1 {
|
|
// It is normal for user to not have the default category.
|
|
// The default "Channels" category is created when the user first logs in,
|
|
// and all their channels are moved to this category at the same time.
|
|
// So its perfectly okay for this condition to occur.
|
|
continue
|
|
}
|
|
|
|
// when we fetch the default "Channels" category from store layer,
|
|
// it auto-fills any channels the user has access to but aren't associated to a category in the database.
|
|
// So what we do is fetch the category, so we get an auto-filled data,
|
|
// then call update category to persist the data and send the websocket events.
|
|
channelsCategory := categories.Categories[0]
|
|
_, appErr = a.UpdateSidebarCategories(rctx, user.Id, gmConversionRequest.TeamID, []*model.SidebarCategoryWithChannels{channelsCategory})
|
|
if appErr != nil {
|
|
rctx.Logger().Error("Failed to add converted GM to default sidebar category for user", mlog.String("user_id", user.Id), mlog.Err(err))
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) validateForConvertGroupMessageToChannel(rctx request.CTX, convertedByUserId string, originalChannel *model.Channel, gmConversionRequest *model.GroupMessageConversionRequestBody) *model.AppError {
|
|
commonTeams, appErr := a.GetDirectOrGroupMessageMembersCommonTeams(rctx, originalChannel.Id)
|
|
if appErr != nil {
|
|
return appErr
|
|
}
|
|
|
|
teamFound := false
|
|
for _, team := range commonTeams {
|
|
if team.Id == gmConversionRequest.TeamID {
|
|
teamFound = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !teamFound {
|
|
return model.NewAppError(
|
|
"validateForConvertGroupMessageToChannel",
|
|
"app.channel.group_message_conversion.incorrect_team",
|
|
nil,
|
|
"",
|
|
http.StatusBadRequest,
|
|
)
|
|
}
|
|
|
|
if originalChannel.Type != model.ChannelTypeGroup {
|
|
return model.NewAppError(
|
|
"ConvertGroupMessageToChannel",
|
|
"app.channel.group_message_conversion.original_channel_not_gm",
|
|
nil,
|
|
"",
|
|
http.StatusNotFound,
|
|
)
|
|
}
|
|
|
|
channelMember, appErr := a.GetChannelMember(rctx, gmConversionRequest.ChannelID, convertedByUserId)
|
|
if appErr != nil {
|
|
return appErr
|
|
}
|
|
|
|
if channelMember == nil {
|
|
return model.NewAppError("ConvertGroupMessageToChannel", "app.channel.group_message_conversion.channel_member_missing", nil, "", http.StatusNotFound)
|
|
}
|
|
|
|
// apply dummy changes to check validity
|
|
clone := originalChannel.DeepCopy()
|
|
clone.Type = model.ChannelTypePrivate
|
|
clone.Name = gmConversionRequest.Name
|
|
clone.DisplayName = gmConversionRequest.DisplayName
|
|
return clone.IsValid()
|
|
}
|
|
|
|
func (a *App) postMessageForConvertGroupMessageToChannel(rctx request.CTX, channelID, convertedByUserId string, channelUsers []*model.User) *model.AppError {
|
|
convertedByUser, appErr := a.GetUser(convertedByUserId)
|
|
if appErr != nil {
|
|
return appErr
|
|
}
|
|
|
|
userIDs := make([]string, len(channelUsers))
|
|
usernames := make([]string, len(channelUsers))
|
|
for i, user := range channelUsers {
|
|
userIDs[i] = user.Id
|
|
usernames[i] = user.Username
|
|
}
|
|
|
|
message := i18n.T(
|
|
"api.channel.group_message.converted.to_private_channel",
|
|
map[string]any{
|
|
"ConvertedByUsername": convertedByUser.Username,
|
|
"GMMembers": utils.JoinList(usernames),
|
|
})
|
|
|
|
post := &model.Post{
|
|
ChannelId: channelID,
|
|
Message: message,
|
|
Type: model.PostTypeGMConvertedToChannel,
|
|
UserId: convertedByUserId,
|
|
}
|
|
|
|
// these props are used for re-constructing a localized message on the client
|
|
post.AddProp("convertedByUserId", convertedByUser.Id)
|
|
post.AddProp("gmMembersDuringConversionIDs", userIDs)
|
|
|
|
channel, appErr := a.GetChannel(rctx, channelID)
|
|
if appErr != nil {
|
|
return appErr
|
|
}
|
|
|
|
if _, appErr := a.CreatePost(rctx, post, channel, model.CreatePostFlags{SetOnline: true}); appErr != nil {
|
|
rctx.Logger().Error("Failed to create post for notifying about GM converted to private channel", mlog.Err(appErr))
|
|
|
|
return model.NewAppError(
|
|
"postMessageForConvertGroupMessageToChannel",
|
|
"app.channel.group_message_conversion.post_message.error",
|
|
nil,
|
|
"",
|
|
http.StatusInternalServerError,
|
|
).Wrap(appErr)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *Server) getDirectChannel(rctx request.CTX, userID, otherUserID string) (*model.Channel, *model.AppError) {
|
|
channel, nErr := s.Store().Channel().GetByName("", model.GetDMNameFromIds(userID, otherUserID), true)
|
|
if nErr != nil {
|
|
var nfErr *store.ErrNotFound
|
|
if errors.As(nErr, &nfErr) {
|
|
return nil, nil
|
|
}
|
|
|
|
return nil, model.NewAppError("GetOrCreateDirectChannel", "web.incoming_webhook.channel.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
|
|
}
|
|
|
|
return channel, nil
|
|
}
|
|
|
|
func (a *App) CheckIfChannelIsRestrictedDM(rctx request.CTX, channel *model.Channel) (bool, *model.AppError) {
|
|
if *a.Config().TeamSettings.RestrictDirectMessage != model.DirectMessageTeam {
|
|
return false, nil
|
|
}
|
|
|
|
if channel.Type != model.ChannelTypeDirect && channel.Type != model.ChannelTypeGroup {
|
|
return false, nil
|
|
}
|
|
|
|
teams, err := a.GetDirectOrGroupMessageMembersCommonTeams(rctx, channel.Id)
|
|
if err != nil {
|
|
return false, model.NewAppError("CheckIfChannelIsRestrictedDM", "app.channel.get_common_teams.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
return len(teams) == 0, nil
|
|
}
|
|
|
|
func (a *App) ChannelAccessControlled(rctx request.CTX, channelID string) (bool, *model.AppError) {
|
|
if l := a.License(); !model.MinimumEnterpriseAdvancedLicense(l) || !*a.Config().AccessControlSettings.EnableAttributeBasedAccessControl {
|
|
return false, nil
|
|
}
|
|
|
|
channel, err := a.Srv().Store().Channel().Get(channelID, true)
|
|
var nfErr *store.ErrNotFound
|
|
if err != nil && !errors.As(err, &nfErr) {
|
|
return false, model.NewAppError("ChannelIsAccessControlled", "app.channel.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
} else if errors.As(err, &nfErr) {
|
|
return false, nil
|
|
}
|
|
|
|
return channel.PolicyEnforced, nil
|
|
}
|
|
|
|
func (a *App) handleChannelCategoryName(channel *model.Channel) {
|
|
if *a.Config().ExperimentalSettings.ExperimentalChannelCategorySorting && strings.Contains(channel.DisplayName, "/") {
|
|
parts := strings.Split(channel.DisplayName, "/")
|
|
channel.DisplayName = strings.TrimSpace(strings.Join(parts[1:], "/"))
|
|
channel.DefaultCategoryName = strings.TrimSpace(parts[0])
|
|
}
|
|
}
|
|
|
|
func (a *App) addChannelToDefaultCategory(rctx request.CTX, userID string, channel *model.Channel) {
|
|
// Add channel to default category if specified
|
|
if channel.DefaultCategoryName != "" && *a.Config().ExperimentalSettings.ExperimentalChannelCategorySorting {
|
|
// Get user's categories for this team
|
|
categories, err := a.GetSidebarCategoriesForTeamForUser(rctx, userID, channel.TeamId)
|
|
if err != nil {
|
|
mlog.Error("Failed to get sidebar categories", mlog.String("user_id", userID), mlog.String("team_id", channel.TeamId), mlog.Err(err))
|
|
return
|
|
}
|
|
// Find or create the category
|
|
var targetCategory *model.SidebarCategoryWithChannels
|
|
for _, category := range categories.Categories {
|
|
if category.Type == model.SidebarCategoryCustom && strings.EqualFold(category.DisplayName, channel.DefaultCategoryName) {
|
|
targetCategory = category
|
|
break
|
|
}
|
|
}
|
|
|
|
if targetCategory == nil {
|
|
// Create new category if it doesn't exist
|
|
targetCategory = &model.SidebarCategoryWithChannels{
|
|
SidebarCategory: model.SidebarCategory{
|
|
UserId: userID,
|
|
TeamId: channel.TeamId,
|
|
Type: model.SidebarCategoryCustom,
|
|
DisplayName: channel.DefaultCategoryName,
|
|
Sorting: model.SidebarCategorySortDefault,
|
|
},
|
|
Channels: []string{channel.Id},
|
|
}
|
|
_, err = a.CreateSidebarCategory(rctx, userID, channel.TeamId, targetCategory)
|
|
if err != nil {
|
|
mlog.Error("Failed to create default category", mlog.String("user_id", userID), mlog.String("team_id", channel.TeamId), mlog.String("category_name", channel.DefaultCategoryName), mlog.Err(err))
|
|
}
|
|
} else {
|
|
// Add channel to existing category
|
|
targetCategory.Channels = append([]string{channel.Id}, targetCategory.Channels...)
|
|
_, err = a.UpdateSidebarCategories(rctx, userID, channel.TeamId, []*model.SidebarCategoryWithChannels{targetCategory})
|
|
if err != nil {
|
|
mlog.Error("Failed to update default category", mlog.String("user_id", userID), mlog.String("team_id", channel.TeamId), mlog.String("category_name", channel.DefaultCategoryName), mlog.Err(err))
|
|
}
|
|
}
|
|
}
|
|
}
|