mattermost-community-enterp.../channels/app/slashcommands/command_remote.go
Claude ec1f89217a Merge: Complete Mattermost Server with Community Enterprise
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>
2025-12-17 23:59:07 +09:00

291 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"
)
const (
AvailableRemoteActions = "create, accept, remove, status"
)
type RemoteProvider struct {
}
// ensure RemoteProvider implements AutocompleteDynamicArgProvider
var _ app.AutocompleteDynamicArgProvider = (*RemoteProvider)(nil)
const (
CommandTriggerRemote = "secure-connection"
)
func init() {
app.RegisterCommandProvider(&RemoteProvider{})
}
func (rp *RemoteProvider) GetTrigger() string {
return CommandTriggerRemote
}
func (rp *RemoteProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model.Command {
remote := model.NewAutocompleteData(rp.GetTrigger(), "[action]", T("api.command_remote.remote_add_remove.help", map[string]any{"Actions": AvailableRemoteActions}))
create := model.NewAutocompleteData("create", "", T("api.command_remote.invite.help"))
create.AddNamedTextArgument("name", T("api.command_remote.name.help"), T("api.command_remote.name.hint"), "", true)
create.AddNamedTextArgument("displayname", T("api.command_remote.displayname.help"), T("api.command_remote.displayname.hint"), "", false)
create.AddNamedTextArgument("password", T("api.command_remote.invite_password.help"), T("api.command_remote.invite_password.hint"), "", true)
accept := model.NewAutocompleteData("accept", "", T("api.command_remote.accept.help"))
accept.AddNamedTextArgument("name", T("api.command_remote.name.help"), T("api.command_remote.name.hint"), "", true)
accept.AddNamedTextArgument("displayname", T("api.command_remote.displayname.help"), T("api.command_remote.displayname.hint"), "", false)
accept.AddNamedTextArgument("password", T("api.command_remote.invite_password.help"), T("api.command_remote.invite_password.hint"), "", true)
accept.AddNamedTextArgument("invite", T("api.command_remote.invitation.help"), T("api.command_remote.invitation.hint"), "", true)
remove := model.NewAutocompleteData("remove", "", T("api.command_remote.remove.help"))
remove.AddNamedDynamicListArgument("connectionID", T("api.command_remote.remove_remote_id.help"), "builtin:"+CommandTriggerRemote, true)
status := model.NewAutocompleteData("status", "", T("api.command_remote.status.help"))
remote.AddCommand(create)
remote.AddCommand(accept)
remote.AddCommand(remove)
remote.AddCommand(status)
return &model.Command{
Trigger: rp.GetTrigger(),
AutoComplete: true,
AutoCompleteDesc: T("api.command_remote.desc"),
AutoCompleteHint: T("api.command_remote.hint"),
DisplayName: T("api.command_remote.name"),
AutocompleteData: remote,
}
}
func (rp *RemoteProvider) DoCommand(a *app.App, rctx request.CTX, args *model.CommandArgs, message string) *model.CommandResponse {
if !a.HasPermissionTo(args.UserId, model.PermissionManageSecureConnections) {
return response(args.T("api.command_remote.permission_required", map[string]any{"Permission": "manage_secure_connections"}))
}
margs := parseNamedArgs(args.Command)
action, ok := margs[ActionKey]
if !ok {
return response(args.T("api.command_remote.missing_command", map[string]any{"Actions": AvailableRemoteActions}))
}
switch action {
case "create":
return rp.doCreate(a, args, margs)
case "accept":
return rp.doAccept(a, args, margs)
case "remove":
return rp.doRemove(a, args, margs)
case "status":
return rp.doStatus(a, args, margs)
}
return response(args.T("api.command_remote.unknown_action", map[string]any{"Action": action}))
}
func (rp *RemoteProvider) GetAutoCompleteListItems(rctx request.CTX, a *app.App, commandArgs *model.CommandArgs, arg *model.AutocompleteArg, parsed, toBeParsed string) ([]model.AutocompleteListItem, error) {
if !a.HasPermissionTo(commandArgs.UserId, model.PermissionManageSecureConnections) {
return nil, errors.New("You require `manage_secure_connections` permission to manage secure connections.")
}
if arg.Name == "connectionID" && strings.Contains(parsed, " remove ") {
return getRemoteClusterAutocompleteListItems(a, true)
}
return nil, fmt.Errorf("`%s` is not a dynamic argument", arg.Name)
}
// doCreate creates and displays an encrypted invite that can be used by a remote site to establish a simple trust.
func (rp *RemoteProvider) doCreate(a *app.App, args *model.CommandArgs, margs map[string]string) *model.CommandResponse {
password := margs["password"]
if password == "" {
return response(args.T("api.command_remote.missing_empty", map[string]any{"Arg": "password"}))
}
name := margs["name"]
if name == "" {
return response(args.T("api.command_remote.missing_empty", map[string]any{"Arg": "name"}))
}
displayname := margs["displayname"]
if displayname == "" {
displayname = name
}
url := a.GetSiteURL()
if url == "" {
return response(args.T("api.command_remote.site_url_not_set"))
}
rc := &model.RemoteCluster{
Name: name,
DisplayName: displayname,
SiteURL: model.SiteURLPending + model.NewId(), // require a unique siteurl
Token: model.NewId(),
CreatorId: args.UserId,
}
rcSaved, appErr := a.AddRemoteCluster(rc)
if appErr != nil {
return response(args.T("api.command_remote.add_remote.error", map[string]any{"Error": appErr.Error()}))
}
// Display the encrypted invitation
inviteCode, err := a.CreateRemoteClusterInvite(rcSaved.RemoteId, url, rcSaved.Token, password)
if err != nil {
return response(args.T("api.command_remote.encrypt_invitation.error", map[string]any{"Error": err.Error()}))
}
return response("##### " + args.T("api.command_remote.invitation_created") + "\n" +
args.T("api.command_remote.invite_summary", map[string]any{"Command": "/secure-connection accept", "Invitation": inviteCode, "SiteURL": url}))
}
// doAccept accepts an invitation generated by a remote site.
func (rp *RemoteProvider) doAccept(a *app.App, args *model.CommandArgs, margs map[string]string) *model.CommandResponse {
password := margs["password"]
if password == "" {
return response(args.T("api.command_remote.missing_empty", map[string]any{"Arg": "password"}))
}
name := margs["name"]
if name == "" {
return response(args.T("api.command_remote.missing_empty", map[string]any{"Arg": "name"}))
}
displayname := margs["displayname"]
if displayname == "" {
displayname = name
}
blob := margs["invite"]
if blob == "" {
return response(args.T("api.command_remote.missing_empty", map[string]any{"Arg": "invite"}))
}
// invite is encoded as base64 and encrypted
invite, dErr := a.DecryptRemoteClusterInvite(blob, password)
if dErr != nil {
return response(args.T("api.command_remote.decode_invitation.error", map[string]any{"Error": dErr.Error()}))
}
rcs, _ := a.GetRemoteClusterService()
if rcs == nil {
return response(args.T("api.command_remote.service_not_enabled"))
}
url := a.GetSiteURL()
if url == "" {
return response(args.T("api.command_remote.site_url_not_set"))
}
rc, err := rcs.AcceptInvitation(invite, name, displayname, args.UserId, url, "")
if err != nil {
return response(args.T("api.command_remote.accept_invitation.error", map[string]any{"Error": err.Error()}))
}
return response("##### " + args.T("api.command_remote.accept_invitation", map[string]any{"SiteURL": rc.GetSiteURL()}))
}
// doRemove removes a remote cluster from the database, effectively revoking the trust relationship.
func (rp *RemoteProvider) doRemove(a *app.App, args *model.CommandArgs, margs map[string]string) *model.CommandResponse {
id, ok := margs["connectionID"]
if !ok {
return response(args.T("api.command_remote.missing_empty", map[string]any{"Arg": "remoteId"}))
}
deleted, err := a.DeleteRemoteCluster(id)
if err != nil {
response(args.T("api.command_remote.remove_remote.error", map[string]any{"Error": err.Error()}))
}
result := "removed"
if !deleted {
result = "**NOT FOUND**"
}
return response("##### " + args.T("api.command_remote.cluster_removed", map[string]any{"RemoteId": id, "Result": result}))
}
// doStatus displays connection status for all remote clusters.
func (rp *RemoteProvider) doStatus(a *app.App, args *model.CommandArgs, _ map[string]string) *model.CommandResponse {
list, err := a.GetAllRemoteClusters(0, 999999, model.RemoteClusterQueryFilter{IncludeDeleted: true})
if err != nil {
response(args.T("api.command_remote.fetch_status.error", map[string]any{"Error": err.Error()}))
}
if len(list) == 0 {
return response("** " + args.T("api.command_remote.remotes_not_found") + " **")
}
var sb strings.Builder
fmt.Fprintf(&sb, "%s \n", args.T("api.command_remote.remote_table_header"))
// | Secure Connection | Display name | ConnectionID | Site URL | Default Team | Invite accepted | Online | Last ping | Deleted |
fmt.Fprintf(&sb, "| :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | | :---- |\n")
for _, rc := range list {
accepted := formatBool(args.T, rc.IsConfirmed())
online := formatBool(args.T, isOnline(rc.LastPingAt))
lastPing := formatTimestamp(rc.LastPingAt)
deleted := formatBool(args.T, rc.DeleteAt != 0)
fmt.Fprintf(&sb, "| %s | %s | %s | %s | %s | %s | %s | %s | %s |\n", rc.Name, rc.DisplayName, rc.RemoteId, rc.GetSiteURL(), rc.DefaultTeamId, accepted, online, lastPing, deleted)
}
return response(sb.String())
}
func isOnline(lastPing int64) bool {
return lastPing > model.GetMillis()-model.RemoteOfflineAfterMillis
}
func getRemoteClusterAutocompleteListItems(a *app.App, includeOffline bool) ([]model.AutocompleteListItem, error) {
filter := model.RemoteClusterQueryFilter{
ExcludeOffline: !includeOffline,
}
clusters, err := a.GetAllRemoteClusters(0, 999999, filter)
if err != nil || len(clusters) == 0 {
return []model.AutocompleteListItem{}, nil
}
list := make([]model.AutocompleteListItem, 0, len(clusters))
for _, rc := range clusters {
item := model.AutocompleteListItem{
Item: rc.RemoteId,
HelpText: fmt.Sprintf("%s (%s)", rc.DisplayName, rc.GetSiteURL())}
list = append(list, item)
}
return list, nil
}
func getRemoteClusterAutocompleteListItemsNotInChannel(a *app.App, channelID string, includeOffline bool) ([]model.AutocompleteListItem, error) {
filter := model.RemoteClusterQueryFilter{
ExcludeOffline: !includeOffline,
NotInChannel: channelID,
}
all, err := a.GetAllRemoteClusters(0, 999999, filter)
if err != nil || len(all) == 0 {
return []model.AutocompleteListItem{}, nil
}
list := make([]model.AutocompleteListItem, 0, len(all))
for _, rc := range all {
item := model.AutocompleteListItem{
Item: rc.RemoteId,
HelpText: fmt.Sprintf("%s (%s)", rc.DisplayName, rc.GetSiteURL())}
list = append(list, item)
}
return list, nil
}