Full Mattermost server source with integrated Community Enterprise features. Includes vendor directory for offline/air-gapped builds. Structure: - enterprise-impl/: Enterprise feature implementations - enterprise-community/: Init files that register implementations - enterprise/: Bridge imports (community_imports.go) - vendor/: All dependencies for offline builds Build (online): go build ./cmd/mattermost Build (offline/air-gapped): go build -mod=vendor ./cmd/mattermost 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
238 lines
6.7 KiB
Go
238 lines
6.7 KiB
Go
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
package teams
|
|
|
|
import (
|
|
"github.com/mattermost/mattermost/server/public/model"
|
|
"github.com/mattermost/mattermost/server/public/shared/i18n"
|
|
"github.com/mattermost/mattermost/server/public/shared/request"
|
|
)
|
|
|
|
func (ts *TeamService) CreateTeam(rctx request.CTX, team *model.Team) (*model.Team, error) {
|
|
team.InviteId = ""
|
|
rteam, err := ts.store.Save(team)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if _, err := ts.createDefaultChannels(rctx, rteam.Id); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return rteam, nil
|
|
}
|
|
|
|
func (ts *TeamService) GetTeam(teamID string) (*model.Team, error) {
|
|
team, err := ts.store.Get(teamID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return team, nil
|
|
}
|
|
|
|
func (ts *TeamService) GetTeams(teamIDs []string) ([]*model.Team, error) {
|
|
teams, err := ts.store.GetMany(teamIDs)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return teams, nil
|
|
}
|
|
|
|
// CreateDefaultChannels creates channels in the given team for each channel returned by (*App).DefaultChannelNames.
|
|
func (ts *TeamService) createDefaultChannels(rctx request.CTX, teamID string) ([]*model.Channel, error) {
|
|
displayNames := map[string]string{
|
|
"town-square": i18n.T("api.channel.create_default_channels.town_square"),
|
|
"off-topic": i18n.T("api.channel.create_default_channels.off_topic"),
|
|
}
|
|
channels := []*model.Channel{}
|
|
defaultChannelNames := ts.DefaultChannelNames()
|
|
for _, name := range defaultChannelNames {
|
|
var displayName string
|
|
if displayNameValue, ok := displayNames[name]; ok {
|
|
displayName = i18n.TDefault(displayNameValue, name)
|
|
} else {
|
|
// If the default channel is experimental (from config.json)
|
|
// we don't have to translate
|
|
displayName = name
|
|
}
|
|
channel := &model.Channel{DisplayName: displayName, Name: name, Type: model.ChannelTypeOpen, TeamId: teamID}
|
|
// We should use the channel service here (coming soon). Ideally, we should just emit an event
|
|
// and let the subscribers do the job, in this case it would be the channels service.
|
|
// Currently we are adding services to the server and because of that we are using
|
|
// the channel store here. This should be replaced in the future.
|
|
if _, err := ts.channelStore.Save(rctx, channel, *ts.config().TeamSettings.MaxChannelsPerTeam); err != nil {
|
|
return nil, err
|
|
}
|
|
channels = append(channels, channel)
|
|
}
|
|
return channels, nil
|
|
}
|
|
|
|
type UpdateOptions struct {
|
|
Sanitized bool
|
|
Imported bool
|
|
}
|
|
|
|
func (ts *TeamService) UpdateTeam(team *model.Team, opts UpdateOptions) (*model.Team, error) {
|
|
oldTeam := team
|
|
var err error
|
|
|
|
if !opts.Imported {
|
|
oldTeam, err = ts.store.Get(team.Id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err = ts.checkValidDomains(team); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
if opts.Sanitized {
|
|
oldTeam.DisplayName = team.DisplayName
|
|
oldTeam.Description = team.Description
|
|
oldTeam.AllowOpenInvite = team.AllowOpenInvite
|
|
oldTeam.CompanyName = team.CompanyName
|
|
oldTeam.AllowedDomains = team.AllowedDomains
|
|
oldTeam.LastTeamIconUpdate = team.LastTeamIconUpdate
|
|
oldTeam.GroupConstrained = team.GroupConstrained
|
|
}
|
|
|
|
oldTeam, err = ts.store.Update(oldTeam)
|
|
if err != nil {
|
|
return team, err
|
|
}
|
|
|
|
return oldTeam, nil
|
|
}
|
|
|
|
func (ts *TeamService) PatchTeam(teamID string, patch *model.TeamPatch) (*model.Team, error) {
|
|
team, err := ts.store.Get(teamID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
team.Patch(patch)
|
|
if patch.AllowOpenInvite != nil && !*patch.AllowOpenInvite {
|
|
team.InviteId = model.NewId()
|
|
}
|
|
|
|
if err = ts.checkValidDomains(team); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
team, err = ts.store.Update(team)
|
|
if err != nil {
|
|
return team, err
|
|
}
|
|
|
|
return team, nil
|
|
}
|
|
|
|
// JoinUserToTeam adds a user to the team and it returns three values:
|
|
// 1. a pointer to the team member, if successful
|
|
// 2. a boolean: true if the user has a non-deleted team member for that team already, otherwise false.
|
|
// 3. a pointer to an AppError if something went wrong.
|
|
func (ts *TeamService) JoinUserToTeam(rctx request.CTX, team *model.Team, user *model.User) (*model.TeamMember, bool, error) {
|
|
if !ts.IsTeamEmailAllowed(user, team) {
|
|
return nil, false, AcceptedDomainError
|
|
}
|
|
|
|
tm := &model.TeamMember{
|
|
TeamId: team.Id,
|
|
UserId: user.Id,
|
|
SchemeGuest: user.IsGuest(),
|
|
SchemeUser: !user.IsGuest(),
|
|
CreateAt: model.GetMillis(),
|
|
}
|
|
|
|
if !user.IsGuest() {
|
|
userShouldBeAdmin, err := ts.userIsInAdminRoleGroup(user.Id, team.Id, model.GroupSyncableTypeTeam)
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
tm.SchemeAdmin = userShouldBeAdmin
|
|
}
|
|
|
|
if team.Email == user.Email {
|
|
tm.SchemeAdmin = true
|
|
}
|
|
|
|
rtm, err := ts.store.GetMember(rctx, team.Id, user.Id)
|
|
if err != nil {
|
|
// Membership appears to be missing. Lets try to add.
|
|
tmr, nErr := ts.store.SaveMember(rctx, tm, *ts.config().TeamSettings.MaxUsersPerTeam)
|
|
if nErr != nil {
|
|
return nil, false, nErr
|
|
}
|
|
return tmr, false, nil
|
|
}
|
|
|
|
// Membership already exists. Check if deleted and update, otherwise do nothing
|
|
// Do nothing if already added
|
|
if rtm.DeleteAt == 0 {
|
|
return rtm, true, nil
|
|
}
|
|
|
|
membersCount, err := ts.store.GetActiveMemberCount(tm.TeamId, nil)
|
|
if err != nil {
|
|
return nil, false, MemberCountError
|
|
}
|
|
|
|
if membersCount >= int64(*ts.config().TeamSettings.MaxUsersPerTeam) {
|
|
return nil, false, MaxMemberCountError
|
|
}
|
|
|
|
member, nErr := ts.store.UpdateMember(rctx, tm)
|
|
if nErr != nil {
|
|
return nil, false, nErr
|
|
}
|
|
|
|
return member, false, nil
|
|
}
|
|
|
|
// RemoveTeamMember removes the team member from the team. This method sends
|
|
// the websocket message before actually removing so the user being removed gets it.
|
|
func (ts *TeamService) RemoveTeamMember(rctx request.CTX, teamMember *model.TeamMember) error {
|
|
/*
|
|
MM-43850: send leave_team event to user using `ReliableClusterSend` to improve safety
|
|
*/
|
|
// message for other team members
|
|
omitUsers := make(map[string]bool, 1)
|
|
omitUsers[teamMember.UserId] = true
|
|
messageTeam := model.NewWebSocketEvent(model.WebsocketEventLeaveTeam, teamMember.TeamId, "", "", omitUsers, "")
|
|
messageTeam.Add("user_id", teamMember.UserId)
|
|
messageTeam.Add("team_id", teamMember.TeamId)
|
|
ts.wh.Publish(messageTeam)
|
|
|
|
// message for teamMember.UserId
|
|
messageUser := model.NewWebSocketEvent(model.WebsocketEventLeaveTeam, "", "", teamMember.UserId, nil, "")
|
|
messageUser.Add("user_id", teamMember.UserId)
|
|
messageUser.Add("team_id", teamMember.TeamId)
|
|
|
|
ts.wh.Publish(messageUser)
|
|
|
|
// delete team member
|
|
teamMember.Roles = ""
|
|
teamMember.DeleteAt = model.GetMillis()
|
|
|
|
if _, nErr := ts.store.UpdateMember(rctx, teamMember); nErr != nil {
|
|
return nErr
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetMember return the team member from the team.
|
|
func (ts *TeamService) GetMember(rctx request.CTX, teamID string, userID string) (*model.TeamMember, error) {
|
|
member, err := ts.store.GetMember(rctx, teamID, userID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return member, err
|
|
}
|