MM-53125 Add feature to convert group message to private channel (#24421)

* Added convert to channel menu item

* WIP

* refactored channel name input field and created conversion modal

* style

* style

* WIP

* wip

* Created API to fetch common teams of GM members

* Added UI for all members deactivated

* Fetched common teams in client

* WIP

* Added a required attribute to DropdownInput component

* Fixed a case with dropdown input required flag

* WIP

* API first draft

* Genetayed layers and mocks

* Fixed create channel bug

* WIP

* Added cache invalidation

* Calling API from client

* Updated API to accept name and display name as well

* WIP

* Moved converted GM to correct category

* Style fixes

* Added logic to move user to new team/channel after GM conversion

* Prevented guest user from performing action

* Added loading indicator

* Added smoother height transistion when loading finishes

* UI imporvements

* WIP

* Formatted GM conversion message on client side

* lint fix

* Moved convert option from sidebar menu to channel header menu

* Some cleanup

* Updated server layers

* Fixed i18n

* Fixed types

* Fix server i18n

* Fixed channel creation bug

* Added store test for GetCommonTeamIDsForMultipleUsers

* Server tests done

* Updated snapshots

* Updated layers

* lint fix

* Update tests

* For CI

* lint

* restored debug code

* Used user ID instead of username in channel conversion post

* WIP

* Review fixes

* LInt fixes

* Test fix

* WIP

* WIP

* WIP

* wip

* Review fixes, lots of them

* Review fix

* Disabled WIP test

* test

* Cleanup

* Test fix

* removed testing line

* Fixed incorrect default message

* Review fixes

* Fixes

* lint and i18n fix

* Setting category on server side

* updated i18n

* Updated tests

* Added tests

* Refs cleanup

* added test

---------

Co-authored-by: Harshil Sharma <harshilsharma@Harshils-MacBook-Pro.local>
Этот коммит содержится в:
Harshil Sharma
2023-09-19 18:11:34 +05:30
коммит произвёл GitHub
родитель 11cdd2b66b
Коммит 39d6cb8008
46 изменённых файлов: 2243 добавлений и 119 удалений

Просмотреть файл

@@ -60,6 +60,8 @@ func (api *API) InitChannel() {
api.BaseRoutes.Channel.Handle("/members_minus_group_members", api.APISessionRequired(channelMembersMinusGroupMembers)).Methods("GET")
api.BaseRoutes.Channel.Handle("/move", api.APISessionRequired(moveChannel)).Methods("POST")
api.BaseRoutes.Channel.Handle("/member_counts_by_group", api.APISessionRequired(channelMemberCountsByGroup)).Methods("GET")
api.BaseRoutes.Channel.Handle("/common_teams", api.APISessionRequired(getGroupMessageMembersCommonTeams)).Methods("GET")
api.BaseRoutes.Channel.Handle("/convert_to_channel", api.APISessionRequired(convertGroupMessageToChannel)).Methods("POST")
api.BaseRoutes.ChannelForUser.Handle("/unread", api.APISessionRequired(getChannelUnread)).Methods("GET")
@@ -2194,3 +2196,86 @@ func moveChannel(c *Context, w http.ResponseWriter, r *http.Request) {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getGroupMessageMembersCommonTeams(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireChannelId()
if c.Err != nil {
return
}
user, err := c.App.GetUser(c.AppContext.Session().UserId)
if err != nil {
c.Err = err
return
}
if user.IsGuest() {
c.Err = model.NewAppError("Api4.getGroupMessageMembersCommonTeams", "api.channel.gm_to_channel_conversion.not_allowed_for_user.request_error", nil, "userId="+c.AppContext.Session().UserId, http.StatusForbidden)
return
}
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionReadChannel) {
c.SetPermissionError(model.PermissionReadChannel)
return
}
teams, appErr := c.App.GetGroupMessageMembersCommonTeams(c.AppContext, c.Params.ChannelId)
if appErr != nil {
c.Err = appErr
return
}
if err := json.NewEncoder(w).Encode(teams); err != nil {
c.Logger.Warn("Error while writing response from getGroupMessageMembersCommonTeams", mlog.Err(err))
}
}
func convertGroupMessageToChannel(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireChannelId()
if c.Err != nil {
return
}
var gmConversionRequest *model.GroupMessageConversionRequestBody
if err := json.NewDecoder(r.Body).Decode(&gmConversionRequest); err != nil {
c.SetInvalidParamWithErr("body", err)
return
}
user, err := c.App.GetUser(c.AppContext.Session().UserId)
if err != nil {
c.Err = err
return
}
if user.IsGuest() {
c.Err = model.NewAppError("Api4.convertGroupMessageToChannel", "api.channel.gm_to_channel_conversion.not_allowed_for_user.request_error", nil, "userId="+c.AppContext.Session().UserId, http.StatusForbidden)
return
}
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), gmConversionRequest.TeamID, model.PermissionCreatePrivateChannel) {
c.SetPermissionError(model.PermissionCreatePrivateChannel)
return
}
// The channel id the payload must be the same one as indicated in the URL.
if gmConversionRequest.ChannelID != c.Params.ChannelId {
c.SetInvalidParam("channel_id")
return
}
auditRec := c.MakeAuditRecord("convertGroupMessageToChannel", audit.Fail)
defer c.LogAuditRec(auditRec)
audit.AddEventParameter(auditRec, "channel_id", gmConversionRequest.ChannelID)
audit.AddEventParameter(auditRec, "team_id", gmConversionRequest.TeamID)
audit.AddEventParameter(auditRec, "user_id", user.Id)
updatedChannel, appErr := c.App.ConvertGroupMessageToChannel(c.AppContext, c.AppContext.Session().UserId, gmConversionRequest)
if appErr != nil {
c.Err = appErr
return
}
auditRec.Success()
if err := json.NewEncoder(w).Encode(updatedChannel); err != nil {
c.Logger.Warn("Error while writing response from convertGroupMessageToChannel", mlog.Err(err))
}
}

Просмотреть файл

@@ -477,6 +477,7 @@ type AppIface interface {
CompleteSwitchWithOAuth(service string, userData io.Reader, email string, tokenUser *model.User) (*model.User, *model.AppError)
Compliance() einterfaces.ComplianceInterface
Config() *model.Config
ConvertGroupMessageToChannel(c request.CTX, convertedByUserId string, gmConversionRequest *model.GroupMessageConversionRequestBody) (*model.Channel, *model.AppError)
CopyFileInfos(userID string, fileIDs []string) ([]string, *model.AppError)
CreateChannel(c request.CTX, channel *model.Channel, addMember bool) (*model.Channel, *model.AppError)
CreateChannelWithUser(c request.CTX, channel *model.Channel, userID string) (*model.Channel, *model.AppError)
@@ -664,6 +665,7 @@ type AppIface interface {
GetGroupMemberUsers(groupID string) ([]*model.User, *model.AppError)
GetGroupMemberUsersPage(groupID string, page int, perPage int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, int, *model.AppError)
GetGroupMemberUsersSortedPage(groupID string, page int, perPage int, viewRestrictions *model.ViewUsersRestrictions, teammateNameDisplay string) ([]*model.User, int, *model.AppError)
GetGroupMessageMembersCommonTeams(c request.CTX, channelID string) ([]*model.Team, *model.AppError)
GetGroupSyncable(groupID string, syncableID string, syncableType model.GroupSyncableType) (*model.GroupSyncable, *model.AppError)
GetGroupSyncables(groupID string, syncableType model.GroupSyncableType) ([]*model.GroupSyncable, *model.AppError)
GetGroups(page, perPage int, opts model.GroupSearchOpts, viewRestrictions *model.ViewUsersRestrictions) ([]*model.Group, *model.AppError)

Просмотреть файл

@@ -11,6 +11,8 @@ import (
"net/http"
"strings"
"github.com/mattermost/mattermost/server/v8/channels/utils"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/plugin"
"github.com/mattermost/mattermost/server/public/shared/i18n"
@@ -3478,6 +3480,231 @@ func (a *App) getDirectChannel(c request.CTX, userID, otherUserID string) (*mode
return a.Srv().getDirectChannel(c, userID, otherUserID)
}
func (a *App) GetGroupMessageMembersCommonTeams(c request.CTX, channelID string) ([]*model.Team, *model.AppError) {
channel, appErr := a.GetChannel(c, channelID)
if appErr != nil {
return nil, appErr
}
if channel.Type != model.ChannelTypeGroup {
return nil, model.NewAppError("GetGroupMessageMembersCommonTeams", "app.channel.get_common_teams.incorrect_channel_type", nil, "", http.StatusBadRequest)
}
users, appErr := a.GetUsersInChannel(&model.UserGetOptions{
PerPage: model.ChannelGroupMaxUsers,
Page: 0,
InChannelId: channelID,
Inactive: false,
Active: true,
})
var userIDs = make([]string, len(users))
for i := 0; i < len(users); i++ {
userIDs[i] = users[i].Id
}
commonTeamIDs, err := a.Srv().Store().Team().GetCommonTeamIDsForMultipleUsers(userIDs)
if err != nil {
return nil, model.NewAppError("GetGroupMessageMembersCommonTeams", "app.channel.get_common_teams.store_get_common_teams_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
teams := []*model.Team{}
if len(commonTeamIDs) > 0 {
teams, appErr = a.GetTeams(commonTeamIDs)
}
return teams, appErr
}
func (a *App) ConvertGroupMessageToChannel(c request.CTX, convertedByUserId string, gmConversionRequest *model.GroupMessageConversionRequestBody) (*model.Channel, *model.AppError) {
originalChannel, appErr := a.GetChannel(c, gmConversionRequest.ChannelID)
if appErr != nil {
return nil, appErr
}
appErr = a.validateForConvertGroupMessageToChannel(c, convertedByUserId, originalChannel, gmConversionRequest)
if appErr != nil {
return nil, appErr
}
toUpdate := originalChannel.DeepCopy()
toUpdate.Type = model.ChannelTypePrivate
toUpdate.TeamId = gmConversionRequest.TeamID
toUpdate.Name = gmConversionRequest.Name
toUpdate.DisplayName = gmConversionRequest.DisplayName
updatedChannel, appErr := a.UpdateChannel(c, toUpdate)
if appErr != nil {
return nil, appErr
}
a.Srv().Platform().InvalidateCacheForChannel(originalChannel)
users, appErr := a.GetUsersInChannelPage(&model.UserGetOptions{
InChannelId: gmConversionRequest.ChannelID,
Page: 0,
PerPage: model.ChannelGroupMaxUsers,
}, false)
if appErr != nil {
return nil, appErr
}
_ = a.setSidebarCategoriesForConvertedGroupMessage(c, gmConversionRequest, users)
_ = a.postMessageForConvertGroupMessageToChannel(c, gmConversionRequest.ChannelID, convertedByUserId, users)
return updatedChannel, nil
}
func (a *App) setSidebarCategoriesForConvertedGroupMessage(c request.CTX, gmConversionRequest *model.GroupMessageConversionRequestBody, channelUsers []*model.User) *model.AppError {
// First we'll delete channel from everyone's sidebar. Only the members of GM
// can have it in sidebar, so we can delete the channel from all SidebarChannels entries.
err := a.Srv().Store().Channel().DeleteAllSidebarChannelForChannel(gmConversionRequest.ChannelID)
if err != nil {
return model.NewAppError(
"setSidebarCategoriesForConvertedGroupMessage",
"app.channel.gm_conversion_set_categories.delete_all.error",
nil,
"",
http.StatusInternalServerError,
).Wrap(err)
}
// Now that we've deleted existing entries, we can set the channel in default "Channels" category
// for all GM members
for _, user := range channelUsers {
categories, appErr := a.GetSidebarCategories(c, user.Id, &store.SidebarCategorySearchOpts{
TeamID: gmConversionRequest.TeamID,
Type: model.SidebarCategoryChannels,
})
if appErr != nil {
mlog.Error("Failed to search sidebar categories for user for adding converted GM")
continue
}
if len(categories.Categories) < 1 {
// It is normal for user to not have the default category.
// The default "Channels" category is created when the user first logs in,
// and all their channels are moved to this category at the same time.
// So its perfectly okay for this condition to occur.
continue
}
// when we fetch the default "Channels" category from store layer,
// it auto-fills any channels the user has access to but aren't associated to a category in the database.
// So what we do is fetch the category, so we get an auto-filled data,
// then call update category to persist the data and send the websocket events.
channelsCategory := categories.Categories[0]
_, appErr = a.UpdateSidebarCategories(c, user.Id, gmConversionRequest.TeamID, []*model.SidebarCategoryWithChannels{channelsCategory})
if appErr != nil {
mlog.Error("Failed to add converted GM to default sidebar category for user", mlog.String("user_id", user.Id), mlog.Err(err))
}
}
return nil
}
func (a *App) validateForConvertGroupMessageToChannel(c request.CTX, convertedByUserId string, originalChannel *model.Channel, gmConversionRequest *model.GroupMessageConversionRequestBody) *model.AppError {
commonTeams, appErr := a.GetGroupMessageMembersCommonTeams(c, originalChannel.Id)
if appErr != nil {
return appErr
}
teamFound := false
for _, team := range commonTeams {
if team.Id == gmConversionRequest.TeamID {
teamFound = true
break
}
}
if !teamFound {
return model.NewAppError(
"validateForConvertGroupMessageToChannel",
"app.channel.group_message_conversion.incorrect_team",
nil,
"",
http.StatusBadRequest,
)
}
if originalChannel.Type != model.ChannelTypeGroup {
return model.NewAppError(
"ConvertGroupMessageToChannel",
"app.channel.group_message_conversion.original_channel_not_gm",
nil,
"",
http.StatusNotFound,
)
}
channelMember, appErr := a.GetChannelMember(c, gmConversionRequest.ChannelID, convertedByUserId)
if appErr != nil {
return appErr
}
if channelMember == nil {
return model.NewAppError("ConvertGroupMessageToChannel", "app.channel.group_message_conversion.channel_member_missing", nil, "", http.StatusNotFound)
}
// apply dummy changes to check validity
clone := originalChannel.DeepCopy()
clone.Type = model.ChannelTypePrivate
clone.Name = gmConversionRequest.Name
clone.DisplayName = gmConversionRequest.DisplayName
return clone.IsValid()
}
func (a *App) postMessageForConvertGroupMessageToChannel(c request.CTX, channelID, convertedByUserId string, channelUsers []*model.User) *model.AppError {
convertedByUser, appErr := a.GetUser(convertedByUserId)
if appErr != nil {
return appErr
}
userIDs := make([]string, len(channelUsers))
usernames := make([]string, len(channelUsers))
for i, user := range channelUsers {
userIDs[i] = user.Id
usernames[i] = user.Username
}
message := i18n.T(
"api.channel.group_message.converted.to_private_channel",
map[string]any{
"ConvertedByUsername": convertedByUser.Username,
"GMMembers": utils.JoinList(usernames),
})
post := &model.Post{
ChannelId: channelID,
Message: message,
Type: model.PostTypeGMConvertedToChannel,
UserId: convertedByUserId,
}
// these props are used for re-constructing a localized message on the client
post.AddProp("convertedByUserId", convertedByUser.Id)
post.AddProp("gmMembersDuringConversionIDs", userIDs)
channel, appErr := a.GetChannel(c, channelID)
if appErr != nil {
return appErr
}
if _, appErr := a.CreatePost(c, post, channel, false, true); appErr != nil {
mlog.Error("Failed to create post for notifying about GM converted to private channel", mlog.Err(appErr))
return model.NewAppError(
"postMessageForConvertGroupMessageToChannel",
"app.channel.group_message_conversion.post_message.error",
nil,
"",
http.StatusInternalServerError,
).Wrap(appErr)
}
return nil
}
func (s *Server) getDirectChannel(c request.CTX, userID, otherUserID string) (*model.Channel, *model.AppError) {
channel, nErr := s.Store().Channel().GetByName("", model.GetDMNameFromIds(userID, otherUserID), true)
if nErr != nil {

Просмотреть файл

@@ -11,6 +11,13 @@ import (
"strings"
"sync"
"testing"
"time"
"github.com/mattermost/mattermost/server/v8/channels/store"
"github.com/mattermost/mattermost/server/v8/channels/app/teams"
"github.com/mattermost/mattermost/server/v8/channels/app/users"
"github.com/mattermost/mattermost/server/v8/channels/store/sqlstore"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
@@ -2450,3 +2457,210 @@ func TestIsCRTEnabledForUser(t *testing.T) {
})
}
}
func TestGetGroupMessageMembersCommonTeams(t *testing.T) {
th := SetupWithStoreMock(t)
defer th.TearDown()
mockStore := th.App.Srv().Store().(*mocks.Store)
mockChannelStore := mocks.ChannelStore{}
mockStore.On("Channel").Return(&mockChannelStore)
mockChannelStore.On("Get", "gm_channel_id", true).Return(&model.Channel{Type: model.ChannelTypeGroup}, nil)
mockTeamStore := mocks.TeamStore{}
mockStore.On("Team").Return(&mockTeamStore)
th.App.Srv().Store().Team()
mockTeamStore.On("GetCommonTeamIDsForMultipleUsers", []string{"user_id_1", "user_id_2"}).Return([]string{"team_id_1", "team_id_2", "team_id_3"}, nil).Times(1)
mockTeamStore.On("GetMany", []string{"team_id_1", "team_id_2", "team_id_3"}).Return(
[]*model.Team{
{DisplayName: "Team 1"},
{DisplayName: "Team 2"},
{DisplayName: "Team 3"},
},
nil,
)
mockUserStore := mocks.UserStore{}
mockStore.On("User").Return(&mockUserStore)
options := &model.UserGetOptions{
PerPage: model.ChannelGroupMaxUsers,
Page: 0,
InChannelId: "gm_channel_id",
Inactive: false,
Active: true,
}
mockUserStore.On("GetProfilesInChannel", options).Return([]*model.User{
{
Id: "user_id_1",
},
{
Id: "user_id_2",
},
}, nil)
var err error
th.App.ch.srv.teamService, err = teams.New(teams.ServiceConfig{
TeamStore: &mockTeamStore,
ChannelStore: &mockChannelStore,
GroupStore: &mocks.GroupStore{},
Users: th.App.ch.srv.userService,
WebHub: th.App.ch.srv.platform,
ConfigFn: th.App.ch.srv.platform.Config,
LicenseFn: th.App.ch.srv.License,
})
require.NoError(t, err)
commonTeams, appErr := th.App.GetGroupMessageMembersCommonTeams(th.Context, "gm_channel_id")
require.Nil(t, appErr)
require.Equal(t, 3, len(commonTeams))
// case of no common teams
mockTeamStore.On("GetCommonTeamIDsForMultipleUsers", []string{"user_id_1", "user_id_2"}).Return([]string{}, nil)
commonTeams, appErr = th.App.GetGroupMessageMembersCommonTeams(th.Context, "gm_channel_id")
require.Nil(t, appErr)
require.Equal(t, 0, len(commonTeams))
}
func TestConvertGroupMessageToChannel(t *testing.T) {
th := SetupWithStoreMock(t)
defer th.TearDown()
mockStore := th.App.Srv().Store().(*mocks.Store)
mockChannelStore := mocks.ChannelStore{}
mockStore.On("Channel").Return(&mockChannelStore)
mockChannelStore.On("Get", "channelidchannelidchanneli", true).Return(&model.Channel{
Id: "channelidchannelidchanneli",
CreateAt: time.Now().Unix(),
UpdateAt: time.Now().Unix(),
Type: model.ChannelTypeGroup,
}, nil)
mockChannelStore.On("Update", mock.AnythingOfType("*model.Channel")).Return(&model.Channel{}, nil)
mockChannelStore.On("InvalidateChannel", "channelidchannelidchanneli")
mockChannelStore.On("InvalidateChannelByName", "team_id_1", "new_name").Times(1)
mockChannelStore.On("InvalidateChannelByName", "dm", "")
mockChannelStore.On("GetMember", sqlstore.WithMaster(context.Background()), "channelidchannelidchanneli", "user_id_1").Return(&model.ChannelMember{}, nil).Times(1)
mockChannelStore.On("GetMember", context.Background(), "channelidchannelidchanneli", "user_id_1").Return(&model.ChannelMember{}, nil).Times(1)
mockChannelStore.On("InvalidatePinnedPostCount", "channelidchannelidchanneli")
mockChannelStore.On("GetAllChannelMembersNotifyPropsForChannel", "channelidchannelidchanneli", true).Return(map[string]model.StringMap{}, nil)
mockChannelStore.On("IncrementMentionCount", "", []string{}, true, false).Return(nil)
mockChannelStore.On("DeleteAllSidebarChannelForChannel", "channelidchannelidchanneli").Return(nil)
mockChannelStore.On("GetSidebarCategories", "user_id_1", &store.SidebarCategorySearchOpts{TeamID: "team_id_1", ExcludeTeam: false, Type: "channels"}).Return(
&model.OrderedSidebarCategories{
Categories: model.SidebarCategoriesWithChannels{
{
SidebarCategory: model.SidebarCategory{
Type: model.SidebarCategoryChannels,
},
},
},
}, nil)
mockChannelStore.On("GetSidebarCategories", "user_id_2", &store.SidebarCategorySearchOpts{TeamID: "team_id_1", ExcludeTeam: false, Type: "channels"}).Return(
&model.OrderedSidebarCategories{
Categories: model.SidebarCategoriesWithChannels{
{
SidebarCategory: model.SidebarCategory{
Type: model.SidebarCategoryChannels,
},
},
},
}, nil)
mockChannelStore.On("UpdateSidebarCategories", "user_id_1", "team_id_1", mock.Anything).Return(
[]*model.SidebarCategoryWithChannels{
{
SidebarCategory: model.SidebarCategory{
Type: model.SidebarCategoryChannels,
},
},
},
[]*model.SidebarCategoryWithChannels{
{
SidebarCategory: model.SidebarCategory{
Type: model.SidebarCategoryChannels,
},
},
},
nil,
)
mockChannelStore.On("UpdateSidebarCategories", "user_id_2", "team_id_1", mock.Anything).Return(
[]*model.SidebarCategoryWithChannels{
{
SidebarCategory: model.SidebarCategory{
Type: model.SidebarCategoryChannels,
},
},
},
[]*model.SidebarCategoryWithChannels{
{
SidebarCategory: model.SidebarCategory{
Type: model.SidebarCategoryChannels,
},
},
},
nil,
)
mockTeamStore := mocks.TeamStore{}
mockStore.On("Team").Return(&mockTeamStore)
mockTeamStore.On("GetMember", sqlstore.WithMaster(context.Background()), "team_id_1", "user_id_1").Return(&model.TeamMember{}, nil)
mockTeamStore.On("GetCommonTeamIDsForMultipleUsers", []string{"user_id_1", "user_id_2"}).Return([]string{"team_id_1", "team_id_2", "team_id_3"}, nil).Times(1)
mockTeamStore.On("GetMany", []string{"team_id_1", "team_id_2", "team_id_3"}).Return(
[]*model.Team{
{Id: "team_id_1", DisplayName: "Team 1"},
{Id: "team_id_2", DisplayName: "Team 2"},
{Id: "team_id_3", DisplayName: "Team 3"},
},
nil,
)
mockUserStore := mocks.UserStore{}
mockStore.On("User").Return(&mockUserStore)
mockUserStore.On("Get", context.Background(), "user_id_1").Return(&model.User{Username: "username_1"}, nil)
mockUserStore.On("GetProfilesInChannel", mock.AnythingOfType("*model.UserGetOptions")).Return([]*model.User{
{Id: "user_id_1", Username: "user_id_1"},
{Id: "user_id_2", Username: "user_id_2"},
}, nil)
mockUserStore.On("GetAllProfilesInChannel", mock.Anything, mock.Anything, mock.Anything).Return(map[string]*model.User{}, nil)
mockPostStore := mocks.PostStore{}
mockStore.On("Post").Return(&mockPostStore)
mockPostStore.On("Save", mock.AnythingOfType("*model.Post")).Return(&model.Post{}, nil)
mockPostStore.On("InvalidateLastPostTimeCache", "channelidchannelidchanneli")
var err error
th.App.ch.srv.userService, err = users.New(users.ServiceConfig{
UserStore: &mockUserStore,
ConfigFn: th.App.ch.srv.platform.Config,
SessionStore: &mocks.SessionStore{},
OAuthStore: &mocks.OAuthStore{},
LicenseFn: th.App.ch.srv.License,
})
require.NoError(t, err)
th.App.ch.srv.teamService, err = teams.New(teams.ServiceConfig{
TeamStore: &mockTeamStore,
ChannelStore: &mockChannelStore,
GroupStore: &mocks.GroupStore{},
Users: th.App.ch.srv.userService,
WebHub: th.App.ch.srv.platform,
ConfigFn: th.App.ch.srv.platform.Config,
LicenseFn: th.App.ch.srv.License,
})
require.NoError(t, err)
conversionRequest := &model.GroupMessageConversionRequestBody{
ChannelID: "channelidchannelidchanneli",
TeamID: "team_id_1",
Name: "new_name",
DisplayName: "New Display Name",
}
convertedChannel, appErr := th.App.ConvertGroupMessageToChannel(th.Context, "user_id_1", conversionRequest)
require.Nil(t, appErr)
require.Equal(t, model.ChannelTypePrivate, convertedChannel.Type)
}

Просмотреть файл

@@ -1819,6 +1819,28 @@ func (a *OpenTracingAppLayer) ConvertBotToUser(c request.CTX, bot *model.Bot, us
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) ConvertGroupMessageToChannel(c request.CTX, convertedByUserId string, gmConversionRequest *model.GroupMessageConversionRequestBody) (*model.Channel, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ConvertGroupMessageToChannel")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.ConvertGroupMessageToChannel(c, convertedByUserId, gmConversionRequest)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) ConvertUserToBot(user *model.User) (*model.Bot, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ConvertUserToBot")
@@ -6666,6 +6688,28 @@ func (a *OpenTracingAppLayer) GetGroupMemberUsersSortedPage(groupID string, page
return resultVar0, resultVar1, resultVar2
}
func (a *OpenTracingAppLayer) GetGroupMessageMembersCommonTeams(c request.CTX, channelID string) ([]*model.Team, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetGroupMessageMembersCommonTeams")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetGroupMessageMembersCommonTeams(c, channelID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetGroupSyncable(groupID string, syncableID string, syncableType model.GroupSyncableType) (*model.GroupSyncable, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetGroupSyncable")

Просмотреть файл

@@ -865,6 +865,24 @@ func (s *OpenTracingLayerChannelStore) Delete(channelID string, timestamp int64)
return err
}
func (s *OpenTracingLayerChannelStore) DeleteAllSidebarChannelForChannel(channelID string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.DeleteAllSidebarChannelForChannel")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.ChannelStore.DeleteAllSidebarChannelForChannel(channelID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerChannelStore) DeleteSidebarCategory(categoryID string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.DeleteSidebarCategory")
@@ -9554,6 +9572,24 @@ func (s *OpenTracingLayerTeamStore) GetChannelUnreadsForTeam(teamID string, user
return result, err
}
func (s *OpenTracingLayerTeamStore) GetCommonTeamIDsForMultipleUsers(userIDs []string) ([]string, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.GetCommonTeamIDsForMultipleUsers")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.TeamStore.GetCommonTeamIDsForMultipleUsers(userIDs)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerTeamStore) GetCommonTeamIDsForTwoUsers(userID string, otherUserID string) ([]string, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.GetCommonTeamIDsForTwoUsers")

Просмотреть файл

@@ -934,6 +934,27 @@ func (s *RetryLayerChannelStore) Delete(channelID string, timestamp int64) error
}
func (s *RetryLayerChannelStore) DeleteAllSidebarChannelForChannel(channelID string) error {
tries := 0
for {
err := s.ChannelStore.DeleteAllSidebarChannelForChannel(channelID)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) DeleteSidebarCategory(categoryID string) error {
tries := 0
@@ -10906,6 +10927,27 @@ func (s *RetryLayerTeamStore) GetChannelUnreadsForTeam(teamID string, userID str
}
func (s *RetryLayerTeamStore) GetCommonTeamIDsForMultipleUsers(userIDs []string) ([]string, error) {
tries := 0
for {
result, err := s.TeamStore.GetCommonTeamIDsForMultipleUsers(userIDs)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTeamStore) GetCommonTeamIDsForTwoUsers(userID string, otherUserID string) ([]string, error) {
tries := 0

Просмотреть файл

@@ -553,6 +553,10 @@ func (s SqlChannelStore) getSidebarCategoriesT(db dbSelecter, userId string, opt
query = query.Where(sq.Eq{"SidebarCategories.TeamId": opts.TeamID})
}
if opts.Type != "" {
query = query.Where(sq.Eq{"SidebarCategories.Type": opts.Type})
}
sql, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "sidebar_categories_tosql")
@@ -1125,3 +1129,18 @@ func (s SqlChannelStore) DeleteSidebarCategory(categoryId string) (err error) {
return nil
}
func (s SqlChannelStore) DeleteAllSidebarChannelForChannel(channelID string) error {
query, args, err := s.getQueryBuilder().
Delete("SidebarChannels").
Where(sq.Eq{
"ChannelId": channelID,
}).ToSql()
if err != nil {
return errors.Wrap(err, "delete_all_sidebar_channel_for_channel_to_sql")
}
_, err = s.GetMasterX().Exec(query, args...)
return err
}

Просмотреть файл

@@ -1513,6 +1513,46 @@ func (s SqlTeamStore) GetCommonTeamIDsForTwoUsers(userID, otherUserID string) ([
return teamIDs, nil
}
func (s SqlTeamStore) GetCommonTeamIDsForMultipleUsers(userIDs []string) ([]string, error) {
subQuery := s.getSubQueryBuilder().
Select("TeamId, UserId").
From("TeamMembers").
Where(sq.Eq{
"UserId": userIDs,
"DeleteAt": 0,
})
subQuerySQL, subQueryParams, err := subQuery.ToSql()
if err != nil {
return nil, errors.Wrap(err, "GetCommonTeamIDsForMultipleUsers_subquery_toSQL")
}
query := s.getQueryBuilder().
Select("t.Id").
From("Teams AS t").
Join("("+subQuerySQL+") AS tm ON t.Id = tm.TeamId", subQueryParams...).
Where(sq.Eq{
"t.DeleteAt": 0,
}).
GroupBy("t.Id").
Having(sq.Eq{
"COUNT(UserId)": len(userIDs),
})
querySQL, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "GetCommonTeamIDsForMultipleUsers_query_toSQL")
}
var teamIDs []string
if err := s.GetReplicaX().Select(&teamIDs, querySQL, args...); err != nil {
return nil, errors.Wrapf(err, "failed to find common team for users %v", userIDs)
}
return teamIDs, nil
}
// GetTeamMembersForExport gets the various teams for which a user, denoted by userId, is a part of.
func (s SqlTeamStore) GetTeamMembersForExport(userId string) ([]*model.TeamMemberForExport, error) {
members := []*model.TeamMemberForExport{}

Просмотреть файл

@@ -174,6 +174,8 @@ type TeamStore interface {
// GetCommonTeamIDsForTwoUsers returns the intersection of all the teams to which the specified
// users belong.
GetCommonTeamIDsForTwoUsers(userID, otherUserID string) ([]string, error)
GetCommonTeamIDsForMultipleUsers(userIDs []string) ([]string, error)
}
type ChannelStore interface {
@@ -287,6 +289,7 @@ type ChannelStore interface {
UpdateSidebarChannelsByPreferences(preferences model.Preferences) error
DeleteSidebarChannelsByPreferences(preferences model.Preferences) error
DeleteSidebarCategory(categoryID string) error
DeleteAllSidebarChannelForChannel(channelID string) error
GetAllChannelsForExportAfter(limit int, afterID string) ([]*model.ChannelForExport, error)
GetAllDirectChannelsForExportAfter(limit int, afterID string) ([]*model.DirectChannelForExport, error)
GetChannelMembersForExport(userID string, teamID string, includeArchivedChannel bool) ([]*model.ChannelMemberForExport, error)
@@ -1100,6 +1103,7 @@ type PostReminderMetadata struct {
type SidebarCategorySearchOpts struct {
TeamID string
ExcludeTeam bool
Type model.SidebarCategoryType
}
// Ensure store service adapter implements `product.StoreService`

Просмотреть файл

@@ -336,6 +336,20 @@ func (_m *ChannelStore) Delete(channelID string, timestamp int64) error {
return r0
}
// DeleteAllSidebarChannelForChannel provides a mock function with given fields: channelID
func (_m *ChannelStore) DeleteAllSidebarChannelForChannel(channelID string) error {
ret := _m.Called(channelID)
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(channelID)
} else {
r0 = ret.Error(0)
}
return r0
}
// DeleteSidebarCategory provides a mock function with given fields: categoryID
func (_m *ChannelStore) DeleteSidebarCategory(categoryID string) error {
ret := _m.Called(categoryID)

Просмотреть файл

@@ -419,6 +419,32 @@ func (_m *TeamStore) GetChannelUnreadsForTeam(teamID string, userID string) ([]*
return r0, r1
}
// GetCommonTeamIDsForMultipleUsers provides a mock function with given fields: userIDs
func (_m *TeamStore) GetCommonTeamIDsForMultipleUsers(userIDs []string) ([]string, error) {
ret := _m.Called(userIDs)
var r0 []string
var r1 error
if rf, ok := ret.Get(0).(func([]string) ([]string, error)); ok {
return rf(userIDs)
}
if rf, ok := ret.Get(0).(func([]string) []string); ok {
r0 = rf(userIDs)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]string)
}
}
if rf, ok := ret.Get(1).(func([]string) error); ok {
r1 = rf(userIDs)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetCommonTeamIDsForTwoUsers provides a mock function with given fields: userID, otherUserID
func (_m *TeamStore) GetCommonTeamIDsForTwoUsers(userID string, otherUserID string) ([]string, error) {
ret := _m.Called(userID, otherUserID)

Просмотреть файл

@@ -73,6 +73,7 @@ func TestTeamStore(t *testing.T, ss store.Store) {
t.Run("GetTeamMembersForExport", func(t *testing.T) { testTeamStoreGetTeamMembersForExport(t, ss) })
t.Run("GetTeamsForUserWithPagination", func(t *testing.T) { testTeamMembersWithPagination(t, ss) })
t.Run("GroupSyncedTeamCount", func(t *testing.T) { testGroupSyncedTeamCount(t, ss) })
t.Run("GetCommonTeamIDsForMultipleUsers", func(t *testing.T) { testGetCommonTeamIDsForMultipleUsers(t, ss) })
}
func testTeamStoreSave(t *testing.T, ss store.Store) {
@@ -3618,3 +3619,203 @@ func testGroupSyncedTeamCount(t *testing.T, ss store.Store) {
require.NoError(t, err)
require.GreaterOrEqual(t, countAfter, count+1)
}
func testGetCommonTeamIDsForMultipleUsers(t *testing.T, ss store.Store) {
// Creating teams
// Team 1
t1 := model.Team{}
t1.DisplayName = "Team 1"
t1.Name = NewTestId()
t1.Email = MakeEmail()
t1.Type = model.TeamOpen
_, err := ss.Team().Save(&t1)
require.NoError(t, err)
// Team 2
t2 := model.Team{}
t2.DisplayName = "Team 2"
t2.Name = NewTestId()
t2.Email = MakeEmail()
t2.Type = model.TeamOpen
_, err = ss.Team().Save(&t2)
require.NoError(t, err)
// Team 3
t3 := model.Team{}
t3.DisplayName = "Team 3"
t3.Name = NewTestId()
t3.Email = MakeEmail()
t3.Type = model.TeamOpen
_, err = ss.Team().Save(&t3)
require.NoError(t, err)
// Creating users
// User 1
u1 := model.User{}
u1.Email = MakeEmail()
u1.Nickname = NewTestId()
_, err = ss.User().Save(&u1)
require.NoError(t, err)
// User 2
u2 := model.User{}
u2.Email = MakeEmail()
u2.Nickname = NewTestId()
_, err = ss.User().Save(&u2)
require.NoError(t, err)
t.Run("multiple common teams exist", func(t *testing.T) {
// Add user 1 in team 1 and 2
m1 := &model.TeamMember{TeamId: t1.Id, UserId: u1.Id}
m2 := &model.TeamMember{TeamId: t2.Id, UserId: u1.Id}
// Add user 2 in team1, 2 and 3
m3 := &model.TeamMember{TeamId: t1.Id, UserId: u2.Id}
m4 := &model.TeamMember{TeamId: t2.Id, UserId: u2.Id}
m5 := &model.TeamMember{TeamId: t3.Id, UserId: u2.Id}
// Save team memberships
_, nErr := ss.Team().SaveMultipleMembers([]*model.TeamMember{m1, m2, m3, m4, m5}, -1)
require.NoError(t, nErr)
// Find common teams between user 1 and user 2
commonTeamIDs, err2 := ss.Team().GetCommonTeamIDsForMultipleUsers([]string{u1.Id, u2.Id})
require.NoError(t, err2)
require.Equal(t, 2, len(commonTeamIDs))
require.Contains(t, commonTeamIDs, t1.Id)
require.Contains(t, commonTeamIDs, t2.Id)
// cleanup
err2 = ss.Team().RemoveAllMembersByUser(u1.Id)
require.NoError(t, err2)
err2 = ss.Team().RemoveAllMembersByUser(u2.Id)
require.NoError(t, err2)
})
t.Run("single common teams exist", func(t *testing.T) {
// Add user 1 in team 1 and 2
m1 := &model.TeamMember{TeamId: t1.Id, UserId: u1.Id}
m2 := &model.TeamMember{TeamId: t2.Id, UserId: u1.Id}
// Add user 2 in team1, 2 and 3
m3 := &model.TeamMember{TeamId: t1.Id, UserId: u2.Id}
m5 := &model.TeamMember{TeamId: t3.Id, UserId: u2.Id}
// Save team memberships
_, nErr := ss.Team().SaveMultipleMembers([]*model.TeamMember{m1, m2, m3, m5}, -1)
require.NoError(t, nErr)
// Find common teams between user 1 and user 2
commonTeamIDs, err2 := ss.Team().GetCommonTeamIDsForMultipleUsers([]string{u1.Id, u2.Id})
require.NoError(t, err2)
require.Equal(t, 1, len(commonTeamIDs))
require.Contains(t, commonTeamIDs, t1.Id)
// cleanup
err2 = ss.Team().RemoveAllMembersByUser(u1.Id)
require.NoError(t, err2)
err2 = ss.Team().RemoveAllMembersByUser(u2.Id)
require.NoError(t, err2)
})
t.Run("no common teams exist", func(t *testing.T) {
// Add user 1 in team 1 and 2
m1 := &model.TeamMember{TeamId: t1.Id, UserId: u1.Id}
// Add user 2 in team1, 2 and 3
m4 := &model.TeamMember{TeamId: t2.Id, UserId: u2.Id}
m5 := &model.TeamMember{TeamId: t3.Id, UserId: u2.Id}
// Save team memberships
_, nErr := ss.Team().SaveMultipleMembers([]*model.TeamMember{m1, m4, m5}, -1)
require.NoError(t, nErr)
// Find common teams between user 1 and user 2
commonTeamIDs, err2 := ss.Team().GetCommonTeamIDsForMultipleUsers([]string{u1.Id, u2.Id})
require.NoError(t, err2)
require.Equal(t, 0, len(commonTeamIDs))
// cleanup
err2 = ss.Team().RemoveAllMembersByUser(u1.Id)
require.NoError(t, err2)
err2 = ss.Team().RemoveAllMembersByUser(u2.Id)
require.NoError(t, err2)
})
t.Run("some user have no team members", func(t *testing.T) {
// Add user 1 in team 1 and 2
m1 := &model.TeamMember{TeamId: t1.Id, UserId: u1.Id}
m2 := &model.TeamMember{TeamId: t2.Id, UserId: u1.Id}
// We'll leave user 2 without any team
// Save team memberships
_, nErr := ss.Team().SaveMultipleMembers([]*model.TeamMember{m1, m2}, -1)
require.NoError(t, nErr)
// Find common teams between user 1 and user 2
commonTeamIDs, err2 := ss.Team().GetCommonTeamIDsForMultipleUsers([]string{u1.Id, u2.Id})
require.NoError(t, err2)
require.Equal(t, 0, len(commonTeamIDs))
// cleanup
err2 = ss.Team().RemoveAllMembersByUser(u1.Id)
require.NoError(t, err2)
})
t.Run("more than two users, common teams exist", func(t *testing.T) {
// User 3
u3 := model.User{}
u3.Email = MakeEmail()
u3.Nickname = NewTestId()
_, err = ss.User().Save(&u3)
require.NoError(t, err)
// User 4
u4 := model.User{}
u4.Email = MakeEmail()
u4.Nickname = NewTestId()
_, err = ss.User().Save(&u4)
require.NoError(t, err)
// Add user 1 in team 1 and 2
m1 := &model.TeamMember{TeamId: t1.Id, UserId: u1.Id}
m2 := &model.TeamMember{TeamId: t2.Id, UserId: u1.Id}
// Add user 2 in team1, 2 and 3
m3 := &model.TeamMember{TeamId: t1.Id, UserId: u2.Id}
m4 := &model.TeamMember{TeamId: t2.Id, UserId: u2.Id}
m5 := &model.TeamMember{TeamId: t3.Id, UserId: u2.Id}
// Add user 3 in team 1 and 2
m6 := &model.TeamMember{TeamId: t1.Id, UserId: u3.Id}
m7 := &model.TeamMember{TeamId: t2.Id, UserId: u3.Id}
// Add user 4 in team 1 and 2
m8 := &model.TeamMember{TeamId: t1.Id, UserId: u4.Id}
m9 := &model.TeamMember{TeamId: t2.Id, UserId: u4.Id}
// Save team memberships
_, nErr := ss.Team().SaveMultipleMembers([]*model.TeamMember{m1, m2, m3, m4, m5, m6, m7, m8, m9}, -1)
require.NoError(t, nErr)
// Find common teams between user 1 and user 2
commonTeamIDs, err := ss.Team().GetCommonTeamIDsForMultipleUsers([]string{u1.Id, u2.Id, u3.Id, u4.Id})
require.NoError(t, err)
require.Equal(t, 2, len(commonTeamIDs))
require.Contains(t, commonTeamIDs, t1.Id)
require.Contains(t, commonTeamIDs, t2.Id)
// cleanup
err = ss.Team().RemoveAllMembersByUser(u1.Id)
require.NoError(t, err)
err = ss.Team().RemoveAllMembersByUser(u2.Id)
require.NoError(t, err)
})
}

Просмотреть файл

@@ -827,6 +827,22 @@ func (s *TimerLayerChannelStore) Delete(channelID string, timestamp int64) error
return err
}
func (s *TimerLayerChannelStore) DeleteAllSidebarChannelForChannel(channelID string) error {
start := time.Now()
err := s.ChannelStore.DeleteAllSidebarChannelForChannel(channelID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.DeleteAllSidebarChannelForChannel", success, elapsed)
}
return err
}
func (s *TimerLayerChannelStore) DeleteSidebarCategory(categoryID string) error {
start := time.Now()
@@ -8606,6 +8622,22 @@ func (s *TimerLayerTeamStore) GetChannelUnreadsForTeam(teamID string, userID str
return result, err
}
func (s *TimerLayerTeamStore) GetCommonTeamIDsForMultipleUsers(userIDs []string) ([]string, error) {
start := time.Now()
result, err := s.TeamStore.GetCommonTeamIDsForMultipleUsers(userIDs)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TeamStore.GetCommonTeamIDsForMultipleUsers", success, elapsed)
}
return result, err
}
func (s *TimerLayerTeamStore) GetCommonTeamIDsForTwoUsers(userID string, otherUserID string) ([]string, error) {
start := time.Now()

25
server/channels/utils/humanize.go Обычный файл
Просмотреть файл

@@ -0,0 +1,25 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package utils
import (
"strings"
"github.com/mattermost/mattermost/server/public/shared/i18n"
)
func JoinList(items []string) string {
if len(items) == 0 {
return ""
} else if len(items) == 1 {
return items[0]
} else {
return i18n.T(
"humanize.list_join",
map[string]any{
"OtherItems": strings.Join(items[:len(items)-1], ", "),
"LastItem": items[len(items)-1],
})
}
}

Просмотреть файл

@@ -295,6 +295,14 @@
"id": "api.channel.get_channel_moderations.license.error",
"translation": "Your license does not support channel moderation"
},
{
"id": "api.channel.gm_to_channel_conversion.not_allowed_for_user.request_error",
"translation": "User is not allowed to convert group message to private channel"
},
{
"id": "api.channel.group_message.converted.to_private_channel",
"translation": "{{.ConvertedByUsername}} created this channel from a group message with {{.GMMembers}}."
},
{
"id": "api.channel.guest_join_channel.post_and_forget",
"translation": "%v joined the channel as guest."
@@ -4810,6 +4818,14 @@
"id": "app.channel.get_channels_with_unreads_and_with_mentions.app_error",
"translation": "Unable to check unreads and mentions"
},
{
"id": "app.channel.get_common_teams.incorrect_channel_type",
"translation": "Channel is not a group message."
},
{
"id": "app.channel.get_common_teams.store_get_common_teams_error",
"translation": "Couldn't fetch common teams."
},
{
"id": "app.channel.get_deleted.existing.app_error",
"translation": "Unable to find the existing deleted channel."
@@ -4870,6 +4886,26 @@
"id": "app.channel.get_unread.app_error",
"translation": "Unable to get the channel unread messages."
},
{
"id": "app.channel.gm_conversion_set_categories.delete_all.error",
"translation": "Couldn't delete existing sidebar categories for converted GM."
},
{
"id": "app.channel.group_message_conversion.channel_member_missing",
"translation": "Cannot find user's channel membership"
},
{
"id": "app.channel.group_message_conversion.incorrect_team",
"translation": "The specified team ID in conversion request does not contain all the group message members"
},
{
"id": "app.channel.group_message_conversion.original_channel_not_gm",
"translation": "The channel being converted is not a group message. You can only convert group messages"
},
{
"id": "app.channel.group_message_conversion.post_message.error",
"translation": "Failed to create group message to channel conversion post"
},
{
"id": "app.channel.migrate_channel_members.select.app_error",
"translation": "Failed to select the batch of channel members."
@@ -8082,6 +8118,10 @@
"id": "groups.unsupported_syncable_type",
"translation": "Unsupported syncable type '{{.Value}}'."
},
{
"id": "humanize.list_join",
"translation": "{{.OtherItems}} and {{.LastItem}}"
},
{
"id": "import_process.worker.do_job.file_exists",
"translation": "Unable to process import: file does not exists."

Просмотреть файл

@@ -443,3 +443,10 @@ func GetGroupNameFromUserIds(userIds []string) string {
return hex.EncodeToString(h.Sum(nil))
}
type GroupMessageConversionRequestBody struct {
ChannelID string `json:"channel_id"`
TeamID string `json:"team_id"`
Name string `json:"name"`
DisplayName string `json:"display_name"`
}

Просмотреть файл

@@ -45,6 +45,7 @@ const (
PostTypeChannelRestored = "system_channel_restored"
PostTypeEphemeral = "system_ephemeral"
PostTypeChangeChannelPrivacy = "system_change_chan_privacy"
PostTypeGMConvertedToChannel = "system_gm_to_channel"
PostTypeAddBotTeamsChannels = "add_bot_teams_channels"
PostTypeSystemWarnMetricStatus = "warn_metric_status"
PostTypeMe = "me"
@@ -428,7 +429,8 @@ func (o *Post) IsValid(maxPostSize int) *AppError {
PostTypeAddBotTeamsChannels,
PostTypeSystemWarnMetricStatus,
PostTypeReminder,
PostTypeMe:
PostTypeMe,
PostTypeGMConvertedToChannel:
default:
if !strings.HasPrefix(o.Type, PostCustomTypePrefix) {
return NewAppError("Post.IsValid", "model.post.is_valid.type.app_error", nil, "id="+o.Type, http.StatusBadRequest)