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>
306 lines
11 KiB
Go
306 lines
11 KiB
Go
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
package slashcommands
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/mattermost/mattermost/server/public/model"
|
|
"github.com/mattermost/mattermost/server/public/shared/i18n"
|
|
"github.com/mattermost/mattermost/server/public/shared/request"
|
|
"github.com/mattermost/mattermost/server/v8/channels/app"
|
|
)
|
|
|
|
type ShareProvider struct {
|
|
}
|
|
|
|
// ensure ShareProvide implements AutocompleteDynamicArgProvider
|
|
var _ app.AutocompleteDynamicArgProvider = (*ShareProvider)(nil)
|
|
|
|
const (
|
|
CommandTriggerShare = "share-channel"
|
|
AvailableShareActions = "invite, uninvite, unshare, status"
|
|
)
|
|
|
|
func init() {
|
|
app.RegisterCommandProvider(&ShareProvider{})
|
|
}
|
|
|
|
func (sp *ShareProvider) GetTrigger() string {
|
|
return CommandTriggerShare
|
|
}
|
|
|
|
func (sp *ShareProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model.Command {
|
|
share := model.NewAutocompleteData(CommandTriggerShare, "[action]", T("api.command_share.available_actions", map[string]any{"Actions": AvailableShareActions}))
|
|
|
|
inviteRemote := model.NewAutocompleteData("invite", "", T("api.command_share.invite_remote.help"))
|
|
inviteRemote.AddNamedDynamicListArgument("connectionID", T("api.command_share.remote_id.help"), "builtin:"+CommandTriggerShare, true)
|
|
inviteRemote.AddNamedTextArgument("readonly", T("api.command_share.share_read_only.help"), T("api.command_share.share_read_only.hint"), "Y|N|y|n", false)
|
|
|
|
unInviteRemote := model.NewAutocompleteData("uninvite", "", T("api.command_share.uninvite_remote.help"))
|
|
unInviteRemote.AddNamedDynamicListArgument("connectionID", T("api.command_share.uninvite_remote_id.help"), "builtin:"+CommandTriggerShare, true)
|
|
|
|
unshareChannel := model.NewAutocompleteData("unshare", "", T("api.command_share.unshare_channel.help"))
|
|
|
|
status := model.NewAutocompleteData("status", "", T("api.command_share.channel_status.help"))
|
|
|
|
share.AddCommand(inviteRemote)
|
|
share.AddCommand(unInviteRemote)
|
|
share.AddCommand(unshareChannel)
|
|
share.AddCommand(status)
|
|
|
|
return &model.Command{
|
|
Trigger: CommandTriggerShare,
|
|
AutoComplete: true,
|
|
AutoCompleteDesc: T("api.command_share.desc"),
|
|
AutoCompleteHint: T("api.command_share.hint"),
|
|
DisplayName: T("api.command_share.name"),
|
|
AutocompleteData: share,
|
|
}
|
|
}
|
|
|
|
func (sp *ShareProvider) GetAutoCompleteListItems(rctx request.CTX, a *app.App, commandArgs *model.CommandArgs, arg *model.AutocompleteArg, parsed, toBeParsed string) ([]model.AutocompleteListItem, error) {
|
|
switch {
|
|
case strings.Contains(parsed, " share "):
|
|
|
|
return sp.getAutoCompleteShareChannel(rctx, a, commandArgs, arg)
|
|
|
|
case strings.Contains(parsed, " invite "):
|
|
|
|
return sp.getAutoCompleteInviteRemote(a, commandArgs, arg)
|
|
|
|
case strings.Contains(parsed, " uninvite "):
|
|
|
|
return sp.getAutoCompleteUnInviteRemote(a, commandArgs, arg)
|
|
}
|
|
return nil, errors.New("invalid action")
|
|
}
|
|
|
|
func (sp *ShareProvider) getAutoCompleteShareChannel(rctx request.CTX, a *app.App, commandArgs *model.CommandArgs, arg *model.AutocompleteArg) ([]model.AutocompleteListItem, error) {
|
|
channel, err := a.GetChannel(rctx, commandArgs.ChannelId)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var item model.AutocompleteListItem
|
|
|
|
switch arg.Name {
|
|
case "name":
|
|
item = model.AutocompleteListItem{
|
|
Item: channel.Name,
|
|
HelpText: channel.DisplayName,
|
|
}
|
|
case "displayname":
|
|
item = model.AutocompleteListItem{
|
|
Item: channel.DisplayName,
|
|
HelpText: channel.Name,
|
|
}
|
|
default:
|
|
return nil, fmt.Errorf("%s not a dynamic argument", arg.Name)
|
|
}
|
|
return []model.AutocompleteListItem{item}, nil
|
|
}
|
|
|
|
func (sp *ShareProvider) getAutoCompleteInviteRemote(a *app.App, commandArgs *model.CommandArgs, arg *model.AutocompleteArg) ([]model.AutocompleteListItem, error) {
|
|
switch arg.Name {
|
|
case "connectionID":
|
|
return getRemoteClusterAutocompleteListItemsNotInChannel(a, commandArgs.ChannelId, true)
|
|
default:
|
|
return nil, fmt.Errorf("%s not a dynamic argument", arg.Name)
|
|
}
|
|
}
|
|
|
|
func (sp *ShareProvider) getAutoCompleteUnInviteRemote(a *app.App, _ *model.CommandArgs, arg *model.AutocompleteArg) ([]model.AutocompleteListItem, error) {
|
|
switch arg.Name {
|
|
case "connectionID":
|
|
return getRemoteClusterAutocompleteListItems(a, true)
|
|
default:
|
|
return nil, fmt.Errorf("%s not a dynamic argument", arg.Name)
|
|
}
|
|
}
|
|
|
|
func (sp *ShareProvider) DoCommand(a *app.App, rctx request.CTX, args *model.CommandArgs, message string) *model.CommandResponse {
|
|
if !a.HasPermissionTo(args.UserId, model.PermissionManageSharedChannels) {
|
|
return response(args.T("api.command_share.permission_required", map[string]any{"Permission": "manage_shared_channels"}))
|
|
}
|
|
|
|
syncService := a.Srv().GetSharedChannelSyncService()
|
|
if syncService == nil || !syncService.Active() {
|
|
return response(args.T("api.command_share.service_disabled"))
|
|
}
|
|
|
|
rcService := a.Srv().GetRemoteClusterService()
|
|
if rcService == nil || !rcService.Active() {
|
|
return response(args.T("api.command_remote.service_disabled"))
|
|
}
|
|
|
|
margs := parseNamedArgs(args.Command)
|
|
action, ok := margs[ActionKey]
|
|
if !ok {
|
|
return response(args.T("api.command_share.missing_action", map[string]any{"Actions": AvailableShareActions}))
|
|
}
|
|
|
|
switch action {
|
|
case "share":
|
|
return sp.doShareChannel(a, rctx, args, margs)
|
|
case "unshare":
|
|
return sp.doUnshareChannel(a, args, margs)
|
|
case "invite":
|
|
return sp.doInviteRemote(a, rctx, args, margs)
|
|
case "uninvite":
|
|
return sp.doUninviteRemote(a, args, margs)
|
|
case "status":
|
|
return sp.doStatus(a, args, margs)
|
|
}
|
|
return response(args.T("api.command_share.unknown_action", map[string]any{"Action": action, "Actions": AvailableShareActions}))
|
|
}
|
|
|
|
func (sp *ShareProvider) doShareChannel(a *app.App, rctx request.CTX, args *model.CommandArgs, margs map[string]string) *model.CommandResponse {
|
|
// fetch defaults for missing channel props
|
|
channel, errApp := a.GetChannel(rctx, args.ChannelId)
|
|
if errApp != nil {
|
|
return response(args.T("api.command_share.share_channel.error", map[string]any{"Error": errApp.Error()}))
|
|
}
|
|
|
|
if name := margs["name"]; name == "" {
|
|
margs["name"] = channel.Name
|
|
}
|
|
if name := margs["displayname"]; name == "" {
|
|
margs["displayname"] = channel.DisplayName
|
|
}
|
|
if name := margs["purpose"]; name == "" {
|
|
margs["purpose"] = channel.Purpose
|
|
}
|
|
if name := margs["header"]; name == "" {
|
|
margs["header"] = channel.Header
|
|
}
|
|
if _, ok := margs["readonly"]; !ok {
|
|
margs["readonly"] = "N"
|
|
}
|
|
|
|
readonly, err := parseBool(margs["readonly"])
|
|
if err != nil {
|
|
return response(args.T("api.command_share.invalid_value.error", map[string]any{"Arg": "readonly", "Error": err.Error()}))
|
|
}
|
|
|
|
sc := &model.SharedChannel{
|
|
ChannelId: args.ChannelId,
|
|
TeamId: args.TeamId,
|
|
Home: true,
|
|
ReadOnly: readonly,
|
|
ShareName: margs["name"],
|
|
ShareDisplayName: margs["displayname"],
|
|
SharePurpose: margs["purpose"],
|
|
ShareHeader: margs["header"],
|
|
CreatorId: args.UserId,
|
|
}
|
|
|
|
if _, err := a.ShareChannel(rctx, sc); err != nil {
|
|
return response(args.T("api.command_share.share_channel.error", map[string]any{"Error": err.Error()}))
|
|
}
|
|
|
|
return response("##### " + args.T("api.command_share.channel_shared"))
|
|
}
|
|
|
|
func (sp *ShareProvider) doUnshareChannel(a *app.App, args *model.CommandArgs, margs map[string]string) *model.CommandResponse {
|
|
deleted, err := a.UnshareChannel(args.ChannelId)
|
|
if err != nil {
|
|
return response(args.T("api.command_share.shared_channel_unshare.error", map[string]any{"Error": err.Error()}))
|
|
}
|
|
if !deleted {
|
|
return response(args.T("api.command_share.not_shared_channel_unshare"))
|
|
}
|
|
|
|
return response("##### " + args.T("api.command_share.shared_channel_unavailable"))
|
|
}
|
|
|
|
func (sp *ShareProvider) doInviteRemote(a *app.App, rctx request.CTX, args *model.CommandArgs, margs map[string]string) (resp *model.CommandResponse) {
|
|
remoteID, ok := margs["connectionID"]
|
|
if !ok || remoteID == "" {
|
|
return response(args.T("api.command_share.must_specify_valid_remote"))
|
|
}
|
|
|
|
hasRemote, err := a.HasRemote(args.ChannelId, remoteID)
|
|
if err != nil {
|
|
return response(args.T("api.command_share.fetch_remote.error", map[string]any{"Error": err.Error()}))
|
|
}
|
|
if hasRemote {
|
|
return response(args.T("api.command_share.remote_already_invited"))
|
|
}
|
|
|
|
// Check if channel is shared or not.
|
|
// TODO: have the share channels service generate the "channel has been shared post" and this section can be removed since
|
|
// since `a.InviteRemoteToChannel` will share the channel automatically.
|
|
hasChan, err := a.HasSharedChannel(args.ChannelId)
|
|
if err != nil {
|
|
return response(args.T("api.command_share.check_channel_exist.error", map[string]any{"ChannelID": args.ChannelId, "Error": err.Error()}))
|
|
}
|
|
if !hasChan {
|
|
// If it doesn't exist, then create it.
|
|
resp2 := sp.doShareChannel(a, rctx, args, margs)
|
|
// We modify the outgoing response by prepending the text
|
|
// from the shareChannel response.
|
|
defer func() {
|
|
resp.Text = resp2.Text + "\n" + resp.Text
|
|
}()
|
|
}
|
|
|
|
rc, appErr := a.GetRemoteCluster(remoteID, false)
|
|
if appErr != nil {
|
|
return response(args.T("api.command_share.remote_id_invalid.error", map[string]any{"Error": appErr.Error()}))
|
|
}
|
|
|
|
if err = a.InviteRemoteToChannel(args.ChannelId, remoteID, args.UserId, true); err != nil {
|
|
return response(args.T("api.command_share.invite_remote_to_channel.error", map[string]any{"Error": err.Error()}))
|
|
}
|
|
|
|
return response("##### " + args.T("api.command_share.invitation_sent", map[string]any{"Name": rc.DisplayName, "SiteURL": rc.SiteURL}))
|
|
}
|
|
|
|
func (sp *ShareProvider) doUninviteRemote(a *app.App, args *model.CommandArgs, margs map[string]string) *model.CommandResponse {
|
|
remoteID, ok := margs["connectionID"]
|
|
if !ok || remoteID == "" {
|
|
return response(args.T("api.command_share.remote_not_valid"))
|
|
}
|
|
|
|
err := a.UninviteRemoteFromChannel(args.ChannelId, remoteID)
|
|
if err != nil {
|
|
return response(err.Error())
|
|
}
|
|
|
|
return response("##### " + args.T("api.command_share.remote_uninvited", map[string]any{"RemoteId": remoteID}))
|
|
}
|
|
|
|
func (sp *ShareProvider) doStatus(a *app.App, args *model.CommandArgs, _ map[string]string) *model.CommandResponse {
|
|
statuses, err := a.GetSharedChannelRemotesStatus(args.ChannelId)
|
|
if err != nil {
|
|
return response(args.T("api.command_share.fetch_remote_status.error", map[string]any{"Error": err.Error()}))
|
|
}
|
|
if len(statuses) == 0 {
|
|
return response(args.T("api.command_share.no_remote_invited"))
|
|
}
|
|
|
|
var sb strings.Builder
|
|
|
|
fmt.Fprintf(&sb, "%s\n\n", args.T("api.command_share.channel_status_id", map[string]any{"ChannelId": statuses[0].ChannelId}))
|
|
|
|
fmt.Fprintf(&sb, "%s \n", args.T("api.command_share.remote_table_header"))
|
|
// "| Secure Connection | SiteURL | ReadOnly | InviteAccepted | Online | Last Sync |"
|
|
fmt.Fprintf(&sb, "| ---- | ---- | ---- | ---- | ---- | ---- | \n")
|
|
|
|
for _, status := range statuses {
|
|
readonly := formatBool(args.T, status.ReadOnly)
|
|
accepted := formatBool(args.T, status.IsInviteAccepted)
|
|
online := formatBool(args.T, isOnline(status.LastPingAt))
|
|
|
|
lastSync := formatTimestamp(status.NextSyncAt)
|
|
|
|
fmt.Fprintf(&sb, "| %s | %s | %s | %s | %s | %s |\n",
|
|
status.DisplayName, status.SiteURL, readonly, accepted, online, lastSync)
|
|
}
|
|
return response(sb.String())
|
|
}
|