* MM-36589: provide previous values for unreads To successfully figure out the new counts of mentions or unread replies for CRT we need to provide previous values alongside with the new. This is needed so we'll know how many to subtract from the total. This commit provides those numbers upon publishing websocket ThreadUpdated and ThreadReadChanged events. * Fixes errors * Removes unneeded lines * Adds GetThreadUnreadReplyCount store method Uses the new store method to get unread replies instead of GetThreadForUser. * Tests, and some changes - Adds api4 tests to test ws events - Uses sqlx instead of gorp for new store method - Fixes case where previous_unread_replies could be a negative value * Refactors tests and adds more cases * Fixes previous and current unread counts for commenter When a user posts a reply to a thread the unread counts had a couple of issues. UnreadMentions where not zeroed out, and previous unread counts where not set correctly. This commit tries to fix that by marking the thread as read for the current poster after we set previous unread counts to the websocket event data. Also MaintainMembership should zero out UnreadMentions when we are setting the thread as read. * Oops * Fixes tests by updating when maintaining membership OK, so some tests broke because we zero UnreadMentions in the membership when we UpdateViewedTimestamp, since the new timestamp is always now. Some tests broke because MaintainMembership for the commenter so that the thread is read each time commenter posts moved further down in the SendNotification method. BOTH those test cases are fixed with this commit. To be sincere though I don't understand why the second one is fixed by this. * SystemAdminUser was not part of the channel Some tests are failing because SystemAdminUser is not part of the team and channels. This commit adds user to team and channels, in an effort to fix api4/user_tests * Fixes tests * Fixes tests * Addresses review comments * Fix if clause Co-authored-by: Mattermod <mattermod@users.noreply.github.com>
3399 строки
126 KiB
Go
3399 строки
126 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"
|
|
"strings"
|
|
|
|
"github.com/mattermost/mattermost-server/v6/app/request"
|
|
"github.com/mattermost/mattermost-server/v6/model"
|
|
"github.com/mattermost/mattermost-server/v6/plugin"
|
|
"github.com/mattermost/mattermost-server/v6/shared/i18n"
|
|
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
|
"github.com/mattermost/mattermost-server/v6/store"
|
|
"github.com/mattermost/mattermost-server/v6/store/sqlstore"
|
|
"github.com/mattermost/mattermost-server/v6/utils"
|
|
)
|
|
|
|
// 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() []string {
|
|
names := []string{"town-square"}
|
|
|
|
if len(a.Config().TeamSettings.ExperimentalDefaultChannels) == 0 {
|
|
names = append(names, "off-topic")
|
|
} else {
|
|
seenChannels := map[string]bool{"town-square": true}
|
|
for _, channelName := range a.Config().TeamSettings.ExperimentalDefaultChannels {
|
|
if !seenChannels[channelName] {
|
|
names = append(names, channelName)
|
|
seenChannels[channelName] = true
|
|
}
|
|
}
|
|
}
|
|
|
|
return names
|
|
}
|
|
|
|
func (a *App) JoinDefaultChannels(c *request.Context, 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, nfErr.Error(), http.StatusNotFound)
|
|
default:
|
|
return model.NewAppError("JoinDefaultChannels", "app.user.get.app_error", nil, nErr.Error(), http.StatusInternalServerError)
|
|
}
|
|
}
|
|
}
|
|
|
|
var err *model.AppError
|
|
for _, channelName := range a.DefaultChannelNames() {
|
|
channel, channelErr := a.Srv().Store.Channel().GetByName(teamID, channelName, true)
|
|
if channelErr != nil {
|
|
var nfErr *store.ErrNotFound
|
|
switch {
|
|
case errors.As(err, &nfErr):
|
|
err = model.NewAppError("JoinDefaultChannels", "app.channel.get_by_name.missing.app_error", nil, nfErr.Error(), http.StatusNotFound)
|
|
default:
|
|
err = model.NewAppError("JoinDefaultChannels", "app.channel.get_by_name.existing.app_error", nil, channelErr.Error(), http.StatusInternalServerError)
|
|
}
|
|
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(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, histErr.Error(), http.StatusInternalServerError)
|
|
}
|
|
|
|
if *a.Config().ServiceSettings.ExperimentalEnableDefaultChannelLeaveJoinMessages {
|
|
if aErr := a.postJoinMessageForDefaultChannel(c, user, requestor, channel); aErr != nil {
|
|
mlog.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, cErr.Error(), http.StatusBadRequest)
|
|
}
|
|
case errors.As(nErr, &appErr):
|
|
return appErr
|
|
default:
|
|
return model.NewAppError("JoinDefaultChannels", "app.channel.create_direct_channel.internal_error", nil, nErr.Error(), http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) postJoinMessageForDefaultChannel(c *request.Context, user *model.User, requestor *model.User, channel *model.Channel) *model.AppError {
|
|
if channel.Name == model.DefaultChannelName {
|
|
if requestor == nil {
|
|
if err := a.postJoinTeamMessage(c, user, channel); err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
if err := a.postAddToTeamMessage(c, requestor, user, channel, ""); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
} else {
|
|
if requestor == nil {
|
|
if err := a.postJoinChannelMessage(c, user, channel); err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
if err := a.PostAddToChannelMessage(c, requestor, user, channel, ""); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) CreateChannelWithUser(c *request.Context, 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(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]interface{}{"MaxChannelsPerTeam": *a.Config().TeamSettings.MaxChannelsPerTeam}, "", http.StatusBadRequest)
|
|
}
|
|
|
|
channel.CreatorId = userID
|
|
|
|
rchannel, err := a.CreateChannel(c, channel, true)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var user *model.User
|
|
if user, err = a.GetUser(userID); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
a.postJoinChannelMessage(c, user, channel)
|
|
|
|
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(channel *model.Channel, newChannelName string, newDisplayName string) (*model.Channel, *model.AppError) {
|
|
if channel.Type == model.ChannelTypeDirect {
|
|
return nil, model.NewAppError("RenameChannel", "api.channel.rename_channel.cant_rename_direct_messages.app_error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
if channel.Type == model.ChannelTypeGroup {
|
|
return nil, model.NewAppError("RenameChannel", "api.channel.rename_channel.cant_rename_group_messages.app_error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
channel.Name = newChannelName
|
|
if newDisplayName != "" {
|
|
channel.DisplayName = newDisplayName
|
|
}
|
|
|
|
newChannel, err := a.UpdateChannel(channel)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return newChannel, nil
|
|
}
|
|
|
|
func (a *App) CreateChannel(c *request.Context, channel *model.Channel, addMember bool) (*model.Channel, *model.AppError) {
|
|
channel.DisplayName = strings.TrimSpace(channel.DisplayName)
|
|
sc, nErr := a.Srv().Store.Channel().Save(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)
|
|
case invErr.Entity == "Channel" && invErr.Field == "Type":
|
|
return nil, model.NewAppError("CreateChannel", "store.sql_channel.save.direct_channel.app_error", nil, "", http.StatusBadRequest)
|
|
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)
|
|
}
|
|
case errors.As(nErr, &cErr):
|
|
return sc, model.NewAppError("CreateChannel", store.ChannelExistsError, nil, cErr.Error(), http.StatusBadRequest)
|
|
case errors.As(nErr, <Err):
|
|
return nil, model.NewAppError("CreateChannel", "store.sql_channel.save_channel.limit.app_error", nil, ltErr.Error(), http.StatusBadRequest)
|
|
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, nErr.Error(), http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
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, nfErr.Error(), http.StatusNotFound)
|
|
default:
|
|
return nil, model.NewAppError("CreateChannel", "app.user.get.app_error", nil, nErr.Error(), http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
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(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, cErr.Error(), http.StatusBadRequest)
|
|
}
|
|
case errors.As(nErr, &appErr):
|
|
return nil, appErr
|
|
default:
|
|
return nil, model.NewAppError("CreateChannel", "app.channel.create_direct_channel.internal_error", nil, nErr.Error(), http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
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, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
|
|
a.InvalidateCacheForUser(channel.CreatorId)
|
|
}
|
|
|
|
if pluginsEnvironment := a.GetPluginsEnvironment(); pluginsEnvironment != nil {
|
|
a.Srv().Go(func() {
|
|
pluginContext := pluginContext(c)
|
|
pluginsEnvironment.RunMultiPluginHook(func(hooks plugin.Hooks) bool {
|
|
hooks.ChannelHasBeenCreated(pluginContext, sc)
|
|
return true
|
|
}, plugin.ChannelHasBeenCreatedID)
|
|
})
|
|
}
|
|
|
|
return sc, nil
|
|
}
|
|
|
|
func (a *App) GetOrCreateDirectChannel(c *request.Context, userID, otherUserID string, channelOptions ...model.ChannelOption) (*model.Channel, *model.AppError) {
|
|
channel, nErr := a.getDirectChannel(userID, otherUserID)
|
|
if nErr != nil {
|
|
return nil, nErr
|
|
}
|
|
|
|
if channel != nil {
|
|
return channel, nil
|
|
}
|
|
|
|
if *a.Config().TeamSettings.RestrictDirectMessage == model.DirectMessageTeam &&
|
|
!a.SessionHasPermissionTo(*c.Session(), model.PermissionManageSystem) {
|
|
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(userID, otherUserID, channelOptions...)
|
|
if err != nil {
|
|
if err.Id == store.ChannelExistsError {
|
|
return channel, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
a.handleCreationEvent(c, userID, otherUserID, channel)
|
|
return channel, nil
|
|
}
|
|
|
|
func (a *App) getOrCreateDirectChannelWithUser(c *request.Context, user, otherUser *model.User) (*model.Channel, *model.AppError) {
|
|
channel, nErr := a.getDirectChannel(user.Id, otherUser.Id)
|
|
if nErr != nil {
|
|
return nil, nErr
|
|
}
|
|
|
|
if channel != nil {
|
|
return channel, nil
|
|
}
|
|
|
|
channel, err := a.createDirectChannelWithUser(user, otherUser)
|
|
if err != nil {
|
|
if err.Id == store.ChannelExistsError {
|
|
return channel, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
a.handleCreationEvent(c, user.Id, otherUser.Id, channel)
|
|
return channel, nil
|
|
}
|
|
|
|
func (a *App) handleCreationEvent(c *request.Context, userID, otherUserID string, channel *model.Channel) {
|
|
a.InvalidateCacheForUser(userID)
|
|
a.InvalidateCacheForUser(otherUserID)
|
|
|
|
if pluginsEnvironment := a.GetPluginsEnvironment(); pluginsEnvironment != nil {
|
|
a.Srv().Go(func() {
|
|
pluginContext := pluginContext(c)
|
|
pluginsEnvironment.RunMultiPluginHook(func(hooks plugin.Hooks) 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(userID string, otherUserID string, channelOptions ...model.ChannelOption) (*model.Channel, *model.AppError) {
|
|
users, err := a.Srv().Store.User().GetMany(context.Background(), []string{userID, otherUserID})
|
|
if err != nil {
|
|
return nil, model.NewAppError("CreateDirectChannel", "api.channel.create_direct_channel.invalid_user.app_error", nil, err.Error(), http.StatusBadRequest)
|
|
}
|
|
|
|
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 bellow 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
|
|
// bellow 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(user, otherUser, channelOptions...)
|
|
}
|
|
|
|
func (a *App) createDirectChannelWithUser(user, otherUser *model.User, channelOptions ...model.ChannelOption) (*model.Channel, *model.AppError) {
|
|
channel, nErr := a.Srv().Store.Channel().CreateDirectChannel(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)
|
|
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)
|
|
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)
|
|
}
|
|
case errors.As(nErr, &cErr):
|
|
switch cErr.Resource {
|
|
case "Channel":
|
|
return channel, model.NewAppError("createDirectChannelWithUser", store.ChannelExistsError, nil, cErr.Error(), http.StatusBadRequest)
|
|
case "ChannelMembers":
|
|
return nil, model.NewAppError("createDirectChannelWithUser", "app.channel.save_member.exists.app_error", nil, cErr.Error(), http.StatusBadRequest)
|
|
}
|
|
case errors.As(nErr, <Err):
|
|
return nil, model.NewAppError("createDirectChannelWithUser", "store.sql_channel.save_channel.limit.app_error", nil, ltErr.Error(), http.StatusBadRequest)
|
|
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, nErr.Error(), http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
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, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
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, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
// 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.SaveSharedChannel(sc); err != nil {
|
|
return nil, model.NewAppError("CreateDirectChannel", "app.sharedchannel.dm_channel_creation.internal_error", nil, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
return channel, nil
|
|
}
|
|
|
|
func (a *App) CreateGroupChannel(userIDs []string, creatorId string) (*model.Channel, *model.AppError) {
|
|
channel, err := a.createGroupChannel(userIDs)
|
|
if err != nil {
|
|
if err.Id == store.ChannelExistsError {
|
|
return channel, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
for _, userID := range userIDs {
|
|
a.InvalidateCacheForUser(userID)
|
|
}
|
|
|
|
message := model.NewWebSocketEvent(model.WebsocketEventGroupAdded, "", channel.Id, "", nil)
|
|
message.Add("teammate_ids", model.ArrayToJSON(userIDs))
|
|
a.Publish(message)
|
|
|
|
return channel, nil
|
|
}
|
|
|
|
func (a *App) createGroupChannel(userIDs []string) (*model.Channel, *model.AppError) {
|
|
if len(userIDs) > model.ChannelGroupMaxUsers || len(userIDs) < model.ChannelGroupMinUsers {
|
|
return nil, model.NewAppError("CreateGroupChannel", "api.channel.create_group.bad_size.app_error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
users, err := a.Srv().Store.User().GetProfileByIds(context.Background(), userIDs, nil, true)
|
|
if err != nil {
|
|
return nil, model.NewAppError("createGroupChannel", "app.user.get_profiles.app_error", nil, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
group := &model.Channel{
|
|
Name: model.GetGroupNameFromUserIds(userIDs),
|
|
DisplayName: model.GetGroupDisplayNameFromUsers(users, true),
|
|
Type: model.ChannelTypeGroup,
|
|
}
|
|
|
|
channel, nErr := a.Srv().Store.Channel().Save(group, *a.Config().TeamSettings.MaxChannelsPerTeam)
|
|
if nErr != nil {
|
|
var invErr *store.ErrInvalidInput
|
|
var cErr *store.ErrConflict
|
|
var ltErr *store.ErrLimitExceeded
|
|
var appErr *model.AppError
|
|
switch {
|
|
case errors.As(nErr, &invErr):
|
|
switch {
|
|
case invErr.Entity == "Channel" && invErr.Field == "DeleteAt":
|
|
return nil, model.NewAppError("CreateChannel", "store.sql_channel.save.archived_channel.app_error", nil, "", http.StatusBadRequest)
|
|
case invErr.Entity == "Channel" && invErr.Field == "Type":
|
|
return nil, model.NewAppError("CreateChannel", "store.sql_channel.save.direct_channel.app_error", nil, "", http.StatusBadRequest)
|
|
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)
|
|
}
|
|
case errors.As(nErr, &cErr):
|
|
return channel, model.NewAppError("CreateChannel", store.ChannelExistsError, nil, cErr.Error(), http.StatusBadRequest)
|
|
case errors.As(nErr, <Err):
|
|
return nil, model.NewAppError("CreateChannel", "store.sql_channel.save_channel.limit.app_error", nil, ltErr.Error(), http.StatusBadRequest)
|
|
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, nErr.Error(), http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
for _, user := range users {
|
|
cm := &model.ChannelMember{
|
|
UserId: user.Id,
|
|
ChannelId: group.Id,
|
|
NotifyProps: model.GetDefaultChannelNotifyProps(),
|
|
SchemeGuest: user.IsGuest(),
|
|
SchemeUser: !user.IsGuest(),
|
|
}
|
|
|
|
if _, nErr = a.Srv().Store.Channel().SaveMember(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, cErr.Error(), http.StatusBadRequest)
|
|
}
|
|
case errors.As(nErr, &appErr):
|
|
return nil, appErr
|
|
default:
|
|
return nil, model.NewAppError("createGroupChannel", "app.channel.create_direct_channel.internal_error", nil, nErr.Error(), http.StatusInternalServerError)
|
|
}
|
|
}
|
|
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, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
return channel, nil
|
|
}
|
|
|
|
func (a *App) GetGroupChannel(userIDs []string) (*model.Channel, *model.AppError) {
|
|
if len(userIDs) > model.ChannelGroupMaxUsers || len(userIDs) < model.ChannelGroupMinUsers {
|
|
return nil, model.NewAppError("GetGroupChannel", "api.channel.create_group.bad_size.app_error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
users, err := a.Srv().Store.User().GetProfileByIds(context.Background(), userIDs, nil, true)
|
|
if err != nil {
|
|
return nil, model.NewAppError("GetGroupChannel", "app.user.get_profiles.app_error", nil, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
|
|
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(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(channel *model.Channel) (*model.Channel, *model.AppError) {
|
|
_, err := a.Srv().Store.Channel().Update(channel)
|
|
if err != nil {
|
|
var appErr *model.AppError
|
|
var invErr *store.ErrInvalidInput
|
|
switch {
|
|
case errors.As(err, &invErr):
|
|
return nil, model.NewAppError("UpdateChannel", "app.channel.update.bad_id", nil, invErr.Error(), http.StatusBadRequest)
|
|
case errors.As(err, &appErr):
|
|
return nil, appErr
|
|
default:
|
|
return nil, model.NewAppError("UpdateChannel", "app.channel.update_channel.internal_error", nil, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
a.invalidateCacheForChannel(channel)
|
|
|
|
messageWs := model.NewWebSocketEvent(model.WebsocketEventChannelUpdated, "", channel.Id, "", nil)
|
|
channelJSON, jsonErr := json.Marshal(channel)
|
|
if jsonErr != nil {
|
|
mlog.Warn("Failed to encode channel to JSON", mlog.Err(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(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(channel); err != nil {
|
|
return nil, err
|
|
}
|
|
return scheme, nil
|
|
}
|
|
|
|
// DeleteChannelScheme deletes a channels scheme and sets its SchemeId to nil.
|
|
func (a *App) DeleteChannelScheme(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(channel)
|
|
}
|
|
|
|
// UpdateChannelScheme saves the new SchemeId of the channel passed.
|
|
func (a *App) UpdateChannelScheme(channel *model.Channel) (*model.Channel, *model.AppError) {
|
|
var oldChannel *model.Channel
|
|
var err *model.AppError
|
|
if oldChannel, err = a.GetChannel(channel.Id); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
oldChannel.SchemeId = channel.SchemeId
|
|
return a.UpdateChannel(oldChannel)
|
|
}
|
|
|
|
func (a *App) UpdateChannelPrivacy(c *request.Context, oldChannel *model.Channel, user *model.User) (*model.Channel, *model.AppError) {
|
|
channel, err := a.UpdateChannel(oldChannel)
|
|
if err != nil {
|
|
return channel, err
|
|
}
|
|
|
|
if err := a.postChannelPrivacyMessage(c, user, channel); err != nil {
|
|
if channel.Type == model.ChannelTypeOpen {
|
|
channel.Type = model.ChannelTypePrivate
|
|
} else {
|
|
channel.Type = model.ChannelTypeOpen
|
|
}
|
|
// revert to previous channel privacy
|
|
a.UpdateChannel(channel)
|
|
return channel, err
|
|
}
|
|
|
|
a.invalidateCacheForChannel(channel)
|
|
|
|
messageWs := model.NewWebSocketEvent(model.WebsocketEventChannelConverted, channel.TeamId, "", "", nil)
|
|
messageWs.Add("channel_id", channel.Id)
|
|
a.Publish(messageWs)
|
|
|
|
return channel, nil
|
|
}
|
|
|
|
func (a *App) postChannelPrivacyMessage(c *request.Context, 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()
|
|
if err != nil {
|
|
return model.NewAppError("postChannelPrivacyMessage", "api.channel.post_channel_privacy_message.error", nil, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
|
|
authorId = systemBot.UserId
|
|
authorUsername = systemBot.Username
|
|
}
|
|
|
|
message := (map[model.ChannelType]string{
|
|
model.ChannelTypeOpen: i18n.T("api.channel.change_channel_privacy.private_to_public"),
|
|
model.ChannelTypePrivate: i18n.T("api.channel.change_channel_privacy.public_to_private"),
|
|
})[channel.Type]
|
|
post := &model.Post{
|
|
ChannelId: channel.Id,
|
|
Message: message,
|
|
Type: model.PostTypeChangeChannelPrivacy,
|
|
UserId: authorId,
|
|
Props: model.StringInterface{
|
|
"username": authorUsername,
|
|
},
|
|
}
|
|
|
|
if _, err := a.CreatePost(c, post, channel, false, true); err != nil {
|
|
return model.NewAppError("postChannelPrivacyMessage", "api.channel.post_channel_privacy_message.error", nil, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) RestoreChannel(c *request.Context, 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, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
channel.DeleteAt = 0
|
|
a.invalidateCacheForChannel(channel)
|
|
|
|
message := model.NewWebSocketEvent(model.WebsocketEventChannelRestored, channel.TeamId, "", "", 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, nfErr.Error(), http.StatusNotFound)
|
|
default:
|
|
return nil, model.NewAppError("RestoreChannel", "app.user.get.app_error", nil, nErr.Error(), http.StatusInternalServerError)
|
|
}
|
|
}
|
|
}
|
|
|
|
if user != nil {
|
|
T := i18n.GetUserTranslations(user.Locale)
|
|
|
|
post := &model.Post{
|
|
ChannelId: channel.Id,
|
|
Message: T("api.channel.restore_channel.unarchived", map[string]interface{}{"Username": user.Username}),
|
|
Type: model.PostTypeChannelRestored,
|
|
UserId: userID,
|
|
Props: model.StringInterface{
|
|
"username": user.Username,
|
|
},
|
|
}
|
|
|
|
if _, err := a.CreatePost(c, post, channel, false, true); err != nil {
|
|
mlog.Warn("Failed to post unarchive message", mlog.Err(err))
|
|
}
|
|
} else {
|
|
a.Srv().Go(func() {
|
|
systemBot, err := a.GetSystemBot()
|
|
if err != nil {
|
|
mlog.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]interface{}{"Username": systemBot.Username}),
|
|
Type: model.PostTypeChannelRestored,
|
|
UserId: systemBot.UserId,
|
|
Props: model.StringInterface{
|
|
"username": systemBot.Username,
|
|
},
|
|
}
|
|
|
|
if _, err := a.CreatePost(c, post, channel, false, true); err != nil {
|
|
mlog.Error("Failed to post unarchive message", mlog.Err(err))
|
|
}
|
|
})
|
|
}
|
|
|
|
return channel, nil
|
|
}
|
|
|
|
func (a *App) PatchChannel(c *request.Context, channel *model.Channel, patch *model.ChannelPatch, userID string) (*model.Channel, *model.AppError) {
|
|
oldChannelDisplayName := channel.DisplayName
|
|
oldChannelHeader := channel.Header
|
|
oldChannelPurpose := channel.Purpose
|
|
|
|
channel.Patch(patch)
|
|
channel, err := a.UpdateChannel(channel)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if oldChannelDisplayName != channel.DisplayName {
|
|
if err = a.PostUpdateChannelDisplayNameMessage(c, userID, channel, oldChannelDisplayName, channel.DisplayName); err != nil {
|
|
mlog.Warn(err.Error())
|
|
}
|
|
}
|
|
|
|
if channel.Header != oldChannelHeader {
|
|
if err = a.PostUpdateChannelHeaderMessage(c, userID, channel, oldChannelHeader, channel.Header); err != nil {
|
|
mlog.Warn(err.Error())
|
|
}
|
|
}
|
|
|
|
if channel.Purpose != oldChannelPurpose {
|
|
if err = a.PostUpdateChannelPurposeMessage(c, userID, channel, oldChannelPurpose, channel.Purpose); err != nil {
|
|
mlog.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(channelID string) (guestRoleName, userRoleName, adminRoleName string, err *model.AppError) {
|
|
channel, err := a.GetChannel(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(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(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(channel *model.Channel) ([]*model.ChannelModeration, *model.AppError) {
|
|
guestRoleName, memberRoleName, _, err := a.GetSchemeRolesForChannel(channel.Id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
memberRole, err := a.GetRoleByName(context.Background(), memberRoleName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var guestRole *model.Role
|
|
if guestRoleName != "" {
|
|
guestRole, err = a.GetRoleByName(context.Background(), guestRoleName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
higherScopedGuestRoleName, higherScopedMemberRoleName, _, err := a.GetTeamSchemeChannelRoles(channel.TeamId)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
higherScopedMemberRole, err := a.GetRoleByName(context.Background(), higherScopedMemberRoleName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var higherScopedGuestRole *model.Role
|
|
if higherScopedGuestRoleName != "" {
|
|
higherScopedGuestRole, err = a.GetRoleByName(context.Background(), higherScopedGuestRoleName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return buildChannelModerations(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(channel *model.Channel, channelModerationsPatch []*model.ChannelModerationPatch) ([]*model.ChannelModeration, *model.AppError) {
|
|
higherScopedGuestRoleName, higherScopedMemberRoleName, _, err := a.GetTeamSchemeChannelRoles(channel.TeamId)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
ctx := sqlstore.WithMaster(context.Background())
|
|
higherScopedMemberRole, err := a.GetRoleByName(ctx, higherScopedMemberRoleName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var higherScopedGuestRole *model.Role
|
|
if higherScopedGuestRoleName != "" {
|
|
higherScopedGuestRole, err = a.GetRoleByName(ctx, higherScopedGuestRoleName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
higherScopedMemberPermissions := higherScopedMemberRole.GetChannelModeratedPermissions(channel.Type)
|
|
|
|
var higherScopedGuestPermissions map[string]bool
|
|
if higherScopedGuestRole != nil {
|
|
higherScopedGuestPermissions = higherScopedGuestRole.GetChannelModeratedPermissions(channel.Type)
|
|
}
|
|
|
|
for _, moderationPatch := range channelModerationsPatch {
|
|
if moderationPatch.Roles.Members != nil && *moderationPatch.Roles.Members && !higherScopedMemberPermissions[*moderationPatch.Name] {
|
|
return nil, &model.AppError{Message: "Cannot add a permission that is restricted by the team or system permission scheme"}
|
|
}
|
|
if moderationPatch.Roles.Guests != nil && *moderationPatch.Roles.Guests && !higherScopedGuestPermissions[*moderationPatch.Name] {
|
|
return nil, &model.AppError{Message: "Cannot add a permission that is restricted by the team or system permission scheme"}
|
|
}
|
|
}
|
|
|
|
var scheme *model.Scheme
|
|
// Channel has no scheme so create one
|
|
if channel.SchemeId == nil || *channel.SchemeId == "" {
|
|
scheme, err = a.CreateChannelScheme(channel)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Send a websocket event about this new role. The other new roles—member and guest—get emitted when they're updated.
|
|
var adminRole *model.Role
|
|
adminRole, err = a.GetRoleByName(ctx, scheme.DefaultChannelAdminRole)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
a.sendUpdatedRoleEvent(adminRole)
|
|
|
|
message := model.NewWebSocketEvent(model.WebsocketEventChannelSchemeUpdated, "", channel.Id, "", nil)
|
|
a.Publish(message)
|
|
mlog.Info("Permission scheme created.", mlog.String("channel_id", channel.Id), mlog.String("channel_name", channel.Name))
|
|
} else {
|
|
scheme, err = a.GetScheme(*channel.SchemeId)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
guestRoleName := scheme.DefaultChannelGuestRole
|
|
memberRoleName := scheme.DefaultChannelUserRole
|
|
memberRole, err := a.GetRoleByName(ctx, memberRoleName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var guestRole *model.Role
|
|
if guestRoleName != "" {
|
|
guestRole, err = a.GetRoleByName(ctx, guestRoleName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
memberRolePatch := memberRole.RolePatchFromChannelModerationsPatch(channelModerationsPatch, "members")
|
|
var guestRolePatch *model.RolePatch
|
|
if guestRole != nil {
|
|
guestRolePatch = guestRole.RolePatchFromChannelModerationsPatch(channelModerationsPatch, "guests")
|
|
}
|
|
|
|
for _, channelModerationPatch := range channelModerationsPatch {
|
|
permissionModified := *channelModerationPatch.Name
|
|
if channelModerationPatch.Roles.Guests != nil && utils.StringInSlice(permissionModified, model.ChannelModeratedPermissionsChangedByPatch(guestRole, guestRolePatch)) {
|
|
if *channelModerationPatch.Roles.Guests {
|
|
mlog.Info("Permission enabled for guests.", mlog.String("permission", permissionModified), mlog.String("channel_id", channel.Id), mlog.String("channel_name", channel.Name))
|
|
} else {
|
|
mlog.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 && utils.StringInSlice(permissionModified, model.ChannelModeratedPermissionsChangedByPatch(memberRole, memberRolePatch)) {
|
|
if *channelModerationPatch.Roles.Members {
|
|
mlog.Info("Permission enabled for members.", mlog.String("permission", permissionModified), mlog.String("channel_id", channel.Id), mlog.String("channel_name", channel.Name))
|
|
} else {
|
|
mlog.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(channel); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
message := model.NewWebSocketEvent(model.WebsocketEventChannelSchemeUpdated, "", channel.Id, "", nil)
|
|
a.Publish(message)
|
|
|
|
memberRole = higherScopedMemberRole
|
|
guestRole = higherScopedGuestRole
|
|
mlog.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(channel.Id, func(channelMember model.ChannelMember) error {
|
|
a.Srv().Store.Channel().InvalidateAllChannelMembersForUser(channelMember.UserId)
|
|
return nil
|
|
})
|
|
if cErr != nil {
|
|
return nil, model.NewAppError("PatchChannelModerationsForChannel", "api.channel.patch_channel_moderations.cache_invalidation.error", nil, cErr.Error(), http.StatusInternalServerError)
|
|
}
|
|
|
|
return buildChannelModerations(channel.Type, memberRole, guestRole, higherScopedMemberRole, higherScopedGuestRole), nil
|
|
}
|
|
|
|
func buildChannelModerations(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" {
|
|
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(channelID string, userID string, newRoles string) (*model.ChannelMember, *model.AppError) {
|
|
var member *model.ChannelMember
|
|
var err *model.AppError
|
|
if member, err = a.GetChannelMember(context.Background(), channelID, userID); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
schemeGuestRole, schemeUserRole, schemeAdminRole, err := a.GetSchemeRolesForChannel(channelID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
prevSchemeGuestValue := member.SchemeGuest
|
|
|
|
var newExplicitRoles []string
|
|
member.SchemeGuest = false
|
|
member.SchemeUser = false
|
|
member.SchemeAdmin = false
|
|
|
|
for _, roleName := range strings.Fields(newRoles) {
|
|
var role *model.Role
|
|
role, err = a.GetRoleByName(context.Background(), roleName)
|
|
if err != nil {
|
|
err.StatusCode = http.StatusBadRequest
|
|
return nil, err
|
|
}
|
|
|
|
if !role.SchemeManaged {
|
|
// The role is not scheme-managed, so it's OK to apply it to the explicit roles field.
|
|
newExplicitRoles = append(newExplicitRoles, roleName)
|
|
} else {
|
|
// The role is scheme-managed, so need to check if it is part of the scheme for this channel or not.
|
|
switch roleName {
|
|
case schemeAdminRole:
|
|
member.SchemeAdmin = true
|
|
case schemeUserRole:
|
|
member.SchemeUser = true
|
|
case schemeGuestRole:
|
|
member.SchemeGuest = true
|
|
default:
|
|
// If not part of the scheme for this channel, then it is not allowed to apply it as an explicit role.
|
|
return nil, model.NewAppError("UpdateChannelMemberRoles", "api.channel.update_channel_member_roles.scheme_role.app_error", nil, "role_name="+roleName, http.StatusBadRequest)
|
|
}
|
|
}
|
|
}
|
|
|
|
if member.SchemeUser && member.SchemeGuest {
|
|
return nil, model.NewAppError("UpdateChannelMemberRoles", "api.channel.update_channel_member_roles.guest_and_user.app_error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
if prevSchemeGuestValue != member.SchemeGuest {
|
|
return nil, model.NewAppError("UpdateChannelMemberRoles", "api.channel.update_channel_member_roles.changing_guest_role.app_error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
member.ExplicitRoles = strings.Join(newExplicitRoles, " ")
|
|
|
|
return a.updateChannelMember(member)
|
|
}
|
|
|
|
func (a *App) UpdateChannelMemberSchemeRoles(channelID string, userID string, isSchemeGuest bool, isSchemeUser bool, isSchemeAdmin bool) (*model.ChannelMember, *model.AppError) {
|
|
member, err := a.GetChannelMember(context.Background(), channelID, userID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
member.SchemeAdmin = isSchemeAdmin
|
|
member.SchemeUser = isSchemeUser
|
|
member.SchemeGuest = isSchemeGuest
|
|
|
|
if member.SchemeUser && member.SchemeGuest {
|
|
return nil, model.NewAppError("UpdateChannelMemberSchemeRoles", "api.channel.update_channel_member_roles.guest_and_user.app_error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
// 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(member)
|
|
}
|
|
|
|
func (a *App) UpdateChannelMemberNotifyProps(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_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
|
|
}
|
|
|
|
member, err := a.Srv().Store.Channel().UpdateMemberNotifyProps(channelID, userID, filteredProps)
|
|
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("updateMemberNotifyProps", MissingChannelMemberError, nil, nfErr.Error(), http.StatusNotFound)
|
|
default:
|
|
return nil, model.NewAppError("updateMemberNotifyProps", "app.channel.get_member.app_error", nil, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
a.InvalidateCacheForUser(member.UserId)
|
|
a.invalidateCacheForChannelMembersNotifyProps(member.ChannelId)
|
|
|
|
// 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 {
|
|
mlog.Warn("Failed to encode channel member to JSON", mlog.Err(jsonErr))
|
|
}
|
|
evt.Add("channelMember", string(memberJSON))
|
|
a.Publish(evt)
|
|
|
|
return member, nil
|
|
}
|
|
|
|
func (a *App) updateChannelMember(member *model.ChannelMember) (*model.ChannelMember, *model.AppError) {
|
|
member, nErr := a.Srv().Store.Channel().UpdateMember(member)
|
|
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("updateChannelMember", MissingChannelMemberError, nil, nfErr.Error(), http.StatusNotFound)
|
|
default:
|
|
return nil, model.NewAppError("updateChannelMember", "app.channel.get_member.app_error", nil, nErr.Error(), http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
a.InvalidateCacheForUser(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 {
|
|
mlog.Warn("Failed to encode channel member to JSON", mlog.Err(jsonErr))
|
|
}
|
|
evt.Add("channelMember", string(memberJSON))
|
|
a.Publish(evt)
|
|
|
|
return member, nil
|
|
}
|
|
|
|
func (a *App) DeleteChannel(c *request.Context, channel *model.Channel, userID string) *model.AppError {
|
|
ihc := make(chan store.StoreResult, 1)
|
|
ohc := make(chan store.StoreResult, 1)
|
|
|
|
go func() {
|
|
webhooks, err := a.Srv().Store.Webhook().GetIncomingByChannel(channel.Id)
|
|
ihc <- store.StoreResult{Data: webhooks, NErr: err}
|
|
close(ihc)
|
|
}()
|
|
|
|
go func() {
|
|
outgoingHooks, err := a.Srv().Store.Webhook().GetOutgoingByChannel(channel.Id, -1, -1)
|
|
ohc <- store.StoreResult{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, nfErr.Error(), http.StatusNotFound)
|
|
default:
|
|
return model.NewAppError("DeleteChannel", "app.user.get.app_error", nil, nErr.Error(), http.StatusInternalServerError)
|
|
}
|
|
}
|
|
}
|
|
|
|
ihcresult := <-ihc
|
|
if ihcresult.NErr != nil {
|
|
return model.NewAppError("DeleteChannel", "app.webhooks.get_incoming_by_channel.app_error", nil, ihcresult.NErr.Error(), http.StatusInternalServerError)
|
|
}
|
|
|
|
ohcresult := <-ohc
|
|
if ohcresult.NErr != nil {
|
|
return model.NewAppError("DeleteChannel", "app.webhooks.get_outgoing_by_channel.app_error", nil, ohcresult.NErr.Error(), http.StatusInternalServerError)
|
|
}
|
|
|
|
incomingHooks := ihcresult.Data.([]*model.IncomingWebhook)
|
|
outgoingHooks := ohcresult.Data.([]*model.OutgoingWebhook)
|
|
|
|
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]interface{}{"Channel": model.DefaultChannelName}, "", http.StatusBadRequest)
|
|
return err
|
|
}
|
|
|
|
if user != nil {
|
|
T := i18n.GetUserTranslations(user.Locale)
|
|
|
|
post := &model.Post{
|
|
ChannelId: channel.Id,
|
|
Message: fmt.Sprintf(T("api.channel.delete_channel.archived"), user.Username),
|
|
Type: model.PostTypeChannelDeleted,
|
|
UserId: userID,
|
|
Props: model.StringInterface{
|
|
"username": user.Username,
|
|
},
|
|
}
|
|
|
|
if _, err := a.CreatePost(c, post, channel, false, true); err != nil {
|
|
mlog.Warn("Failed to post archive message", mlog.Err(err))
|
|
}
|
|
} else {
|
|
a.Srv().Go(func() {
|
|
systemBot, err := a.GetSystemBot()
|
|
if err != nil {
|
|
mlog.Error("Failed to post archive message", mlog.Err(err))
|
|
return
|
|
}
|
|
|
|
post := &model.Post{
|
|
ChannelId: channel.Id,
|
|
Message: fmt.Sprintf(i18n.T("api.channel.delete_channel.archived"), systemBot.Username),
|
|
Type: model.PostTypeChannelDeleted,
|
|
UserId: systemBot.UserId,
|
|
Props: model.StringInterface{
|
|
"username": systemBot.Username,
|
|
},
|
|
}
|
|
|
|
if _, err := a.CreatePost(c, post, channel, false, true); err != nil {
|
|
mlog.Error("Failed to post archive message", mlog.Err(err))
|
|
}
|
|
})
|
|
}
|
|
|
|
now := model.GetMillis()
|
|
for _, hook := range incomingHooks {
|
|
if err := a.Srv().Store.Webhook().DeleteIncoming(hook.Id, now); err != nil {
|
|
mlog.Warn("Encountered error deleting incoming webhook", mlog.String("hook_id", hook.Id), mlog.Err(err))
|
|
}
|
|
a.invalidateCacheForWebhook(hook.Id)
|
|
}
|
|
|
|
for _, hook := range outgoingHooks {
|
|
if err := a.Srv().Store.Webhook().DeleteOutgoing(hook.Id, now); err != nil {
|
|
mlog.Warn("Encountered error deleting outgoing webhook", mlog.String("hook_id", hook.Id), mlog.Err(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, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
a.invalidateCacheForChannel(channel)
|
|
|
|
message := model.NewWebSocketEvent(model.WebsocketEventChannelDeleted, channel.TeamId, "", "", nil)
|
|
message.Add("channel_id", channel.Id)
|
|
message.Add("delete_at", deleteAt)
|
|
a.Publish(message)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) addUserToChannel(user *model.User, channel *model.Channel) (*model.ChannelMember, *model.AppError) {
|
|
if channel.Type != model.ChannelTypeOpen && channel.Type != model.ChannelTypePrivate {
|
|
return nil, model.NewAppError("AddUserToChannel", "api.channel.add_user_to_channel.type.app_error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
channelMember, nErr := a.Srv().Store.Channel().GetMember(context.Background(), channel.Id, user.Id)
|
|
if nErr != nil {
|
|
var nfErr *store.ErrNotFound
|
|
if !errors.As(nErr, &nfErr) {
|
|
return nil, model.NewAppError("AddUserToChannel", "app.channel.get_member.app_error", nil, nErr.Error(), http.StatusInternalServerError)
|
|
}
|
|
} else {
|
|
return channelMember, nil
|
|
}
|
|
|
|
if channel.IsGroupConstrained() {
|
|
nonMembers, err := a.FilterNonGroupChannelMembers([]string{user.Id}, channel)
|
|
if err != nil {
|
|
return nil, model.NewAppError("addUserToChannel", "api.channel.add_user_to_channel.type.app_error", nil, "", http.StatusInternalServerError)
|
|
}
|
|
if len(nonMembers) > 0 {
|
|
return nil, model.NewAppError("addUserToChannel", "api.channel.add_members.user_denied", map[string]interface{}{"UserIDs": nonMembers}, "", http.StatusBadRequest)
|
|
}
|
|
}
|
|
|
|
newMember := &model.ChannelMember{
|
|
ChannelId: channel.Id,
|
|
UserId: user.Id,
|
|
NotifyProps: model.GetDefaultChannelNotifyProps(),
|
|
SchemeGuest: user.IsGuest(),
|
|
SchemeUser: !user.IsGuest(),
|
|
}
|
|
|
|
if !user.IsGuest() {
|
|
var userShouldBeAdmin bool
|
|
userShouldBeAdmin, appErr := a.UserIsInAdminRoleGroup(user.Id, channel.Id, model.GroupSyncableTypeChannel)
|
|
if appErr != nil {
|
|
return nil, appErr
|
|
}
|
|
newMember.SchemeAdmin = userShouldBeAdmin
|
|
}
|
|
|
|
newMember, nErr = a.Srv().Store.Channel().SaveMember(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, nErr.Error(), http.StatusInternalServerError)
|
|
}
|
|
|
|
a.InvalidateCacheForUser(user.Id)
|
|
a.invalidateCacheForChannelMembers(channel.Id)
|
|
|
|
return newMember, nil
|
|
}
|
|
|
|
// AddUserToChannel adds a user to a given channel.
|
|
func (a *App) AddUserToChannel(user *model.User, channel *model.Channel, skipTeamMemberIntegrityCheck bool) (*model.ChannelMember, *model.AppError) {
|
|
if !skipTeamMemberIntegrityCheck {
|
|
teamMember, nErr := a.Srv().Store.Team().GetMember(context.Background(), 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, nfErr.Error(), http.StatusNotFound)
|
|
default:
|
|
return nil, model.NewAppError("AddUserToChannel", "app.team.get_member.app_error", nil, nErr.Error(), http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
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(user, channel)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
message := model.NewWebSocketEvent(model.WebsocketEventUserAdded, "", channel.Id, "", nil)
|
|
message.Add("user_id", user.Id)
|
|
message.Add("team_id", channel.TeamId)
|
|
a.Publish(message)
|
|
|
|
return newMember, nil
|
|
}
|
|
|
|
type ChannelMemberOpts struct {
|
|
UserRequestorID string
|
|
PostRootID string
|
|
// SkipTeamMemberIntegrityCheck is used to indicate whether it should be checked
|
|
// that a user has already been removed from that team or not.
|
|
// This is useful to avoid in scenarios when we just added the team member,
|
|
// and thereby know that there is no need to check this.
|
|
SkipTeamMemberIntegrityCheck bool
|
|
}
|
|
|
|
// AddChannelMember adds a user to a channel. It is a wrapper over AddUserToChannel.
|
|
func (a *App) AddChannelMember(c *request.Context, userID string, channel *model.Channel, opts ChannelMemberOpts) (*model.ChannelMember, *model.AppError) {
|
|
if member, err := a.Srv().Store.Channel().GetMember(context.Background(), channel.Id, userID); err != nil {
|
|
var nfErr *store.ErrNotFound
|
|
if !errors.As(err, &nfErr) {
|
|
return nil, model.NewAppError("AddChannelMember", "app.channel.get_member.app_error", nil, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
} else {
|
|
return member, nil
|
|
}
|
|
|
|
var user *model.User
|
|
var err *model.AppError
|
|
|
|
if user, err = a.GetUser(userID); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var userRequestor *model.User
|
|
if opts.UserRequestorID != "" {
|
|
if userRequestor, err = a.GetUser(opts.UserRequestorID); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
cm, err := a.AddUserToChannel(user, channel, opts.SkipTeamMemberIntegrityCheck)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if pluginsEnvironment := a.GetPluginsEnvironment(); pluginsEnvironment != nil {
|
|
a.Srv().Go(func() {
|
|
pluginContext := pluginContext(c)
|
|
pluginsEnvironment.RunMultiPluginHook(func(hooks plugin.Hooks) bool {
|
|
hooks.UserHasJoinedChannel(pluginContext, cm, userRequestor)
|
|
return true
|
|
}, plugin.UserHasJoinedChannelID)
|
|
})
|
|
}
|
|
|
|
if opts.UserRequestorID == "" || userID == opts.UserRequestorID {
|
|
if err := a.postJoinChannelMessage(c, user, channel); err != nil {
|
|
mlog.Error("Failed to post join channel message", mlog.Err(err))
|
|
}
|
|
} else {
|
|
a.Srv().Go(func() {
|
|
a.PostAddToChannelMessage(c, userRequestor, user, channel, opts.PostRootID)
|
|
})
|
|
}
|
|
|
|
return cm, nil
|
|
}
|
|
|
|
func (a *App) AddDirectChannels(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]interface{}{"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]interface{}{"UserId": user.Id, "TeamId": teamID, "Error": err.Error()}, "", http.StatusInternalServerError)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) PostUpdateChannelHeaderMessage(c *request.Context, 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, err.Error(), http.StatusBadRequest)
|
|
}
|
|
|
|
var message string
|
|
if oldChannelHeader == "" {
|
|
message = fmt.Sprintf(i18n.T("api.channel.post_update_channel_header_message_and_forget.updated_to"), user.Username, newChannelHeader)
|
|
} else if newChannelHeader == "" {
|
|
message = fmt.Sprintf(i18n.T("api.channel.post_update_channel_header_message_and_forget.removed"), user.Username, oldChannelHeader)
|
|
} else {
|
|
message = fmt.Sprintf(i18n.T("api.channel.post_update_channel_header_message_and_forget.updated_from"), user.Username, oldChannelHeader, newChannelHeader)
|
|
}
|
|
|
|
post := &model.Post{
|
|
ChannelId: channel.Id,
|
|
Message: message,
|
|
Type: model.PostTypeHeaderChange,
|
|
UserId: userID,
|
|
Props: model.StringInterface{
|
|
"username": user.Username,
|
|
"old_header": oldChannelHeader,
|
|
"new_header": newChannelHeader,
|
|
},
|
|
}
|
|
|
|
if _, err := a.CreatePost(c, post, channel, false, true); err != nil {
|
|
return model.NewAppError("", "api.channel.post_update_channel_header_message_and_forget.post.error", nil, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) PostUpdateChannelPurposeMessage(c *request.Context, 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, err.Error(), http.StatusBadRequest)
|
|
}
|
|
|
|
var message string
|
|
if oldChannelPurpose == "" {
|
|
message = fmt.Sprintf(i18n.T("app.channel.post_update_channel_purpose_message.updated_to"), user.Username, newChannelPurpose)
|
|
} else if newChannelPurpose == "" {
|
|
message = fmt.Sprintf(i18n.T("app.channel.post_update_channel_purpose_message.removed"), user.Username, oldChannelPurpose)
|
|
} else {
|
|
message = fmt.Sprintf(i18n.T("app.channel.post_update_channel_purpose_message.updated_from"), user.Username, oldChannelPurpose, newChannelPurpose)
|
|
}
|
|
|
|
post := &model.Post{
|
|
ChannelId: channel.Id,
|
|
Message: message,
|
|
Type: model.PostTypePurposeChange,
|
|
UserId: userID,
|
|
Props: model.StringInterface{
|
|
"username": user.Username,
|
|
"old_purpose": oldChannelPurpose,
|
|
"new_purpose": newChannelPurpose,
|
|
},
|
|
}
|
|
if _, err := a.CreatePost(c, post, channel, false, true); err != nil {
|
|
return model.NewAppError("", "app.channel.post_update_channel_purpose_message.post.error", nil, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) PostUpdateChannelDisplayNameMessage(c *request.Context, 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, err.Error(), http.StatusBadRequest)
|
|
}
|
|
|
|
message := fmt.Sprintf(i18n.T("api.channel.post_update_channel_displayname_message_and_forget.updated_from"), user.Username, oldChannelDisplayName, newChannelDisplayName)
|
|
|
|
post := &model.Post{
|
|
ChannelId: channel.Id,
|
|
Message: message,
|
|
Type: model.PostTypeDisplaynameChange,
|
|
UserId: userID,
|
|
Props: model.StringInterface{
|
|
"username": user.Username,
|
|
"old_displayname": oldChannelDisplayName,
|
|
"new_displayname": newChannelDisplayName,
|
|
},
|
|
}
|
|
|
|
if _, err := a.CreatePost(c, post, channel, false, true); err != nil {
|
|
return model.NewAppError("PostUpdateChannelDisplayNameMessage", "api.channel.post_update_channel_displayname_message_and_forget.create_post.error", nil, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) GetChannel(channelID string) (*model.Channel, *model.AppError) {
|
|
channel, err := a.Srv().Store.Channel().Get(channelID, true)
|
|
if err != nil {
|
|
var nfErr *store.ErrNotFound
|
|
switch {
|
|
case errors.As(err, &nfErr):
|
|
return nil, model.NewAppError("GetChannel", "app.channel.get.existing.app_error", nil, nfErr.Error(), http.StatusNotFound)
|
|
default:
|
|
return nil, model.NewAppError("GetChannel", "app.channel.get.find.app_error", nil, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
}
|
|
return channel, nil
|
|
}
|
|
|
|
func (a *App) GetChannelByName(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, nfErr.Error(), http.StatusNotFound)
|
|
default:
|
|
return nil, model.NewAppError("GetChannelByName", "app.channel.get_by_name.existing.app_error", nil, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
return channel, nil
|
|
}
|
|
|
|
func (a *App) GetChannelsByNames(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, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
return channels, nil
|
|
}
|
|
|
|
func (a *App) GetChannelByNameForTeamName(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, nfErr.Error(), http.StatusNotFound)
|
|
default:
|
|
return nil, model.NewAppError("GetChannelByNameForTeamName", "app.team.get_by_name.app_error", nil, err.Error(), http.StatusNotFound)
|
|
}
|
|
}
|
|
|
|
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, nfErr.Error(), http.StatusNotFound)
|
|
default:
|
|
return nil, model.NewAppError("GetChannelByNameForTeamName", "app.channel.get_by_name.existing.app_error", nil, nErr.Error(), http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
func (a *App) GetChannelsForTeamForUser(teamID string, userID string, opts *model.ChannelSearchOpts) (model.ChannelList, *model.AppError) {
|
|
list, err := a.Srv().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, nfErr.Error(), http.StatusNotFound)
|
|
default:
|
|
return nil, model.NewAppError("GetChannelsForUser", "app.channel.get_channels.get.app_error", nil, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
return list, nil
|
|
}
|
|
|
|
func (a *App) GetChannelsForTeamForUserWithCursor(teamID string, userID string, opts *model.ChannelSearchOpts, afterChannelID string) (model.ChannelList, *model.AppError) {
|
|
list, err := a.Srv().Store.Channel().GetChannelsWithCursor(teamID, userID, opts, afterChannelID)
|
|
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, nfErr.Error(), http.StatusNotFound)
|
|
default:
|
|
return nil, model.NewAppError("GetChannelsForUser", "app.channel.get_channels.get.app_error", nil, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
return list, nil
|
|
}
|
|
|
|
func (a *App) GetChannelsForUser(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, nfErr.Error(), http.StatusNotFound)
|
|
default:
|
|
return nil, model.NewAppError("GetChannelsForUser", "app.channel.get_channels.get.app_error", nil, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
return list, nil
|
|
}
|
|
|
|
func (a *App) GetAllChannels(page, perPage int, opts model.ChannelSearchOpts) (model.ChannelListWithTeamData, *model.AppError) {
|
|
if opts.ExcludeDefaultChannels {
|
|
opts.ExcludeChannelNames = a.DefaultChannelNames()
|
|
}
|
|
storeOpts := store.ChannelSearchOpts{
|
|
ExcludeChannelNames: opts.ExcludeChannelNames,
|
|
NotAssociatedToGroup: opts.NotAssociatedToGroup,
|
|
IncludeDeleted: opts.IncludeDeleted,
|
|
ExcludePolicyConstrained: opts.ExcludePolicyConstrained,
|
|
IncludePolicyID: opts.IncludePolicyID,
|
|
}
|
|
channels, err := a.Srv().Store.Channel().GetAllChannels(page*perPage, perPage, storeOpts)
|
|
if err != nil {
|
|
return nil, model.NewAppError("GetAllChannels", "app.channel.get_all_channels.app_error", nil, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
|
|
return channels, nil
|
|
}
|
|
|
|
func (a *App) GetAllChannelsCount(opts model.ChannelSearchOpts) (int64, *model.AppError) {
|
|
if opts.ExcludeDefaultChannels {
|
|
opts.ExcludeChannelNames = a.DefaultChannelNames()
|
|
}
|
|
storeOpts := store.ChannelSearchOpts{
|
|
ExcludeChannelNames: opts.ExcludeChannelNames,
|
|
NotAssociatedToGroup: opts.NotAssociatedToGroup,
|
|
IncludeDeleted: opts.IncludeDeleted,
|
|
}
|
|
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, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
|
|
return count, nil
|
|
}
|
|
|
|
func (a *App) GetDeletedChannels(teamID string, offset int, limit int, userID string) (model.ChannelList, *model.AppError) {
|
|
list, err := a.Srv().Store.Channel().GetDeleted(teamID, offset, limit, userID)
|
|
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, err.Error(), http.StatusNotFound)
|
|
default:
|
|
return nil, model.NewAppError("GetDeletedChannels", "app.channel.get_deleted.existing.app_error", nil, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
return list, nil
|
|
}
|
|
|
|
func (a *App) GetChannelsUserNotIn(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, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
return channels, nil
|
|
}
|
|
|
|
func (a *App) GetPublicChannelsByIdsForTeam(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, nfErr.Error(), http.StatusNotFound)
|
|
default:
|
|
return nil, model.NewAppError("GetPublicChannelsByIdsForTeam", "app.channel.get_channels_by_ids.get.app_error", nil, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
return list, nil
|
|
}
|
|
|
|
func (a *App) GetPublicChannelsForTeam(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, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
|
|
return list, nil
|
|
}
|
|
|
|
func (a *App) GetPrivateChannelsForTeam(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, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
|
|
return list, nil
|
|
}
|
|
|
|
func (a *App) GetChannelMember(ctx context.Context, channelID string, userID string) (*model.ChannelMember, *model.AppError) {
|
|
channelMember, err := a.Srv().Store.Channel().GetMember(ctx, channelID, userID)
|
|
if err != nil {
|
|
var nfErr *store.ErrNotFound
|
|
switch {
|
|
case errors.As(err, &nfErr):
|
|
return nil, model.NewAppError("GetChannelMember", MissingChannelMemberError, nil, nfErr.Error(), http.StatusNotFound)
|
|
default:
|
|
return nil, model.NewAppError("GetChannelMember", "app.channel.get_member.app_error", nil, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
return channelMember, nil
|
|
}
|
|
|
|
func (a *App) GetChannelMembersPage(channelID string, page, perPage int) (model.ChannelMembers, *model.AppError) {
|
|
channelMembers, err := a.Srv().Store.Channel().GetMembers(channelID, page*perPage, perPage)
|
|
if err != nil {
|
|
return nil, model.NewAppError("GetChannelMembersPage", "app.channel.get_members.app_error", nil, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
|
|
return channelMembers, nil
|
|
}
|
|
|
|
func (a *App) GetChannelMembersTimezones(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, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
|
|
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(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, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
|
|
return members, nil
|
|
}
|
|
|
|
func (a *App) GetChannelMembersForUser(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, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
|
|
return channelMembers, nil
|
|
}
|
|
|
|
func (a *App) GetChannelMembersForUserWithPagination(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, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
|
|
members := make([]*model.ChannelMember, 0, len(m))
|
|
for _, member := range m {
|
|
member := member
|
|
members = append(members, &member.ChannelMember)
|
|
}
|
|
return members, nil
|
|
}
|
|
|
|
func (a *App) GetChannelMembersWithTeamDataForUserWithPagination(userID string, page, perPage int) (model.ChannelMembersWithTeamData, *model.AppError) {
|
|
m, err := a.Srv().Store.Channel().GetMembersForUserWithPagination(userID, page, perPage)
|
|
if err != nil {
|
|
return nil, model.NewAppError("GetChannelMembersForUserWithPagination", "app.channel.get_members.app_error", nil, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
|
|
return m, nil
|
|
}
|
|
|
|
func (a *App) GetChannelMemberCount(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, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
|
|
return count, nil
|
|
}
|
|
|
|
func (a *App) GetChannelGuestCount(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, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
|
|
return count, nil
|
|
}
|
|
|
|
func (a *App) GetChannelPinnedPostCount(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, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
|
|
return count, nil
|
|
}
|
|
|
|
func (a *App) GetChannelCounts(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, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
|
|
return counts, nil
|
|
}
|
|
|
|
func (a *App) GetChannelUnread(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, nfErr.Error(), http.StatusNotFound)
|
|
default:
|
|
return nil, model.NewAppError("GetChannelUnread", "app.channel.get_unread.app_error", nil, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
if channelUnread.NotifyProps[model.MarkUnreadNotifyProp] == model.ChannelMarkUnreadMention {
|
|
channelUnread.MsgCount = 0
|
|
channelUnread.MsgCountRoot = 0
|
|
}
|
|
return channelUnread, nil
|
|
}
|
|
|
|
func (a *App) JoinChannel(c *request.Context, channel *model.Channel, userID string) *model.AppError {
|
|
userChan := make(chan store.StoreResult, 1)
|
|
memberChan := make(chan store.StoreResult, 1)
|
|
go func() {
|
|
user, err := a.Srv().Store.User().Get(context.Background(), userID)
|
|
userChan <- store.StoreResult{Data: user, NErr: err}
|
|
close(userChan)
|
|
}()
|
|
go func() {
|
|
member, err := a.Srv().Store.Channel().GetMember(context.Background(), channel.Id, userID)
|
|
memberChan <- store.StoreResult{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, nfErr.Error(), http.StatusNotFound)
|
|
default:
|
|
return model.NewAppError("CreateChannel", "app.user.get.app_error", nil, uresult.NErr.Error(), http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
mresult := <-memberChan
|
|
if mresult.NErr == nil && mresult.Data != nil {
|
|
// user is already in the channel
|
|
return nil
|
|
}
|
|
|
|
user := uresult.Data.(*model.User)
|
|
|
|
if channel.Type != model.ChannelTypeOpen {
|
|
return model.NewAppError("JoinChannel", "api.channel.join_channel.permissions.app_error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
cm, err := a.AddUserToChannel(user, channel, false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if pluginsEnvironment := a.GetPluginsEnvironment(); pluginsEnvironment != nil {
|
|
a.Srv().Go(func() {
|
|
pluginContext := pluginContext(c)
|
|
pluginsEnvironment.RunMultiPluginHook(func(hooks plugin.Hooks) bool {
|
|
hooks.UserHasJoinedChannel(pluginContext, cm, nil)
|
|
return true
|
|
}, plugin.UserHasJoinedChannelID)
|
|
})
|
|
}
|
|
|
|
if err := a.postJoinChannelMessage(c, user, channel); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) postJoinChannelMessage(c *request.Context, user *model.User, channel *model.Channel) *model.AppError {
|
|
message := fmt.Sprintf(i18n.T("api.channel.join_channel.post_and_forget"), user.Username)
|
|
postType := model.PostTypeJoinChannel
|
|
|
|
if user.IsGuest() {
|
|
message = fmt.Sprintf(i18n.T("api.channel.guest_join_channel.post_and_forget"), user.Username)
|
|
postType = model.PostTypeGuestJoinChannel
|
|
}
|
|
|
|
post := &model.Post{
|
|
ChannelId: channel.Id,
|
|
Message: message,
|
|
Type: postType,
|
|
UserId: user.Id,
|
|
Props: model.StringInterface{
|
|
"username": user.Username,
|
|
},
|
|
}
|
|
|
|
if _, err := a.CreatePost(c, post, channel, false, true); err != nil {
|
|
return model.NewAppError("postJoinChannelMessage", "api.channel.post_user_add_remove_message_and_forget.error", nil, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) postJoinTeamMessage(c *request.Context, user *model.User, channel *model.Channel) *model.AppError {
|
|
post := &model.Post{
|
|
ChannelId: channel.Id,
|
|
Message: fmt.Sprintf(i18n.T("api.team.join_team.post_and_forget"), user.Username),
|
|
Type: model.PostTypeJoinTeam,
|
|
UserId: user.Id,
|
|
Props: model.StringInterface{
|
|
"username": user.Username,
|
|
},
|
|
}
|
|
|
|
if _, err := a.CreatePost(c, post, channel, false, true); err != nil {
|
|
return model.NewAppError("postJoinTeamMessage", "api.channel.post_user_add_remove_message_and_forget.error", nil, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) LeaveChannel(c *request.Context, channelID string, userID string) *model.AppError {
|
|
sc := make(chan store.StoreResult, 1)
|
|
go func() {
|
|
channel, err := a.Srv().Store.Channel().Get(channelID, true)
|
|
sc <- store.StoreResult{Data: channel, NErr: err}
|
|
close(sc)
|
|
}()
|
|
|
|
uc := make(chan store.StoreResult, 1)
|
|
go func() {
|
|
user, err := a.Srv().Store.User().Get(context.Background(), userID)
|
|
uc <- store.StoreResult{Data: user, NErr: err}
|
|
close(uc)
|
|
}()
|
|
|
|
mcc := make(chan store.StoreResult, 1)
|
|
go func() {
|
|
count, err := a.Srv().Store.Channel().GetMemberCount(channelID, false)
|
|
mcc <- store.StoreResult{Data: count, NErr: err}
|
|
close(mcc)
|
|
}()
|
|
|
|
cresult := <-sc
|
|
if cresult.NErr != nil {
|
|
var nfErr *store.ErrNotFound
|
|
switch {
|
|
case errors.As(cresult.NErr, &nfErr):
|
|
return model.NewAppError("LeaveChannel", "app.channel.get.existing.app_error", nil, nfErr.Error(), http.StatusNotFound)
|
|
default:
|
|
return model.NewAppError("LeaveChannel", "app.channel.get.find.app_error", nil, cresult.NErr.Error(), http.StatusInternalServerError)
|
|
}
|
|
}
|
|
uresult := <-uc
|
|
if uresult.NErr != nil {
|
|
var nfErr *store.ErrNotFound
|
|
switch {
|
|
case errors.As(uresult.NErr, &nfErr):
|
|
return model.NewAppError("LeaveChannel", MissingAccountError, nil, nfErr.Error(), http.StatusNotFound)
|
|
default:
|
|
return model.NewAppError("LeaveChannel", "app.user.get.app_error", nil, uresult.NErr.Error(), http.StatusInternalServerError)
|
|
}
|
|
}
|
|
ccresult := <-mcc
|
|
if ccresult.NErr != nil {
|
|
return model.NewAppError("LeaveChannel", "app.channel.get_member_count.app_error", nil, ccresult.NErr.Error(), http.StatusInternalServerError)
|
|
}
|
|
|
|
channel := cresult.Data.(*model.Channel)
|
|
user := uresult.Data.(*model.User)
|
|
membersCount := ccresult.Data.(int64)
|
|
|
|
if channel.IsGroupOrDirect() {
|
|
err := model.NewAppError("LeaveChannel", "api.channel.leave.direct.app_error", nil, "", http.StatusBadRequest)
|
|
return err
|
|
}
|
|
|
|
if channel.Type == model.ChannelTypePrivate && membersCount == 1 {
|
|
err := model.NewAppError("LeaveChannel", "api.channel.leave.last_member.app_error", nil, "userId="+user.Id, http.StatusBadRequest)
|
|
return err
|
|
}
|
|
|
|
if err := a.removeUserFromChannel(c, userID, userID, channel); err != nil {
|
|
return err
|
|
}
|
|
|
|
if channel.Name == model.DefaultChannelName && !*a.Config().ServiceSettings.ExperimentalEnableDefaultChannelLeaveJoinMessages {
|
|
return nil
|
|
}
|
|
|
|
a.Srv().Go(func() {
|
|
a.postLeaveChannelMessage(c, user, channel)
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) postLeaveChannelMessage(c *request.Context, user *model.User, channel *model.Channel) *model.AppError {
|
|
post := &model.Post{
|
|
ChannelId: channel.Id,
|
|
// Message here embeds `@username`, not just `username`, to ensure that mentions
|
|
// treat this as a username mention even though the user has now left the channel.
|
|
// The client renders its own system message, ignoring this value altogether.
|
|
Message: fmt.Sprintf(i18n.T("api.channel.leave.left"), fmt.Sprintf("@%s", user.Username)),
|
|
Type: model.PostTypeLeaveChannel,
|
|
UserId: user.Id,
|
|
Props: model.StringInterface{
|
|
"username": user.Username,
|
|
},
|
|
}
|
|
|
|
if _, err := a.CreatePost(c, post, channel, false, true); err != nil {
|
|
return model.NewAppError("postLeaveChannelMessage", "api.channel.post_user_add_remove_message_and_forget.error", nil, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) PostAddToChannelMessage(c *request.Context, 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,
|
|
RootId: postRootId,
|
|
Props: model.StringInterface{
|
|
"userId": user.Id,
|
|
"username": user.Username,
|
|
model.PostPropsAddedUserId: addedUser.Id,
|
|
"addedUsername": addedUser.Username,
|
|
},
|
|
}
|
|
|
|
if _, err := a.CreatePost(c, post, channel, false, true); err != nil {
|
|
return model.NewAppError("postAddToChannelMessage", "api.channel.post_user_add_remove_message_and_forget.error", nil, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) postAddToTeamMessage(c *request.Context, user *model.User, addedUser *model.User, channel *model.Channel, postRootId string) *model.AppError {
|
|
post := &model.Post{
|
|
ChannelId: channel.Id,
|
|
Message: fmt.Sprintf(i18n.T("api.team.add_user_to_team.added"), addedUser.Username, user.Username),
|
|
Type: model.PostTypeAddToTeam,
|
|
UserId: user.Id,
|
|
RootId: postRootId,
|
|
Props: model.StringInterface{
|
|
"userId": user.Id,
|
|
"username": user.Username,
|
|
model.PostPropsAddedUserId: addedUser.Id,
|
|
"addedUsername": addedUser.Username,
|
|
},
|
|
}
|
|
|
|
if _, err := a.CreatePost(c, post, channel, false, true); err != nil {
|
|
return model.NewAppError("postAddToTeamMessage", "api.channel.post_user_add_remove_message_and_forget.error", nil, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) postRemoveFromChannelMessage(c *request.Context, removerUserId string, removedUser *model.User, channel *model.Channel) *model.AppError {
|
|
messageUserId := removerUserId
|
|
if messageUserId == "" {
|
|
systemBot, err := a.GetSystemBot()
|
|
if err != nil {
|
|
return model.NewAppError("postRemoveFromChannelMessage", "api.channel.post_user_add_remove_message_and_forget.error", nil, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
|
|
messageUserId = systemBot.UserId
|
|
}
|
|
|
|
post := &model.Post{
|
|
ChannelId: channel.Id,
|
|
// Message here embeds `@username`, not just `username`, to ensure that mentions
|
|
// treat this as a username mention even though the user has now left the channel.
|
|
// The client renders its own system message, ignoring this value altogether.
|
|
Message: fmt.Sprintf(i18n.T("api.channel.remove_member.removed"), fmt.Sprintf("@%s", removedUser.Username)),
|
|
Type: model.PostTypeRemoveFromChannel,
|
|
UserId: messageUserId,
|
|
Props: model.StringInterface{
|
|
"removedUserId": removedUser.Id,
|
|
"removedUsername": removedUser.Username,
|
|
},
|
|
}
|
|
|
|
if _, err := a.CreatePost(c, post, channel, false, true); err != nil {
|
|
return model.NewAppError("postRemoveFromChannelMessage", "api.channel.post_user_add_remove_message_and_forget.error", nil, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) removeUserFromChannel(c *request.Context, 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, nfErr.Error(), http.StatusNotFound)
|
|
default:
|
|
return model.NewAppError("removeUserFromChannel", "app.user.get.app_error", nil, nErr.Error(), http.StatusInternalServerError)
|
|
}
|
|
}
|
|
isGuest := user.IsGuest()
|
|
|
|
if channel.Name == model.DefaultChannelName {
|
|
if !isGuest {
|
|
return model.NewAppError("RemoveUserFromChannel", "api.channel.remove.default.app_error", map[string]interface{}{"Channel": model.DefaultChannelName}, "", http.StatusBadRequest)
|
|
}
|
|
}
|
|
|
|
if channel.IsGroupConstrained() && userIDToRemove != removerUserId && !user.IsBot {
|
|
nonMembers, err := a.FilterNonGroupChannelMembers([]string{userIDToRemove}, channel)
|
|
if err != nil {
|
|
return model.NewAppError("removeUserFromChannel", "api.channel.remove_user_from_channel.app_error", nil, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
if len(nonMembers) == 0 {
|
|
return model.NewAppError("removeUserFromChannel", "api.channel.remove_members.denied", map[string]interface{}{"UserIDs": nonMembers}, "", http.StatusBadRequest)
|
|
}
|
|
}
|
|
|
|
cm, err := a.GetChannelMember(context.Background(), channel.Id, userIDToRemove)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := a.Srv().Store.Channel().RemoveMember(channel.Id, userIDToRemove); err != nil {
|
|
return model.NewAppError("removeUserFromChannel", "app.channel.remove_member.app_error", nil, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
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, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
|
|
if isGuest {
|
|
currentMembers, err := a.GetChannelMembersForUser(channel.TeamId, userIDToRemove)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(currentMembers) == 0 {
|
|
teamMember, err := a.GetTeamMember(channel.TeamId, userIDToRemove)
|
|
if err != nil {
|
|
return model.NewAppError("removeUserFromChannel", "api.team.remove_user_from_team.missing.app_error", nil, err.Error(), http.StatusBadRequest)
|
|
}
|
|
|
|
if err := a.ch.srv.teamService.RemoveTeamMember(teamMember); err != nil {
|
|
return model.NewAppError("removeUserFromChannel", "api.team.remove_user_from_team.missing.app_error", nil, err.Error(), http.StatusBadRequest)
|
|
}
|
|
|
|
if err = a.postProcessTeamMemberLeave(c, teamMember, removerUserId); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
a.InvalidateCacheForUser(userIDToRemove)
|
|
a.invalidateCacheForChannelMembers(channel.Id)
|
|
|
|
if pluginsEnvironment := a.GetPluginsEnvironment(); pluginsEnvironment != nil {
|
|
var actorUser *model.User
|
|
if removerUserId != "" {
|
|
actorUser, _ = a.GetUser(removerUserId)
|
|
}
|
|
|
|
a.Srv().Go(func() {
|
|
pluginContext := pluginContext(c)
|
|
pluginsEnvironment.RunMultiPluginHook(func(hooks plugin.Hooks) bool {
|
|
hooks.UserHasLeftChannel(pluginContext, cm, actorUser)
|
|
return true
|
|
}, plugin.UserHasLeftChannelID)
|
|
})
|
|
}
|
|
|
|
message := model.NewWebSocketEvent(model.WebsocketEventUserRemoved, "", channel.Id, "", nil)
|
|
message.Add("user_id", userIDToRemove)
|
|
message.Add("remover_id", removerUserId)
|
|
a.Publish(message)
|
|
|
|
// because the removed user no longer belongs to the channel we need to send a separate websocket event
|
|
userMsg := model.NewWebSocketEvent(model.WebsocketEventUserRemoved, "", "", userIDToRemove, nil)
|
|
userMsg.Add("channel_id", channel.Id)
|
|
userMsg.Add("remover_id", removerUserId)
|
|
a.Publish(userMsg)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) RemoveUserFromChannel(c *request.Context, userIDToRemove string, removerUserId string, channel *model.Channel) *model.AppError {
|
|
var err *model.AppError
|
|
|
|
if err = a.removeUserFromChannel(c, userIDToRemove, removerUserId, channel); err != nil {
|
|
return err
|
|
}
|
|
|
|
var user *model.User
|
|
if user, err = a.GetUser(userIDToRemove); err != nil {
|
|
return err
|
|
}
|
|
|
|
if userIDToRemove == removerUserId {
|
|
if err := a.postLeaveChannelMessage(c, user, channel); err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
a.Srv().Go(func() {
|
|
if err := a.postRemoveFromChannelMessage(c, removerUserId, user, channel); err != nil {
|
|
mlog.Error("Failed to post user removal message", mlog.Err(err))
|
|
}
|
|
})
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) GetNumberOfChannelsOnTeam(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, nfErr.Error(), http.StatusNotFound)
|
|
default:
|
|
return 0, model.NewAppError("GetNumberOfChannelsOnTeam", "app.channel.get_channels.get.app_error", nil, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
}
|
|
return len(list), nil
|
|
}
|
|
|
|
func (a *App) SetActiveChannel(userID string, channelID string) *model.AppError {
|
|
status, err := a.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.AddStatusCache(status)
|
|
|
|
if status.Status != oldStatus {
|
|
a.BroadcastStatus(status)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) UpdateChannelLastViewedAt(channelIDs []string, userID string) *model.AppError {
|
|
if _, err := a.Srv().Store.Channel().UpdateLastViewedAt(channelIDs, userID, *a.Config().ServiceSettings.ThreadAutoFollow); err != nil {
|
|
var invErr *store.ErrInvalidInput
|
|
switch {
|
|
case errors.As(err, &invErr):
|
|
return model.NewAppError("UpdateChannelLastViewedAt", "app.channel.update_last_viewed_at.app_error", nil, invErr.Error(), http.StatusBadRequest)
|
|
default:
|
|
return model.NewAppError("UpdateChannelLastViewedAt", "app.channel.update_last_viewed_at.app_error", nil, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
if *a.Config().ServiceSettings.EnableChannelViewedMessages {
|
|
for _, channelID := range channelIDs {
|
|
message := model.NewWebSocketEvent(model.WebsocketEventChannelViewed, "", "", userID, nil)
|
|
message.Add("channel_id", channelID)
|
|
a.Publish(message)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) IsCRTEnabledForUser(userID string) bool {
|
|
if *a.Config().ServiceSettings.CollapsedThreads == model.CollapsedThreadsDisabled {
|
|
return false
|
|
}
|
|
threadsEnabled := *a.Config().ServiceSettings.CollapsedThreads == 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
|
|
}
|
|
|
|
// MarkChanelAsUnreadFromPost will take a post and set the channel as unread from that one.
|
|
func (a *App) MarkChannelAsUnreadFromPost(postID string, userID string, collapsedThreadsSupported, followThread bool) (*model.ChannelUnreadAt, *model.AppError) {
|
|
if !collapsedThreadsSupported || !a.IsCRTEnabledForUser(userID) {
|
|
return a.markChannelAsUnreadFromPostCRTUnsupported(postID, userID)
|
|
}
|
|
post, err := a.GetSinglePost(postID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
user, err := a.GetUser(userID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
unreadMentions, unreadMentionsRoot, err := a.countMentionsFromPost(user, post)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// if auto-follow is on
|
|
// if threadmembership does not exists we create one and update
|
|
if *a.Config().ServiceSettings.ThreadAutoFollow {
|
|
threadId := post.RootId
|
|
if post.RootId == "" {
|
|
threadId = post.Id
|
|
}
|
|
|
|
var nfErr *store.ErrNotFound
|
|
threadMembership, storeErr := a.Srv().Store.Thread().GetMembershipForUser(user.Id, threadId)
|
|
if storeErr != nil && !errors.As(storeErr, &nfErr) {
|
|
return nil, model.NewAppError("MarkChannelAsUnreadFromPost", "app.channel.update_last_viewed_at_post.app_error", nil, storeErr.Error(), http.StatusInternalServerError)
|
|
}
|
|
var opts store.ThreadMembershipOpts
|
|
// if this post was not followed before, create thread membership and update mention count
|
|
if threadMembership == nil {
|
|
opts = store.ThreadMembershipOpts{
|
|
Following: followThread,
|
|
IncrementMentions: false,
|
|
UpdateFollowing: true,
|
|
UpdateViewedTimestamp: true,
|
|
UpdateParticipants: false,
|
|
}
|
|
} else if !threadMembership.Following && followThread {
|
|
opts = store.ThreadMembershipOpts{
|
|
Following: true,
|
|
UpdateFollowing: true,
|
|
}
|
|
}
|
|
if opts.UpdateFollowing || threadMembership == nil {
|
|
threadMembership, storeErr = a.Srv().Store.Thread().MaintainMembership(user.Id, threadId, opts)
|
|
if storeErr != nil && !errors.As(storeErr, &nfErr) {
|
|
return nil, model.NewAppError("MarkChannelAsUnreadFromPost", "app.channel.update_last_viewed_at_post.app_error", nil, storeErr.Error(), http.StatusInternalServerError)
|
|
}
|
|
threadData, storeErr2 := a.Srv().Store.Thread().Get(threadId)
|
|
if storeErr2 != nil {
|
|
return nil, model.NewAppError("MarkChannelAsUnreadFromPost", "app.channel.update_last_viewed_at_post.app_error", nil, storeErr2.Error(), http.StatusInternalServerError)
|
|
}
|
|
if threadData != nil && threadMembership != nil && threadMembership.Following {
|
|
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, nErr.Error(), http.StatusInternalServerError)
|
|
}
|
|
threadMembership.UnreadMentions, err = a.countThreadMentions(user, post, channel.TeamId, post.UpdateAt-1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
_, nErr = a.Srv().Store.Thread().UpdateMembership(threadMembership)
|
|
if nErr != nil {
|
|
return nil, model.NewAppError("MarkChannelAsUnreadFromPost", "app.channel.update_last_viewed_at_post.app_error", nil, nErr.Error(), http.StatusInternalServerError)
|
|
}
|
|
thread, nErr := a.Srv().Store.Thread().GetThreadForUser(channel.TeamId, threadMembership, true)
|
|
if nErr != nil {
|
|
return nil, model.NewAppError("MarkChannelAsUnreadFromPost", "app.channel.update_last_viewed_at_post.app_error", nil, nErr.Error(), http.StatusInternalServerError)
|
|
}
|
|
a.sanitizeProfiles(thread.Participants, false)
|
|
thread.Post.SanitizeProps()
|
|
payload, jsonErr := json.Marshal(thread)
|
|
if jsonErr != nil {
|
|
mlog.Warn("Failed to encode thread to JSON")
|
|
}
|
|
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, unreadMentionsRoot, *a.Config().ServiceSettings.ThreadAutoFollow, true)
|
|
if nErr != nil {
|
|
return channelUnread, model.NewAppError("MarkChannelAsUnreadFromPost", "app.channel.update_last_viewed_at_post.app_error", nil, nErr.Error(), http.StatusInternalServerError)
|
|
}
|
|
|
|
a.sendWebSocketPostUnreadEvent(channelUnread, postID, false)
|
|
a.UpdateMobileAppBadge(userID)
|
|
|
|
return channelUnread, nil
|
|
}
|
|
|
|
func (a *App) markChannelAsUnreadFromPostCRTUnsupported(postID string, userID string) (*model.ChannelUnreadAt, *model.AppError) {
|
|
post, err := a.GetSinglePost(postID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
user, err := a.GetUser(userID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
threadId := post.RootId
|
|
if post.RootId == "" {
|
|
threadId = post.Id
|
|
}
|
|
|
|
unreadMentions, unreadMentionsRoot, err := a.countMentionsFromPost(user, post)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// 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, false, true)
|
|
if nErr != nil {
|
|
return channelUnread, model.NewAppError("MarkChannelAsUnreadFromPost", "app.channel.update_last_viewed_at_post.app_error", nil, nErr.Error(), http.StatusInternalServerError)
|
|
}
|
|
|
|
a.sendWebSocketPostUnreadEvent(channelUnread, postID, true)
|
|
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, err := a.GetSinglePost(post.RootId)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
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, nErr.Error(), http.StatusInternalServerError)
|
|
}
|
|
|
|
if *a.Config().ServiceSettings.ThreadAutoFollow {
|
|
threadMembership, sErr := a.Srv().Store.Thread().GetMembershipForUser(user.Id, threadId)
|
|
var errNotFound *store.ErrNotFound
|
|
if sErr != nil && !errors.As(sErr, &errNotFound) {
|
|
return nil, model.NewAppError("MarkChannelAsUnreadFromPost", "app.channel.update_last_viewed_at_post.app_error", nil, sErr.Error(), http.StatusInternalServerError)
|
|
}
|
|
// 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, sErr = a.Srv().Store.Thread().MaintainMembership(user.Id, threadId, opts)
|
|
if sErr != nil {
|
|
return nil, model.NewAppError("MarkChannelAsUnreadFromPost", "app.channel.update_last_viewed_at_post.app_error", nil, sErr.Error(), http.StatusInternalServerError)
|
|
}
|
|
}
|
|
// If threadmembership already exists but user had previously unfollowed the thread, then follow the thread again.
|
|
threadMembership.Following = true
|
|
threadMembership.LastViewed = post.UpdateAt - 1
|
|
threadMembership.UnreadMentions, err = a.countThreadMentions(user, rootPost, channel.TeamId, post.UpdateAt-1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
threadMembership, sErr = a.Srv().Store.Thread().UpdateMembership(threadMembership)
|
|
if sErr != nil {
|
|
return nil, model.NewAppError("MarkChannelAsUnreadFromPost", "app.channel.update_last_viewed_at_post.app_error", nil, sErr.Error(), http.StatusInternalServerError)
|
|
}
|
|
thread, sErr := a.Srv().Store.Thread().GetThreadForUser(channel.TeamId, threadMembership, true)
|
|
if sErr != nil {
|
|
return nil, model.NewAppError("MarkChannelAsUnreadFromPost", "app.channel.update_last_viewed_at_post.app_error", nil, sErr.Error(), http.StatusInternalServerError)
|
|
}
|
|
a.sanitizeProfiles(thread.Participants, false)
|
|
thread.Post.SanitizeProps()
|
|
|
|
if a.IsCRTEnabledForUser(userID) {
|
|
payload, jsonErr := json.Marshal(thread)
|
|
if jsonErr != nil {
|
|
mlog.Warn("Failed to encode thread to JSON")
|
|
}
|
|
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, false, false)
|
|
if nErr != nil {
|
|
return channelUnread, model.NewAppError("MarkChannelAsUnreadFromPost", "app.channel.update_last_viewed_at_post.app_error", nil, nErr.Error(), http.StatusInternalServerError)
|
|
}
|
|
return channelUnread, nil
|
|
}
|
|
|
|
func (a *App) sendWebSocketPostUnreadEvent(channelUnread *model.ChannelUnreadAt, postID string, withMsgCountRoot bool) {
|
|
message := model.NewWebSocketEvent(model.WebsocketEventPostUnread, channelUnread.TeamId, channelUnread.ChannelId, channelUnread.UserId, nil)
|
|
message.Add("msg_count", channelUnread.MsgCount)
|
|
if withMsgCountRoot {
|
|
message.Add("msg_count_root", channelUnread.MsgCountRoot)
|
|
}
|
|
message.Add("mention_count", channelUnread.MentionCount)
|
|
message.Add("mention_count_root", channelUnread.MentionCountRoot)
|
|
message.Add("last_viewed_at", channelUnread.LastViewedAt)
|
|
message.Add("post_id", postID)
|
|
a.Publish(message)
|
|
}
|
|
|
|
func (a *App) AutocompleteChannels(userID, term string) (model.ChannelListWithTeamData, *model.AppError) {
|
|
includeDeleted := *a.Config().TeamSettings.ExperimentalViewArchivedChannels
|
|
term = strings.TrimSpace(term)
|
|
|
|
channelList, err := a.Srv().Store.Channel().Autocomplete(userID, term, includeDeleted)
|
|
if err != nil {
|
|
return nil, model.NewAppError("AutocompleteChannels", "app.channel.search.app_error", nil, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
|
|
return channelList, nil
|
|
}
|
|
|
|
func (a *App) AutocompleteChannelsForTeam(teamID, userID, term string) (model.ChannelList, *model.AppError) {
|
|
includeDeleted := *a.Config().TeamSettings.ExperimentalViewArchivedChannels
|
|
term = strings.TrimSpace(term)
|
|
|
|
channelList, err := a.Srv().Store.Channel().AutocompleteInTeam(teamID, userID, term, includeDeleted)
|
|
if err != nil {
|
|
return nil, model.NewAppError("AutocompleteChannels", "app.channel.search.app_error", nil, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
|
|
return channelList, nil
|
|
}
|
|
|
|
func (a *App) AutocompleteChannelsForSearch(teamID string, userID string, term string) (model.ChannelList, *model.AppError) {
|
|
includeDeleted := *a.Config().TeamSettings.ExperimentalViewArchivedChannels
|
|
|
|
term = strings.TrimSpace(term)
|
|
|
|
channelList, err := a.Srv().Store.Channel().AutocompleteInTeamForSearch(teamID, userID, term, includeDeleted)
|
|
if err != nil {
|
|
return nil, model.NewAppError("AutocompleteChannelsForSearch", "app.channel.search.app_error", nil, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
|
|
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(term string, opts model.ChannelSearchOpts) (model.ChannelListWithTeamData, int64, *model.AppError) {
|
|
if opts.ExcludeDefaultChannels {
|
|
opts.ExcludeChannelNames = a.DefaultChannelNames()
|
|
}
|
|
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,
|
|
ExcludePolicyConstrained: opts.ExcludePolicyConstrained,
|
|
Public: opts.Public,
|
|
Private: opts.Private,
|
|
Page: opts.Page,
|
|
PerPage: opts.PerPage,
|
|
}
|
|
|
|
term = strings.TrimSpace(term)
|
|
|
|
channelList, totalCount, err := a.Srv().Store.Channel().SearchAllChannels(term, storeOpts)
|
|
if err != nil {
|
|
return nil, 0, model.NewAppError("SearchAllChannels", "app.channel.search.app_error", nil, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
|
|
return channelList, totalCount, nil
|
|
}
|
|
|
|
func (a *App) SearchChannels(teamID string, term string) (model.ChannelList, *model.AppError) {
|
|
includeDeleted := *a.Config().TeamSettings.ExperimentalViewArchivedChannels
|
|
|
|
term = strings.TrimSpace(term)
|
|
|
|
channelList, err := a.Srv().Store.Channel().SearchInTeam(teamID, term, includeDeleted)
|
|
if err != nil {
|
|
return nil, model.NewAppError("SearchChannels", "app.channel.search.app_error", nil, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
|
|
return channelList, nil
|
|
}
|
|
|
|
func (a *App) SearchArchivedChannels(teamID string, term string, userID string) (model.ChannelList, *model.AppError) {
|
|
term = strings.TrimSpace(term)
|
|
|
|
channelList, err := a.Srv().Store.Channel().SearchArchivedInTeam(teamID, term, userID)
|
|
if err != nil {
|
|
return nil, model.NewAppError("SearchArchivedChannels", "app.channel.search.app_error", nil, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
|
|
return channelList, nil
|
|
}
|
|
|
|
func (a *App) SearchChannelsForUser(userID, teamID, term string) (model.ChannelList, *model.AppError) {
|
|
includeDeleted := *a.Config().TeamSettings.ExperimentalViewArchivedChannels
|
|
|
|
term = strings.TrimSpace(term)
|
|
|
|
channelList, err := a.Srv().Store.Channel().SearchForUserInTeam(userID, teamID, term, includeDeleted)
|
|
if err != nil {
|
|
return nil, model.NewAppError("SearchChannelsForUser", "app.channel.search.app_error", nil, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
|
|
return channelList, nil
|
|
}
|
|
|
|
func (a *App) SearchGroupChannels(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, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
return channelList, nil
|
|
}
|
|
|
|
func (a *App) SearchChannelsUserNotIn(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, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
|
|
return channelList, nil
|
|
}
|
|
|
|
func (a *App) MarkChannelsAsViewed(channelIDs []string, userID string, currentSessionId string, collapsedThreadsSupported bool) (map[string]int64, *model.AppError) {
|
|
// I start looking for channels with notifications before I mark it as read, to clear the push notifications if needed
|
|
channelsToClearPushNotifications := []string{}
|
|
if *a.Config().EmailSettings.SendPushNotifications {
|
|
for _, channelID := range channelIDs {
|
|
channel, errCh := a.Srv().Store.Channel().Get(channelID, true)
|
|
if errCh != nil {
|
|
mlog.Warn("Failed to get channel", mlog.Err(errCh))
|
|
continue
|
|
}
|
|
|
|
member, err := a.Srv().Store.Channel().GetMember(context.Background(), channelID, userID)
|
|
if err != nil {
|
|
mlog.Warn("Failed to get membership", mlog.Err(err))
|
|
continue
|
|
}
|
|
|
|
notify := member.NotifyProps[model.PushNotifyProp]
|
|
if notify == model.ChannelNotifyDefault {
|
|
user, err := a.GetUser(userID)
|
|
if err != nil {
|
|
mlog.Warn("Failed to get user", mlog.String("user_id", userID), mlog.Err(err))
|
|
continue
|
|
}
|
|
notify = user.NotifyProps[model.PushNotifyProp]
|
|
}
|
|
if notify == model.UserNotifyAll {
|
|
if count, err := a.Srv().Store.User().GetAnyUnreadPostCountForChannel(userID, channelID); err == nil {
|
|
if count > 0 {
|
|
channelsToClearPushNotifications = append(channelsToClearPushNotifications, channelID)
|
|
}
|
|
}
|
|
} else if notify == model.UserNotifyMention || channel.Type == model.ChannelTypeDirect {
|
|
if count, err := a.Srv().Store.User().GetUnreadCountForChannel(userID, channelID); err == nil {
|
|
if count > 0 {
|
|
channelsToClearPushNotifications = append(channelsToClearPushNotifications, channelID)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
times, err := a.Srv().Store.Channel().UpdateLastViewedAt(channelIDs, userID, false)
|
|
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, invErr.Error(), http.StatusBadRequest)
|
|
default:
|
|
return nil, model.NewAppError("MarkChannelsAsViewed", "app.channel.update_last_viewed_at.app_error", nil, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
if *a.Config().ServiceSettings.EnableChannelViewedMessages {
|
|
for _, channelID := range channelIDs {
|
|
message := model.NewWebSocketEvent(model.WebsocketEventChannelViewed, "", "", userID, nil)
|
|
message.Add("channel_id", channelID)
|
|
a.Publish(message)
|
|
}
|
|
}
|
|
for _, channelID := range channelsToClearPushNotifications {
|
|
a.clearPushNotification(currentSessionId, userID, channelID, "")
|
|
}
|
|
|
|
if *a.Config().ServiceSettings.ThreadAutoFollow && (!collapsedThreadsSupported || !a.IsCRTEnabledForUser(userID)) {
|
|
if err := a.Srv().Store.Thread().MarkAllAsReadInChannels(userID, channelIDs); err != nil {
|
|
return nil, model.NewAppError("MarkChannelsAsViewed", "app.channel.update_last_viewed_at.app_error", nil, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
|
|
if a.IsCRTEnabledForUser(userID) {
|
|
timestamp := model.GetMillis()
|
|
for _, channelID := range channelIDs {
|
|
message := model.NewWebSocketEvent(model.WebsocketEventThreadReadChanged, "", channelID, userID, nil)
|
|
message.Add("timestamp", timestamp)
|
|
a.Publish(message)
|
|
}
|
|
}
|
|
}
|
|
|
|
return times, nil
|
|
}
|
|
|
|
func (a *App) ViewChannel(view *model.ChannelView, userID string, currentSessionId string, collapsedThreadsSupported bool) (map[string]int64, *model.AppError) {
|
|
if err := a.SetActiveChannel(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(channelIDs, userID, currentSessionId, collapsedThreadsSupported)
|
|
}
|
|
|
|
func (a *App) PermanentDeleteChannel(channel *model.Channel) *model.AppError {
|
|
if err := a.Srv().Store.Post().PermanentDeleteByChannel(channel.Id); err != nil {
|
|
return model.NewAppError("PermanentDeleteChannel", "app.post.permanent_delete_by_channel.app_error", nil, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
|
|
if err := a.Srv().Store.Channel().PermanentDeleteMembersByChannel(channel.Id); err != nil {
|
|
return model.NewAppError("PermanentDeleteChannel", "app.channel.remove_member.app_error", nil, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
|
|
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, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
|
|
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, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
|
|
deleteAt := model.GetMillis()
|
|
|
|
if nErr := a.Srv().Store.Channel().PermanentDelete(channel.Id); nErr != nil {
|
|
return model.NewAppError("PermanentDeleteChannel", "app.channel.permanent_delete.app_error", nil, nErr.Error(), http.StatusInternalServerError)
|
|
}
|
|
|
|
a.invalidateCacheForChannel(channel)
|
|
message := model.NewWebSocketEvent(model.WebsocketEventChannelDeleted, channel.TeamId, "", "", nil)
|
|
message.Add("channel_id", channel.Id)
|
|
message.Add("delete_at", deleteAt)
|
|
a.Publish(message)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) RemoveAllDeactivatedMembersFromChannel(channel *model.Channel) *model.AppError {
|
|
err := a.Srv().Store.Channel().RemoveAllDeactivatedMembers(channel.Id)
|
|
if err != nil {
|
|
return model.NewAppError("RemoveAllDeactivatedMembersFromChannel", "app.channel.remove_all_deactivated_members.app_error", nil, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// MoveChannel method is prone to data races if someone joins to channel during the move process. However this
|
|
// function is only exposed to sysadmins and the possibility of this edge case is relatively small.
|
|
func (a *App) MoveChannel(c *request.Context, 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(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 {
|
|
mlog.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, nfErr.Error(), http.StatusNotFound)
|
|
default:
|
|
return model.NewAppError("MoveChannel", "app.team.get.finding.app_error", nil, nErr.Error(), http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
if nErr := a.Srv().Store.Channel().UpdateSidebarChannelCategoryOnMove(channel, team.Id); nErr != nil {
|
|
return model.NewAppError("MoveChannel", "app.channel.sidebar_categories.app_error", nil, nErr.Error(), http.StatusInternalServerError)
|
|
}
|
|
|
|
channel.TeamId = team.Id
|
|
if _, err := a.Srv().Store.Channel().Update(channel); err != nil {
|
|
var appErr *model.AppError
|
|
var invErr *store.ErrInvalidInput
|
|
switch {
|
|
case errors.As(err, &invErr):
|
|
return model.NewAppError("MoveChannel", "app.channel.update.bad_id", nil, invErr.Error(), http.StatusBadRequest)
|
|
case errors.As(err, &appErr):
|
|
return appErr
|
|
default:
|
|
return model.NewAppError("MoveChannel", "app.channel.update_channel.internal_error", nil, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
if incomingWebhooks, err := a.GetIncomingWebhooksForTeamPage(previousTeam.Id, 0, 10000000); err != nil {
|
|
mlog.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 {
|
|
mlog.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 {
|
|
mlog.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 {
|
|
mlog.Warn("Failed to move outgoing webhook to new team.", mlog.String("webhook id", webhook.Id))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if err := a.RemoveUsersFromChannelNotMemberOfTeam(c, user, channel, team); err != nil {
|
|
mlog.Warn("error while removing non-team member users", mlog.Err(err))
|
|
}
|
|
|
|
if user != nil {
|
|
if err := a.postChannelMoveMessage(c, user, channel, previousTeam); err != nil {
|
|
mlog.Warn("error while posting move channel message", mlog.Err(err))
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) postChannelMoveMessage(c *request.Context, user *model.User, channel *model.Channel, previousTeam *model.Team) *model.AppError {
|
|
|
|
post := &model.Post{
|
|
ChannelId: channel.Id,
|
|
Message: fmt.Sprintf(i18n.T("api.team.move_channel.success"), previousTeam.Name),
|
|
Type: model.PostTypeMoveChannel,
|
|
UserId: user.Id,
|
|
Props: model.StringInterface{
|
|
"username": user.Username,
|
|
},
|
|
}
|
|
|
|
if _, err := a.CreatePost(c, post, channel, false, true); err != nil {
|
|
return model.NewAppError("postChannelMoveMessage", "api.team.move_channel.post.error", nil, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) RemoveUsersFromChannelNotMemberOfTeam(c *request.Context, remover *model.User, channel *model.Channel, team *model.Team) *model.AppError {
|
|
channelMembers, err := a.GetChannelMembersPage(channel.Id, 0, 10000000)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
channelMemberIds := []string{}
|
|
channelMemberMap := make(map[string]struct{})
|
|
for _, channelMember := range channelMembers {
|
|
channelMemberMap[channelMember.UserId] = struct{}{}
|
|
channelMemberIds = append(channelMemberIds, channelMember.UserId)
|
|
}
|
|
|
|
if len(channelMemberIds) > 0 {
|
|
teamMembers, err := a.GetTeamMembersByIds(team.Id, channelMemberIds, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(teamMembers) != len(channelMembers) {
|
|
for _, teamMember := range teamMembers {
|
|
delete(channelMemberMap, teamMember.UserId)
|
|
}
|
|
|
|
var removerId string
|
|
if remover != nil {
|
|
removerId = remover.Id
|
|
}
|
|
for userID := range channelMemberMap {
|
|
if err := a.removeUserFromChannel(c, userID, removerId, channel); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) GetPinnedPosts(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, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
|
|
return posts, nil
|
|
}
|
|
|
|
func (a *App) ToggleMuteChannel(channelID, userID string) (*model.ChannelMember, *model.AppError) {
|
|
member, nErr := a.Srv().Store.Channel().GetMember(context.Background(), channelID, userID)
|
|
if nErr != nil {
|
|
var appErr *model.AppError
|
|
var nfErr *store.ErrNotFound
|
|
switch {
|
|
case errors.As(nErr, &appErr):
|
|
return nil, appErr
|
|
case errors.As(nErr, &nfErr):
|
|
return nil, model.NewAppError("ToggleMuteChannel", MissingChannelMemberError, nil, nfErr.Error(), http.StatusNotFound)
|
|
default:
|
|
return nil, model.NewAppError("ToggleMuteChannel", "app.channel.get_member.app_error", nil, nErr.Error(), http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
member.SetChannelMuted(!member.IsChannelMuted())
|
|
|
|
member, err := a.updateChannelMember(member)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
a.invalidateCacheForChannelMembersNotifyProps(member.ChannelId)
|
|
|
|
return member, nil
|
|
}
|
|
|
|
func (a *App) setChannelsMuted(channelIDs []string, userID string, muted bool) ([]*model.ChannelMember, *model.AppError) {
|
|
members, nErr := a.Srv().Store.Channel().GetMembersByChannelIds(channelIDs, userID)
|
|
if nErr != nil {
|
|
var appErr *model.AppError
|
|
switch {
|
|
case errors.As(nErr, &appErr):
|
|
return nil, appErr
|
|
default:
|
|
return nil, model.NewAppError("setChannelsMuted", "app.channel.get_member.app_error", nil, nErr.Error(), http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
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, nErr := a.Srv().Store.Channel().UpdateMultipleMembers(membersToUpdate)
|
|
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("setChannelsMuted", MissingChannelMemberError, nil, nfErr.Error(), http.StatusNotFound)
|
|
default:
|
|
return nil, model.NewAppError("setChannelsMuted", "app.channel.get_member.app_error", nil, nErr.Error(), http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
for _, member := range updated {
|
|
a.invalidateCacheForChannelMembersNotifyProps(member.ChannelId)
|
|
|
|
evt := model.NewWebSocketEvent(model.WebsocketEventChannelMemberUpdated, "", "", member.UserId, nil)
|
|
|
|
memberJSON, jsonErr := json.Marshal(member)
|
|
if jsonErr != nil {
|
|
mlog.Warn("Failed to encode channel member to JSON", mlog.Err(jsonErr))
|
|
}
|
|
|
|
evt.Add("channelMember", string(memberJSON))
|
|
a.Publish(evt)
|
|
}
|
|
|
|
return updated, nil
|
|
}
|
|
|
|
func (a *App) FillInChannelProps(channel *model.Channel) *model.AppError {
|
|
return a.FillInChannelsProps(model.ChannelList{channel})
|
|
}
|
|
|
|
func (a *App) FillInChannelsProps(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(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]interface{}, len(channelMentions[channel]))
|
|
for _, channelMention := range channelMentions[channel] {
|
|
if mentioned, ok := mentionedChannelsByName[channelMention]; ok {
|
|
if mentioned.Type == model.ChannelTypeOpen {
|
|
channelMentionsProp[mentioned.Name] = map[string]interface{}{
|
|
"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(channelID string, f func(model.ChannelMember) error) error {
|
|
perPage := 100
|
|
page := 0
|
|
|
|
for {
|
|
channelMembers, err := a.Srv().Store.Channel().GetMembers(channelID, page*perPage, perPage)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, channelMember := range channelMembers {
|
|
if err = f(channelMember); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
length := len(channelMembers)
|
|
if length < perPage {
|
|
break
|
|
}
|
|
|
|
page++
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) ClearChannelMembersCache(channelID string) {
|
|
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 {
|
|
mlog.Warn("Failed to encode channel member to JSON", mlog.Err(jsonErr))
|
|
}
|
|
message.Add("channelMember", string(memberJSON))
|
|
a.Publish(message)
|
|
return nil
|
|
}
|
|
if err := a.forEachChannelMember(channelID, clearSessionCache); err != nil {
|
|
a.Log().Warn("error clearing cache for channel members", mlog.String("channel_id", channelID))
|
|
}
|
|
}
|
|
|
|
func (a *App) GetMemberCountsByGroup(ctx context.Context, channelID string, includeTimezones bool) ([]*model.ChannelMemberCountByGroup, *model.AppError) {
|
|
channelMemberCounts, err := a.Srv().Store.Channel().GetMemberCountsByGroup(ctx, channelID, includeTimezones)
|
|
if err != nil {
|
|
return nil, model.NewAppError("GetMemberCountsByGroup", "app.channel.get_member_count.app_error", nil, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
|
|
return channelMemberCounts, nil
|
|
}
|
|
|
|
func (a *App) getDirectChannel(userID, otherUserID string) (*model.Channel, *model.AppError) {
|
|
channel, nErr := a.Srv().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, nErr.Error(), http.StatusInternalServerError)
|
|
}
|
|
|
|
return channel, nil
|
|
}
|