Doug Lauder
2023-03-22 17:22:27 -04:00
коммит произвёл GitHub
родитель b61c096497
Коммит c943ed6859
13276 изменённых файлов: 1695615 добавлений и 223189 удалений

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

@@ -0,0 +1,79 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
)
type AutoChannelCreator struct {
a *app.App
userID string
team *model.Team
Fuzzy bool
DisplayNameLen utils.Range
DisplayNameCharset string
NameLen utils.Range
NameCharset string
ChannelType model.ChannelType
CreateTime int64
}
func NewAutoChannelCreator(a *app.App, team *model.Team, userID string) *AutoChannelCreator {
return &AutoChannelCreator{
a: a,
team: team,
userID: userID,
Fuzzy: false,
DisplayNameLen: ChannelDisplayNameLen,
DisplayNameCharset: utils.ALPHANUMERIC,
NameLen: ChannelNameLen,
NameCharset: utils.LOWERCASE,
ChannelType: ChannelType,
CreateTime: 0,
}
}
func (cfg *AutoChannelCreator) createRandomChannel(c request.CTX) (*model.Channel, error) {
var displayName string
if cfg.Fuzzy {
displayName = utils.FuzzName()
} else {
displayName = utils.RandomName(cfg.NameLen, cfg.NameCharset)
}
name := utils.RandomName(cfg.NameLen, cfg.NameCharset)
channel := &model.Channel{
TeamId: cfg.team.Id,
DisplayName: displayName,
Name: name,
Type: cfg.ChannelType,
CreatorId: cfg.userID,
CreateAt: cfg.CreateTime,
}
channel, err := cfg.a.CreateChannel(c, channel, true)
if err != nil {
return nil, err
}
return channel, nil
}
func (cfg *AutoChannelCreator) CreateTestChannels(c request.CTX, num utils.Range) ([]*model.Channel, error) {
numChannels := utils.RandIntFromRange(num)
channels := make([]*model.Channel, numChannels)
for i := 0; i < numChannels; i++ {
var err error
channels[i], err = cfg.createRandomChannel(c)
if err != nil {
return nil, err
}
}
return channels, nil
}

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

@@ -0,0 +1,32 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
)
const (
UserPassword = "Usr@MMTest123"
ChannelType = model.ChannelTypeOpen
BTestTeamDisplayName = "TestTeam"
BTestTeamName = "z-z-testdomaina"
BTestTeamEmail = "test@nowhere.com"
BTestTeamType = model.TeamOpen
BTestUserName = "Mr. Testing Tester"
BTestUserEmail = "success+ttester@simulator.amazonses.com"
BTestUserPassword = "passwd"
)
var (
TeamNameLen = utils.Range{Begin: 10, End: 20}
TeamDomainNameLen = utils.Range{Begin: 10, End: 20}
TeamEmailLen = utils.Range{Begin: 15, End: 30}
UserNameLen = utils.Range{Begin: 5, End: 20}
UserEmailLen = utils.Range{Begin: 15, End: 30}
ChannelDisplayNameLen = utils.Range{Begin: 10, End: 20}
ChannelNameLen = utils.Range{Begin: 5, End: 20}
TestImageFileNames = []string{"test.png", "testjpg.jpg", "testgif.gif"}
)

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

@@ -0,0 +1,114 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"math/rand"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
)
type TestEnvironment struct {
Teams []*model.Team
Environments []TeamEnvironment
}
func CreateTestEnvironmentWithTeams(a *app.App, c request.CTX, client *model.Client4, rangeTeams utils.Range, rangeChannels utils.Range, rangeUsers utils.Range, rangePosts utils.Range, fuzzy bool) (TestEnvironment, error) {
rand.Seed(time.Now().UTC().UnixNano())
teamCreator := NewAutoTeamCreator(client)
teamCreator.Fuzzy = fuzzy
teams, err := teamCreator.CreateTestTeams(rangeTeams)
if err != nil {
return TestEnvironment{}, err
}
environment := TestEnvironment{teams, make([]TeamEnvironment, len(teams))}
for i, team := range teams {
userCreator := NewAutoUserCreator(a, client, team)
userCreator.Fuzzy = fuzzy
randomUser, err := userCreator.createRandomUser(c)
if err != nil {
return TestEnvironment{}, err
}
client.LoginById(randomUser.Id, UserPassword)
teamEnvironment, err := CreateTestEnvironmentInTeam(a, c, client, team, rangeChannels, rangeUsers, rangePosts, fuzzy)
if err != nil {
return TestEnvironment{}, err
}
environment.Environments[i] = teamEnvironment
}
return environment, nil
}
func CreateTestEnvironmentInTeam(a *app.App, c request.CTX, client *model.Client4, team *model.Team, rangeChannels utils.Range, rangeUsers utils.Range, rangePosts utils.Range, fuzzy bool) (TeamEnvironment, error) {
rand.Seed(time.Now().UTC().UnixNano())
// We need to create at least one user
if rangeUsers.Begin <= 0 {
rangeUsers.Begin = 1
}
userCreator := NewAutoUserCreator(a, client, team)
userCreator.Fuzzy = fuzzy
users, err := userCreator.CreateTestUsers(c, rangeUsers)
if err != nil {
return TeamEnvironment{}, nil
}
usernames := make([]string, len(users))
for i, user := range users {
usernames[i] = user.Username
}
channelCreator := NewAutoChannelCreator(a, team, users[0].Id)
channelCreator.Fuzzy = fuzzy
channels, err := channelCreator.CreateTestChannels(c, rangeChannels)
if err != nil {
return TeamEnvironment{}, nil
}
// Have every user join every channel
for _, user := range users {
for _, channel := range channels {
_, _, err := client.LoginById(user.Id, UserPassword)
if err != nil {
return TeamEnvironment{}, err
}
_, _, err = client.AddChannelMember(channel.Id, user.Id)
if err != nil {
return TeamEnvironment{}, err
}
}
}
numPosts := utils.RandIntFromRange(rangePosts)
numImages := utils.RandIntFromRange(rangePosts) / 4
for j := 0; j < numPosts; j++ {
user := users[utils.RandIntFromRange(utils.Range{Begin: 0, End: len(users) - 1})]
_, _, err := client.LoginById(user.Id, UserPassword)
if err != nil {
return TeamEnvironment{}, err
}
for i, channel := range channels {
postCreator := NewAutoPostCreator(a, channel.Id, user.Id)
postCreator.HasImage = i < numImages
postCreator.Users = usernames
postCreator.Fuzzy = fuzzy
_, err := postCreator.CreateRandomPost(c)
if err != nil {
return TeamEnvironment{}, err
}
}
}
return TeamEnvironment{users, channels}, nil
}

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

@@ -0,0 +1,125 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"bytes"
"io"
"os"
"path/filepath"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/mattermost/mattermost-server/v6/server/channels/utils/fileutils"
)
type AutoPostCreator struct {
a *app.App
channelid string
userid string
Fuzzy bool
TextLength utils.Range
HasImage bool
ImageFilenames []string
Users []string
UsersToPostFrom []string
Mentions utils.Range
Tags utils.Range
CreateTime int64
postsCreated int
}
// Automatic poster used for testing
func NewAutoPostCreator(a *app.App, channelid, userid string) *AutoPostCreator {
return &AutoPostCreator{
a: a,
channelid: channelid,
userid: userid,
Fuzzy: false,
TextLength: utils.Range{Begin: 100, End: 200},
HasImage: false,
ImageFilenames: TestImageFileNames,
Users: []string{},
UsersToPostFrom: []string{},
Mentions: utils.Range{Begin: 0, End: 5},
Tags: utils.Range{Begin: 0, End: 7},
CreateTime: 0,
postsCreated: 0,
}
}
func (cfg *AutoPostCreator) UploadTestFile(c request.CTX) ([]string, error) {
filename := cfg.ImageFilenames[utils.RandIntFromRange(utils.Range{Begin: 0, End: len(cfg.ImageFilenames) - 1})]
path, _ := fileutils.FindDir("tests")
file, err := os.Open(filepath.Join(path, filename))
if err != nil {
return nil, err
}
defer file.Close()
data := &bytes.Buffer{}
_, err = io.Copy(data, file)
if err != nil {
return nil, err
}
fileResp, err2 := cfg.a.UploadFile(c, data.Bytes(), cfg.channelid, filename)
if err2 != nil {
return nil, err2
}
return []string{fileResp.Id}, nil
}
func (cfg *AutoPostCreator) CreateRandomPost(c request.CTX) (*model.Post, error) {
return cfg.CreateRandomPostNested(c, "")
}
func (cfg *AutoPostCreator) CreateRandomPostNested(c request.CTX, rootId string) (*model.Post, error) {
var fileIDs []string
if cfg.HasImage {
var err error
fileIDs, err = cfg.UploadTestFile(c)
if err != nil {
return nil, err
}
}
var postText string
if cfg.Fuzzy {
postText = utils.FuzzPost()
} else {
postText = utils.RandomText(cfg.TextLength, cfg.Tags, cfg.Mentions, cfg.Users)
}
post := &model.Post{
ChannelId: cfg.channelid,
UserId: cfg.userid,
RootId: rootId,
Message: postText,
FileIds: fileIDs,
}
if cfg.CreateTime != 0 {
// Creating posts with the exact same timestamp results in some posts being skipped
// when they are retrieved by the API based on timestamp.
post.CreateAt = cfg.CreateTime + int64(cfg.postsCreated)
}
if len(cfg.UsersToPostFrom) != 0 {
i := utils.RandIntFromRange(utils.Range{Begin: 0, End: len(cfg.UsersToPostFrom)})
if i < len(cfg.UsersToPostFrom) {
post.UserId = cfg.UsersToPostFrom[i]
}
}
rpost, err := cfg.a.CreatePostMissingChannel(c, post, true, true)
if err != nil {
return nil, err
}
cfg.postsCreated += 1
return rpost, nil
}

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

@@ -0,0 +1,80 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
)
type TeamEnvironment struct {
Users []*model.User
Channels []*model.Channel
}
type AutoTeamCreator struct {
client *model.Client4
Fuzzy bool
NameLength utils.Range
NameCharset string
DomainLength utils.Range
DomainCharset string
EmailLength utils.Range
EmailCharset string
}
func NewAutoTeamCreator(client *model.Client4) *AutoTeamCreator {
return &AutoTeamCreator{
client: client,
Fuzzy: false,
NameLength: TeamNameLen,
NameCharset: utils.LOWERCASE,
DomainLength: TeamDomainNameLen,
DomainCharset: utils.LOWERCASE,
EmailLength: TeamEmailLen,
EmailCharset: utils.LOWERCASE,
}
}
func (cfg *AutoTeamCreator) createRandomTeam() (*model.Team, error) {
var teamEmail string
var teamDisplayName string
var teamName string
if cfg.Fuzzy {
teamEmail = "success+" + model.NewId() + "simulator.amazonses.com"
teamDisplayName = utils.FuzzName()
teamName = model.NewRandomTeamName()
} else {
teamEmail = "success+" + model.NewId() + "simulator.amazonses.com"
teamDisplayName = utils.RandomName(cfg.NameLength, cfg.NameCharset)
teamName = utils.RandomName(cfg.NameLength, cfg.NameCharset) + model.NewId()
}
team := &model.Team{
DisplayName: teamDisplayName,
Name: teamName,
Email: teamEmail,
Type: model.TeamOpen,
}
createdTeam, _, err := cfg.client.CreateTeam(team)
if err != nil {
return nil, err
}
return createdTeam, nil
}
func (cfg *AutoTeamCreator) CreateTestTeams(num utils.Range) ([]*model.Team, error) {
numTeams := utils.RandIntFromRange(num)
teams := make([]*model.Team, numTeams)
for i := 0; i < numTeams; i++ {
var err error
teams[i], err = cfg.createRandomTeam()
if err != nil {
return nil, err
}
}
return teams, nil
}

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

@@ -0,0 +1,151 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"errors"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
)
type AutoUserCreator struct {
app *app.App
client *model.Client4
team *model.Team
EmailLength utils.Range
EmailCharset string
NameLength utils.Range
NameCharset string
Fuzzy bool
JoinTime int64
}
func NewAutoUserCreator(a *app.App, client *model.Client4, team *model.Team) *AutoUserCreator {
return &AutoUserCreator{
app: a,
client: client,
team: team,
EmailLength: UserEmailLen,
EmailCharset: utils.LOWERCASE,
NameLength: UserNameLen,
NameCharset: utils.LOWERCASE,
Fuzzy: false,
JoinTime: 0,
}
}
// Basic test team and user so you always know one
func CreateBasicUser(a *app.App, client *model.Client4) error {
found, _, _ := client.TeamExists(BTestTeamName, "")
if found {
return nil
}
newteam := &model.Team{DisplayName: BTestTeamDisplayName, Name: BTestTeamName, Email: BTestTeamEmail, Type: BTestTeamType}
basicteam, _, err := client.CreateTeam(newteam)
if err != nil {
return err
}
newuser := &model.User{Email: BTestUserEmail, Nickname: BTestUserName, Password: BTestUserPassword}
ruser, _, err := client.CreateUser(newuser)
if err != nil {
return err
}
_, err = a.Srv().Store().User().VerifyEmail(ruser.Id, ruser.Email)
if err != nil {
return model.NewAppError("CreateBasicUser", "app.user.verify_email.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if _, nErr := a.Srv().Store().Team().SaveMember(&model.TeamMember{TeamId: basicteam.Id, UserId: ruser.Id, CreateAt: model.GetMillis()}, *a.Config().TeamSettings.MaxUsersPerTeam); nErr != nil {
var appErr *model.AppError
var conflictErr *store.ErrConflict
var limitExceededErr *store.ErrLimitExceeded
switch {
case errors.As(nErr, &appErr): // in case we haven't converted to plain error.
return appErr
case errors.As(nErr, &conflictErr):
return model.NewAppError("CreateBasicUser", "app.create_basic_user.save_member.conflict.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
case errors.As(nErr, &limitExceededErr):
return model.NewAppError("CreateBasicUser", "app.create_basic_user.save_member.max_accounts.app_error", nil, "", http.StatusBadRequest).Wrap(nErr)
default: // last fallback in case it doesn't map to an existing app error.
return model.NewAppError("CreateBasicUser", "app.create_basic_user.save_member.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
return nil
}
func (cfg *AutoUserCreator) createRandomUser(c request.CTX) (*model.User, error) {
var userEmail string
var userName string
if cfg.Fuzzy {
userEmail = "success+" + model.NewId() + "@simulator.amazonses.com"
userName = utils.FuzzName()
} else {
userEmail = "success+" + model.NewId() + "@simulator.amazonses.com"
userName = utils.RandomName(cfg.NameLength, cfg.NameCharset)
}
user := &model.User{
Email: userEmail,
Nickname: userName,
Password: UserPassword,
CreateAt: cfg.JoinTime,
}
ruser, appErr := cfg.app.CreateUserWithInviteId(c, user, cfg.team.InviteId, "")
if appErr != nil {
return nil, appErr
}
status := &model.Status{
UserId: ruser.Id,
Status: model.StatusOnline,
Manual: false,
LastActivityAt: ruser.CreateAt,
ActiveChannel: "",
}
if err := cfg.app.Srv().Store().Status().SaveOrUpdate(status); err != nil {
return nil, err
}
// We need to cheat to verify the user's email
_, err := cfg.app.Srv().Store().User().VerifyEmail(ruser.Id, ruser.Email)
if err != nil {
return nil, err
}
if cfg.JoinTime != 0 {
teamMember, appErr := cfg.app.GetTeamMember(cfg.team.Id, ruser.Id)
if appErr != nil {
return nil, appErr
}
teamMember.CreateAt = cfg.JoinTime
_, err := cfg.app.Srv().Store().Team().UpdateMember(teamMember)
if err != nil {
return nil, err
}
}
return ruser, nil
}
func (cfg *AutoUserCreator) CreateTestUsers(c request.CTX, num utils.Range) ([]*model.User, error) {
numUsers := utils.RandIntFromRange(num)
users := make([]*model.User, numUsers)
for i := 0; i < numUsers; i++ {
var err error
users[i], err = cfg.createRandomUser(c)
if err != nil {
return nil, err
}
}
return users, nil
}

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

@@ -0,0 +1,41 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
)
type AwayProvider struct {
}
const (
CmdAway = "away"
)
func init() {
app.RegisterCommandProvider(&AwayProvider{})
}
func (*AwayProvider) GetTrigger() string {
return CmdAway
}
func (*AwayProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model.Command {
return &model.Command{
Trigger: CmdAway,
AutoComplete: true,
AutoCompleteDesc: T("api.command_away.desc"),
DisplayName: T("api.command_away.name"),
}
}
func (*AwayProvider) DoCommand(a *app.App, _ request.CTX, args *model.CommandArgs, message string) *model.CommandResponse {
a.SetStatusAwayIfNeeded(args.UserId, true)
return &model.CommandResponse{ResponseType: model.CommandResponseTypeEphemeral, Text: args.T("api.command_away.success")}
}

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

@@ -0,0 +1,110 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
)
type HeaderProvider struct {
}
const (
CmdHeader = "header"
)
func init() {
app.RegisterCommandProvider(&HeaderProvider{})
}
func (*HeaderProvider) GetTrigger() string {
return CmdHeader
}
func (*HeaderProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model.Command {
return &model.Command{
Trigger: CmdHeader,
AutoComplete: true,
AutoCompleteDesc: T("api.command_channel_header.desc"),
AutoCompleteHint: T("api.command_channel_header.hint"),
DisplayName: T("api.command_channel_header.name"),
}
}
func (*HeaderProvider) DoCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) *model.CommandResponse {
channel, err := a.GetChannel(c, args.ChannelId)
if err != nil {
return &model.CommandResponse{
Text: args.T("api.command_channel_header.channel.app_error"),
ResponseType: model.CommandResponseTypeEphemeral,
}
}
switch channel.Type {
case model.ChannelTypeOpen:
if !a.HasPermissionToChannel(c, args.UserId, args.ChannelId, model.PermissionManagePublicChannelProperties) {
return &model.CommandResponse{
Text: args.T("api.command_channel_header.permission.app_error"),
ResponseType: model.CommandResponseTypeEphemeral,
}
}
case model.ChannelTypePrivate:
if !a.HasPermissionToChannel(c, args.UserId, args.ChannelId, model.PermissionManagePrivateChannelProperties) {
return &model.CommandResponse{
Text: args.T("api.command_channel_header.permission.app_error"),
ResponseType: model.CommandResponseTypeEphemeral,
}
}
case model.ChannelTypeGroup, model.ChannelTypeDirect:
// Modifying the header is not linked to any specific permission for group/dm channels, so just check for membership.
var channelMember *model.ChannelMember
channelMember, err = a.GetChannelMember(c, args.ChannelId, args.UserId)
if err != nil || channelMember == nil {
return &model.CommandResponse{
Text: args.T("api.command_channel_header.permission.app_error"),
ResponseType: model.CommandResponseTypeEphemeral,
}
}
default:
return &model.CommandResponse{
Text: args.T("api.command_channel_header.permission.app_error"),
ResponseType: model.CommandResponseTypeEphemeral,
}
}
if message == "" {
return &model.CommandResponse{
Text: args.T("api.command_channel_header.message.app_error"),
ResponseType: model.CommandResponseTypeEphemeral,
}
}
patch := &model.ChannelPatch{
Header: new(string),
}
*patch.Header = message
_, err = a.PatchChannel(c, channel, patch, args.UserId)
if err != nil {
text := args.T("api.command_channel_header.update_channel.app_error")
if err.Id == "model.channel.is_valid.header.app_error" {
text = args.T("api.command_channel_header.update_channel.max_length", map[string]any{
"MaxLength": model.ChannelHeaderMaxRunes,
})
}
return &model.CommandResponse{
Text: text,
ResponseType: model.CommandResponseTypeEphemeral,
}
}
return &model.CommandResponse{}
}

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

@@ -0,0 +1,122 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/mattermost/mattermost-server/v6/model"
)
func TestHeaderProviderDoCommand(t *testing.T) {
th := setup(t).initBasic()
defer th.tearDown()
hp := HeaderProvider{}
th.addPermissionToRole(model.PermissionManagePublicChannelProperties.Id, model.ChannelUserRoleId)
// Try a public channel *with* permission.
args := &model.CommandArgs{
T: func(s string, args ...any) string { return s },
ChannelId: th.BasicChannel.Id,
UserId: th.BasicUser.Id,
}
for msg, expected := range map[string]string{
"": "api.command_channel_header.message.app_error",
"hello": "",
} {
actual := hp.DoCommand(th.App, th.Context, args, msg).Text
assert.Equal(t, expected, actual)
}
th.removePermissionFromRole(model.PermissionManagePublicChannelProperties.Id, model.ChannelUserRoleId)
// Try a public channel *without* permission.
args = &model.CommandArgs{
T: func(s string, args ...any) string { return s },
ChannelId: th.BasicChannel.Id,
UserId: th.BasicUser.Id,
}
actual := hp.DoCommand(th.App, th.Context, args, "hello").Text
assert.Equal(t, "api.command_channel_header.permission.app_error", actual)
th.addPermissionToRole(model.PermissionManagePrivateChannelProperties.Id, model.ChannelUserRoleId)
// Try a private channel *with* permission.
privateChannel := th.createPrivateChannel(th.BasicTeam)
args = &model.CommandArgs{
T: func(s string, args ...any) string { return s },
ChannelId: privateChannel.Id,
UserId: th.BasicUser.Id,
}
actual = hp.DoCommand(th.App, th.Context, args, "hello").Text
assert.Equal(t, "", actual)
th.removePermissionFromRole(model.PermissionManagePrivateChannelProperties.Id, model.ChannelUserRoleId)
// Try a private channel *without* permission.
args = &model.CommandArgs{
T: func(s string, args ...any) string { return s },
ChannelId: privateChannel.Id,
UserId: th.BasicUser.Id,
}
actual = hp.DoCommand(th.App, th.Context, args, "hello").Text
assert.Equal(t, "api.command_channel_header.permission.app_error", actual)
// Try a group channel *with* being a member.
user1 := th.createUser()
user2 := th.createUser()
user3 := th.createUser()
groupChannel := th.createGroupChannel(user1, user2)
args = &model.CommandArgs{
T: func(s string, args ...any) string { return s },
ChannelId: groupChannel.Id,
UserId: user1.Id,
}
actual = hp.DoCommand(th.App, th.Context, args, "hello").Text
assert.Equal(t, "", actual)
// Try a group channel *without* being a member.
args = &model.CommandArgs{
T: func(s string, args ...any) string { return s },
ChannelId: groupChannel.Id,
UserId: user3.Id,
}
actual = hp.DoCommand(th.App, th.Context, args, "hello").Text
assert.Equal(t, "api.command_channel_header.permission.app_error", actual)
// Try a direct channel *with* being a member.
directChannel := th.createDmChannel(user1)
args = &model.CommandArgs{
T: func(s string, args ...any) string { return s },
ChannelId: directChannel.Id,
UserId: th.BasicUser.Id,
}
actual = hp.DoCommand(th.App, th.Context, args, "hello").Text
assert.Equal(t, "", actual)
// Try a direct channel *without* being a member.
args = &model.CommandArgs{
T: func(s string, args ...any) string { return s },
ChannelId: directChannel.Id,
UserId: user2.Id,
}
actual = hp.DoCommand(th.App, th.Context, args, "hello").Text
assert.Equal(t, "api.command_channel_header.permission.app_error", actual)
}

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

@@ -0,0 +1,97 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
)
type PurposeProvider struct {
}
const (
CmdPurpose = "purpose"
)
func init() {
app.RegisterCommandProvider(&PurposeProvider{})
}
func (*PurposeProvider) GetTrigger() string {
return CmdPurpose
}
func (*PurposeProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model.Command {
return &model.Command{
Trigger: CmdPurpose,
AutoComplete: true,
AutoCompleteDesc: T("api.command_channel_purpose.desc"),
AutoCompleteHint: T("api.command_channel_purpose.hint"),
DisplayName: T("api.command_channel_purpose.name"),
}
}
func (*PurposeProvider) DoCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) *model.CommandResponse {
channel, err := a.GetChannel(c, args.ChannelId)
if err != nil {
return &model.CommandResponse{
Text: args.T("api.command_channel_purpose.channel.app_error"),
ResponseType: model.CommandResponseTypeEphemeral,
}
}
switch channel.Type {
case model.ChannelTypeOpen:
if !a.HasPermissionToChannel(c, args.UserId, args.ChannelId, model.PermissionManagePublicChannelProperties) {
return &model.CommandResponse{
Text: args.T("api.command_channel_purpose.permission.app_error"),
ResponseType: model.CommandResponseTypeEphemeral,
}
}
case model.ChannelTypePrivate:
if !a.HasPermissionToChannel(c, args.UserId, args.ChannelId, model.PermissionManagePrivateChannelProperties) {
return &model.CommandResponse{
Text: args.T("api.command_channel_purpose.permission.app_error"),
ResponseType: model.CommandResponseTypeEphemeral,
}
}
default:
return &model.CommandResponse{
Text: args.T("api.command_channel_purpose.direct_group.app_error"),
ResponseType: model.CommandResponseTypeEphemeral,
}
}
if message == "" {
return &model.CommandResponse{
Text: args.T("api.command_channel_purpose.message.app_error"),
ResponseType: model.CommandResponseTypeEphemeral,
}
}
patch := &model.ChannelPatch{
Purpose: new(string),
}
*patch.Purpose = message
_, err = a.PatchChannel(c, channel, patch, args.UserId)
if err != nil {
text := args.T("api.command_channel_purpose.update_channel.app_error")
if err.Id == "model.channel.is_valid.purpose.app_error" {
text = args.T("api.command_channel_purpose.update_channel.max_length", map[string]any{
"MaxLength": model.ChannelPurposeMaxRunes,
})
}
return &model.CommandResponse{
Text: text,
ResponseType: model.CommandResponseTypeEphemeral,
}
}
return &model.CommandResponse{}
}

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

@@ -0,0 +1,97 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/mattermost/mattermost-server/v6/model"
)
func TestPurposeProviderDoCommand(t *testing.T) {
th := setup(t).initBasic()
defer th.tearDown()
pp := PurposeProvider{}
// Try a public channel *with* permission.
th.addPermissionToRole(model.PermissionManagePublicChannelProperties.Id, model.ChannelUserRoleId)
args := &model.CommandArgs{
T: func(s string, args ...any) string { return s },
ChannelId: th.BasicChannel.Id,
UserId: th.BasicUser.Id,
}
for msg, expected := range map[string]string{
"": "api.command_channel_purpose.message.app_error",
"hello": "",
} {
actual := pp.DoCommand(th.App, th.Context, args, msg).Text
assert.Equal(t, expected, actual)
}
// Try a public channel *without* permission.
th.removePermissionFromRole(model.PermissionManagePublicChannelProperties.Id, model.ChannelUserRoleId)
args = &model.CommandArgs{
T: func(s string, args ...any) string { return s },
ChannelId: th.BasicChannel.Id,
}
actual := pp.DoCommand(th.App, th.Context, args, "hello").Text
assert.Equal(t, "api.command_channel_purpose.permission.app_error", actual)
// Try a private channel *with* permission.
privateChannel := th.createPrivateChannel(th.BasicTeam)
th.addPermissionToRole(model.PermissionManagePrivateChannelProperties.Id, model.ChannelUserRoleId)
args = &model.CommandArgs{
T: func(s string, args ...any) string { return s },
ChannelId: privateChannel.Id,
UserId: th.BasicUser.Id,
}
actual = pp.DoCommand(th.App, th.Context, args, "hello").Text
assert.Equal(t, "", actual)
// Try a private channel *without* permission.
th.removePermissionFromRole(model.PermissionManagePrivateChannelProperties.Id, model.ChannelUserRoleId)
args = &model.CommandArgs{
T: func(s string, args ...any) string { return s },
ChannelId: privateChannel.Id,
}
actual = pp.DoCommand(th.App, th.Context, args, "hello").Text
assert.Equal(t, "api.command_channel_purpose.permission.app_error", actual)
// Try a group channel *with* being a member.
user1 := th.createUser()
user2 := th.createUser()
groupChannel := th.createGroupChannel(user1, user2)
args = &model.CommandArgs{
T: func(s string, args ...any) string { return s },
ChannelId: groupChannel.Id,
}
actual = pp.DoCommand(th.App, th.Context, args, "hello").Text
assert.Equal(t, "api.command_channel_purpose.direct_group.app_error", actual)
// Try a direct channel *with* being a member.
directChannel := th.createDmChannel(user1)
args = &model.CommandArgs{
T: func(s string, args ...any) string { return s },
ChannelId: directChannel.Id,
}
actual = pp.DoCommand(th.App, th.Context, args, "hello").Text
assert.Equal(t, "api.command_channel_purpose.direct_group.app_error", actual)
}

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

@@ -0,0 +1,104 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
)
type RenameProvider struct {
}
const (
CmdRename = "rename"
)
func init() {
app.RegisterCommandProvider(&RenameProvider{})
}
func (*RenameProvider) GetTrigger() string {
return CmdRename
}
func (*RenameProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model.Command {
renameAutocompleteData := model.NewAutocompleteData(CmdRename, T("api.command_channel_rename.hint"), T("api.command_channel_rename.desc"))
renameAutocompleteData.AddTextArgument(T("api.command_channel_rename.hint"), "[text]", "")
return &model.Command{
Trigger: CmdRename,
AutoComplete: true,
AutoCompleteDesc: T("api.command_channel_rename.desc"),
AutoCompleteHint: T("api.command_channel_rename.hint"),
DisplayName: T("api.command_channel_rename.name"),
AutocompleteData: renameAutocompleteData,
}
}
func (*RenameProvider) DoCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) *model.CommandResponse {
channel, err := a.GetChannel(c, args.ChannelId)
if err != nil {
return &model.CommandResponse{
Text: args.T("api.command_channel_rename.channel.app_error"),
ResponseType: model.CommandResponseTypeEphemeral,
}
}
switch channel.Type {
case model.ChannelTypeOpen:
if !a.HasPermissionToChannel(c, args.UserId, args.ChannelId, model.PermissionManagePublicChannelProperties) {
return &model.CommandResponse{
Text: args.T("api.command_channel_rename.permission.app_error"),
ResponseType: model.CommandResponseTypeEphemeral,
}
}
case model.ChannelTypePrivate:
if !a.HasPermissionToChannel(c, args.UserId, args.ChannelId, model.PermissionManagePrivateChannelProperties) {
return &model.CommandResponse{
Text: args.T("api.command_channel_rename.permission.app_error"),
ResponseType: model.CommandResponseTypeEphemeral,
}
}
default:
return &model.CommandResponse{Text: args.T("api.command_channel_rename.direct_group.app_error"), ResponseType: model.CommandResponseTypeEphemeral}
}
if message == "" {
return &model.CommandResponse{
Text: args.T("api.command_channel_rename.message.app_error"),
ResponseType: model.CommandResponseTypeEphemeral,
}
} else if len(message) > model.ChannelNameMaxLength {
return &model.CommandResponse{
Text: args.T("api.command_channel_rename.too_long.app_error", map[string]any{
"Length": model.ChannelNameMaxLength,
}),
ResponseType: model.CommandResponseTypeEphemeral,
}
} else if len(message) < model.ChannelNameMinLength {
return &model.CommandResponse{
Text: args.T("api.command_channel_rename.too_short.app_error", map[string]any{
"Length": model.ChannelNameMinLength,
}),
ResponseType: model.CommandResponseTypeEphemeral,
}
}
patch := &model.ChannelPatch{
DisplayName: new(string),
}
*patch.DisplayName = message
_, err = a.PatchChannel(c, channel, patch, args.UserId)
if err != nil {
return &model.CommandResponse{
Text: args.T("api.command_channel_rename.update_channel.app_error"),
ResponseType: model.CommandResponseTypeEphemeral,
}
}
return &model.CommandResponse{}
}

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

@@ -0,0 +1,104 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/mattermost/mattermost-server/v6/model"
)
func TestRenameProviderDoCommand(t *testing.T) {
th := setup(t).initBasic()
defer th.tearDown()
th.addPermissionToRole(model.PermissionManagePublicChannelProperties.Id, model.ChannelUserRoleId)
rp := RenameProvider{}
args := &model.CommandArgs{
T: func(s string, args ...any) string { return s },
ChannelId: th.BasicChannel.Id,
UserId: th.BasicUser.Id,
}
// Table Test for basic cases. Blank text in response indicates success
for msg, expected := range map[string]string{
"": "api.command_channel_rename.message.app_error",
"o": "",
"joram": "",
"More than 22 chars but less than 64": "",
strings.Repeat("12345", 13): "api.command_channel_rename.too_long.app_error",
} {
actual := rp.DoCommand(th.App, th.Context, args, msg).Text
assert.Equal(t, expected, actual)
}
// Try a public channel *without* permission.
th.removePermissionFromRole(model.PermissionManagePublicChannelProperties.Id, model.ChannelUserRoleId)
args = &model.CommandArgs{
T: func(s string, args ...any) string { return s },
ChannelId: th.BasicChannel.Id,
UserId: th.BasicUser.Id,
}
actual := rp.DoCommand(th.App, th.Context, args, "hello").Text
assert.Equal(t, "api.command_channel_rename.permission.app_error", actual)
// Try a private channel *with* permission.
privateChannel := th.createPrivateChannel(th.BasicTeam)
th.addPermissionToRole(model.PermissionManagePrivateChannelProperties.Id, model.ChannelUserRoleId)
args = &model.CommandArgs{
T: func(s string, args ...any) string { return s },
ChannelId: privateChannel.Id,
UserId: th.BasicUser.Id,
}
actual = rp.DoCommand(th.App, th.Context, args, "hello").Text
assert.Equal(t, "", actual)
// Try a private channel *without* permission.
th.removePermissionFromRole(model.PermissionManagePrivateChannelProperties.Id, model.ChannelUserRoleId)
args = &model.CommandArgs{
T: func(s string, args ...any) string { return s },
ChannelId: privateChannel.Id,
UserId: th.BasicUser.Id,
}
actual = rp.DoCommand(th.App, th.Context, args, "hello").Text
assert.Equal(t, "api.command_channel_rename.permission.app_error", actual)
// Try a group channel *with* being a member.
user1 := th.createUser()
user2 := th.createUser()
groupChannel := th.createGroupChannel(user1, user2)
args = &model.CommandArgs{
T: func(s string, args ...any) string { return s },
ChannelId: groupChannel.Id,
UserId: th.BasicUser.Id,
}
actual = rp.DoCommand(th.App, th.Context, args, "hello").Text
assert.Equal(t, "api.command_channel_rename.direct_group.app_error", actual)
// Try a direct channel *with* being a member.
directChannel := th.createDmChannel(user1)
args = &model.CommandArgs{
T: func(s string, args ...any) string { return s },
ChannelId: directChannel.Id,
UserId: th.BasicUser.Id,
}
actual = rp.DoCommand(th.App, th.Context, args, "hello").Text
assert.Equal(t, "api.command_channel_rename.direct_group.app_error", actual)
}

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

@@ -0,0 +1,46 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"strings"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
)
type CodeProvider struct {
}
const (
CmdCode = "code"
)
func init() {
app.RegisterCommandProvider(&CodeProvider{})
}
func (*CodeProvider) GetTrigger() string {
return CmdCode
}
func (*CodeProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model.Command {
return &model.Command{
Trigger: CmdCode,
AutoComplete: true,
AutoCompleteDesc: T("api.command_code.desc"),
AutoCompleteHint: T("api.command_code.hint"),
DisplayName: T("api.command_code.name"),
}
}
func (*CodeProvider) DoCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) *model.CommandResponse {
if message == "" {
return &model.CommandResponse{Text: args.T("api.command_code.message.app_error"), ResponseType: model.CommandResponseTypeEphemeral}
}
rmsg := " " + strings.Join(strings.Split(message, "\n"), "\n ")
return &model.CommandResponse{ResponseType: model.CommandResponseTypeInChannel, Text: rmsg, SkipSlackParsing: true}
}

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

@@ -0,0 +1,29 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"testing"
"github.com/mattermost/mattermost-server/v6/model"
)
func TestCodeProviderDoCommand(t *testing.T) {
cp := CodeProvider{}
args := &model.CommandArgs{
T: func(s string, args ...any) string { return s },
}
for msg, expected := range map[string]string{
"": "api.command_code.message.app_error",
"foo": " foo",
"foo\nbar": " foo\n bar",
"foo\nbar\n": " foo\n bar\n ",
} {
actual := cp.DoCommand(nil, nil, args, msg).Text
if actual != expected {
t.Errorf("expected `%v`, got `%v`", expected, actual)
}
}
}

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

@@ -0,0 +1,140 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"regexp"
"strings"
"unicode/utf8"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type CustomStatusProvider struct {
}
const (
CmdCustomStatus = app.CmdCustomStatusTrigger
CmdCustomStatusClear = "clear"
)
func init() {
app.RegisterCommandProvider(&CustomStatusProvider{})
}
func (*CustomStatusProvider) GetTrigger() string {
return CmdCustomStatus
}
func (*CustomStatusProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model.Command {
return &model.Command{
Trigger: CmdCustomStatus,
AutoComplete: true,
AutoCompleteDesc: T("api.command_custom_status.desc"),
AutoCompleteHint: T("api.command_custom_status.hint"),
DisplayName: T("api.command_custom_status.name"),
}
}
func (*CustomStatusProvider) DoCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) *model.CommandResponse {
if !*a.Config().TeamSettings.EnableCustomUserStatuses {
return nil
}
message = strings.TrimSpace(message)
if message == CmdCustomStatusClear {
if err := a.RemoveCustomStatus(c, args.UserId); err != nil {
mlog.Debug(err.Error())
return &model.CommandResponse{Text: args.T("api.command_custom_status.clear.app_error"), ResponseType: model.CommandResponseTypeEphemeral}
}
return &model.CommandResponse{
ResponseType: model.CommandResponseTypeEphemeral,
Text: args.T("api.command_custom_status.clear.success"),
}
}
customStatus := GetCustomStatus(message)
customStatus.PreSave()
if err := a.SetCustomStatus(c, args.UserId, customStatus); err != nil {
mlog.Debug(err.Error())
return &model.CommandResponse{Text: args.T("api.command_custom_status.app_error"), ResponseType: model.CommandResponseTypeEphemeral}
}
return &model.CommandResponse{
ResponseType: model.CommandResponseTypeEphemeral,
Text: args.T("api.command_custom_status.success", map[string]any{
"EmojiName": ":" + customStatus.Emoji + ":",
"StatusMessage": customStatus.Text,
}),
}
}
func GetCustomStatus(message string) *model.CustomStatus {
customStatus := &model.CustomStatus{
Emoji: model.DefaultCustomStatusEmoji,
Text: message,
}
firstEmojiLocations := model.EmojiPattern.FindIndex([]byte(message))
if len(firstEmojiLocations) > 0 && firstEmojiLocations[0] == 0 {
// emoji found at starting index
customStatus.Emoji = message[firstEmojiLocations[0]+1 : firstEmojiLocations[1]-1]
customStatus.Text = strings.TrimSpace(message[firstEmojiLocations[1]:])
return customStatus
}
if message == "" {
return customStatus
}
spaceSeparatedMessage := strings.Fields(message)
if len(spaceSeparatedMessage) == 0 {
return customStatus
}
emojiString := spaceSeparatedMessage[0]
var unicode []string
for utf8.RuneCountInString(emojiString) >= 1 {
codepoint, size := utf8.DecodeRuneInString(emojiString)
code := model.RuneToHexadecimalString(codepoint)
unicode = append(unicode, code)
emojiString = emojiString[size:]
}
unicodeString := removeUnicodeSkinTone(strings.Join(unicode, "-"))
emoji, count := model.GetEmojiNameFromUnicode(unicodeString)
if count > 0 {
customStatus.Emoji = emoji
textString := strings.Join(spaceSeparatedMessage[1:], " ")
customStatus.Text = strings.TrimSpace(textString)
}
return customStatus
}
func removeUnicodeSkinTone(unicodeString string) string {
skinToneDetectorRegex := regexp.MustCompile("-(1f3fb|1f3fc|1f3fd|1f3fe|1f3ff)")
skinToneLocations := skinToneDetectorRegex.FindIndex([]byte(unicodeString))
if len(skinToneLocations) == 0 {
return unicodeString
}
if _, count := model.GetEmojiNameFromUnicode(unicodeString); count > 0 {
return unicodeString
}
unicodeWithRemovedSkinTone := unicodeString[:skinToneLocations[0]] + unicodeString[skinToneLocations[1]:]
unicodeWithVariationSelector := unicodeString[:skinToneLocations[0]] + "-fe0f" + unicodeString[skinToneLocations[1]:]
if _, count := model.GetEmojiNameFromUnicode(unicodeWithRemovedSkinTone); count > 0 {
unicodeString = unicodeWithRemovedSkinTone
} else if _, count := model.GetEmojiNameFromUnicode(unicodeWithVariationSelector); count > 0 {
unicodeString = unicodeWithVariationSelector
}
return unicodeString
}

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

@@ -0,0 +1,34 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"testing"
"github.com/mattermost/mattermost-server/v6/model"
)
func TestGetCustomStatus(t *testing.T) {
for msg, expected := range map[string]model.CustomStatus{
"": {Emoji: model.DefaultCustomStatusEmoji, Text: ""},
"Hey": {Emoji: model.DefaultCustomStatusEmoji, Text: "Hey"},
":cactus: Hurt": {Emoji: "cactus", Text: "Hurt"},
"👅": {Emoji: "tongue", Text: ""},
"👅 Eating": {Emoji: "tongue", Text: "Eating"},
"💪🏻 Working out": {Emoji: "muscle_light_skin_tone", Text: "Working out"},
"👙 Swimming": {Emoji: "bikini", Text: "Swimming"},
"👙Swimming": {Emoji: model.DefaultCustomStatusEmoji, Text: "👙Swimming"},
"👍🏿 Okay": {Emoji: "+1_dark_skin_tone", Text: "Okay"},
"🤴🏾 Dark king": {Emoji: "prince_medium_dark_skin_tone", Text: "Dark king"},
"⛹🏾‍♀️ Playing basketball": {Emoji: "basketball_woman_medium_dark_skin_tone", Text: "Playing basketball"},
"🏋🏿‍♀️ Weightlifting": {Emoji: "weight_lifting_woman_dark_skin_tone", Text: "Weightlifting"},
"🏄 Surfing": {Emoji: "surfer", Text: "Surfing"},
"👨‍👨‍👦‍👦 Family": {Emoji: "family_man_man_boy_boy", Text: "Family"},
} {
actual := GetCustomStatus(msg)
if actual.Emoji != expected.Emoji || actual.Text != expected.Text {
t.Errorf("expected `%v`, got `%v`", expected, *actual)
}
}
}

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

@@ -0,0 +1,41 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
)
type DndProvider struct {
}
const (
CmdDND = "dnd"
)
func init() {
app.RegisterCommandProvider(&DndProvider{})
}
func (*DndProvider) GetTrigger() string {
return CmdDND
}
func (*DndProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model.Command {
return &model.Command{
Trigger: CmdDND,
AutoComplete: true,
AutoCompleteDesc: T("api.command_dnd.desc"),
DisplayName: T("api.command_dnd.name"),
}
}
func (*DndProvider) DoCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) *model.CommandResponse {
a.SetStatusDoNotDisturb(args.UserId)
return &model.CommandResponse{ResponseType: model.CommandResponseTypeEphemeral, Text: args.T("api.command_dnd.success")}
}

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

@@ -0,0 +1,98 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"strconv"
"strings"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
var echoSem chan bool
type EchoProvider struct {
}
const (
CmdEcho = "echo"
)
func init() {
app.RegisterCommandProvider(&EchoProvider{})
}
func (*EchoProvider) GetTrigger() string {
return CmdEcho
}
func (*EchoProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model.Command {
return &model.Command{
Trigger: CmdEcho,
AutoComplete: true,
AutoCompleteDesc: T("api.command_echo.desc"),
AutoCompleteHint: T("api.command_echo.hint"),
DisplayName: T("api.command_echo.name"),
}
}
func (*EchoProvider) DoCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) *model.CommandResponse {
if message == "" {
return &model.CommandResponse{Text: args.T("api.command_echo.message.app_error"), ResponseType: model.CommandResponseTypeEphemeral}
}
maxThreads := 100
delay := 0
if endMsg := strings.LastIndex(message, "\""); string(message[0]) == "\"" && endMsg > 1 {
if checkDelay, err := strconv.Atoi(strings.Trim(message[endMsg:], " \"")); err == nil {
delay = checkDelay
}
message = message[1:endMsg]
} else if strings.Contains(message, " ") {
delayIdx := strings.LastIndex(message, " ")
delayStr := strings.Trim(message[delayIdx:], " ")
if checkDelay, err := strconv.Atoi(delayStr); err == nil {
delay = checkDelay
message = message[:delayIdx]
}
}
if delay > 10000 {
return &model.CommandResponse{Text: args.T("api.command_echo.delay.app_error"), ResponseType: model.CommandResponseTypeEphemeral}
}
if echoSem == nil {
// We want one additional thread allowed so we never reach channel lockup
echoSem = make(chan bool, maxThreads+1)
}
if len(echoSem) >= maxThreads {
return &model.CommandResponse{Text: args.T("api.command_echo.high_volume.app_error"), ResponseType: model.CommandResponseTypeEphemeral}
}
echoSem <- true
a.Srv().Go(func() {
defer func() { <-echoSem }()
post := &model.Post{}
post.ChannelId = args.ChannelId
post.RootId = args.RootId
post.Message = message
post.UserId = args.UserId
time.Sleep(time.Duration(delay) * time.Second)
if _, err := a.CreatePostMissingChannel(c, post, true, true); err != nil {
mlog.Error("Unable to create /echo post.", mlog.Err(err))
}
})
return &model.CommandResponse{}
}

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

@@ -0,0 +1,95 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"encoding/json"
"strconv"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
)
type ExpandProvider struct {
}
type CollapseProvider struct {
}
const (
CmdExpand = "expand"
CmdCollapse = "collapse"
)
func init() {
app.RegisterCommandProvider(&ExpandProvider{})
app.RegisterCommandProvider(&CollapseProvider{})
}
func (*ExpandProvider) GetTrigger() string {
return CmdExpand
}
func (*CollapseProvider) GetTrigger() string {
return CmdCollapse
}
func (*ExpandProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model.Command {
return &model.Command{
Trigger: CmdExpand,
AutoComplete: true,
AutoCompleteDesc: T("api.command_expand.desc"),
DisplayName: T("api.command_expand.name"),
}
}
func (*CollapseProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model.Command {
return &model.Command{
Trigger: CmdCollapse,
AutoComplete: true,
AutoCompleteDesc: T("api.command_collapse.desc"),
DisplayName: T("api.command_collapse.name"),
}
}
func (*ExpandProvider) DoCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) *model.CommandResponse {
return setCollapsePreference(a, args, false)
}
func (*CollapseProvider) DoCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) *model.CommandResponse {
return setCollapsePreference(a, args, true)
}
func setCollapsePreference(a *app.App, args *model.CommandArgs, isCollapse bool) *model.CommandResponse {
pref := model.Preference{
UserId: args.UserId,
Category: model.PreferenceCategoryDisplaySettings,
Name: model.PreferenceNameCollapseSetting,
Value: strconv.FormatBool(isCollapse),
}
if err := a.Srv().Store().Preference().Save(model.Preferences{pref}); err != nil {
return &model.CommandResponse{Text: args.T("api.command_expand_collapse.fail.app_error") + err.Error(), ResponseType: model.CommandResponseTypeEphemeral}
}
socketMessage := model.NewWebSocketEvent(model.WebsocketEventPreferenceChanged, "", "", args.UserId, nil, "")
prefJSON, err := json.Marshal(pref)
if err != nil {
return &model.CommandResponse{Text: args.T("api.marshal_error") + err.Error(), ResponseType: model.CommandResponseTypeEphemeral}
}
socketMessage.Add("preference", string(prefJSON))
a.Publish(socketMessage)
var rmsg string
if isCollapse {
rmsg = args.T("api.command_collapse.success")
} else {
rmsg = args.T("api.command_expand.success")
}
return &model.CommandResponse{ResponseType: model.CommandResponseTypeEphemeral, Text: rmsg}
}

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

@@ -0,0 +1,160 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"fmt"
"strings"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type groupmsgProvider struct {
}
const (
CmdGroupMsg = "groupmsg"
)
func init() {
app.RegisterCommandProvider(&groupmsgProvider{})
}
func (*groupmsgProvider) GetTrigger() string {
return CmdGroupMsg
}
func (*groupmsgProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model.Command {
return &model.Command{
Trigger: CmdGroupMsg,
AutoComplete: true,
AutoCompleteDesc: T("api.command_groupmsg.desc"),
AutoCompleteHint: T("api.command_groupmsg.hint"),
DisplayName: T("api.command_groupmsg.name"),
}
}
func (*groupmsgProvider) DoCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) *model.CommandResponse {
targetUsers := map[string]*model.User{}
targetUsersSlice := []string{args.UserId}
invalidUsernames := []string{}
users, parsedMessage := groupMsgUsernames(message)
for _, username := range users {
username = strings.TrimSpace(username)
username = strings.TrimPrefix(username, "@")
targetUser, nErr := a.Srv().Store().User().GetByUsername(username)
if nErr != nil {
invalidUsernames = append(invalidUsernames, username)
continue
}
canSee, err := a.UserCanSeeOtherUser(args.UserId, targetUser.Id)
if err != nil {
return &model.CommandResponse{Text: args.T("api.command_groupmsg.fail.app_error"), ResponseType: model.CommandResponseTypeEphemeral}
}
if !canSee {
invalidUsernames = append(invalidUsernames, username)
continue
}
_, exists := targetUsers[targetUser.Id]
if !exists && targetUser.Id != args.UserId {
targetUsers[targetUser.Id] = targetUser
targetUsersSlice = append(targetUsersSlice, targetUser.Id)
}
}
if len(invalidUsernames) > 0 {
invalidUsersString := map[string]any{
"Users": "@" + strings.Join(invalidUsernames, ", @"),
}
return &model.CommandResponse{
Text: args.T("api.command_groupmsg.invalid_user.app_error", len(invalidUsernames), invalidUsersString),
ResponseType: model.CommandResponseTypeEphemeral,
}
}
if len(targetUsersSlice) == 2 {
return app.GetCommandProvider("msg").DoCommand(a, c, args, fmt.Sprintf("%s %s", targetUsers[targetUsersSlice[1]].Username, parsedMessage))
}
if len(targetUsersSlice) < model.ChannelGroupMinUsers {
minUsers := map[string]any{
"MinUsers": model.ChannelGroupMinUsers - 1,
}
return &model.CommandResponse{
Text: args.T("api.command_groupmsg.min_users.app_error", minUsers),
ResponseType: model.CommandResponseTypeEphemeral,
}
}
if len(targetUsersSlice) > model.ChannelGroupMaxUsers {
maxUsers := map[string]any{
"MaxUsers": model.ChannelGroupMaxUsers - 1,
}
return &model.CommandResponse{
Text: args.T("api.command_groupmsg.max_users.app_error", maxUsers),
ResponseType: model.CommandResponseTypeEphemeral,
}
}
var groupChannel *model.Channel
var channelErr *model.AppError
if a.HasPermissionTo(args.UserId, model.PermissionCreateGroupChannel) {
groupChannel, channelErr = a.CreateGroupChannel(c, targetUsersSlice, args.UserId)
if channelErr != nil {
mlog.Error(channelErr.Error())
return &model.CommandResponse{Text: args.T("api.command_groupmsg.group_fail.app_error"), ResponseType: model.CommandResponseTypeEphemeral}
}
} else {
groupChannel, channelErr = a.GetGroupChannel(c, targetUsersSlice)
if channelErr != nil {
return &model.CommandResponse{Text: args.T("api.command_groupmsg.permission.app_error"), ResponseType: model.CommandResponseTypeEphemeral}
}
}
if parsedMessage != "" {
post := &model.Post{}
post.Message = parsedMessage
post.ChannelId = groupChannel.Id
post.UserId = args.UserId
if _, err := a.CreatePostMissingChannel(c, post, true, true); err != nil {
return &model.CommandResponse{Text: args.T("api.command_groupmsg.fail.app_error"), ResponseType: model.CommandResponseTypeEphemeral}
}
}
team, err := a.GetTeam(args.TeamId)
if err != nil {
return &model.CommandResponse{Text: args.T("api.command_groupmsg.fail.app_error"), ResponseType: model.CommandResponseTypeEphemeral}
}
return &model.CommandResponse{GotoLocation: args.SiteURL + "/" + team.Name + "/channels/" + groupChannel.Name, Text: "", ResponseType: model.CommandResponseTypeEphemeral}
}
func groupMsgUsernames(message string) ([]string, string) {
result := []string{}
resultMessage := ""
for idx, part := range strings.Split(message, ",") {
clean := strings.TrimPrefix(strings.TrimSpace(part), "@")
split := strings.Fields(clean)
if len(split) > 0 {
result = append(result, split[0])
}
if len(split) > 1 {
splitted := strings.SplitN(message, ",", idx+1)
resultMessage = strings.TrimPrefix(strings.TrimSpace(splitted[len(splitted)-1]), "@")
resultMessage = strings.TrimSpace(strings.TrimPrefix(resultMessage, split[0]))
break
}
}
return result, resultMessage
}

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

@@ -0,0 +1,122 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
)
func TestGroupMsgUsernames(t *testing.T) {
assert := assert.New(t)
users, parsedMessage := groupMsgUsernames("")
assert.Len(users, 0)
assert.Empty(parsedMessage)
users, parsedMessage = groupMsgUsernames("test")
assert.Len(users, 1)
assert.Empty(parsedMessage)
users, parsedMessage = groupMsgUsernames("test1, test2, test3 , test4")
assert.Len(users, 4)
assert.Empty(parsedMessage)
users, parsedMessage = groupMsgUsernames("test1, test2 message with spaces")
assert.Len(users, 2)
assert.Equal(parsedMessage, "message with spaces", "error parsing message")
users, parsedMessage = groupMsgUsernames("test1, test2 message with, comma")
assert.Len(users, 2)
assert.Equal(parsedMessage, "message with, comma", "error parsing messages with comma")
users, parsedMessage = groupMsgUsernames("test1,,,test2")
assert.Len(users, 2)
assert.Empty(parsedMessage)
users, parsedMessage = groupMsgUsernames(" test1, test2 other message ")
assert.Len(users, 2)
assert.Equal(parsedMessage, "other message", "error parsing strange usage of spaces")
users, _ = groupMsgUsernames(" test1, test2,,123,@321,+123")
assert.Len(users, 5)
assert.Equal(users[0], "test1")
assert.Equal(users[1], "test2")
assert.Equal(users[2], "123")
assert.Equal(users[3], "321")
assert.Equal(users[4], "+123")
assert.Equal(parsedMessage, "other message", "error parsing different types of users")
}
func TestGroupMsgProvider(t *testing.T) {
th := setup(t).initBasic()
defer th.tearDown()
user3 := th.createUser()
targetUsers := "@" + th.BasicUser2.Username + ",@" + user3.Username + " "
team := th.createTeam()
th.linkUserToTeam(th.BasicUser, team)
cmd := &groupmsgProvider{}
th.removePermissionFromRole(model.PermissionCreateGroupChannel.Id, model.SystemUserRoleId)
t.Run("Check without permission to create a GM channel.", func(t *testing.T) {
resp := cmd.DoCommand(th.App, th.Context, &model.CommandArgs{
T: i18n.IdentityTfunc(),
SiteURL: "http://test.url",
TeamId: team.Id,
UserId: th.BasicUser.Id,
}, targetUsers+"hello")
assert.Equal(t, "api.command_groupmsg.permission.app_error", resp.Text)
assert.Equal(t, "", resp.GotoLocation)
})
th.addPermissionToRole(model.PermissionCreateGroupChannel.Id, model.SystemUserRoleId)
t.Run("Check without permissions to view a user in the list.", func(t *testing.T) {
th.removePermissionFromRole(model.PermissionViewMembers.Id, model.SystemUserRoleId)
defer th.addPermissionToRole(model.PermissionViewMembers.Id, model.SystemUserRoleId)
resp := cmd.DoCommand(th.App, th.Context, &model.CommandArgs{
T: i18n.IdentityTfunc(),
SiteURL: "http://test.url",
TeamId: team.Id,
UserId: th.BasicUser.Id,
}, targetUsers+"hello")
assert.Equal(t, "api.command_groupmsg.invalid_user.app_error", resp.Text)
assert.Equal(t, "", resp.GotoLocation)
})
t.Run("Check with permission to create a GM channel.", func(t *testing.T) {
resp := cmd.DoCommand(th.App, th.Context, &model.CommandArgs{
T: i18n.IdentityTfunc(),
SiteURL: "http://test.url",
TeamId: team.Id,
UserId: th.BasicUser.Id,
}, targetUsers+"hello")
channelName := model.GetGroupNameFromUserIds([]string{th.BasicUser.Id, th.BasicUser2.Id, user3.Id})
assert.Equal(t, "", resp.Text)
assert.Equal(t, "http://test.url/"+team.Name+"/channels/"+channelName, resp.GotoLocation)
})
t.Run("Check without permission to post to an existing GM channel.", func(t *testing.T) {
resp := cmd.DoCommand(th.App, th.Context, &model.CommandArgs{
T: i18n.IdentityTfunc(),
SiteURL: "http://test.url",
TeamId: team.Id,
UserId: th.BasicUser.Id,
}, targetUsers+"hello")
channelName := model.GetGroupNameFromUserIds([]string{th.BasicUser.Id, th.BasicUser2.Id, user3.Id})
assert.Equal(t, "", resp.Text)
assert.Equal(t, "http://test.url/"+team.Name+"/channels/"+channelName, resp.GotoLocation)
})
}

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

@@ -0,0 +1,50 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
)
type HelpProvider struct {
}
const (
CmdHelp = "help"
)
func init() {
app.RegisterCommandProvider(&HelpProvider{})
}
func (h *HelpProvider) GetTrigger() string {
return CmdHelp
}
func (h *HelpProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model.Command {
return &model.Command{
Trigger: CmdHelp,
AutoComplete: true,
AutoCompleteDesc: T("api.command_help.desc"),
DisplayName: T("api.command_help.name"),
}
}
func (h *HelpProvider) DoCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) *model.CommandResponse {
helpLink := *a.Config().SupportSettings.HelpLink
if helpLink == "" {
helpLink = model.SupportSettingsDefaultHelpLink
}
return &model.CommandResponse{
ResponseType: model.CommandResponseTypeEphemeral,
Text: args.T("api.command_help.success", map[string]any{
"HelpLink": helpLink,
}),
}
}

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

@@ -0,0 +1,213 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"strings"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
)
type InviteProvider struct {
}
const (
CmdInvite = "invite"
)
func init() {
app.RegisterCommandProvider(&InviteProvider{})
}
func (*InviteProvider) GetTrigger() string {
return CmdInvite
}
func (*InviteProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model.Command {
return &model.Command{
Trigger: CmdInvite,
AutoComplete: true,
AutoCompleteDesc: T("api.command_invite.desc"),
AutoCompleteHint: T("api.command_invite.hint"),
DisplayName: T("api.command_invite.name"),
}
}
func (i *InviteProvider) DoCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) *model.CommandResponse {
return &model.CommandResponse{
Text: i.doCommand(a, c, args, message),
ResponseType: model.CommandResponseTypeEphemeral,
}
}
func (i *InviteProvider) doCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) string {
if message == "" {
return args.T("api.command_invite.missing_message.app_error")
}
resps := &[]string{}
targetUsers, targetChannels, resp := i.parseMessage(a, c, args, resps, message)
if resp != "" {
return resp
}
// Verify that the inviter has permissions to invite users to the every channel.
targetChannels = i.checkPermissions(a, c, args, resps, targetUsers[0], targetChannels)
for _, targetUser := range targetUsers {
for _, targetChannel := range targetChannels {
if resp = i.addUserToChannel(a, c, args, targetUser, targetChannel); resp != "" {
*resps = append(*resps, resp)
continue
}
if args.ChannelId != targetChannel.Id {
*resps = append(*resps, args.T("api.command_invite.success", map[string]any{
"User": targetUser.Username,
"Channel": targetChannel.Name,
}))
}
}
}
if len(*resps) > 0 {
return strings.Join(*resps, "\n")
}
return ""
}
func (i *InviteProvider) parseMessage(a *app.App, c request.CTX, args *model.CommandArgs, resps *[]string, message string) ([]*model.User, []*model.Channel, string) {
splitMessage := strings.Split(message, " ")
targetUsers := make([]*model.User, 0, 1)
targetChannels := make([]*model.Channel, 0)
for j, msg := range splitMessage {
if msg == "" {
continue
}
if msg[0] == '@' || (msg[0] != '~' && j == 0) {
targetUsername := strings.TrimPrefix(msg, "@")
userProfile := i.getUserProfile(a, targetUsername)
if userProfile == nil {
*resps = append(*resps, args.T("api.command_invite.missing_user.app_error", map[string]any{
"User": targetUsername,
}))
continue
}
targetUsers = append(targetUsers, userProfile)
} else {
targetChannelName := strings.TrimPrefix(msg, "~")
channelToJoin, err := a.GetChannelByName(c, targetChannelName, args.TeamId, false)
if err != nil {
*resps = append(*resps, args.T("api.command_invite.channel.error", map[string]any{
"Channel": targetChannelName,
}))
continue
}
targetChannels = append(targetChannels, channelToJoin)
}
}
if len(targetUsers) == 0 {
if len(*resps) != 0 {
return nil, nil, strings.Join(*resps, "\n")
}
return nil, nil, args.T("api.command_invite.missing_message.app_error")
}
if len(targetChannels) == 0 {
if len(*resps) != 0 {
return nil, nil, strings.Join(*resps, "\n")
}
channelToJoin, err := a.GetChannel(c, args.ChannelId)
if err != nil {
return nil, nil, args.T("api.command_invite.channel.app_error")
}
targetChannels = append(targetChannels, channelToJoin)
}
return targetUsers, targetChannels, ""
}
func (i *InviteProvider) getUserProfile(a *app.App, username string) *model.User {
userProfile, nErr := a.Srv().Store().User().GetByUsername(username)
if nErr != nil {
return nil
}
if userProfile.DeleteAt != 0 {
return nil
}
return userProfile
}
func (i *InviteProvider) checkPermissions(a *app.App, c request.CTX, args *model.CommandArgs, resps *[]string, targetUser *model.User, targetChannels []*model.Channel) []*model.Channel {
var err *model.AppError
validChannels := make([]*model.Channel, 0, len(targetChannels))
for _, targetChannel := range targetChannels {
switch targetChannel.Type {
case model.ChannelTypeOpen:
if !a.HasPermissionToChannel(c, args.UserId, targetChannel.Id, model.PermissionManagePublicChannelMembers) {
*resps = append(*resps, args.T("api.command_invite.permission.app_error", map[string]any{
"User": targetUser.Username,
"Channel": targetChannel.Name,
}))
continue
}
case model.ChannelTypePrivate:
if !a.HasPermissionToChannel(c, args.UserId, targetChannel.Id, model.PermissionManagePrivateChannelMembers) {
if _, err = a.GetChannelMember(c, targetChannel.Id, args.UserId); err == nil {
// User doing the inviting is a member of the channel.
*resps = append(*resps, args.T("api.command_invite.permission.app_error", map[string]any{
"User": targetUser.Username,
"Channel": targetChannel.Name,
}))
continue
}
// User doing the inviting is *not* a member of the channel.
*resps = append(*resps, args.T("api.command_invite.private_channel.app_error", map[string]any{
"Channel": targetChannel.Name,
}))
continue
}
default:
*resps = append(*resps, args.T("api.command_invite.directchannel.app_error"))
continue
}
validChannels = append(validChannels, targetChannel)
}
return validChannels
}
func (i *InviteProvider) addUserToChannel(a *app.App, c request.CTX, args *model.CommandArgs, userProfile *model.User, channelToJoin *model.Channel) string {
// Check if user is already in the channel
_, err := a.GetChannelMember(c, channelToJoin.Id, userProfile.Id)
if err == nil {
return args.T("api.command_invite.user_already_in_channel.app_error", map[string]any{
"User": userProfile.Username,
})
}
if _, err = a.AddChannelMember(c, userProfile.Id, channelToJoin, app.ChannelMemberOpts{UserRequestorID: args.UserId}); err != nil {
if err.Id == "api.channel.add_members.user_denied" {
return args.T("api.command_invite.group_constrained_user_denied")
} else if err.Id == "app.team.get_member.missing.app_error" ||
err.Id == "api.channel.add_user.to.channel.failed.deleted.app_error" {
return args.T("api.command_invite.user_not_in_team.app_error", map[string]any{
"Username": userProfile.Username,
})
}
return args.T("api.command_invite.fail.app_error")
}
return ""
}

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

@@ -0,0 +1,85 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"strings"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type InvitePeopleProvider struct {
}
const (
CmdInvite_PEOPLE = "invite_people"
)
func init() {
app.RegisterCommandProvider(&InvitePeopleProvider{})
}
func (*InvitePeopleProvider) GetTrigger() string {
return CmdInvite_PEOPLE
}
func (*InvitePeopleProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model.Command {
autoComplete := true
if !*a.Config().EmailSettings.SendEmailNotifications || !*a.Config().TeamSettings.EnableUserCreation || !*a.Config().ServiceSettings.EnableEmailInvitations {
autoComplete = false
}
return &model.Command{
Trigger: CmdInvite_PEOPLE,
AutoComplete: autoComplete,
AutoCompleteDesc: T("api.command.invite_people.desc"),
AutoCompleteHint: T("api.command.invite_people.hint"),
DisplayName: T("api.command.invite_people.name"),
}
}
func (*InvitePeopleProvider) DoCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) *model.CommandResponse {
if !a.HasPermissionToTeam(args.UserId, args.TeamId, model.PermissionInviteUser) {
return &model.CommandResponse{Text: args.T("api.command_invite_people.permission.app_error"), ResponseType: model.CommandResponseTypeEphemeral}
}
if !a.HasPermissionToTeam(args.UserId, args.TeamId, model.PermissionAddUserToTeam) {
return &model.CommandResponse{Text: args.T("api.command_invite_people.permission.app_error"), ResponseType: model.CommandResponseTypeEphemeral}
}
if !*a.Config().EmailSettings.SendEmailNotifications {
return &model.CommandResponse{ResponseType: model.CommandResponseTypeEphemeral, Text: args.T("api.command.invite_people.email_off")}
}
if !*a.Config().TeamSettings.EnableUserCreation {
return &model.CommandResponse{ResponseType: model.CommandResponseTypeEphemeral, Text: args.T("api.command.invite_people.invite_off")}
}
if !*a.Config().ServiceSettings.EnableEmailInvitations {
return &model.CommandResponse{ResponseType: model.CommandResponseTypeEphemeral, Text: args.T("api.command.invite_people.email_invitations_off")}
}
emailList := strings.Fields(message)
for i := len(emailList) - 1; i >= 0; i-- {
emailList[i] = strings.Trim(emailList[i], ",")
if !strings.Contains(emailList[i], "@") {
emailList = append(emailList[:i], emailList[i+1:]...)
}
}
if len(emailList) == 0 {
return &model.CommandResponse{ResponseType: model.CommandResponseTypeEphemeral, Text: args.T("api.command.invite_people.no_email")}
}
if err := a.InviteNewUsersToTeam(emailList, args.TeamId, args.UserId); err != nil {
mlog.Error(err.Error())
return &model.CommandResponse{ResponseType: model.CommandResponseTypeEphemeral, Text: args.T("api.command.invite_people.fail")}
}
return &model.CommandResponse{ResponseType: model.CommandResponseTypeEphemeral, Text: args.T("api.command.invite_people.sent")}
}

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

@@ -0,0 +1,42 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/mattermost/mattermost-server/v6/model"
)
func TestInvitePeopleProvider(t *testing.T) {
th := setup(t).initBasic()
defer th.tearDown()
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.EmailSettings.SendEmailNotifications = true
*cfg.ServiceSettings.EnableEmailInvitations = true
})
cmd := InvitePeopleProvider{}
notTeamUser := th.createUser()
// Test without required permissions
args := &model.CommandArgs{
T: func(s string, args ...any) string { return s },
ChannelId: th.BasicChannel.Id,
TeamId: th.BasicTeam.Id,
UserId: notTeamUser.Id,
}
actual := cmd.DoCommand(th.App, th.Context, args, model.NewId()+"@simulator.amazonses.com")
assert.Equal(t, "api.command_invite_people.permission.app_error", actual.Text)
// Test with required permissions.
args.UserId = th.BasicUser.Id
actual = cmd.DoCommand(th.App, th.Context, args, model.NewId()+"@simulator.amazonses.com")
assert.Equal(t, "api.command.invite_people.sent", actual.Text)
}

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

@@ -0,0 +1,266 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
)
func TestInviteProvider(t *testing.T) {
th := setup(t).initBasic()
defer th.tearDown()
inviteProvider := InviteProvider{}
args := &model.CommandArgs{
T: func(s string, args ...any) string { return s },
ChannelId: th.BasicChannel.Id,
TeamId: th.BasicTeam.Id,
UserId: th.BasicUser.Id,
}
runCmd := func(msg string, expected string) {
actual := inviteProvider.DoCommand(th.App, th.Context, args, msg).Text
assert.Equal(t, expected, actual)
}
checkIsMember := func(channelID, userID string) {
_, channelMemberErr := th.App.GetChannelMember(th.Context, channelID, userID)
require.Nil(t, channelMemberErr, "Failed to add user to channel")
}
checkIsNotMember := func(channelID, userID string) {
_, channelMemberErr := th.App.GetChannelMember(th.Context, channelID, userID)
require.NotNil(t, channelMemberErr, "Failed to add user to channel")
}
t.Run("try to add missing user and channel in the command", func(t *testing.T) {
msg := ""
runCmd(msg, "api.command_invite.missing_message.app_error")
})
t.Run("user added in the current channel", func(t *testing.T) {
msg := th.BasicUser2.Username
runCmd(msg, "")
checkIsMember(th.BasicChannel.Id, th.BasicUser2.Id)
})
t.Run("add user to another channel not the current", func(t *testing.T) {
channel := th.createChannel(th.BasicTeam, model.ChannelTypeOpen)
msg := "@" + th.BasicUser2.Username + " ~" + channel.Name + " "
runCmd(msg, "api.command_invite.success")
checkIsMember(channel.Id, th.BasicUser2.Id)
})
t.Run("add a user to a private channel", func(t *testing.T) {
privateChannel := th.createChannel(th.BasicTeam, model.ChannelTypePrivate)
msg := "@" + th.BasicUser2.Username + " ~" + privateChannel.Name
runCmd(msg, "api.command_invite.success")
checkIsMember(privateChannel.Id, th.BasicUser2.Id)
})
t.Run("add multiple users to multiple channels", func(t *testing.T) {
anotherUser := th.createUser()
th.linkUserToTeam(anotherUser, th.BasicTeam)
channel1 := th.createChannel(th.BasicTeam, model.ChannelTypeOpen)
channel2 := th.createChannel(th.BasicTeam, model.ChannelTypeOpen)
msg := "@" + th.BasicUser2.Username + " @" + anotherUser.Username + " ~" + channel1.Name + " ~" + channel2.Name
expected := "api.command_invite.success\napi.command_invite.success\napi.command_invite.success\napi.command_invite.success"
runCmd(msg, expected)
checkIsMember(channel1.Id, th.BasicUser2.Id)
checkIsMember(channel2.Id, th.BasicUser2.Id)
checkIsMember(channel1.Id, anotherUser.Id)
checkIsMember(channel2.Id, anotherUser.Id)
})
t.Run("adds multiple users even when some are invalid or already members", func(t *testing.T) {
channel := th.createChannel(th.BasicTeam, model.ChannelTypeOpen)
userAlreadyInChannel := th.createUser()
th.linkUserToTeam(userAlreadyInChannel, th.BasicTeam)
th.addUserToChannel(userAlreadyInChannel, channel)
userInTeam := th.createUser()
th.linkUserToTeam(userInTeam, th.BasicTeam)
userNotInTeam := th.createUser()
msg := "@invalidUser123 @" + userAlreadyInChannel.Username + " @" + userInTeam.Username + " @" + userNotInTeam.Username + " ~" + channel.Name
expected := "api.command_invite.missing_user.app_error\n"
expected += "api.command_invite.user_already_in_channel.app_error\n"
expected += "api.command_invite.success\n"
expected += "api.command_invite.user_not_in_team.app_error"
runCmd(msg, expected)
checkIsMember(channel.Id, userInTeam.Id)
})
t.Run("try to add a user to a direct channel", func(t *testing.T) {
anotherUser := th.createUser()
th.linkUserToTeam(anotherUser, th.BasicTeam)
directChannel := th.createDmChannel(th.BasicUser2)
msg := "@" + anotherUser.Username + " ~" + directChannel.Name
runCmd(msg, "api.command_invite.directchannel.app_error")
checkIsNotMember(directChannel.Id, anotherUser.Id)
})
t.Run("try to add a user to an invalid channel", func(t *testing.T) {
msg := "@" + th.BasicUser2.Username + " wrongchannel1"
runCmd(msg, "api.command_invite.channel.error")
})
t.Run("try to add a user using channel's display name", func(t *testing.T) {
channel := th.createChannel(th.BasicTeam, model.ChannelTypeOpen)
msg := "@" + th.BasicUser2.Username + " ~" + channel.DisplayName
runCmd(msg, "api.command_invite.channel.error")
checkIsNotMember(channel.Id, th.BasicUser2.Id)
})
t.Run("try add invalid user to current channel", func(t *testing.T) {
msg := "@invalidUser123"
runCmd(msg, "api.command_invite.missing_user.app_error")
})
t.Run("invalid user to current channel without @", func(t *testing.T) {
msg := "invalidUser123"
runCmd(msg, "api.command_invite.missing_user.app_error")
})
t.Run("try to add a user which is not part of the team", func(t *testing.T) {
anotherUser := th.createUser()
// Do not add user to the team
msg := anotherUser.Username
runCmd(msg, "api.command_invite.user_not_in_team.app_error")
})
t.Run("try to add a user not part of the group to a group channel", func(t *testing.T) {
groupChannel := th.createChannel(th.BasicTeam, model.ChannelTypePrivate)
_, err := th.App.AddChannelMember(th.Context, th.BasicUser.Id, groupChannel, app.ChannelMemberOpts{})
require.Nil(t, err)
groupChannel.GroupConstrained = model.NewBool(true)
groupChannel, _ = th.App.UpdateChannel(th.Context, groupChannel)
msg := "@" + th.BasicUser2.Username + " ~" + groupChannel.Name
runCmd(msg, "api.command_invite.group_constrained_user_denied")
checkIsNotMember(groupChannel.Id, th.BasicUser2.Id)
})
t.Run("try to add a user to a private channel with no permission", func(t *testing.T) {
anotherUser := th.createUser()
th.linkUserToTeam(anotherUser, th.BasicTeam)
privateChannel := th.createChannelWithAnotherUser(th.BasicTeam, model.ChannelTypePrivate, th.BasicUser2.Id)
msg := "@" + anotherUser.Username + " ~" + privateChannel.Name
runCmd(msg, "api.command_invite.private_channel.app_error")
checkIsNotMember(privateChannel.Id, anotherUser.Id)
})
t.Run("try to add a deleted user to a public channel", func(t *testing.T) {
channel := th.createChannel(th.BasicTeam, model.ChannelTypeOpen)
deactivatedUser := th.createUser()
_, appErr := th.App.UpdateActive(th.Context, deactivatedUser, false)
require.Nil(t, appErr)
msg := "@" + deactivatedUser.Username + " ~" + channel.Name
runCmd(msg, "api.command_invite.missing_user.app_error")
checkIsNotMember(channel.Id, deactivatedUser.Id)
})
t.Run("add bot to a public channel", func(t *testing.T) {
bot, appErr := th.App.CreateBot(th.Context, &model.Bot{Username: "bot_" + model.NewId(), OwnerId: th.BasicUser2.Id})
require.Nil(t, appErr)
_, _, appErr = th.App.AddUserToTeam(th.Context, th.BasicTeam.Id, bot.UserId, th.BasicUser2.Id)
require.Nil(t, appErr)
msg := "@" + bot.Username
runCmd(msg, "")
checkIsMember(th.BasicChannel.Id, bot.UserId)
})
t.Run("try to add bot to a public channel without being a member", func(t *testing.T) {
bot, appErr := th.App.CreateBot(th.Context, &model.Bot{Username: "bot_" + model.NewId(), OwnerId: th.BasicUser2.Id})
require.Nil(t, appErr)
// Do not add to the team
msg := "@" + bot.Username
runCmd(msg, "api.command_invite.user_not_in_team.app_error")
checkIsNotMember(th.BasicChannel.Id, bot.UserId)
})
t.Run("try to add bot removed from a team to a public channel", func(t *testing.T) {
bot, appErr := th.App.CreateBot(th.Context, &model.Bot{Username: "bot_" + model.NewId(), OwnerId: th.BasicUser2.Id})
require.Nil(t, appErr)
_, _, appErr = th.App.AddUserToTeam(th.Context, th.BasicTeam.Id, bot.UserId, th.BasicUser2.Id)
require.Nil(t, appErr)
appErr = th.App.RemoveUserFromTeam(th.Context, th.BasicTeam.Id, bot.UserId, th.BasicUser2.Id)
require.Nil(t, appErr)
msg := "@" + bot.Username
runCmd(msg, "api.command_invite.user_not_in_team.app_error")
checkIsNotMember(th.BasicChannel.Id, bot.UserId)
})
}
func TestInviteGroup(t *testing.T) {
th := setup(t).initBasic()
defer th.tearDown()
th.BasicTeam.GroupConstrained = model.NewBool(true)
var err *model.AppError
_, _ = th.App.AddTeamMember(th.Context, th.BasicTeam.Id, th.BasicUser.Id)
_, err = th.App.AddTeamMember(th.Context, th.BasicTeam.Id, th.BasicUser2.Id)
require.Nil(t, err)
th.BasicTeam, _ = th.App.UpdateTeam(th.BasicTeam)
privateChannel := th.createChannel(th.BasicTeam, model.ChannelTypePrivate)
groupChannelUser1 := "@" + th.BasicUser.Username + " ~" + privateChannel.Name
groupChannelUser2 := "@" + th.BasicUser2.Username + " ~" + privateChannel.Name
basicUser3 := th.createUser()
groupChannelUser3 := "@" + basicUser3.Username + " ~" + privateChannel.Name
InviteP := InviteProvider{}
args := &model.CommandArgs{
T: func(s string, args ...any) string { return s },
ChannelId: th.BasicChannel.Id,
TeamId: th.BasicTeam.Id,
UserId: th.BasicUser.Id,
}
tests := []struct {
desc string
expected string
msg string
}{
{
desc: "try to add an existing user part of the group to a group channel",
expected: "api.command_invite.user_already_in_channel.app_error",
msg: groupChannelUser1,
},
{
desc: "try to add a user part of the group to a group channel",
expected: "api.command_invite.success",
msg: groupChannelUser2,
},
{
desc: "try to add a user NOT part of the group to a group channel",
expected: "api.command_invite.user_not_in_team.app_error",
msg: groupChannelUser3,
},
}
for _, test := range tests {
t.Run(test.desc, func(t *testing.T) {
actual := InviteP.DoCommand(th.App, th.Context, args, test.msg).Text
assert.Equal(t, test.expected, actual)
})
}
}

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

@@ -0,0 +1,79 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"strings"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
)
type JoinProvider struct {
}
const (
CmdJoin = "join"
)
func init() {
app.RegisterCommandProvider(&JoinProvider{})
}
func (*JoinProvider) GetTrigger() string {
return CmdJoin
}
func (*JoinProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model.Command {
return &model.Command{
Trigger: CmdJoin,
AutoComplete: true,
AutoCompleteDesc: T("api.command_join.desc"),
AutoCompleteHint: T("api.command_join.hint"),
DisplayName: T("api.command_join.name"),
}
}
func (*JoinProvider) DoCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) *model.CommandResponse {
channelName := strings.ToLower(message)
if strings.HasPrefix(message, "~") {
channelName = message[1:]
}
channel, err := a.Srv().Store().Channel().GetByName(args.TeamId, channelName, true)
if err != nil {
return &model.CommandResponse{Text: args.T("api.command_join.list.app_error"), ResponseType: model.CommandResponseTypeEphemeral}
}
if channel.Name != channelName {
return &model.CommandResponse{ResponseType: model.CommandResponseTypeEphemeral, Text: args.T("api.command_join.missing.app_error")}
}
switch channel.Type {
case model.ChannelTypeOpen:
if !a.HasPermissionToChannel(c, args.UserId, channel.Id, model.PermissionJoinPublicChannels) {
return &model.CommandResponse{Text: args.T("api.command_join.fail.app_error"), ResponseType: model.CommandResponseTypeEphemeral}
}
case model.ChannelTypePrivate:
if !a.HasPermissionToChannel(c, args.UserId, channel.Id, model.PermissionReadChannel) {
return &model.CommandResponse{Text: args.T("api.command_join.fail.app_error"), ResponseType: model.CommandResponseTypeEphemeral}
}
default:
return &model.CommandResponse{Text: args.T("api.command_join.fail.app_error"), ResponseType: model.CommandResponseTypeEphemeral}
}
if appErr := a.JoinChannel(c, channel, args.UserId); appErr != nil {
return &model.CommandResponse{Text: args.T("api.command_join.fail.app_error"), ResponseType: model.CommandResponseTypeEphemeral}
}
team, appErr := a.GetTeam(channel.TeamId)
if appErr != nil {
return &model.CommandResponse{Text: args.T("api.command_join.fail.app_error"), ResponseType: model.CommandResponseTypeEphemeral}
}
return &model.CommandResponse{GotoLocation: args.SiteURL + "/" + team.Name + "/channels/" + channel.Name}
}

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

@@ -0,0 +1,146 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
)
func TestJoinCommandNoChannel(t *testing.T) {
th := setup(t).initBasic()
defer th.tearDown()
if testing.Short() {
t.SkipNow()
}
cmd := &JoinProvider{}
resp := cmd.DoCommand(th.App, th.Context, &model.CommandArgs{
T: i18n.IdentityTfunc(),
UserId: th.BasicUser2.Id,
SiteURL: "http://test.url",
TeamId: th.BasicTeam.Id,
}, "asdsad")
assert.Equal(t, "api.command_join.list.app_error", resp.Text)
}
func TestJoinCommandForExistingChannel(t *testing.T) {
th := setup(t).initBasic()
defer th.tearDown()
if testing.Short() {
t.SkipNow()
}
channel2, _ := th.App.CreateChannel(th.Context, &model.Channel{
DisplayName: "AA",
Name: "aa" + model.NewId() + "a",
Type: model.ChannelTypeOpen,
TeamId: th.BasicTeam.Id,
CreatorId: th.BasicUser.Id,
}, false)
cmd := &JoinProvider{}
resp := cmd.DoCommand(th.App, th.Context, &model.CommandArgs{
T: i18n.IdentityTfunc(),
UserId: th.BasicUser2.Id,
SiteURL: "http://test.url",
TeamId: th.BasicTeam.Id,
}, channel2.Name)
assert.Equal(t, "", resp.Text)
assert.Equal(t, "http://test.url/"+th.BasicTeam.Name+"/channels/"+channel2.Name, resp.GotoLocation)
}
func TestJoinCommandWithTilde(t *testing.T) {
th := setup(t).initBasic()
defer th.tearDown()
if testing.Short() {
t.SkipNow()
}
channel2, _ := th.App.CreateChannel(th.Context, &model.Channel{
DisplayName: "AA",
Name: "aa" + model.NewId() + "a",
Type: model.ChannelTypeOpen,
TeamId: th.BasicTeam.Id,
CreatorId: th.BasicUser.Id,
}, false)
cmd := &JoinProvider{}
resp := cmd.DoCommand(th.App, th.Context, &model.CommandArgs{
T: i18n.IdentityTfunc(),
UserId: th.BasicUser2.Id,
SiteURL: "http://test.url",
TeamId: th.BasicTeam.Id,
}, "~"+channel2.Name)
assert.Equal(t, "", resp.Text)
assert.Equal(t, "http://test.url/"+th.BasicTeam.Name+"/channels/"+channel2.Name, resp.GotoLocation)
}
func TestJoinCommandPermissions(t *testing.T) {
th := setup(t).initBasic()
defer th.tearDown()
channel2, _ := th.App.CreateChannel(th.Context, &model.Channel{
DisplayName: "AA",
Name: "aa" + model.NewId() + "a",
Type: model.ChannelTypeOpen,
TeamId: th.BasicTeam.Id,
CreatorId: th.BasicUser.Id,
}, false)
cmd := &JoinProvider{}
user3 := th.createUser()
// Try a public channel *without* permission.
args := &model.CommandArgs{
T: i18n.IdentityTfunc(),
UserId: user3.Id,
SiteURL: "http://test.url",
TeamId: th.BasicTeam.Id,
}
actual := cmd.DoCommand(th.App, th.Context, args, "~"+channel2.Name).Text
assert.Equal(t, "api.command_join.fail.app_error", actual)
// Try a public channel with permission.
args = &model.CommandArgs{
T: i18n.IdentityTfunc(),
UserId: th.BasicUser2.Id,
SiteURL: "http://test.url",
TeamId: th.BasicTeam.Id,
}
actual = cmd.DoCommand(th.App, th.Context, args, "~"+channel2.Name).Text
assert.Equal(t, "", actual)
// Try a private channel *without* permission.
channel3, _ := th.App.CreateChannel(th.Context, &model.Channel{
DisplayName: "BB",
Name: "aa" + model.NewId() + "a",
Type: model.ChannelTypePrivate,
TeamId: th.BasicTeam.Id,
CreatorId: th.BasicUser.Id,
}, false)
args = &model.CommandArgs{
T: i18n.IdentityTfunc(),
UserId: th.BasicUser2.Id,
SiteURL: "http://test.url",
TeamId: th.BasicTeam.Id,
}
actual = cmd.DoCommand(th.App, th.Context, args, "~"+channel3.Name).Text
assert.Equal(t, "api.command_join.fail.app_error", actual)
}

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

@@ -0,0 +1,80 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
)
type LeaveProvider struct {
}
const (
CmdLeave = "leave"
)
func init() {
app.RegisterCommandProvider(&LeaveProvider{})
}
func (*LeaveProvider) GetTrigger() string {
return CmdLeave
}
func (*LeaveProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model.Command {
return &model.Command{
Trigger: CmdLeave,
AutoComplete: true,
AutoCompleteDesc: T("api.command_leave.desc"),
DisplayName: T("api.command_leave.name"),
}
}
func (*LeaveProvider) DoCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) *model.CommandResponse {
var channel *model.Channel
var noChannelErr *model.AppError
if channel, noChannelErr = a.GetChannel(c, args.ChannelId); noChannelErr != nil {
return &model.CommandResponse{Text: args.T("api.command_leave.fail.app_error"), ResponseType: model.CommandResponseTypeEphemeral}
}
team, err := a.GetTeam(args.TeamId)
if err != nil {
return &model.CommandResponse{Text: args.T("api.command_leave.fail.app_error"), ResponseType: model.CommandResponseTypeEphemeral}
}
err = a.LeaveChannel(c, args.ChannelId, args.UserId)
if err != nil {
if channel.Name == model.DefaultChannelName {
return &model.CommandResponse{Text: args.T("api.channel.leave.default.app_error", map[string]any{"Channel": model.DefaultChannelName}), ResponseType: model.CommandResponseTypeEphemeral}
}
return &model.CommandResponse{Text: args.T("api.command_leave.fail.app_error"), ResponseType: model.CommandResponseTypeEphemeral}
}
member, err := a.GetTeamMember(team.Id, args.UserId)
if err != nil || member.DeleteAt != 0 {
return &model.CommandResponse{GotoLocation: args.SiteURL + "/"}
}
user, err := a.GetUser(args.UserId)
if err != nil {
return &model.CommandResponse{Text: args.T("api.command_leave.fail.app_error"), ResponseType: model.CommandResponseTypeEphemeral}
}
if user.IsGuest() {
members, err := a.GetChannelMembersForUser(c, team.Id, args.UserId)
if err != nil || len(members) == 0 {
return &model.CommandResponse{Text: args.T("api.command_leave.fail.app_error"), ResponseType: model.CommandResponseTypeEphemeral}
}
channel, err := a.GetChannel(c, members[0].ChannelId)
if err != nil {
return &model.CommandResponse{Text: args.T("api.command_leave.fail.app_error"), ResponseType: model.CommandResponseTypeEphemeral}
}
return &model.CommandResponse{GotoLocation: args.SiteURL + "/" + team.Name + "/channels/" + channel.Name}
}
return &model.CommandResponse{GotoLocation: args.SiteURL + "/" + team.Name + "/channels/" + model.DefaultChannelName}
}

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

@@ -0,0 +1,147 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
)
func TestLeaveProviderDoCommand(t *testing.T) {
th := setup(t).initBasic()
defer th.tearDown()
lp := LeaveProvider{}
publicChannel, _ := th.App.CreateChannel(th.Context, &model.Channel{
DisplayName: "AA",
Name: "aa" + model.NewId() + "a",
Type: model.ChannelTypeOpen,
TeamId: th.BasicTeam.Id,
CreatorId: th.BasicUser.Id,
}, false)
privateChannel, _ := th.App.CreateChannel(th.Context, &model.Channel{
DisplayName: "BB",
Name: "aa" + model.NewId() + "a",
Type: model.ChannelTypeOpen,
TeamId: th.BasicTeam.Id,
CreatorId: th.BasicUser.Id,
}, false)
defaultChannel, err := th.App.GetChannelByName(th.Context, model.DefaultChannelName, th.BasicTeam.Id, false)
require.Nil(t, err)
guest := th.createGuest()
th.App.AddUserToTeam(th.Context, th.BasicTeam.Id, th.BasicUser.Id, th.BasicUser.Id)
th.App.AddUserToChannel(th.Context, th.BasicUser, publicChannel, false)
th.App.AddUserToChannel(th.Context, th.BasicUser, privateChannel, false)
th.App.AddUserToTeam(th.Context, th.BasicTeam.Id, guest.Id, guest.Id)
th.App.AddUserToChannel(th.Context, guest, publicChannel, false)
th.App.AddUserToChannel(th.Context, guest, defaultChannel, false)
t.Run("Should error when no Channel ID in args", func(t *testing.T) {
args := &model.CommandArgs{
UserId: th.BasicUser.Id,
T: func(s string, args ...any) string { return s },
}
actual := lp.DoCommand(th.App, th.Context, args, "")
assert.Equal(t, "api.command_leave.fail.app_error", actual.Text)
assert.Equal(t, model.CommandResponseTypeEphemeral, actual.ResponseType)
})
t.Run("Should error when no Team ID in args", func(t *testing.T) {
args := &model.CommandArgs{
UserId: th.BasicUser.Id,
ChannelId: publicChannel.Id,
T: func(s string, args ...any) string { return s },
}
actual := lp.DoCommand(th.App, th.Context, args, "")
assert.Equal(t, "api.command_leave.fail.app_error", actual.Text)
assert.Equal(t, model.CommandResponseTypeEphemeral, actual.ResponseType)
})
t.Run("Leave a public channel", func(t *testing.T) {
args := &model.CommandArgs{
UserId: th.BasicUser.Id,
ChannelId: publicChannel.Id,
T: func(s string, args ...any) string { return s },
TeamId: th.BasicTeam.Id,
SiteURL: "http://localhost:8065",
}
actual := lp.DoCommand(th.App, th.Context, args, "")
assert.Equal(t, "", actual.Text)
assert.Equal(t, args.SiteURL+"/"+th.BasicTeam.Name+"/channels/"+model.DefaultChannelName, actual.GotoLocation)
assert.Equal(t, "", actual.ResponseType)
_, err = th.App.GetChannelMember(th.Context, publicChannel.Id, th.BasicUser.Id)
assert.NotNil(t, err)
assert.NotNil(t, err.Id, "app.channel.get_member.missing.app_error")
})
t.Run("Leave a private channel", func(t *testing.T) {
args := &model.CommandArgs{
UserId: th.BasicUser.Id,
ChannelId: privateChannel.Id,
T: func(s string, args ...any) string { return s },
TeamId: th.BasicTeam.Id,
SiteURL: "http://localhost:8065",
}
actual := lp.DoCommand(th.App, th.Context, args, "")
assert.Equal(t, "", actual.Text)
})
t.Run("Should not leave a default channel", func(t *testing.T) {
args := &model.CommandArgs{
UserId: th.BasicUser.Id,
ChannelId: defaultChannel.Id,
T: func(s string, args ...any) string { return s },
TeamId: th.BasicTeam.Id,
SiteURL: "http://localhost:8065",
}
actual := lp.DoCommand(th.App, th.Context, args, "")
assert.Equal(t, "api.channel.leave.default.app_error", actual.Text)
})
t.Run("Should allow to leave a default channel if user is guest", func(t *testing.T) {
args := &model.CommandArgs{
UserId: guest.Id,
ChannelId: defaultChannel.Id,
T: func(s string, args ...any) string { return s },
TeamId: th.BasicTeam.Id,
SiteURL: "http://localhost:8065",
}
actual := lp.DoCommand(th.App, th.Context, args, "")
assert.Equal(t, "", actual.Text)
assert.Equal(t, args.SiteURL+"/"+th.BasicTeam.Name+"/channels/"+publicChannel.Name, actual.GotoLocation)
assert.Equal(t, "", actual.ResponseType)
_, err = th.App.GetChannelMember(th.Context, defaultChannel.Id, guest.Id)
assert.NotNil(t, err)
assert.NotNil(t, err.Id, "app.channel.get_member.missing.app_error")
})
t.Run("Should redirect to the team if is the last channel", func(t *testing.T) {
args := &model.CommandArgs{
UserId: guest.Id,
ChannelId: publicChannel.Id,
T: func(s string, args ...any) string { return s },
TeamId: th.BasicTeam.Id,
SiteURL: "http://localhost:8065",
}
actual := lp.DoCommand(th.App, th.Context, args, "")
assert.Equal(t, "", actual.Text)
assert.Equal(t, args.SiteURL+"/", actual.GotoLocation)
assert.Equal(t, "", actual.ResponseType)
_, err = th.App.GetChannelMember(th.Context, publicChannel.Id, guest.Id)
assert.NotNil(t, err)
assert.NotNil(t, err.Id, "app.channel.get_member.missing.app_error")
})
}

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

@@ -0,0 +1,755 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"encoding/json"
"io"
"net/http"
"path"
"regexp"
"strconv"
"strings"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
var usage = `Mattermost testing commands to help configure the system
COMMANDS:
Setup - Creates a testing environment in current team.
/test setup [teams] [fuzz] <Num Channels> <Num Users> <NumPosts>
Example:
/test setup teams fuzz 10 20 50
Users - Add a specified number of random users with fuzz text to current team, at the specified Unix timestamp in milliseconds.
/test users [fuzz] [range=min[,max]] [time=user_join_timestamp]
Default: range=2,5 time=
Examples:
/test users fuzz range=3,8 time=1565076128000
/test users range=1
Channels - Add a specified number of random public (o) or private (p) channels with fuzz text to current team, at the specified Unix timestamp in milliseconds.
/test channels [fuzz] [range=min[,max]] [type=(o|p)] [time=channel_create_timestamp]
Default: range=2,5 type=o time=
Examples:
/test channels fuzz range=5,10 type=p time=1565076128000
/test channels range=1
DMs - Add a specified number of random DM messages between the current user and a specified user, at the specified Unix timestamp in milliseconds. If a timestamp is provided, posts are created one millisecond apart. Note: You may need to clear your browser cache in order to see these posts in the UI.
/test dms u=@username [range=min[,max]] [time=dm_create_timestamp]
Default: range=2,5 time=
Examples:
/test dms u=@user range=5,10 time=1565076128000
/test dms u=@user range=2
ThreadedPost - Create a threaded post with a specified number of replies at the specified Unix timestamp in milliseconds. If a timestamp is provided, posts are created one millisecond apart. Note: You may need to clear your browser cache in order to see these posts in the UI.
/test threaded_post [range=min[,max]] [time=post_timestamp]
Default: range=1000 time=
Examples:
/test threaded_post
/test threaded_post range=100,200 time=1565076128000
Posts - Add some random posts with fuzz text to current channel, at the specified Unix timestamp in milliseconds. If a timestamp is provided, posts are created one millisecond apart. Note: You may need to clear your browser cache in order to see these posts in the UI.
/test posts [fuzz] [range=min[,max]] [images=max_images] [time=post_timestamp]
Default: range=2,5 images=0 time=
Example:
/test posts fuzz range=5,10 images=3 time=1565076128000
/test posts range=2
Post - Add post to a channel as another user.
/test post u=@username p=passwd c=~channelname t=teamname "message"
Example:
/test post u=@user-1 p=user-1 c=~town-square t=ad-1 "message"
Url - Add a post containing the text from a given url to current channel.
/test url
Example:
/test http://www.example.com/sample_file.md
Json - Add a post using the JSON file as payload to the current channel.
/test json url
Example:
/test json http://www.example.com/sample_body.json
`
const (
CmdTest = "test"
)
var (
userRE = regexp.MustCompile(`u=@?([^\s]+)`)
passwdRE = regexp.MustCompile(`p=([^\s]+)`)
teamRE = regexp.MustCompile(`t=([^\s]+)`)
channelRE = regexp.MustCompile(`c=~([^\s]+)`)
messageRE = regexp.MustCompile(`"(.*)"`)
fuzzRE = regexp.MustCompile(`fuzz`)
rangeRE = regexp.MustCompile(`range=([^\s]+)`)
timeRE = regexp.MustCompile(`time=([^\s]+)`)
imagesRE = regexp.MustCompile(`images=([^\s]+)`)
typeRE = regexp.MustCompile(`type=([^\s])+`)
)
type LoadTestProvider struct {
}
func init() {
app.RegisterCommandProvider(&LoadTestProvider{})
}
func (*LoadTestProvider) GetTrigger() string {
return CmdTest
}
func (*LoadTestProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model.Command {
if !*a.Config().ServiceSettings.EnableTesting {
return nil
}
return &model.Command{
Trigger: CmdTest,
AutoComplete: false,
AutoCompleteDesc: "Debug Load Testing",
AutoCompleteHint: "help",
DisplayName: "test",
}
}
func (lt *LoadTestProvider) DoCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) *model.CommandResponse {
commandResponse, err := lt.doCommand(a, c, args, message)
if err != nil {
c.Logger().Error("failed command /"+CmdTest, mlog.Err(err))
}
return commandResponse
}
func (lt *LoadTestProvider) doCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) (*model.CommandResponse, error) {
//This command is only available when EnableTesting is true
if !*a.Config().ServiceSettings.EnableTesting {
return &model.CommandResponse{}, nil
}
if strings.HasPrefix(message, "setup") {
return lt.SetupCommand(a, c, args, message)
}
if strings.HasPrefix(message, "users") {
return lt.UsersCommand(a, c, args, message)
}
if strings.HasPrefix(message, "activate_user") {
return lt.ActivateUserCommand(a, c, args, message)
}
if strings.HasPrefix(message, "deactivate_user") {
return lt.DeActivateUserCommand(a, c, args, message)
}
if strings.HasPrefix(message, "channels") {
return lt.ChannelsCommand(a, c, args, message)
}
if strings.HasPrefix(message, "dms") {
return lt.DMsCommand(a, c, args, message)
}
if strings.HasPrefix(message, "posts") {
return lt.PostsCommand(a, c, args, message)
}
if strings.HasPrefix(message, "post") {
return lt.PostCommand(a, c, args, message)
}
if strings.HasPrefix(message, "threaded_post") {
return lt.ThreadedPostCommand(a, c, args, message)
}
if strings.HasPrefix(message, "url") {
return lt.URLCommand(a, c, args, message)
}
if strings.HasPrefix(message, "json") {
return lt.JsonCommand(a, c, args, message)
}
return lt.HelpCommand(args, message), nil
}
func (*LoadTestProvider) HelpCommand(args *model.CommandArgs, message string) *model.CommandResponse {
return &model.CommandResponse{Text: usage, ResponseType: model.CommandResponseTypeEphemeral}
}
func (*LoadTestProvider) SetupCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) (*model.CommandResponse, error) {
tokens := strings.Fields(strings.TrimPrefix(message, "setup"))
doTeams := contains(tokens, "teams")
doFuzz := contains(tokens, "fuzz")
numArgs := 0
if doTeams {
numArgs++
}
if doFuzz {
numArgs++
}
var numTeams int
var numChannels int
var numUsers int
var numPosts int
// Defaults
numTeams = 10
numChannels = 10
numUsers = 10
numPosts = 10
if doTeams {
if (len(tokens) - numArgs) >= 4 {
numTeams, _ = strconv.Atoi(tokens[numArgs+0])
numChannels, _ = strconv.Atoi(tokens[numArgs+1])
numUsers, _ = strconv.Atoi(tokens[numArgs+2])
numPosts, _ = strconv.Atoi(tokens[numArgs+3])
}
} else {
if (len(tokens) - numArgs) >= 3 {
numChannels, _ = strconv.Atoi(tokens[numArgs+0])
numUsers, _ = strconv.Atoi(tokens[numArgs+1])
numPosts, _ = strconv.Atoi(tokens[numArgs+2])
}
}
client := model.NewAPIv4Client(args.SiteURL)
if doTeams {
if err := CreateBasicUser(a, client); err != nil {
return &model.CommandResponse{Text: "Failed to create testing environment", ResponseType: model.CommandResponseTypeEphemeral}, err
}
_, _, err := client.Login(BTestUserEmail, BTestUserPassword)
if err != nil {
return &model.CommandResponse{Text: "Failed to create testing environment", ResponseType: model.CommandResponseTypeEphemeral}, err
}
environment, err := CreateTestEnvironmentWithTeams(
a,
c,
client,
utils.Range{Begin: numTeams, End: numTeams},
utils.Range{Begin: numChannels, End: numChannels},
utils.Range{Begin: numUsers, End: numUsers},
utils.Range{Begin: numPosts, End: numPosts},
doFuzz)
if err != nil {
return &model.CommandResponse{Text: "Failed to create testing environment", ResponseType: model.CommandResponseTypeEphemeral}, err
}
c.Logger().Info("Testing environment created")
for i := 0; i < len(environment.Teams); i++ {
c.Logger().Info("Team Created: " + environment.Teams[i].Name)
c.Logger().Info("\t User to login: " + environment.Environments[i].Users[0].Email + ", " + UserPassword)
}
} else {
team, err := a.Srv().Store().Team().Get(args.TeamId)
if err != nil {
return &model.CommandResponse{Text: "Failed to create testing environment", ResponseType: model.CommandResponseTypeEphemeral}, err
}
CreateTestEnvironmentInTeam(
a,
c,
client,
team,
utils.Range{Begin: numChannels, End: numChannels},
utils.Range{Begin: numUsers, End: numUsers},
utils.Range{Begin: numPosts, End: numPosts},
doFuzz)
}
return &model.CommandResponse{Text: "Created environment", ResponseType: model.CommandResponseTypeEphemeral}, nil
}
func (*LoadTestProvider) ActivateUserCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) (*model.CommandResponse, error) {
user_id := strings.TrimSpace(strings.TrimPrefix(message, "activate_user"))
if err := a.UpdateUserActive(c, user_id, true); err != nil {
return &model.CommandResponse{Text: "Failed to activate user", ResponseType: model.CommandResponseTypeEphemeral}, err
}
return &model.CommandResponse{Text: "Activated user", ResponseType: model.CommandResponseTypeEphemeral}, nil
}
func (*LoadTestProvider) DeActivateUserCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) (*model.CommandResponse, error) {
user_id := strings.TrimSpace(strings.TrimPrefix(message, "deactivate_user"))
if err := a.UpdateUserActive(c, user_id, false); err != nil {
return &model.CommandResponse{Text: "Failed to deactivate user", ResponseType: model.CommandResponseTypeEphemeral}, err
}
return &model.CommandResponse{Text: "DeActivated user", ResponseType: model.CommandResponseTypeEphemeral}, nil
}
func (*LoadTestProvider) UsersCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) (*model.CommandResponse, error) {
cmd := strings.TrimSpace(strings.TrimPrefix(message, "users"))
doFuzz := false
if fuzzRE.MatchString(cmd) {
doFuzz = true
}
var err error
rng := utils.Range{Begin: 2, End: 5}
rangeParam := getMatch(rangeRE, cmd)
if rangeParam != "" {
rng, err = parseRange(rangeParam)
if err != nil {
return &model.CommandResponse{Text: "Failed to add users: " + err.Error(), ResponseType: model.CommandResponseTypeEphemeral}, err
}
}
team, err := a.Srv().Store().Team().Get(args.TeamId)
if err != nil {
return &model.CommandResponse{Text: "Failed to add users", ResponseType: model.CommandResponseTypeEphemeral}, err
}
time := int64(0)
timeParam := getMatch(timeRE, cmd)
if timeParam != "" {
time, err = strconv.ParseInt(timeParam, 10, 64)
if err != nil || time < 0 {
return &model.CommandResponse{Text: "Failed to add users: Invalid time parameter", ResponseType: model.CommandResponseTypeEphemeral}, errors.New("Invalid time parameter")
}
}
client := model.NewAPIv4Client(args.SiteURL)
userCreator := NewAutoUserCreator(a, client, team)
userCreator.Fuzzy = doFuzz
userCreator.JoinTime = time
if _, err := userCreator.CreateTestUsers(c, rng); err != nil {
return &model.CommandResponse{Text: "Failed to add users: " + err.Error(), ResponseType: model.CommandResponseTypeEphemeral}, err
}
return &model.CommandResponse{Text: "Added users", ResponseType: model.CommandResponseTypeEphemeral}, nil
}
func (*LoadTestProvider) ChannelsCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) (*model.CommandResponse, error) {
cmd := strings.TrimSpace(strings.TrimPrefix(message, "channels"))
doFuzz := false
if fuzzRE.MatchString(cmd) {
doFuzz = true
}
var err error
rng := utils.Range{Begin: 2, End: 5}
rangeParam := getMatch(rangeRE, cmd)
if rangeParam != "" {
rng, err = parseRange(rangeParam)
if err != nil {
return &model.CommandResponse{Text: "Failed to add channels: " + err.Error(), ResponseType: model.CommandResponseTypeEphemeral}, err
}
}
team, err := a.Srv().Store().Team().Get(args.TeamId)
if err != nil {
return &model.CommandResponse{Text: "Failed to add channels", ResponseType: model.CommandResponseTypeEphemeral}, err
}
typ := model.ChannelTypeOpen
typeParam := getMatch(typeRE, cmd)
if typeParam != "" {
switch strings.ToUpper(typeParam) {
case "O":
case "P":
typ = model.ChannelTypePrivate
default:
return &model.CommandResponse{Text: "Failed to add channels: Invalid type parameter", ResponseType: model.CommandResponseTypeEphemeral}, errors.New("Invalid type parameter")
}
}
time := int64(0)
timeParam := getMatch(timeRE, cmd)
if timeParam != "" {
time, err = strconv.ParseInt(timeParam, 10, 64)
if err != nil || time < 0 {
return &model.CommandResponse{Text: "Failed to add channels: Invalid time parameter", ResponseType: model.CommandResponseTypeEphemeral}, errors.New("Invalid time parameter")
}
}
channelCreator := NewAutoChannelCreator(a, team, args.UserId)
channelCreator.Fuzzy = doFuzz
channelCreator.CreateTime = time
channelCreator.ChannelType = typ
if _, err := channelCreator.CreateTestChannels(c, rng); err != nil {
return &model.CommandResponse{Text: "Failed to create test channels: " + err.Error(), ResponseType: model.CommandResponseTypeEphemeral}, err
}
return &model.CommandResponse{Text: "Added channels", ResponseType: model.CommandResponseTypeEphemeral}, nil
}
func (*LoadTestProvider) DMsCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) (*model.CommandResponse, error) {
cmd := strings.TrimSpace(strings.TrimPrefix(message, "dms"))
var err error
username := getMatch(userRE, message)
user, appErr := a.GetUserByUsername(username)
if appErr != nil {
return &model.CommandResponse{Text: "Failed to add DMS: Invalid username", ResponseType: model.CommandResponseTypeEphemeral}, appErr
}
rng := utils.Range{Begin: 2, End: 5}
rangeParam := getMatch(rangeRE, cmd)
if rangeParam != "" {
rng, err = parseRange(rangeParam)
if err != nil {
return &model.CommandResponse{Text: "Failed to add DMs: " + err.Error(), ResponseType: model.CommandResponseTypeEphemeral}, err
}
}
time := int64(0)
timeParam := getMatch(timeRE, cmd)
if timeParam != "" {
time, err = strconv.ParseInt(timeParam, 10, 64)
if err != nil || time < 0 {
return &model.CommandResponse{Text: "Failed to add DMs: Invalid time parameter", ResponseType: model.CommandResponseTypeEphemeral}, errors.New("Invalid time parameter")
}
}
channel, err := a.GetOrCreateDirectChannel(c, args.UserId, user.Id)
postCreator := NewAutoPostCreator(a, channel.Id, args.UserId)
postCreator.CreateTime = time
postCreator.UsersToPostFrom = []string{user.Id}
numPosts := utils.RandIntFromRange(rng)
for i := 0; i < numPosts; i++ {
if _, err := postCreator.CreateRandomPost(c); err != nil {
return &model.CommandResponse{Text: "Failed to create test DMs: " + err.Error(), ResponseType: model.CommandResponseTypeEphemeral}, err
}
}
return &model.CommandResponse{Text: "Added DMs", ResponseType: model.CommandResponseTypeEphemeral}, nil
}
func (*LoadTestProvider) ThreadedPostCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) (*model.CommandResponse, error) {
cmd := strings.TrimSpace(strings.TrimPrefix(message, "threaded_post"))
var err error
rng := utils.Range{Begin: 1000, End: 1000}
rangeParam := getMatch(rangeRE, cmd)
if rangeParam != "" {
rng, err = parseRange(rangeParam)
if err != nil {
return &model.CommandResponse{Text: "Failed to create post: " + err.Error(), ResponseType: model.CommandResponseTypeEphemeral}, err
}
}
time := int64(0)
timeParam := getMatch(timeRE, cmd)
if timeParam != "" {
time, err = strconv.ParseInt(timeParam, 10, 64)
if err != nil || time < 0 {
return &model.CommandResponse{Text: "Failed to create post: Invalid time parameter", ResponseType: model.CommandResponseTypeEphemeral}, errors.New("Invalid time parameter")
}
}
var usernames []string
options := &model.UserGetOptions{InTeamId: args.TeamId, Page: 0, PerPage: 1000}
if profileUsers, err := a.Srv().Store().User().GetProfiles(options); err == nil {
usernames = make([]string, len(profileUsers))
i := 0
for _, userprof := range profileUsers {
usernames[i] = userprof.Username
i++
}
}
testPoster := NewAutoPostCreator(a, args.ChannelId, args.UserId)
testPoster.Fuzzy = true
testPoster.Users = usernames
testPoster.CreateTime = time
rpost, err2 := testPoster.CreateRandomPost(c)
if err2 != nil {
return &model.CommandResponse{Text: "Failed to create a post", ResponseType: model.CommandResponseTypeEphemeral}, err2
}
numPosts := utils.RandIntFromRange(rng)
for i := 0; i < numPosts; i++ {
testPoster.CreateRandomPostNested(c, rpost.Id)
}
return &model.CommandResponse{Text: "Added threaded post", ResponseType: model.CommandResponseTypeEphemeral}, nil
}
func (*LoadTestProvider) PostsCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) (*model.CommandResponse, error) {
cmd := strings.TrimSpace(strings.TrimPrefix(message, "posts"))
doFuzz := false
if fuzzRE.MatchString(cmd) {
doFuzz = true
}
var err error
rng := utils.Range{Begin: 2, End: 5}
rangeParam := getMatch(rangeRE, cmd)
if rangeParam != "" {
rng, err = parseRange(rangeParam)
if err != nil {
return &model.CommandResponse{Text: "Failed to add posts: " + err.Error(), ResponseType: model.CommandResponseTypeEphemeral}, err
}
}
maxImages := 0
imagesParam := getMatch(imagesRE, cmd)
if imagesParam != "" {
maxImages, err = strconv.Atoi(imagesParam)
if err != nil {
return &model.CommandResponse{Text: "Failed to add posts: Invalid images parameter", ResponseType: model.CommandResponseTypeEphemeral}, errors.New("Invalid images parameter")
}
}
time := int64(0)
timeParam := getMatch(timeRE, cmd)
if timeParam != "" {
time, err = strconv.ParseInt(timeParam, 10, 64)
if err != nil || time < 0 {
return &model.CommandResponse{Text: "Failed to add posts: Invalid time parameter", ResponseType: model.CommandResponseTypeEphemeral}, errors.New("Invalid time parameter")
}
}
var usernames []string
options := &model.UserGetOptions{InTeamId: args.TeamId, Page: 0, PerPage: 1000}
if profileUsers, err := a.Srv().Store().User().GetProfiles(options); err == nil {
usernames = make([]string, len(profileUsers))
i := 0
for _, userprof := range profileUsers {
usernames[i] = userprof.Username
i++
}
}
testPoster := NewAutoPostCreator(a, args.ChannelId, args.UserId)
testPoster.Fuzzy = doFuzz
testPoster.Users = usernames
testPoster.CreateTime = time
numImages := utils.RandIntFromRange(utils.Range{Begin: 0, End: maxImages})
numPosts := utils.RandIntFromRange(rng)
for i := 0; i < numPosts; i++ {
testPoster.HasImage = (i < numImages)
_, err := testPoster.CreateRandomPost(c)
if err != nil {
return &model.CommandResponse{Text: "Failed to add posts", ResponseType: model.CommandResponseTypeEphemeral}, err
}
}
return &model.CommandResponse{Text: "Added posts", ResponseType: model.CommandResponseTypeEphemeral}, nil
}
func getMatch(re *regexp.Regexp, text string) string {
if match := re.FindStringSubmatch(text); match != nil {
return match[1]
}
return ""
}
func (*LoadTestProvider) PostCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) (*model.CommandResponse, error) {
textMessage := getMatch(messageRE, message)
if textMessage == "" {
return &model.CommandResponse{Text: "No message to post", ResponseType: model.CommandResponseTypeEphemeral}, nil
}
teamName := getMatch(teamRE, message)
team, err := a.GetTeamByName(teamName)
if err != nil {
return &model.CommandResponse{Text: "Failed to get a team", ResponseType: model.CommandResponseTypeEphemeral}, err
}
channelName := getMatch(channelRE, message)
channel, err := a.GetChannelByName(c, channelName, team.Id, true)
if err != nil {
return &model.CommandResponse{Text: "Failed to get a channel", ResponseType: model.CommandResponseTypeEphemeral}, err
}
passwd := getMatch(passwdRE, message)
username := getMatch(userRE, message)
user, err := a.GetUserByUsername(username)
if err != nil {
return &model.CommandResponse{Text: "Failed to get a user", ResponseType: model.CommandResponseTypeEphemeral}, err
}
client := model.NewAPIv4Client(args.SiteURL)
_, _, nErr := client.LoginById(user.Id, passwd)
if nErr != nil {
return &model.CommandResponse{Text: "Failed to login a user", ResponseType: model.CommandResponseTypeEphemeral}, nErr
}
post := &model.Post{
ChannelId: channel.Id,
Message: textMessage,
}
_, _, nErr = client.CreatePost(post)
if nErr != nil {
return &model.CommandResponse{Text: "Failed to create a post", ResponseType: model.CommandResponseTypeEphemeral}, nErr
}
return &model.CommandResponse{Text: "Added a post to " + channel.DisplayName, ResponseType: model.CommandResponseTypeEphemeral}, nil
}
func (*LoadTestProvider) URLCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) (*model.CommandResponse, error) {
url := strings.TrimSpace(strings.TrimPrefix(message, "url"))
if url == "" {
return &model.CommandResponse{Text: "Command must contain a url", ResponseType: model.CommandResponseTypeEphemeral}, nil
}
// provide a shortcut to easily access tests stored in doc/developer/tests
if !strings.HasPrefix(url, "http") {
url = "https://raw.githubusercontent.com/mattermost/mattermost-server/master/tests/" + url
if path.Ext(url) == "" {
url += ".md"
}
}
r, err := http.Get(url)
if err != nil {
return &model.CommandResponse{Text: "Unable to get file", ResponseType: model.CommandResponseTypeEphemeral}, err
}
defer func() {
io.Copy(io.Discard, r.Body)
r.Body.Close()
}()
if r.StatusCode > 400 {
return &model.CommandResponse{Text: "Unable to get file", ResponseType: model.CommandResponseTypeEphemeral}, errors.Errorf("unexpected status code %d", r.StatusCode)
}
bytes := make([]byte, 4000)
// break contents into 4000 byte posts
for {
length, err := r.Body.Read(bytes)
if err != nil && err != io.EOF {
return &model.CommandResponse{Text: "Encountered error reading file", ResponseType: model.CommandResponseTypeEphemeral}, err
}
if length == 0 {
break
}
post := &model.Post{}
post.Message = string(bytes[:length])
post.ChannelId = args.ChannelId
post.UserId = args.UserId
if _, err := a.CreatePostMissingChannel(c, post, false, true); err != nil {
return &model.CommandResponse{Text: "Unable to create post", ResponseType: model.CommandResponseTypeEphemeral}, err
}
}
return &model.CommandResponse{Text: "Loaded data", ResponseType: model.CommandResponseTypeEphemeral}, nil
}
func (*LoadTestProvider) JsonCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) (*model.CommandResponse, error) {
url := strings.TrimSpace(strings.TrimPrefix(message, "json"))
if url == "" {
return &model.CommandResponse{Text: "Command must contain a url", ResponseType: model.CommandResponseTypeEphemeral}, nil
}
// provide a shortcut to easily access tests stored in doc/developer/tests
if !strings.HasPrefix(url, "http") {
url = "https://raw.githubusercontent.com/mattermost/mattermost-server/master/tests/" + url
if path.Ext(url) == "" {
url += ".json"
}
}
r, err := http.Get(url)
if err != nil {
return &model.CommandResponse{Text: "Unable to get file", ResponseType: model.CommandResponseTypeEphemeral}, err
}
if r.StatusCode > 400 {
return &model.CommandResponse{Text: "Unable to get file", ResponseType: model.CommandResponseTypeEphemeral}, errors.Errorf("unexpected status code %d", r.StatusCode)
}
defer func() {
io.Copy(io.Discard, r.Body)
r.Body.Close()
}()
var post model.Post
if jsonErr := json.NewDecoder(r.Body).Decode(&post); jsonErr != nil {
return &model.CommandResponse{Text: "Unable to decode post", ResponseType: model.CommandResponseTypeEphemeral}, errors.Wrapf(jsonErr, "could not decode post from json")
}
post.ChannelId = args.ChannelId
post.UserId = args.UserId
if post.Message == "" {
post.Message = message
}
if _, err := a.CreatePostMissingChannel(c, &post, false, true); err != nil {
return &model.CommandResponse{Text: "Unable to create post", ResponseType: model.CommandResponseTypeEphemeral}, err
}
return &model.CommandResponse{Text: "Loaded data", ResponseType: model.CommandResponseTypeEphemeral}, nil
}
func parseRange(rng string) (utils.Range, error) {
tokens := strings.Split(rng, ",")
var begin int
var end int
var err1 error
var err2 error
switch {
case len(tokens) == 1:
begin, err1 = strconv.Atoi(tokens[0])
if err1 != nil || begin < 0 {
return utils.Range{Begin: 0, End: 0}, errors.New("Invalid range parameter")
}
end = begin
case len(tokens) == 2:
begin, err1 = strconv.Atoi(tokens[0])
end, err2 = strconv.Atoi(tokens[1])
if err1 != nil || err2 != nil || begin < 0 || end < begin {
return utils.Range{Begin: 0, End: 0}, errors.New("Invalid range parameter")
}
default:
return utils.Range{Begin: 0, End: 0}, errors.New("Invalid range parameter")
}
return utils.Range{Begin: begin, End: end}, nil
}
func contains(items []string, token string) bool {
for _, elem := range items {
if elem == token {
return true
}
}
return false
}

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

@@ -0,0 +1,41 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
)
type LogoutProvider struct {
}
const (
CmdLogout = "logout"
)
func init() {
app.RegisterCommandProvider(&LogoutProvider{})
}
func (*LogoutProvider) GetTrigger() string {
return CmdLogout
}
func (*LogoutProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model.Command {
return &model.Command{
Trigger: CmdLogout,
AutoComplete: true,
AutoCompleteDesc: T("api.command_logout.desc"),
AutoCompleteHint: "",
DisplayName: T("api.command_logout.name"),
}
}
func (*LogoutProvider) DoCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) *model.CommandResponse {
// Actual logout is handled client side.
return &model.CommandResponse{GotoLocation: "/login"}
}

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

@@ -0,0 +1,49 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
)
type MarketplaceProvider struct {
}
const (
CmdMarketplace = "marketplace"
)
func init() {
app.RegisterCommandProvider(&MarketplaceProvider{})
}
func (h *MarketplaceProvider) GetTrigger() string {
return CmdMarketplace
}
func (h *MarketplaceProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model.Command {
enabled := false
pluginSettings := a.Config().PluginSettings
if *pluginSettings.Enable && *pluginSettings.EnableMarketplace {
enabled = true
}
return &model.Command{
Trigger: CmdMarketplace,
AutoComplete: enabled,
AutoCompleteDesc: T("api.command_marketplace.desc"),
DisplayName: T("api.command_marketplace.name"),
}
}
func (h *MarketplaceProvider) DoCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) *model.CommandResponse {
// This command is handled client-side and shouldn't hit the server.
return &model.CommandResponse{
Text: args.T("api.command_marketplace.unsupported.app_error"),
ResponseType: model.CommandResponseTypeEphemeral,
}
}

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

@@ -0,0 +1,61 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
)
func TestMarketplaceProviderGetCommand(t *testing.T) {
th := setup(t).initBasic()
defer th.tearDown()
mp := MarketplaceProvider{}
testCases := []struct {
TestName string
PluginEnabled bool
MarketplaceEnabled bool
MustAutocomplete bool
}{
{
"All true",
true, true,
true,
},
{
"Plugin false",
false, true,
false,
},
{
"Marketplace false",
true, false,
false,
},
{
"All false",
false, false,
false,
},
}
for _, tc := range testCases {
t.Run(tc.TestName, func(t *testing.T) {
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.PluginSettings.Enable = tc.PluginEnabled
*cfg.PluginSettings.EnableMarketplace = tc.MarketplaceEnabled
})
cmd := mp.GetCommand(th.App, th.Context.T)
require.Equal(t, tc.MustAutocomplete, cmd.AutoComplete)
})
}
}

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

@@ -0,0 +1,44 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
)
type MeProvider struct {
}
const (
CmdMe = "me"
)
func init() {
app.RegisterCommandProvider(&MeProvider{})
}
func (*MeProvider) GetTrigger() string {
return CmdMe
}
func (*MeProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model.Command {
return &model.Command{
Trigger: CmdMe,
AutoComplete: true,
AutoCompleteDesc: T("api.command_me.desc"),
AutoCompleteHint: T("api.command_me.hint"),
DisplayName: T("api.command_me.name"),
}
}
func (*MeProvider) DoCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) *model.CommandResponse {
return &model.CommandResponse{
ResponseType: model.CommandResponseTypeInChannel,
Type: model.PostTypeMe,
Text: "*" + message + "*",
}
}

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

@@ -0,0 +1,27 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/mattermost/mattermost-server/v6/model"
)
func TestMeProviderDoCommand(t *testing.T) {
th := setup(t)
defer th.tearDown()
mp := MeProvider{}
msg := "hello"
resp := mp.DoCommand(th.App, th.Context, &model.CommandArgs{}, msg)
assert.Equal(t, model.CommandResponseTypeInChannel, resp.ResponseType)
assert.Equal(t, model.PostTypeMe, resp.Type)
assert.Equal(t, "*"+msg+"*", resp.Text)
}

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

@@ -0,0 +1,115 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"errors"
"strings"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type msgProvider struct {
}
const (
CmdMsg = "msg"
)
func init() {
app.RegisterCommandProvider(&msgProvider{})
}
func (*msgProvider) GetTrigger() string {
return CmdMsg
}
func (*msgProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model.Command {
return &model.Command{
Trigger: CmdMsg,
AutoComplete: true,
AutoCompleteDesc: T("api.command_msg.desc"),
AutoCompleteHint: T("api.command_msg.hint"),
DisplayName: T("api.command_msg.name"),
}
}
func (*msgProvider) DoCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) *model.CommandResponse {
splitMessage := strings.SplitN(message, " ", 2)
parsedMessage := ""
targetUsername := ""
if len(splitMessage) > 1 {
parsedMessage = strings.SplitN(message, " ", 2)[1]
}
targetUsername = strings.SplitN(message, " ", 2)[0]
targetUsername = strings.TrimPrefix(targetUsername, "@")
userProfile, nErr := a.Srv().Store().User().GetByUsername(targetUsername)
if nErr != nil {
mlog.Error(nErr.Error())
return &model.CommandResponse{Text: args.T("api.command_msg.missing.app_error"), ResponseType: model.CommandResponseTypeEphemeral}
}
if userProfile.Id == args.UserId {
return &model.CommandResponse{Text: args.T("api.command_msg.missing.app_error"), ResponseType: model.CommandResponseTypeEphemeral}
}
canSee, err := a.UserCanSeeOtherUser(args.UserId, userProfile.Id)
if err != nil {
mlog.Error(err.Error())
return &model.CommandResponse{Text: args.T("api.command_msg.fail.app_error"), ResponseType: model.CommandResponseTypeEphemeral}
}
if !canSee {
return &model.CommandResponse{Text: args.T("api.command_msg.missing.app_error"), ResponseType: model.CommandResponseTypeEphemeral}
}
// Find the channel based on this user
channelName := model.GetDMNameFromIds(args.UserId, userProfile.Id)
targetChannelId := ""
if channel, channelErr := a.Srv().Store().Channel().GetByName(args.TeamId, channelName, true); channelErr != nil {
var nfErr *store.ErrNotFound
if errors.As(channelErr, &nfErr) {
if !a.HasPermissionTo(args.UserId, model.PermissionCreateDirectChannel) {
return &model.CommandResponse{Text: args.T("api.command_msg.permission.app_error"), ResponseType: model.CommandResponseTypeEphemeral}
}
var directChannel *model.Channel
if directChannel, err = a.GetOrCreateDirectChannel(c, args.UserId, userProfile.Id); err != nil {
mlog.Error(err.Error())
return &model.CommandResponse{Text: args.T(err.Id), ResponseType: model.CommandResponseTypeEphemeral}
}
targetChannelId = directChannel.Id
} else {
mlog.Error(channelErr.Error())
return &model.CommandResponse{Text: args.T("api.command_msg.dm_fail.app_error"), ResponseType: model.CommandResponseTypeEphemeral}
}
} else {
targetChannelId = channel.Id
}
if parsedMessage != "" {
post := &model.Post{}
post.Message = parsedMessage
post.ChannelId = targetChannelId
post.UserId = args.UserId
if _, err = a.CreatePostMissingChannel(c, post, true, true); err != nil {
return &model.CommandResponse{Text: args.T("api.command_msg.fail.app_error"), ResponseType: model.CommandResponseTypeEphemeral}
}
}
team, err := a.GetTeam(args.TeamId)
if err != nil {
return &model.CommandResponse{Text: args.T("api.command_msg.fail.app_error"), ResponseType: model.CommandResponseTypeEphemeral}
}
return &model.CommandResponse{GotoLocation: args.SiteURL + "/" + team.Name + "/channels/" + channelName, Text: "", ResponseType: model.CommandResponseTypeEphemeral}
}

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

@@ -0,0 +1,94 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
)
func TestMsgProvider(t *testing.T) {
th := setup(t).initBasic()
defer th.tearDown()
team := th.createTeam()
th.linkUserToTeam(th.BasicUser, team)
cmd := &msgProvider{}
th.removePermissionFromRole(model.PermissionCreateDirectChannel.Id, model.SystemUserRoleId)
// Check without permission to create a DM channel.
resp := cmd.DoCommand(th.App, th.Context, &model.CommandArgs{
T: i18n.IdentityTfunc(),
SiteURL: "http://test.url",
TeamId: team.Id,
UserId: th.BasicUser.Id,
}, "@"+th.BasicUser2.Username+" hello")
channelName := model.GetDMNameFromIds(th.BasicUser.Id, th.BasicUser2.Id)
assert.Equal(t, "api.command_msg.permission.app_error", resp.Text)
assert.Equal(t, "", resp.GotoLocation)
th.addPermissionToRole(model.PermissionCreateDirectChannel.Id, model.SystemUserRoleId)
// Check with permission to create a DM channel.
resp = cmd.DoCommand(th.App, th.Context, &model.CommandArgs{
T: i18n.IdentityTfunc(),
SiteURL: "http://test.url",
TeamId: team.Id,
UserId: th.BasicUser.Id,
}, "@"+th.BasicUser2.Username+" hello")
assert.Equal(t, "", resp.Text)
assert.Equal(t, "http://test.url/"+team.Name+"/channels/"+channelName, resp.GotoLocation)
// Check without permission to post to an existing DM channel.
resp = cmd.DoCommand(th.App, th.Context, &model.CommandArgs{
T: i18n.IdentityTfunc(),
SiteURL: "http://test.url",
TeamId: team.Id,
UserId: th.BasicUser.Id,
}, "@"+th.BasicUser2.Username+" hello")
assert.Equal(t, "", resp.Text)
assert.Equal(t, "http://test.url/"+team.Name+"/channels/"+channelName, resp.GotoLocation)
// Check that a guest user cannot message a user who is not in a channel/team with him
guest := th.createGuest()
user := th.createUser()
th.linkUserToTeam(user, team)
th.linkUserToTeam(guest, th.BasicTeam)
th.addUserToChannel(guest, th.BasicChannel)
resp = cmd.DoCommand(th.App, th.Context, &model.CommandArgs{
T: i18n.IdentityTfunc(),
SiteURL: "http://test.url",
TeamId: th.BasicTeam.Id,
UserId: guest.Id,
}, "@"+user.Username+" hello")
assert.Equal(t, "api.command_msg.missing.app_error", resp.Text)
assert.Equal(t, "", resp.GotoLocation)
// Check that a guest user can message a user who is in a channel/team with him
th.linkUserToTeam(user, th.BasicTeam)
th.addUserToChannel(user, th.BasicChannel)
resp = cmd.DoCommand(th.App, th.Context, &model.CommandArgs{
T: i18n.IdentityTfunc(),
SiteURL: "http://test.url",
TeamId: th.BasicTeam.Id,
UserId: guest.Id,
}, "@"+user.Username+" hello")
channelName = model.GetDMNameFromIds(guest.Id, user.Id)
assert.Equal(t, "", resp.Text)
assert.Equal(t, "http://test.url/"+th.BasicTeam.Name+"/channels/"+channelName, resp.GotoLocation)
}

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

@@ -0,0 +1,82 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"strings"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
)
type MuteProvider struct {
}
const (
CmdMute = "mute"
)
func init() {
app.RegisterCommandProvider(&MuteProvider{})
}
func (*MuteProvider) GetTrigger() string {
return CmdMute
}
func (*MuteProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model.Command {
return &model.Command{
Trigger: CmdMute,
AutoComplete: true,
AutoCompleteDesc: T("api.command_mute.desc"),
AutoCompleteHint: T("api.command_mute.hint"),
DisplayName: T("api.command_mute.name"),
}
}
func (*MuteProvider) DoCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) *model.CommandResponse {
var channel *model.Channel
var noChannelErr *model.AppError
if channel, noChannelErr = a.GetChannel(c, args.ChannelId); noChannelErr != nil {
return &model.CommandResponse{Text: args.T("api.command_mute.no_channel.error"), ResponseType: model.CommandResponseTypeEphemeral}
}
channelName := ""
splitMessage := strings.Split(message, " ")
// Overwrite channel with channel-handle if set
if strings.HasPrefix(message, "~") {
channelName = splitMessage[0][1:]
} else {
channelName = splitMessage[0]
}
if channelName != "" && message != "" {
channel, _ = a.Srv().Store().Channel().GetByName(channel.TeamId, channelName, true)
if channel == nil {
return &model.CommandResponse{Text: args.T("api.command_mute.error", map[string]any{"Channel": channelName}), ResponseType: model.CommandResponseTypeEphemeral}
}
}
channelMember, err := a.ToggleMuteChannel(c, channel.Id, args.UserId)
if err != nil {
return &model.CommandResponse{Text: args.T("api.command_mute.not_member.error", map[string]any{"Channel": channelName}), ResponseType: model.CommandResponseTypeEphemeral}
}
// Direct and Group messages won't have a nice channel title, omit it
if channel.Type == model.ChannelTypeDirect || channel.Type == model.ChannelTypeGroup {
if channelMember.NotifyProps[model.MarkUnreadNotifyProp] == model.ChannelNotifyMention {
return &model.CommandResponse{Text: args.T("api.command_mute.success_mute_direct_msg"), ResponseType: model.CommandResponseTypeEphemeral}
}
return &model.CommandResponse{Text: args.T("api.command_mute.success_unmute_direct_msg"), ResponseType: model.CommandResponseTypeEphemeral}
}
if channelMember.NotifyProps[model.MarkUnreadNotifyProp] == model.ChannelNotifyMention {
return &model.CommandResponse{Text: args.T("api.command_mute.success_mute", map[string]any{"Channel": channel.DisplayName}), ResponseType: model.CommandResponseTypeEphemeral}
}
return &model.CommandResponse{Text: args.T("api.command_mute.success_unmute", map[string]any{"Channel": channel.DisplayName}), ResponseType: model.CommandResponseTypeEphemeral}
}

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

@@ -0,0 +1,204 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
)
func TestMuteCommandNoChannel(t *testing.T) {
th := setup(t).initBasic()
defer th.tearDown()
if testing.Short() {
t.SkipNow()
}
channel1 := th.BasicChannel
channel1M, channel1MError := th.App.GetChannelMember(th.Context, channel1.Id, th.BasicUser.Id)
assert.Nil(t, channel1MError, "User is not a member of channel 1")
assert.NotEqual(
t,
channel1M.NotifyProps[model.MarkUnreadNotifyProp],
model.ChannelNotifyMention,
"Channel shouldn't be muted on initial setup",
)
cmd := &MuteProvider{}
resp := cmd.DoCommand(th.App, th.Context, &model.CommandArgs{
T: i18n.IdentityTfunc(),
UserId: th.BasicUser.Id,
}, "")
assert.Equal(t, "api.command_mute.no_channel.error", resp.Text)
}
func TestMuteCommandNoArgs(t *testing.T) {
th := setup(t).initBasic()
defer th.tearDown()
channel1 := th.BasicChannel
channel1M, _ := th.App.GetChannelMember(th.Context, channel1.Id, th.BasicUser.Id)
assert.Equal(t, model.ChannelNotifyAll, channel1M.NotifyProps[model.MarkUnreadNotifyProp])
cmd := &MuteProvider{}
// First mute the channel
resp := cmd.DoCommand(th.App, th.Context, &model.CommandArgs{
T: i18n.IdentityTfunc(),
ChannelId: channel1.Id,
UserId: th.BasicUser.Id,
}, "")
assert.Equal(t, "api.command_mute.success_mute", resp.Text)
// Now unmute the channel
time.Sleep(time.Millisecond)
resp = cmd.DoCommand(th.App, th.Context, &model.CommandArgs{
T: i18n.IdentityTfunc(),
ChannelId: channel1.Id,
UserId: th.BasicUser.Id,
}, "")
assert.Equal(t, "api.command_mute.success_unmute", resp.Text)
}
func TestMuteCommandSpecificChannel(t *testing.T) {
th := setup(t).initBasic()
defer th.tearDown()
if testing.Short() {
t.SkipNow()
}
channel1 := th.BasicChannel
channel2, _ := th.App.CreateChannel(th.Context, &model.Channel{
DisplayName: "AA",
Name: "aa" + model.NewId() + "a",
Type: model.ChannelTypeOpen,
TeamId: th.BasicTeam.Id,
CreatorId: th.BasicUser.Id,
}, true)
channel2M, _ := th.App.GetChannelMember(th.Context, channel2.Id, th.BasicUser.Id)
assert.Equal(t, model.ChannelNotifyAll, channel2M.NotifyProps[model.MarkUnreadNotifyProp])
cmd := &MuteProvider{}
// First mute the channel
resp := cmd.DoCommand(th.App, th.Context, &model.CommandArgs{
T: i18n.IdentityTfunc(),
ChannelId: channel1.Id,
UserId: th.BasicUser.Id,
}, channel2.Name)
assert.Equal(t, "api.command_mute.success_mute", resp.Text)
channel2M, _ = th.App.GetChannelMember(th.Context, channel2.Id, th.BasicUser.Id)
assert.Equal(t, model.ChannelNotifyMention, channel2M.NotifyProps[model.MarkUnreadNotifyProp])
// Now unmute the channel
resp = cmd.DoCommand(th.App, th.Context, &model.CommandArgs{
T: i18n.IdentityTfunc(),
ChannelId: channel1.Id,
UserId: th.BasicUser.Id,
}, "~"+channel2.Name)
assert.Equal(t, "api.command_mute.success_unmute", resp.Text)
channel2M, _ = th.App.GetChannelMember(th.Context, channel2.Id, th.BasicUser.Id)
assert.Equal(t, model.ChannelNotifyAll, channel2M.NotifyProps[model.MarkUnreadNotifyProp])
}
func TestMuteCommandNotMember(t *testing.T) {
th := setup(t).initBasic()
defer th.tearDown()
if testing.Short() {
t.SkipNow()
}
channel1 := th.BasicChannel
channel2, _ := th.App.CreateChannel(th.Context, &model.Channel{
DisplayName: "AA",
Name: "aa" + model.NewId() + "a",
Type: model.ChannelTypeOpen,
TeamId: th.BasicTeam.Id,
CreatorId: th.BasicUser.Id,
}, false)
cmd := &MuteProvider{}
// First mute the channel
resp := cmd.DoCommand(th.App, th.Context, &model.CommandArgs{
T: i18n.IdentityTfunc(),
ChannelId: channel1.Id,
UserId: th.BasicUser.Id,
}, channel2.Name)
assert.Equal(t, "api.command_mute.not_member.error", resp.Text)
}
func TestMuteCommandNotChannel(t *testing.T) {
th := setup(t).initBasic()
defer th.tearDown()
if testing.Short() {
t.SkipNow()
}
channel1 := th.BasicChannel
cmd := &MuteProvider{}
// First mute the channel
resp := cmd.DoCommand(th.App, th.Context, &model.CommandArgs{
T: i18n.IdentityTfunc(),
ChannelId: channel1.Id,
UserId: th.BasicUser.Id,
}, "~noexists")
assert.Equal(t, "api.command_mute.error", resp.Text)
}
func TestMuteCommandDMChannel(t *testing.T) {
th := setup(t).initBasic()
defer th.tearDown()
if testing.Short() {
t.SkipNow()
}
channel2, _ := th.App.GetOrCreateDirectChannel(th.Context, th.BasicUser.Id, th.BasicUser2.Id)
channel2M, _ := th.App.GetChannelMember(th.Context, channel2.Id, th.BasicUser.Id)
assert.Equal(t, model.ChannelNotifyAll, channel2M.NotifyProps[model.MarkUnreadNotifyProp])
cmd := &MuteProvider{}
// First mute the channel
resp := cmd.DoCommand(th.App, th.Context, &model.CommandArgs{
T: i18n.IdentityTfunc(),
ChannelId: channel2.Id,
UserId: th.BasicUser.Id,
}, "")
assert.Equal(t, "api.command_mute.success_mute_direct_msg", resp.Text)
time.Sleep(time.Millisecond)
channel2M, _ = th.App.GetChannelMember(th.Context, channel2.Id, th.BasicUser.Id)
assert.Equal(t, model.ChannelNotifyMention, channel2M.NotifyProps[model.MarkUnreadNotifyProp])
// Now unmute the channel
resp = cmd.DoCommand(th.App, th.Context, &model.CommandArgs{
T: i18n.IdentityTfunc(),
ChannelId: channel2.Id,
UserId: th.BasicUser.Id,
}, "")
assert.Equal(t, "api.command_mute.success_unmute_direct_msg", resp.Text)
time.Sleep(time.Millisecond)
channel2M, _ = th.App.GetChannelMember(th.Context, channel2.Id, th.BasicUser.Id)
assert.Equal(t, model.ChannelNotifyAll, channel2M.NotifyProps[model.MarkUnreadNotifyProp])
}

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

@@ -0,0 +1,41 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
)
type OfflineProvider struct {
}
const (
CmdOffline = "offline"
)
func init() {
app.RegisterCommandProvider(&OfflineProvider{})
}
func (*OfflineProvider) GetTrigger() string {
return CmdOffline
}
func (*OfflineProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model.Command {
return &model.Command{
Trigger: CmdOffline,
AutoComplete: true,
AutoCompleteDesc: T("api.command_offline.desc"),
DisplayName: T("api.command_offline.name"),
}
}
func (*OfflineProvider) DoCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) *model.CommandResponse {
a.SetStatusOffline(args.UserId, true)
return &model.CommandResponse{ResponseType: model.CommandResponseTypeEphemeral, Text: args.T("api.command_offline.success")}
}

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

@@ -0,0 +1,41 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
)
type OnlineProvider struct {
}
const (
CmdOnline = "online"
)
func init() {
app.RegisterCommandProvider(&OnlineProvider{})
}
func (*OnlineProvider) GetTrigger() string {
return CmdOnline
}
func (*OnlineProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model.Command {
return &model.Command{
Trigger: CmdOnline,
AutoComplete: true,
AutoCompleteDesc: T("api.command_online.desc"),
DisplayName: T("api.command_online.name"),
}
}
func (*OnlineProvider) DoCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) *model.CommandResponse {
a.SetStatusOnline(args.UserId, true)
return &model.CommandResponse{ResponseType: model.CommandResponseTypeEphemeral, Text: args.T("api.command_online.success")}
}

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

@@ -0,0 +1,33 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
)
type OpenProvider struct {
JoinProvider
}
const (
CmdOpen = "open"
)
func init() {
app.RegisterCommandProvider(&OpenProvider{})
}
func (open *OpenProvider) GetTrigger() string {
return CmdOpen
}
func (open *OpenProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model.Command {
cmd := open.JoinProvider.GetCommand(a, T)
cmd.Trigger = CmdOpen
cmd.DisplayName = T("api.command_open.name")
return cmd
}

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

@@ -0,0 +1,299 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"encoding/base64"
"errors"
"fmt"
"strings"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
)
const (
AvailableRemoteActions = "create, accept, remove, status"
)
type RemoteProvider struct {
}
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, c request.CTX, args *model.CommandArgs, message string) *model.CommandResponse {
if !a.HasPermissionTo(args.UserId, model.PermissionManageSecureConnections) {
return responsef(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 responsef(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 responsef(args.T("api.command_remote.unknown_action", map[string]any{"Action": action}))
}
func (rp *RemoteProvider) GetAutoCompleteListItems(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 responsef(args.T("api.command_remote.missing_empty", map[string]any{"Arg": "password"}))
}
name := margs["name"]
if name == "" {
return responsef(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 responsef(args.T("api.command_remote.site_url_not_set"))
}
rc := &model.RemoteCluster{
Name: name,
DisplayName: displayname,
Token: model.NewId(),
CreatorId: args.UserId,
}
rcSaved, appErr := a.AddRemoteCluster(rc)
if appErr != nil {
return responsef(args.T("api.command_remote.add_remote.error", map[string]any{"Error": appErr.Error()}))
}
// Display the encrypted invitation
invite := &model.RemoteClusterInvite{
RemoteId: rcSaved.RemoteId,
RemoteTeamId: args.TeamId,
SiteURL: url,
Token: rcSaved.Token,
}
encrypted, err := invite.Encrypt(password)
if err != nil {
return responsef(args.T("api.command_remote.encrypt_invitation.error", map[string]any{"Error": err.Error()}))
}
encoded := base64.URLEncoding.EncodeToString(encrypted)
return responsef("##### " + args.T("api.command_remote.invitation_created") + "\n" +
args.T("api.command_remote.invite_summary", map[string]any{"Command": "/secure-connection accept", "Invitation": encoded, "SiteURL": invite.SiteURL}))
}
// 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 responsef(args.T("api.command_remote.missing_empty", map[string]any{"Arg": "password"}))
}
name := margs["name"]
if name == "" {
return responsef(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 responsef(args.T("api.command_remote.missing_empty", map[string]any{"Arg": "invite"}))
}
// invite is encoded as base64 and encrypted
decoded, err := base64.URLEncoding.DecodeString(blob)
if err != nil {
return responsef(args.T("api.command_remote.decode_invitation.error", map[string]any{"Error": err.Error()}))
}
invite := &model.RemoteClusterInvite{}
err = invite.Decrypt(decoded, password)
if err != nil {
return responsef(args.T("api.command_remote.incorrect_password.error", map[string]any{"Error": err.Error()}))
}
rcs, _ := a.GetRemoteClusterService()
if rcs == nil {
return responsef(args.T("api.command_remote.service_not_enabled"))
}
url := a.GetSiteURL()
if url == "" {
return responsef(args.T("api.command_remote.site_url_not_set"))
}
rc, err := rcs.AcceptInvitation(invite, name, displayname, args.UserId, args.TeamId, url)
if err != nil {
return responsef(args.T("api.command_remote.accept_invitation.error", map[string]any{"Error": err.Error()}))
}
return responsef("##### " + args.T("api.command_remote.accept_invitation", map[string]any{"SiteURL": rc.SiteURL}))
}
// 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 responsef(args.T("api.command_remote.missing_empty", map[string]any{"Arg": "remoteId"}))
}
deleted, err := a.DeleteRemoteCluster(id)
if err != nil {
responsef(args.T("api.command_remote.remove_remote.error", map[string]any{"Error": err.Error()}))
}
result := "removed"
if !deleted {
result = "**NOT FOUND**"
}
return responsef("##### " + 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(model.RemoteClusterQueryFilter{})
if err != nil {
responsef(args.T("api.command_remote.fetch_status.error", map[string]any{"Error": err.Error()}))
}
if len(list) == 0 {
return responsef("** " + args.T("api.command_remote.remotes_not_found") + " **")
}
var sb strings.Builder
fmt.Fprintf(&sb, args.T("api.command_remote.remote_table_header")+" \n")
// | Secure Connection | Display name | ConnectionID | Site URL | Invite accepted | Online | Last ping |
fmt.Fprintf(&sb, "| :---- | :---- | :---- | :---- | :---- | :---- | :---- | \n")
for _, rc := range list {
accepted := formatBool(args.T, rc.SiteURL != "")
online := formatBool(args.T, isOnline(rc.LastPingAt))
lastPing := formatTimestamp(rc.LastPingAt)
fmt.Fprintf(&sb, "| %s | %s | %s | %s | %s | %s | %s |\n", rc.Name, rc.DisplayName, rc.RemoteId, rc.SiteURL, accepted, online, lastPing)
}
return responsef(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(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.SiteURL)}
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(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.SiteURL)}
list = append(list, item)
}
return list, nil
}

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

@@ -0,0 +1,153 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"strings"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type RemoveProvider struct {
}
type KickProvider struct {
}
const (
CmdRemove = "remove"
CmdKick = "kick"
)
func init() {
app.RegisterCommandProvider(&RemoveProvider{})
app.RegisterCommandProvider(&KickProvider{})
}
func (*RemoveProvider) GetTrigger() string {
return CmdRemove
}
func (*KickProvider) GetTrigger() string {
return CmdKick
}
func (*RemoveProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model.Command {
return &model.Command{
Trigger: CmdRemove,
AutoComplete: true,
AutoCompleteDesc: T("api.command_remove.desc"),
AutoCompleteHint: T("api.command_remove.hint"),
DisplayName: T("api.command_remove.name"),
}
}
func (*KickProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model.Command {
return &model.Command{
Trigger: CmdKick,
AutoComplete: true,
AutoCompleteDesc: T("api.command_remove.desc"),
AutoCompleteHint: T("api.command_remove.hint"),
DisplayName: T("api.command_kick.name"),
}
}
func (*RemoveProvider) DoCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) *model.CommandResponse {
return doCommand(a, c, args, message)
}
func (*KickProvider) DoCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) *model.CommandResponse {
return doCommand(a, c, args, message)
}
func doCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) *model.CommandResponse {
channel, err := a.GetChannel(c, args.ChannelId)
if err != nil {
return &model.CommandResponse{
Text: args.T("api.command_channel_remove.channel.app_error"),
ResponseType: model.CommandResponseTypeEphemeral,
}
}
switch channel.Type {
case model.ChannelTypeOpen:
if !a.HasPermissionToChannel(c, args.UserId, args.ChannelId, model.PermissionManagePublicChannelMembers) {
return &model.CommandResponse{
Text: args.T("api.command_remove.permission.app_error"),
ResponseType: model.CommandResponseTypeEphemeral,
}
}
case model.ChannelTypePrivate:
if !a.HasPermissionToChannel(c, args.UserId, args.ChannelId, model.PermissionManagePrivateChannelMembers) {
return &model.CommandResponse{
Text: args.T("api.command_remove.permission.app_error"),
ResponseType: model.CommandResponseTypeEphemeral,
}
}
default:
return &model.CommandResponse{
Text: args.T("api.command_remove.direct_group.app_error"),
ResponseType: model.CommandResponseTypeEphemeral,
}
}
if message == "" {
return &model.CommandResponse{
Text: args.T("api.command_remove.message.app_error"),
ResponseType: model.CommandResponseTypeEphemeral,
}
}
targetUsername := ""
targetUsername = strings.SplitN(message, " ", 2)[0]
targetUsername = strings.TrimPrefix(targetUsername, "@")
userProfile, nErr := a.Srv().Store().User().GetByUsername(targetUsername)
if nErr != nil {
mlog.Error(nErr.Error())
return &model.CommandResponse{
Text: args.T("api.command_remove.missing.app_error"),
ResponseType: model.CommandResponseTypeEphemeral,
}
}
if userProfile.DeleteAt != 0 {
return &model.CommandResponse{
Text: args.T("api.command_remove.missing.app_error"),
ResponseType: model.CommandResponseTypeEphemeral,
}
}
_, err = a.GetChannelMember(c, args.ChannelId, userProfile.Id)
if err != nil {
nameFormat := *a.Config().TeamSettings.TeammateNameDisplay
return &model.CommandResponse{
Text: args.T("api.command_remove.user_not_in_channel", map[string]any{
"Username": userProfile.GetDisplayName(nameFormat),
}),
ResponseType: model.CommandResponseTypeEphemeral,
}
}
if err = a.RemoveUserFromChannel(c, userProfile.Id, args.UserId, channel); err != nil {
var text string
if err.Id == "api.channel.remove_members.denied" {
text = args.T("api.command_remove.group_constrained_user_denied")
} else {
text = args.T(err.Id, map[string]any{
"Channel": model.DefaultChannelName,
})
}
return &model.CommandResponse{
Text: text,
ResponseType: model.CommandResponseTypeEphemeral,
}
}
return &model.CommandResponse{}
}

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

@@ -0,0 +1,124 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/mattermost/mattermost-server/v6/model"
)
func TestRemoveProviderDoCommand(t *testing.T) {
th := setup(t).initBasic()
defer th.tearDown()
rp := RemoveProvider{}
publicChannel, _ := th.App.CreateChannel(th.Context, &model.Channel{
DisplayName: "AA",
Name: "aa" + model.NewId() + "a",
Type: model.ChannelTypeOpen,
TeamId: th.BasicTeam.Id,
CreatorId: th.BasicUser.Id,
}, false)
privateChannel, _ := th.App.CreateChannel(th.Context, &model.Channel{
DisplayName: "BB",
Name: "aa" + model.NewId() + "a",
Type: model.ChannelTypeOpen,
TeamId: th.BasicTeam.Id,
CreatorId: th.BasicUser.Id,
}, false)
targetUser := th.createUser()
th.App.AddUserToTeam(th.Context, th.BasicTeam.Id, targetUser.Id, targetUser.Id)
th.App.AddUserToChannel(th.Context, targetUser, publicChannel, false)
th.App.AddUserToChannel(th.Context, targetUser, privateChannel, false)
// Try a public channel *without* permission.
args := &model.CommandArgs{
T: func(s string, args ...any) string { return s },
ChannelId: publicChannel.Id,
UserId: th.BasicUser.Id,
}
actual := rp.DoCommand(th.App, th.Context, args, targetUser.Username).Text
assert.Equal(t, "api.command_remove.permission.app_error", actual)
// Try a public channel *with* permission.
th.App.AddUserToChannel(th.Context, th.BasicUser, publicChannel, false)
args = &model.CommandArgs{
T: func(s string, args ...any) string { return s },
ChannelId: publicChannel.Id,
UserId: th.BasicUser.Id,
}
actual = rp.DoCommand(th.App, th.Context, args, targetUser.Username).Text
assert.Equal(t, "", actual)
// Try a private channel *without* permission.
args = &model.CommandArgs{
T: func(s string, args ...any) string { return s },
ChannelId: privateChannel.Id,
UserId: th.BasicUser.Id,
}
actual = rp.DoCommand(th.App, th.Context, args, targetUser.Username).Text
assert.Equal(t, "api.command_remove.permission.app_error", actual)
// Try a private channel *with* permission.
th.App.AddUserToChannel(th.Context, th.BasicUser, privateChannel, false)
args = &model.CommandArgs{
T: func(s string, args ...any) string { return s },
ChannelId: privateChannel.Id,
UserId: th.BasicUser.Id,
}
actual = rp.DoCommand(th.App, th.Context, args, targetUser.Username).Text
assert.Equal(t, "", actual)
// Try a group channel
user1 := th.createUser()
user2 := th.createUser()
groupChannel := th.createGroupChannel(user1, user2)
args = &model.CommandArgs{
T: func(s string, args ...any) string { return s },
ChannelId: groupChannel.Id,
UserId: th.BasicUser.Id,
}
actual = rp.DoCommand(th.App, th.Context, args, user1.Username).Text
assert.Equal(t, "api.command_remove.direct_group.app_error", actual)
// Try a direct channel *with* being a member.
directChannel := th.createDmChannel(user1)
args = &model.CommandArgs{
T: func(s string, args ...any) string { return s },
ChannelId: directChannel.Id,
UserId: th.BasicUser.Id,
}
actual = rp.DoCommand(th.App, th.Context, args, user1.Username).Text
assert.Equal(t, "api.command_remove.direct_group.app_error", actual)
// Try a public channel with a deactivated user.
deactivatedUser := th.createUser()
th.App.AddUserToTeam(th.Context, th.BasicTeam.Id, deactivatedUser.Id, deactivatedUser.Id)
th.App.AddUserToChannel(th.Context, deactivatedUser, publicChannel, false)
th.App.UpdateActive(th.Context, deactivatedUser, false)
args = &model.CommandArgs{
T: func(s string, args ...any) string { return s },
ChannelId: publicChannel.Id,
UserId: th.BasicUser.Id,
}
actual = rp.DoCommand(th.App, th.Context, args, deactivatedUser.Username).Text
assert.Equal(t, "api.command_remove.missing.app_error", actual)
}

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

@@ -0,0 +1,44 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
)
type SearchProvider struct {
}
const (
CmdSearch = "search"
)
func init() {
app.RegisterCommandProvider(&SearchProvider{})
}
func (search *SearchProvider) GetTrigger() string {
return CmdSearch
}
func (search *SearchProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model.Command {
return &model.Command{
Trigger: CmdSearch,
AutoComplete: true,
AutoCompleteDesc: T("api.command_search.desc"),
AutoCompleteHint: T("api.command_search.hint"),
DisplayName: T("api.command_search.name"),
}
}
func (search *SearchProvider) DoCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) *model.CommandResponse {
// This command is handled client-side and shouldn't hit the server.
return &model.CommandResponse{
Text: args.T("api.command_search.unsupported.app_error"),
ResponseType: model.CommandResponseTypeEphemeral,
}
}

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

@@ -0,0 +1,44 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
)
type SettingsProvider struct {
}
const (
CmdSettings = "settings"
)
func init() {
app.RegisterCommandProvider(&SettingsProvider{})
}
func (settings *SettingsProvider) GetTrigger() string {
return CmdSettings
}
func (settings *SettingsProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model.Command {
return &model.Command{
Trigger: CmdSettings,
AutoComplete: true,
AutoCompleteDesc: T("api.command_settings.desc"),
AutoCompleteHint: "",
DisplayName: T("api.command_settings.name"),
}
}
func (settings *SettingsProvider) DoCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) *model.CommandResponse {
// This command is handled client-side and shouldn't hit the server.
return &model.CommandResponse{
Text: args.T("api.command_settings.unsupported.app_error"),
ResponseType: model.CommandResponseTypeEphemeral,
}
}

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

@@ -0,0 +1,329 @@
// 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/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
)
type ShareProvider struct {
}
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(c 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(c, 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(c request.CTX, a *app.App, commandArgs *model.CommandArgs, arg *model.AutocompleteArg) ([]model.AutocompleteListItem, error) {
channel, err := a.GetChannel(c, 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, c request.CTX, args *model.CommandArgs, message string) *model.CommandResponse {
if !a.HasPermissionTo(args.UserId, model.PermissionManageSharedChannels) {
return responsef(args.T("api.command_share.permission_required", map[string]any{"Permission": "manage_shared_channels"}))
}
if a.Srv().GetSharedChannelSyncService() == nil {
return responsef(args.T("api.command_share.service_disabled"))
}
if a.Srv().GetRemoteClusterService() == nil {
return responsef(args.T("api.command_remote.service_disabled"))
}
margs := parseNamedArgs(args.Command)
action, ok := margs[ActionKey]
if !ok {
return responsef(args.T("api.command_share.missing_action", map[string]any{"Actions": AvailableShareActions}))
}
switch action {
case "share":
return sp.doShareChannel(a, c, args, margs)
case "unshare":
return sp.doUnshareChannel(a, args, margs)
case "invite":
return sp.doInviteRemote(a, c, args, margs)
case "uninvite":
return sp.doUninviteRemote(a, args, margs)
case "status":
return sp.doStatus(a, args, margs)
}
return responsef(args.T("api.command_share.unknown_action", map[string]any{"Action": action, "Actions": AvailableShareActions}))
}
func (sp *ShareProvider) doShareChannel(a *app.App, c request.CTX, args *model.CommandArgs, margs map[string]string) *model.CommandResponse {
// check that channel exists.
channel, errApp := a.GetChannel(c, args.ChannelId)
if errApp != nil {
return responsef(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 responsef(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.SaveSharedChannel(c, sc); err != nil {
return responsef(args.T("api.command_share.share_channel.error", map[string]any{"Error": err.Error()}))
}
notifyClientsForChannelUpdate(a, sc)
return responsef("##### " + args.T("api.command_share.channel_shared"))
}
func (sp *ShareProvider) doUnshareChannel(a *app.App, args *model.CommandArgs, margs map[string]string) *model.CommandResponse {
sc, appErr := a.GetSharedChannel(args.ChannelId)
if appErr != nil {
return responsef(args.T("api.command_share.shared_channel_unshare.error", map[string]any{"Error": appErr.Error()}))
}
deleted, err := a.DeleteSharedChannel(args.ChannelId)
if err != nil {
return responsef(args.T("api.command_share.shared_channel_unshare.error", map[string]any{"Error": err.Error()}))
}
if !deleted {
return responsef(args.T("api.command_share.not_shared_channel_unshare"))
}
notifyClientsForChannelUpdate(a, sc)
return responsef("##### " + args.T("api.command_share.shared_channel_unavailable"))
}
func (sp *ShareProvider) doInviteRemote(a *app.App, c request.CTX, args *model.CommandArgs, margs map[string]string) (resp *model.CommandResponse) {
remoteId, ok := margs["connectionID"]
if !ok || remoteId == "" {
return responsef(args.T("api.command_share.must_specify_valid_remote"))
}
hasRemote, err := a.HasRemote(args.ChannelId, remoteId)
if err != nil {
return responsef(args.T("api.command_share.fetch_remote.error", map[string]any{"Error": err.Error()}))
}
if hasRemote {
return responsef(args.T("api.command_share.remote_already_invited"))
}
// Check if channel is shared or not.
hasChan, err := a.HasSharedChannel(args.ChannelId)
if err != nil {
return responsef(args.T("api.command_share.check_channel_exist.error", map[string]any{"Error": err.Error()}))
}
if !hasChan {
// If it doesn't exist, then create it.
resp2 := sp.doShareChannel(a, c, args, margs)
// We modify the outgoing response by prepending the text
// from the shareChannel response.
defer func() {
resp.Text = resp2.Text + "\n" + resp.Text
}()
}
// don't allow invitation to shared channel originating from remote.
// (also blocks cyclic invitations)
if err := a.CheckCanInviteToSharedChannel(args.ChannelId); err != nil {
return responsef(args.T("api.command_share.channel_invite_not_home.error"))
}
rc, appErr := a.GetRemoteCluster(remoteId)
if appErr != nil {
return responsef(args.T("api.command_share.remote_id_invalid.error", map[string]any{"Error": appErr.Error()}))
}
channel, errApp := a.GetChannel(c, args.ChannelId)
if errApp != nil {
return responsef(args.T("api.command_share.channel_invite.error", map[string]any{"Name": rc.DisplayName, "Error": errApp.Error()}))
}
// send channel invite to remote cluster
if err := a.Srv().GetSharedChannelSyncService().SendChannelInvite(channel, args.UserId, rc); err != nil {
return responsef(args.T("api.command_share.channel_invite.error", map[string]any{"Name": rc.DisplayName, "Error": err.Error()}))
}
return responsef("##### " + 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 responsef(args.T("api.command_share.remote_not_valid"))
}
scr, err := a.GetSharedChannelRemoteByIds(args.ChannelId, remoteId)
if err != nil || scr.ChannelId != args.ChannelId {
return responsef(args.T("api.command_share.channel_remote_id_not_exists", map[string]any{"RemoteId": remoteId}))
}
deleted, err := a.DeleteSharedChannelRemote(scr.Id)
if err != nil || !deleted {
return responsef(args.T("api.command_share.could_not_uninvite.error", map[string]any{"RemoteId": remoteId, "Error": err.Error()}))
}
return responsef("##### " + 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 responsef(args.T("api.command_share.fetch_remote_status.error", map[string]any{"Error": err.Error()}))
}
if len(statuses) == 0 {
return responsef(args.T("api.command_share.no_remote_invited"))
}
var sb strings.Builder
fmt.Fprintf(&sb, args.T("api.command_share.channel_status_id", map[string]any{"ChannelId": statuses[0].ChannelId})+"\n\n")
fmt.Fprintf(&sb, args.T("api.command_share.remote_table_header")+" \n")
// "| 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 responsef(sb.String())
}
func notifyClientsForChannelUpdate(a *app.App, sharedChannel *model.SharedChannel) {
messageWs := model.NewWebSocketEvent(model.WebsocketEventChannelConverted, sharedChannel.TeamId, "", "", nil, "")
messageWs.Add("channel_id", sharedChannel.ChannelId)
a.Publish(messageWs)
}

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

@@ -0,0 +1,93 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"bytes"
"testing"
"github.com/stretchr/testify/assert"
"github.com/mattermost/mattermost-server/v6/server/channels/testlib"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/platform/services/remotecluster"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
)
func TestShareProviderDoCommand(t *testing.T) {
t.Run("share command sends a websocket channel converted event", func(t *testing.T) {
th := setup(t).initBasic()
defer th.tearDown()
th.addPermissionToRole(model.PermissionManageSharedChannels.Id, th.BasicUser.Roles)
mockSyncService := app.NewMockSharedChannelService(nil)
th.Server.SetSharedChannelSyncService(mockSyncService)
mockRemoteCluster, err := remotecluster.NewRemoteClusterService(th.Server)
require.NoError(t, err)
th.Server.SetRemoteClusterService(mockRemoteCluster)
testCluster := &testlib.FakeClusterInterface{}
th.Server.Platform().SetCluster(testCluster)
commandProvider := ShareProvider{}
channel := th.CreateChannel(th.BasicTeam, WithShared(false))
args := &model.CommandArgs{
T: func(s string, args ...any) string { return s },
ChannelId: channel.Id,
UserId: th.BasicUser.Id,
TeamId: th.BasicTeam.Id,
Command: "/share-channel share",
}
response := commandProvider.DoCommand(th.App, th.Context, args, "")
require.Equal(t, "##### "+args.T("api.command_share.channel_shared"), response.Text)
channelConvertedMessages := testCluster.SelectMessages(func(msg *model.ClusterMessage) bool {
event, err := model.WebSocketEventFromJSON(bytes.NewReader(msg.Data))
return err == nil && event.EventType() == model.WebsocketEventChannelConverted
})
assert.Len(t, channelConvertedMessages, 1)
})
t.Run("unshare command sends a websocket channel converted event", func(t *testing.T) {
th := setup(t).initBasic()
defer th.tearDown()
th.addPermissionToRole(model.PermissionManageSharedChannels.Id, th.BasicUser.Roles)
mockSyncService := app.NewMockSharedChannelService(nil)
th.Server.SetSharedChannelSyncService(mockSyncService)
mockRemoteCluster, err := remotecluster.NewRemoteClusterService(th.Server)
require.NoError(t, err)
th.Server.SetRemoteClusterService(mockRemoteCluster)
testCluster := &testlib.FakeClusterInterface{}
th.Server.Platform().SetCluster(testCluster)
commandProvider := ShareProvider{}
channel := th.CreateChannel(th.BasicTeam, WithShared(true))
args := &model.CommandArgs{
T: func(s string, args ...any) string { return s },
ChannelId: channel.Id,
UserId: th.BasicUser.Id,
TeamId: th.BasicTeam.Id,
Command: "/share-channel unshare",
}
response := commandProvider.DoCommand(th.App, th.Context, args, "")
require.Equal(t, "##### "+args.T("api.command_share.shared_channel_unavailable"), response.Text)
channelConvertedMessages := testCluster.SelectMessages(func(msg *model.ClusterMessage) bool {
event, err := model.WebSocketEventFromJSON(bytes.NewReader(msg.Data))
return err == nil && event.EventType() == model.WebsocketEventChannelConverted
})
require.Len(t, channelConvertedMessages, 1)
})
}

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

@@ -0,0 +1,44 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
)
type ShortcutsProvider struct {
}
const (
CmdShortcuts = "shortcuts"
)
func init() {
app.RegisterCommandProvider(&ShortcutsProvider{})
}
func (*ShortcutsProvider) GetTrigger() string {
return CmdShortcuts
}
func (*ShortcutsProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model.Command {
return &model.Command{
Trigger: CmdShortcuts,
AutoComplete: true,
AutoCompleteDesc: T("api.command_shortcuts.desc"),
AutoCompleteHint: "",
DisplayName: T("api.command_shortcuts.name"),
}
}
func (*ShortcutsProvider) DoCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) *model.CommandResponse {
// This command is handled client-side and shouldn't hit the server.
return &model.CommandResponse{
Text: args.T("api.command_shortcuts.unsupported.app_error"),
ResponseType: model.CommandResponseTypeEphemeral,
}
}

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

@@ -0,0 +1,45 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
)
type ShrugProvider struct {
}
const (
CmdShrug = "shrug"
)
func init() {
app.RegisterCommandProvider(&ShrugProvider{})
}
func (*ShrugProvider) GetTrigger() string {
return CmdShrug
}
func (*ShrugProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model.Command {
return &model.Command{
Trigger: CmdShrug,
AutoComplete: true,
AutoCompleteDesc: T("api.command_shrug.desc"),
AutoCompleteHint: T("api.command_shrug.hint"),
DisplayName: T("api.command_shrug.name"),
}
}
func (*ShrugProvider) DoCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) *model.CommandResponse {
rmsg := `¯\\\_(ツ)\_/¯`
if message != "" {
rmsg = message + " " + rmsg
}
return &model.CommandResponse{ResponseType: model.CommandResponseTypeInChannel, Text: rmsg}
}

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

@@ -0,0 +1,53 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
)
type TemplatesProvider struct {
}
const (
CmdTemplates = "templates"
)
func init() {
app.RegisterCommandProvider(&TemplatesProvider{})
}
func (h *TemplatesProvider) GetTrigger() string {
return CmdTemplates
}
func (h *TemplatesProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model.Command {
workTemplateEnabled := a.Config().FeatureFlags.WorkTemplate
pbActive, err := a.IsPluginActive(model.PluginIdPlaybooks)
if err != nil {
pbActive = false
}
hasBoard, err := a.HasBoardProduct()
if err != nil {
hasBoard = false
}
return &model.Command{
Trigger: CmdTemplates,
AutoComplete: hasBoard && pbActive && workTemplateEnabled,
AutoCompleteDesc: T("api.command_templates.desc"),
DisplayName: T("api.command_templates.name"),
}
}
func (h *TemplatesProvider) DoCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) *model.CommandResponse {
// This command is handled client-side and shouldn't hit the server.
return &model.CommandResponse{
Text: args.T("api.command_templates.unsupported.app_error"),
ResponseType: model.CommandResponseTypeEphemeral,
}
}

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

@@ -0,0 +1,616 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"fmt"
"io"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/services/httpservice"
)
type InfiniteReader struct {
Prefix string
}
func (r InfiniteReader) Read(p []byte) (n int, err error) {
for i := range p {
p[i] = 'a'
}
return len(p), nil
}
func TestMoveCommand(t *testing.T) {
th := setup(t)
defer th.tearDown()
sourceTeam := th.createTeam()
targetTeam := th.createTeam()
command := &model.Command{}
command.CreatorId = model.NewId()
command.Method = model.CommandMethodPost
command.TeamId = sourceTeam.Id
command.URL = "http://nowhere.com/"
command.Trigger = "trigger1"
command, err := th.App.CreateCommand(command)
assert.Nil(t, err)
defer func() {
th.App.PermanentDeleteTeam(th.Context, sourceTeam)
th.App.PermanentDeleteTeam(th.Context, targetTeam)
}()
// Move a command and check the team is updated.
assert.Nil(t, th.App.MoveCommand(targetTeam, command))
retrievedCommand, err := th.App.GetCommand(command.Id)
assert.Nil(t, err)
assert.EqualValues(t, targetTeam.Id, retrievedCommand.TeamId)
// Move it to the team it's already in. Nothing should change.
assert.Nil(t, th.App.MoveCommand(targetTeam, command))
retrievedCommand, err = th.App.GetCommand(command.Id)
assert.Nil(t, err)
assert.EqualValues(t, targetTeam.Id, retrievedCommand.TeamId)
}
func TestCreateCommandPost(t *testing.T) {
th := setup(t).initBasic()
defer th.tearDown()
post := &model.Post{
ChannelId: th.BasicChannel.Id,
UserId: th.BasicUser.Id,
Type: model.PostTypeSystemGeneric,
}
resp := &model.CommandResponse{
Text: "some message",
}
skipSlackParsing := false
_, err := th.App.CreateCommandPost(th.Context, post, th.BasicTeam.Id, resp, skipSlackParsing)
require.NotNil(t, err)
require.Equal(t, err.Id, "api.context.invalid_param.app_error")
}
func TestExecuteCommand(t *testing.T) {
th := setup(t).initBasic()
defer th.tearDown()
t.Run("valid tests with different whitespace characters", func(t *testing.T) {
TestCases := map[string]string{
"/code happy path": " happy path",
"/code\nnewline path": " newline path",
"/code\n/nDouble newline path": " /nDouble newline path",
"/code double space": " double space",
"/code\ttab": " tab",
}
for TestCase, result := range TestCases {
args := &model.CommandArgs{
Command: TestCase,
TeamId: th.BasicTeam.Id,
ChannelId: th.BasicChannel.Id,
UserId: th.BasicUser.Id,
T: func(s string, args ...any) string { return s },
}
resp, err := th.App.ExecuteCommand(th.Context, args)
require.Nil(t, err)
require.NotNil(t, resp)
assert.Equal(t, resp.Text, result)
}
})
t.Run("missing slash character", func(t *testing.T) {
argsMissingSlashCharacter := &model.CommandArgs{
Command: "missing leading slash character",
T: func(s string, args ...any) string { return s },
}
_, err := th.App.ExecuteCommand(th.Context, argsMissingSlashCharacter)
require.Equal(t, "api.command.execute_command.format.app_error", err.Id)
})
t.Run("empty", func(t *testing.T) {
argsMissingSlashCharacter := &model.CommandArgs{
Command: "",
T: func(s string, args ...any) string { return s },
}
_, err := th.App.ExecuteCommand(th.Context, argsMissingSlashCharacter)
require.Equal(t, "api.command.execute_command.format.app_error", err.Id)
})
}
func TestHandleCommandResponsePost(t *testing.T) {
th := setup(t).initBasic()
defer th.tearDown()
command := &model.Command{}
args := &model.CommandArgs{
ChannelId: th.BasicChannel.Id,
TeamId: th.BasicTeam.Id,
UserId: th.BasicUser.Id,
RootId: "",
}
resp := &model.CommandResponse{
Type: model.PostTypeDefault,
ResponseType: model.CommandResponseTypeInChannel,
Props: model.StringInterface{"some_key": "some value"},
Text: "some message",
}
builtIn := true
post, err := th.App.HandleCommandResponsePost(th.Context, command, args, resp, builtIn)
assert.Nil(t, err)
assert.Equal(t, args.ChannelId, post.ChannelId)
assert.Equal(t, args.RootId, post.RootId)
assert.Equal(t, args.UserId, post.UserId)
assert.Equal(t, resp.Type, post.Type)
assert.Equal(t, resp.Props, post.GetProps())
assert.Equal(t, resp.Text, post.Message)
assert.Nil(t, post.GetProp("override_icon_url"))
assert.Nil(t, post.GetProp("override_username"))
assert.Nil(t, post.GetProp("from_webhook"))
// Command is not built in, so it is a bot command.
builtIn = false
post, err = th.App.HandleCommandResponsePost(th.Context, command, args, resp, builtIn)
assert.Nil(t, err)
assert.Equal(t, "true", post.GetProp("from_webhook"))
builtIn = true
// Channel id is specified by response, it should override the command args value.
channel := th.CreateChannel(th.BasicTeam)
resp.ChannelId = channel.Id
th.addUserToChannel(th.BasicUser, channel)
post, err = th.App.HandleCommandResponsePost(th.Context, command, args, resp, builtIn)
assert.Nil(t, err)
assert.Equal(t, resp.ChannelId, post.ChannelId)
assert.NotEqual(t, args.ChannelId, post.ChannelId)
// Override username config is turned off. No override should occur.
*th.App.Config().ServiceSettings.EnablePostUsernameOverride = false
resp.ChannelId = ""
command.Username = "Command username"
resp.Username = "Response username"
post, err = th.App.HandleCommandResponsePost(th.Context, command, args, resp, builtIn)
assert.Nil(t, err)
assert.Nil(t, post.GetProp("override_username"))
*th.App.Config().ServiceSettings.EnablePostUsernameOverride = true
// Override username config is turned on. Override username through command property.
post, err = th.App.HandleCommandResponsePost(th.Context, command, args, resp, builtIn)
assert.Nil(t, err)
assert.Equal(t, command.Username, post.GetProp("override_username"))
assert.Equal(t, "true", post.GetProp("from_webhook"))
command.Username = ""
// Override username through response property.
post, err = th.App.HandleCommandResponsePost(th.Context, command, args, resp, builtIn)
assert.Nil(t, err)
assert.Equal(t, resp.Username, post.GetProp("override_username"))
assert.Equal(t, "true", post.GetProp("from_webhook"))
*th.App.Config().ServiceSettings.EnablePostUsernameOverride = false
// Override icon url config is turned off. No override should occur.
*th.App.Config().ServiceSettings.EnablePostIconOverride = false
command.IconURL = "Command icon url"
resp.IconURL = "Response icon url"
post, err = th.App.HandleCommandResponsePost(th.Context, command, args, resp, builtIn)
assert.Nil(t, err)
assert.Nil(t, post.GetProp("override_icon_url"))
*th.App.Config().ServiceSettings.EnablePostIconOverride = true
// Override icon url config is turned on. Override icon url through command property.
post, err = th.App.HandleCommandResponsePost(th.Context, command, args, resp, builtIn)
assert.Nil(t, err)
assert.Equal(t, command.IconURL, post.GetProp("override_icon_url"))
assert.Equal(t, "true", post.GetProp("from_webhook"))
command.IconURL = ""
// Override icon url through response property.
post, err = th.App.HandleCommandResponsePost(th.Context, command, args, resp, builtIn)
assert.Nil(t, err)
assert.Equal(t, resp.IconURL, post.GetProp("override_icon_url"))
assert.Equal(t, "true", post.GetProp("from_webhook"))
// Test Slack text conversion.
resp.Text = "<!channel>"
post, err = th.App.HandleCommandResponsePost(th.Context, command, args, resp, builtIn)
assert.Nil(t, err)
assert.Equal(t, "@channel", post.Message)
assert.Equal(t, "true", post.GetProp("from_webhook"))
// Test Slack attachments text conversion.
resp.Attachments = []*model.SlackAttachment{
{
Text: "<!here>",
},
}
post, err = th.App.HandleCommandResponsePost(th.Context, command, args, resp, builtIn)
assert.Nil(t, err)
assert.Equal(t, "@channel", post.Message)
if assert.Len(t, post.Attachments(), 1) {
assert.Equal(t, "@here", post.Attachments()[0].Text)
}
assert.Equal(t, "true", post.GetProp("from_webhook"))
channel = th.createPrivateChannel(th.BasicTeam)
resp.ChannelId = channel.Id
args.UserId = th.BasicUser2.Id
_, err = th.App.HandleCommandResponsePost(th.Context, command, args, resp, builtIn)
require.NotNil(t, err)
require.Equal(t, err.Id, "api.command.command_post.forbidden.app_error")
// Test that /code text is not converted with the Slack text conversion.
command.Trigger = "code"
resp.ChannelId = ""
resp.Text = "<test.com|test website>"
resp.Attachments = []*model.SlackAttachment{
{
Text: "<!here>",
},
}
// set and unset SkipSlackParsing here seems the nicest way as no separate response objects are created for every testcase.
resp.SkipSlackParsing = true
post, err = th.App.HandleCommandResponsePost(th.Context, command, args, resp, builtIn)
resp.SkipSlackParsing = false
assert.Nil(t, err)
assert.Equal(t, resp.Text, post.Message, "/code text should not be converted to Slack links")
assert.Equal(t, "<!here>", resp.Attachments[0].Text)
}
func TestHandleCommandResponse(t *testing.T) {
th := setup(t).initBasic()
defer th.tearDown()
command := &model.Command{}
args := &model.CommandArgs{
Command: "/invite username",
UserId: th.BasicUser.Id,
ChannelId: th.BasicChannel.Id,
}
resp := &model.CommandResponse{
Text: "message 1",
Type: model.PostTypeSystemGeneric,
}
builtIn := true
_, err := th.App.HandleCommandResponse(th.Context, command, args, resp, builtIn)
require.NotNil(t, err)
require.Equal(t, err.Id, "api.command.execute_command.create_post_failed.app_error")
resp = &model.CommandResponse{
Text: "message 1",
}
_, err = th.App.HandleCommandResponse(th.Context, command, args, resp, builtIn)
assert.Nil(t, err)
resp = &model.CommandResponse{
Text: "message 1",
ExtraResponses: []*model.CommandResponse{
{
Text: "message 2",
},
{
Type: model.PostTypeSystemGeneric,
Text: "message 3",
},
},
}
_, err = th.App.HandleCommandResponse(th.Context, command, args, resp, builtIn)
require.NotNil(t, err)
require.Equal(t, err.Id, "api.command.execute_command.create_post_failed.app_error")
resp = &model.CommandResponse{
ExtraResponses: []*model.CommandResponse{
{},
{},
},
}
_, err = th.App.HandleCommandResponse(th.Context, command, args, resp, builtIn)
assert.Nil(t, err)
}
func TestDoCommandRequest(t *testing.T) {
th := setup(t)
defer th.tearDown()
th.App.UpdateConfig(func(cfg *model.Config) {
cfg.ServiceSettings.AllowedUntrustedInternalConnections = model.NewString("127.0.0.1")
cfg.ServiceSettings.EnableCommands = model.NewBool(true)
})
t.Run("with a valid text response", func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
io.Copy(w, strings.NewReader("Hello, World!"))
}))
defer server.Close()
_, resp, err := th.App.DoCommandRequest(&model.Command{URL: server.URL}, url.Values{})
require.Nil(t, err)
assert.NotNil(t, resp)
assert.Equal(t, "Hello, World!", resp.Text)
})
t.Run("with a valid json response", func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Content-Type", "application/json")
io.Copy(w, strings.NewReader(`{"text": "Hello, World!"}`))
}))
defer server.Close()
_, resp, err := th.App.DoCommandRequest(&model.Command{URL: server.URL}, url.Values{})
require.Nil(t, err)
assert.NotNil(t, resp)
assert.Equal(t, "Hello, World!", resp.Text)
})
t.Run("with a large text response", func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
io.Copy(w, InfiniteReader{})
}))
defer server.Close()
// Since we limit the length of the response, no error will be returned and resp.Text will be a finite string
_, resp, err := th.App.DoCommandRequest(&model.Command{URL: server.URL}, url.Values{})
require.Nil(t, err)
require.NotNil(t, resp)
})
t.Run("with a large, valid json response", func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Content-Type", "application/json")
io.Copy(w, io.MultiReader(strings.NewReader(`{"text": "`), InfiniteReader{}, strings.NewReader(`"}`)))
}))
defer server.Close()
_, _, err := th.App.DoCommandRequest(&model.Command{URL: server.URL}, url.Values{})
require.NotNil(t, err)
require.Equal(t, "api.command.execute_command.failed.app_error", err.Id)
})
t.Run("with a large, invalid json response", func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Content-Type", "application/json")
io.Copy(w, InfiniteReader{})
}))
defer server.Close()
_, _, err := th.App.DoCommandRequest(&model.Command{URL: server.URL}, url.Values{})
require.NotNil(t, err)
require.Equal(t, "api.command.execute_command.failed.app_error", err.Id)
})
t.Run("with a slow response", func(t *testing.T) {
done := make(chan bool)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
<-done
io.Copy(w, strings.NewReader(`{"text": "Hello, World!"}`))
}))
defer server.Close()
th.App.HTTPService().(*httpservice.HTTPServiceImpl).RequestTimeout = 100 * time.Millisecond
defer func() {
th.App.HTTPService().(*httpservice.HTTPServiceImpl).RequestTimeout = httpservice.RequestTimeout
}()
_, _, err := th.App.DoCommandRequest(&model.Command{URL: server.URL}, url.Values{})
require.NotNil(t, err)
require.Equal(t, "api.command.execute_command.failed.app_error", err.Id)
close(done)
})
}
func TestMentionsToTeamMembers(t *testing.T) {
th := setup(t).initBasic()
defer th.tearDown()
otherTeam := th.createTeam()
otherUser := th.createUser()
th.linkUserToTeam(otherUser, otherTeam)
fixture := []struct {
message string
inTeam string
expectedMap model.UserMentionMap
}{
{
"",
th.BasicTeam.Id,
model.UserMentionMap{},
},
{
"/trigger",
th.BasicTeam.Id,
model.UserMentionMap{},
},
{
"/trigger 0 mentions",
th.BasicTeam.Id,
model.UserMentionMap{},
},
{
fmt.Sprintf("/trigger 1 valid user @%s", th.BasicUser.Username),
th.BasicTeam.Id,
model.UserMentionMap{th.BasicUser.Username: th.BasicUser.Id},
},
{
fmt.Sprintf("/trigger 2 valid users @%s @%s",
th.BasicUser.Username, th.BasicUser2.Username,
),
th.BasicTeam.Id,
model.UserMentionMap{
th.BasicUser.Username: th.BasicUser.Id,
th.BasicUser2.Username: th.BasicUser2.Id,
},
},
{
fmt.Sprintf("/trigger 1 user from another team @%s", otherUser.Username),
th.BasicTeam.Id,
model.UserMentionMap{},
},
{
fmt.Sprintf("/trigger 2 valid users + 1 from another team @%s @%s @%s",
th.BasicUser.Username, th.BasicUser2.Username, otherUser.Username,
),
th.BasicTeam.Id,
model.UserMentionMap{
th.BasicUser.Username: th.BasicUser.Id,
th.BasicUser2.Username: th.BasicUser2.Id,
},
},
{
fmt.Sprintf("/trigger a valid channel ~%s", th.BasicChannel.Name),
th.BasicTeam.Id,
model.UserMentionMap{},
},
{
fmt.Sprintf("/trigger channel and mentions ~%s @%s",
th.BasicChannel.Name, th.BasicUser.Username),
th.BasicTeam.Id,
model.UserMentionMap{th.BasicUser.Username: th.BasicUser.Id},
},
{
fmt.Sprintf("/trigger repeated users @%s @%s @%s",
th.BasicUser.Username, th.BasicUser2.Username, th.BasicUser.Username),
th.BasicTeam.Id,
model.UserMentionMap{
th.BasicUser.Username: th.BasicUser.Id,
th.BasicUser2.Username: th.BasicUser2.Id,
},
},
}
for _, data := range fixture {
actualMap := th.App.MentionsToTeamMembers(th.Context, data.message, data.inTeam)
require.Equal(t, actualMap, data.expectedMap)
}
}
func TestMentionsToPublicChannels(t *testing.T) {
th := setup(t).initBasic()
defer th.tearDown()
otherPublicChannel := th.CreateChannel(th.BasicTeam)
privateChannel := th.createPrivateChannel(th.BasicTeam)
fixture := []struct {
message string
inTeam string
expectedMap model.ChannelMentionMap
}{
{
"",
th.BasicTeam.Id,
model.ChannelMentionMap{},
},
{
"/trigger",
th.BasicTeam.Id,
model.ChannelMentionMap{},
},
{
"/trigger 0 mentions",
th.BasicTeam.Id,
model.ChannelMentionMap{},
},
{
fmt.Sprintf("/trigger 1 public channel ~%s", th.BasicChannel.Name),
th.BasicTeam.Id,
model.ChannelMentionMap{th.BasicChannel.Name: th.BasicChannel.Id},
},
{
fmt.Sprintf("/trigger 2 public channels ~%s ~%s",
th.BasicChannel.Name, otherPublicChannel.Name,
),
th.BasicTeam.Id,
model.ChannelMentionMap{
th.BasicChannel.Name: th.BasicChannel.Id,
otherPublicChannel.Name: otherPublicChannel.Id,
},
},
{
fmt.Sprintf("/trigger 1 private channel ~%s", privateChannel.Name),
th.BasicTeam.Id,
model.ChannelMentionMap{},
},
{
fmt.Sprintf("/trigger 2 public channel + 1 private ~%s ~%s ~%s",
th.BasicChannel.Name, otherPublicChannel.Name, privateChannel.Name,
),
th.BasicTeam.Id,
model.ChannelMentionMap{
th.BasicChannel.Name: th.BasicChannel.Id,
otherPublicChannel.Name: otherPublicChannel.Id,
},
},
{
fmt.Sprintf("/trigger a valid user @%s", th.BasicUser.Username),
th.BasicTeam.Id,
model.ChannelMentionMap{},
},
{
fmt.Sprintf("/trigger channel and mentions ~%s @%s",
th.BasicChannel.Name, th.BasicUser.Username),
th.BasicTeam.Id,
model.ChannelMentionMap{th.BasicChannel.Name: th.BasicChannel.Id},
},
{
fmt.Sprintf("/trigger repeated channels ~%s ~%s ~%s",
th.BasicChannel.Name, otherPublicChannel.Name, th.BasicChannel.Name),
th.BasicTeam.Id,
model.ChannelMentionMap{
th.BasicChannel.Name: th.BasicChannel.Id,
otherPublicChannel.Name: otherPublicChannel.Id,
},
},
}
for _, data := range fixture {
actualMap := th.App.MentionsToPublicChannels(th.Context, data.message, data.inTeam)
require.Equal(t, actualMap, data.expectedMap)
}
}

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

@@ -0,0 +1,456 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"bytes"
"context"
"os"
"path/filepath"
"strings"
"sync"
"testing"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/app"
"github.com/mattermost/mattermost-server/v6/server/channels/app/request"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/config"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type TestHelper struct {
App *app.App
Context *request.Context
Server *app.Server
BasicTeam *model.Team
BasicUser *model.User
BasicUser2 *model.User
BasicChannel *model.Channel
BasicPost *model.Post
SystemAdminUser *model.User
LogBuffer *bytes.Buffer
TestLogger *mlog.Logger
IncludeCacheLayer bool
tempWorkspace string
boardsProductEnvValue string
playbooksDisableEnvValue string
}
func setupTestHelper(dbStore store.Store, enterprise bool, includeCacheLayer bool, tb testing.TB, configSet func(*model.Config)) *TestHelper {
tempWorkspace, err := os.MkdirTemp("", "apptest")
if err != nil {
panic(err)
}
memoryStore := config.NewTestMemoryStore()
memoryConfig := memoryStore.Get()
if configSet != nil {
configSet(memoryConfig)
}
// disable Boards through the feature flag
boardsProductEnvValue := os.Getenv("MM_FEATUREFLAGS_BoardsProduct")
os.Unsetenv("MM_FEATUREFLAGS_BoardsProduct")
memoryConfig.FeatureFlags.BoardsProduct = false
// disable Playbooks (temporarily) as it causes many more mocked methods to get
// called, and cannot receieve a mocked database.
playbooksDisableEnvValue := os.Getenv("MM_DISABLE_PLAYBOOKS")
os.Setenv("MM_DISABLE_PLAYBOOKS", "true")
*memoryConfig.PluginSettings.Directory = filepath.Join(tempWorkspace, "plugins")
*memoryConfig.PluginSettings.ClientDirectory = filepath.Join(tempWorkspace, "webapp")
*memoryConfig.PluginSettings.AutomaticPrepackagedPlugins = false
*memoryConfig.LogSettings.EnableSentry = false // disable error reporting during tests
memoryStore.Set(memoryConfig)
buffer := &bytes.Buffer{}
var options []app.Option
options = append(options, app.ConfigStore(memoryStore))
if includeCacheLayer {
options = append(options, app.StoreOverrideWithCache(dbStore))
} else {
options = append(options, app.StoreOverride(dbStore))
}
testLogger, _ := mlog.NewLogger()
logCfg, _ := config.MloggerConfigFromLoggerConfig(&memoryConfig.LogSettings, nil, config.GetLogFileLocation)
if errCfg := testLogger.ConfigureTargets(logCfg, nil); errCfg != nil {
panic("failed to configure test logger: " + errCfg.Error())
}
// lock logger config so server init cannot override it during testing.
testLogger.LockConfiguration()
options = append(options, app.SetLogger(testLogger))
s, err := app.NewServer(options...)
if err != nil {
panic(err)
}
th := &TestHelper{
App: app.New(app.ServerConnector(s.Channels())),
Context: request.EmptyContext(testLogger),
Server: s,
LogBuffer: buffer,
TestLogger: testLogger,
IncludeCacheLayer: includeCacheLayer,
boardsProductEnvValue: boardsProductEnvValue,
playbooksDisableEnvValue: playbooksDisableEnvValue,
}
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.TeamSettings.MaxUsersPerTeam = 50 })
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.RateLimitSettings.Enable = false })
prevListenAddress := *th.App.Config().ServiceSettings.ListenAddress
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.ListenAddress = ":0" })
serverErr := th.Server.Start()
if serverErr != nil {
panic(serverErr)
}
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.ListenAddress = prevListenAddress })
th.App.Srv().Platform().SearchEngine = mainHelper.SearchEngine
th.App.Srv().Store().MarkSystemRanUnitTests()
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.TeamSettings.EnableOpenServer = true })
// Disable strict password requirements for test
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.PasswordSettings.MinimumLength = 5
*cfg.PasswordSettings.Lowercase = false
*cfg.PasswordSettings.Uppercase = false
*cfg.PasswordSettings.Symbol = false
*cfg.PasswordSettings.Number = false
})
if enterprise {
th.App.Srv().Jobs.StopWorkers()
th.App.Srv().Jobs.StopSchedulers()
th.App.Srv().SetLicense(model.NewTestLicense())
th.App.Srv().Jobs.StartWorkers()
th.App.Srv().Jobs.StartSchedulers()
} else {
th.App.Srv().SetLicense(nil)
}
if th.tempWorkspace == "" {
th.tempWorkspace = tempWorkspace
}
return th
}
func setup(tb testing.TB) *TestHelper {
if testing.Short() {
tb.SkipNow()
}
dbStore := mainHelper.GetStore()
dbStore.DropAllTables()
dbStore.MarkSystemRanUnitTests()
return setupTestHelper(dbStore, false, true, tb, nil)
}
var initBasicOnce sync.Once
var userCache struct {
SystemAdminUser *model.User
BasicUser *model.User
BasicUser2 *model.User
}
func (th *TestHelper) initBasic() *TestHelper {
// create users once and cache them because password hashing is slow
initBasicOnce.Do(func() {
th.SystemAdminUser = th.createUser()
th.App.UpdateUserRoles(th.Context, th.SystemAdminUser.Id, model.SystemUserRoleId+" "+model.SystemAdminRoleId, false)
th.SystemAdminUser, _ = th.App.GetUser(th.SystemAdminUser.Id)
userCache.SystemAdminUser = th.SystemAdminUser.DeepCopy()
th.BasicUser = th.createUser()
th.BasicUser, _ = th.App.GetUser(th.BasicUser.Id)
userCache.BasicUser = th.BasicUser.DeepCopy()
th.BasicUser2 = th.createUser()
th.BasicUser2, _ = th.App.GetUser(th.BasicUser2.Id)
userCache.BasicUser2 = th.BasicUser2.DeepCopy()
})
// restore cached users
th.SystemAdminUser = userCache.SystemAdminUser.DeepCopy()
th.BasicUser = userCache.BasicUser.DeepCopy()
th.BasicUser2 = userCache.BasicUser2.DeepCopy()
users := []*model.User{th.SystemAdminUser, th.BasicUser, th.BasicUser2}
mainHelper.GetSQLStore().User().InsertUsers(users)
th.BasicTeam = th.createTeam()
th.linkUserToTeam(th.BasicUser, th.BasicTeam)
th.linkUserToTeam(th.BasicUser2, th.BasicTeam)
th.BasicChannel = th.CreateChannel(th.BasicTeam)
th.BasicPost = th.createPost(th.BasicChannel)
return th
}
func (th *TestHelper) createTeam() *model.Team {
id := model.NewId()
team := &model.Team{
DisplayName: "dn_" + id,
Name: "name" + id,
Email: "success+" + id + "@simulator.amazonses.com",
Type: model.TeamOpen,
}
var err *model.AppError
if team, err = th.App.CreateTeam(th.Context, team); err != nil {
panic(err)
}
return team
}
func (th *TestHelper) createUser() *model.User {
return th.createUserOrGuest(false)
}
func (th *TestHelper) createGuest() *model.User {
return th.createUserOrGuest(true)
}
func (th *TestHelper) createUserOrGuest(guest bool) *model.User {
id := model.NewId()
user := &model.User{
Email: "success+" + id + "@simulator.amazonses.com",
Username: "un_" + id,
Nickname: "nn_" + id,
Password: "Password1",
EmailVerified: true,
}
var err *model.AppError
if guest {
if user, err = th.App.CreateGuest(th.Context, user); err != nil {
panic(err)
}
} else {
if user, err = th.App.CreateUser(th.Context, user); err != nil {
panic(err)
}
}
return user
}
type ChannelOption func(*model.Channel)
func WithShared(v bool) ChannelOption {
return func(channel *model.Channel) {
channel.Shared = model.NewBool(v)
}
}
func (th *TestHelper) CreateChannel(team *model.Team, options ...ChannelOption) *model.Channel {
return th.createChannel(team, model.ChannelTypeOpen, options...)
}
func (th *TestHelper) createPrivateChannel(team *model.Team) *model.Channel {
return th.createChannel(team, model.ChannelTypePrivate)
}
func (th *TestHelper) createChannel(team *model.Team, channelType model.ChannelType, options ...ChannelOption) *model.Channel {
id := model.NewId()
channel := &model.Channel{
DisplayName: "dn_" + id,
Name: "name_" + id,
Type: channelType,
TeamId: team.Id,
CreatorId: th.BasicUser.Id,
}
for _, option := range options {
option(channel)
}
var err *model.AppError
if channel, err = th.App.CreateChannel(th.Context, channel, true); err != nil {
panic(err)
}
if channel.IsShared() {
id := model.NewId()
_, err := th.App.SaveSharedChannel(th.Context, &model.SharedChannel{
ChannelId: channel.Id,
TeamId: channel.TeamId,
Home: false,
ReadOnly: false,
ShareName: "shared-" + id,
ShareDisplayName: "shared-" + id,
CreatorId: th.BasicUser.Id,
RemoteId: model.NewId(),
})
if err != nil {
panic(err)
}
}
return channel
}
func (th *TestHelper) createChannelWithAnotherUser(team *model.Team, channelType model.ChannelType, userID string) *model.Channel {
id := model.NewId()
channel := &model.Channel{
DisplayName: "dn_" + id,
Name: "name_" + id,
Type: channelType,
TeamId: team.Id,
CreatorId: userID,
}
var err *model.AppError
if channel, err = th.App.CreateChannel(th.Context, channel, true); err != nil {
panic(err)
}
return channel
}
func (th *TestHelper) createDmChannel(user *model.User) *model.Channel {
var err *model.AppError
var channel *model.Channel
if channel, err = th.App.GetOrCreateDirectChannel(th.Context, th.BasicUser.Id, user.Id); err != nil {
panic(err)
}
return channel
}
func (th *TestHelper) createGroupChannel(user1 *model.User, user2 *model.User) *model.Channel {
var err *model.AppError
var channel *model.Channel
if channel, err = th.App.CreateGroupChannel(th.Context, []string{th.BasicUser.Id, user1.Id, user2.Id}, th.BasicUser.Id); err != nil {
panic(err)
}
return channel
}
func (th *TestHelper) createPost(channel *model.Channel) *model.Post {
id := model.NewId()
post := &model.Post{
UserId: th.BasicUser.Id,
ChannelId: channel.Id,
Message: "message_" + id,
CreateAt: model.GetMillis() - 10000,
}
var err *model.AppError
if post, err = th.App.CreatePost(th.Context, post, channel, false, true); err != nil {
panic(err)
}
return post
}
func (th *TestHelper) linkUserToTeam(user *model.User, team *model.Team) {
_, err := th.App.JoinUserToTeam(th.Context, team, user, "")
if err != nil {
panic(err)
}
}
func (th *TestHelper) addUserToChannel(user *model.User, channel *model.Channel) *model.ChannelMember {
member, err := th.App.AddUserToChannel(th.Context, user, channel, false)
if err != nil {
panic(err)
}
return member
}
func (th *TestHelper) shutdownApp() {
done := make(chan bool)
go func() {
th.Server.Shutdown()
close(done)
}()
select {
case <-done:
case <-time.After(30 * time.Second):
// panic instead of fatal to terminate all tests in this package, otherwise the
// still running App could spuriously fail subsequent tests.
panic("failed to shutdown App within 30 seconds")
}
}
func (th *TestHelper) tearDown() {
// reset board and playbooks product setting to original
if th.boardsProductEnvValue != "" {
os.Setenv("MM_FEATUREFLAGS_BoardsProduct", th.boardsProductEnvValue)
}
if th.playbooksDisableEnvValue != "" {
os.Setenv("MM_DISABLE_PLAYBOOKS", th.playbooksDisableEnvValue)
} else {
os.Unsetenv("MM_DISABLE_PLAYBOOKS")
}
if th.IncludeCacheLayer {
// Clean all the caches
th.App.Srv().InvalidateAllCaches()
}
th.shutdownApp()
if th.tempWorkspace != "" {
os.RemoveAll(th.tempWorkspace)
}
}
func (th *TestHelper) removePermissionFromRole(permission string, roleName string) {
role, err1 := th.App.GetRoleByName(context.Background(), roleName)
if err1 != nil {
panic(err1)
}
var newPermissions []string
for _, p := range role.Permissions {
if p != permission {
newPermissions = append(newPermissions, p)
}
}
if strings.Join(role.Permissions, " ") == strings.Join(newPermissions, " ") {
return
}
role.Permissions = newPermissions
_, err2 := th.App.UpdateRole(role)
if err2 != nil {
panic(err2)
}
}
func (th *TestHelper) addPermissionToRole(permission string, roleName string) {
role, err1 := th.App.GetRoleByName(context.Background(), roleName)
if err1 != nil {
panic(err1)
}
for _, existingPermission := range role.Permissions {
if existingPermission == permission {
return
}
}
role.Permissions = append(role.Permissions, permission)
_, err2 := th.App.UpdateRole(role)
if err2 != nil {
panic(err2)
}
}

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

@@ -0,0 +1,24 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"testing"
"github.com/mattermost/mattermost-server/v6/server/channels/testlib"
)
var mainHelper *testlib.MainHelper
func TestMain(m *testing.M) {
var options = testlib.HelperOptions{
EnableStore: true,
EnableResources: true,
}
mainHelper = testlib.NewMainHelperWithOptions(&options)
defer mainHelper.Close()
mainHelper.Main(m)
}

102
server/channels/app/slashcommands/util.go Обычный файл
Просмотреть файл

@@ -0,0 +1,102 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"fmt"
"strings"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
)
const (
ActionKey = "-action"
)
// responsef creates an ephemeral command response using printf syntax.
func responsef(format string, args ...any) *model.CommandResponse {
return &model.CommandResponse{
ResponseType: model.CommandResponseTypeEphemeral,
Text: fmt.Sprintf(format, args...),
Type: model.PostTypeDefault,
}
}
// parseNamedArgs parses a command string into a map of arguments. It is assumed the
// command string is of the form `<action> --arg1 value1 ...` Supports empty values.
// Arg names are limited to [0-9a-zA-Z_].
func parseNamedArgs(cmd string) map[string]string {
m := make(map[string]string)
split := strings.Fields(cmd)
// check for optional action
if len(split) >= 2 && !strings.HasPrefix(split[1], "--") {
m[ActionKey] = split[1] // prefix with hyphen to avoid collision with arg named "action"
}
for i := 0; i < len(split); i++ {
if !strings.HasPrefix(split[i], "--") {
continue
}
var val string
arg := trimSpaceAndQuotes(strings.Trim(split[i], "-"))
if i < len(split)-1 && !strings.HasPrefix(split[i+1], "--") {
val = trimSpaceAndQuotes(split[i+1])
}
if arg != "" {
m[arg] = val
}
}
return m
}
func trimSpaceAndQuotes(s string) string {
trimmed := strings.TrimSpace(s)
trimmed = strings.TrimPrefix(trimmed, "\"")
trimmed = strings.TrimPrefix(trimmed, "'")
trimmed = strings.TrimSuffix(trimmed, "\"")
trimmed = strings.TrimSuffix(trimmed, "'")
return trimmed
}
func parseBool(s string) (bool, error) {
switch strings.ToLower(s) {
case "1", "t", "true", "yes", "y":
return true, nil
case "0", "f", "false", "no", "n":
return false, nil
}
return false, fmt.Errorf("cannot parse '%s' as a boolean", s)
}
func formatBool(fn i18n.TranslateFunc, b bool) string {
if b {
return fn("True")
}
return fn("False")
}
func formatTimestamp(timestamp int64) string {
if timestamp == 0 {
return "--"
}
ts := model.GetTimeForMillis(timestamp)
if !isToday(ts) {
return ts.Format("Jan 2 15:04:05 MST 2006")
}
date := ts.Format("15:04:05 MST 2006")
return fmt.Sprintf("Today %s", date)
}
func isToday(ts time.Time) bool {
now := time.Now()
year, month, day := ts.Date()
nowYear, nowMonth, nowDay := now.Date()
return year == nowYear && month == nowMonth && day == nowDay
}

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

@@ -0,0 +1,40 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package slashcommands
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestParseNamedArgs(t *testing.T) {
data := []struct {
name string
s string
m map[string]string
}{
{"empty", "", map[string]string{}},
{"gibberish", "ifu3ue-h29f8", map[string]string{}},
{"action only", "remote status", map[string]string{ActionKey: "status"}},
{"no action", "remote --arg1 val1 --arg2 val2", map[string]string{"arg1": "val1", "arg2": "val2"}},
{"command only", "remote", map[string]string{}},
{"trailing empty arg", "remote add --arg1 val1 --arg2", map[string]string{ActionKey: "add", "arg1": "val1", "arg2": ""}},
{"leading empty arg", "remote add --arg1 --arg2 val2", map[string]string{ActionKey: "add", "arg1": "", "arg2": "val2"}},
{"weird", "-- -- -- --", map[string]string{}},
{"hyphen before action", "remote -- add", map[string]string{}},
{"trailing hyphen", "remote add -- ", map[string]string{ActionKey: "add"}},
{"hyphen in val", "remote add --arg1 val-1 ", map[string]string{ActionKey: "add", "arg1": "val-1"}},
{"quote prefix and suffix", "remote add --arg1 \"val-1\"", map[string]string{ActionKey: "add", "arg1": "val-1"}},
{"quote embedded", "remote add --arg1 O'Brien", map[string]string{ActionKey: "add", "arg1": "O'Brien"}},
{"quote prefix, suffix, and embedded", "remote add --arg1 \"O'Brien\"", map[string]string{ActionKey: "add", "arg1": "O'Brien"}},
{"empty quotes", "remote add --arg1 \"\"", map[string]string{ActionKey: "add", "arg1": ""}},
}
for _, tt := range data {
m := parseNamedArgs(tt.s)
assert.NotNil(t, m)
assert.Equal(t, tt.m, m, tt.name)
}
}