Files
mostlymatter/app/channel.go
Kyriakos Z 457aeb8e7e MM-36589: provide previous values for unreads (#18492)
* 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>
2022-02-09 15:41:30 -05:00

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, &ltErr):
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, &ltErr):
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, &ltErr):
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
}