Mono repo -> Master (#22553)
Combines the following repositories into one: https://github.com/mattermost/mattermost-server https://github.com/mattermost/mattermost-webapp https://github.com/mattermost/focalboard https://github.com/mattermost/mattermost-plugin-playbooks
Этот коммит содержится в:
79
server/channels/app/slashcommands/auto_channels.go
Обычный файл
79
server/channels/app/slashcommands/auto_channels.go
Обычный файл
@@ -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
|
||||
}
|
||||
32
server/channels/app/slashcommands/auto_constants.go
Обычный файл
32
server/channels/app/slashcommands/auto_constants.go
Обычный файл
@@ -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"}
|
||||
)
|
||||
114
server/channels/app/slashcommands/auto_environment.go
Обычный файл
114
server/channels/app/slashcommands/auto_environment.go
Обычный файл
@@ -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
|
||||
}
|
||||
125
server/channels/app/slashcommands/auto_posts.go
Обычный файл
125
server/channels/app/slashcommands/auto_posts.go
Обычный файл
@@ -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
|
||||
}
|
||||
80
server/channels/app/slashcommands/auto_teams.go
Обычный файл
80
server/channels/app/slashcommands/auto_teams.go
Обычный файл
@@ -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
|
||||
}
|
||||
151
server/channels/app/slashcommands/auto_users.go
Обычный файл
151
server/channels/app/slashcommands/auto_users.go
Обычный файл
@@ -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
|
||||
}
|
||||
41
server/channels/app/slashcommands/command_away.go
Обычный файл
41
server/channels/app/slashcommands/command_away.go
Обычный файл
@@ -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")}
|
||||
}
|
||||
110
server/channels/app/slashcommands/command_channel_header.go
Обычный файл
110
server/channels/app/slashcommands/command_channel_header.go
Обычный файл
@@ -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{}
|
||||
}
|
||||
122
server/channels/app/slashcommands/command_channel_header_test.go
Обычный файл
122
server/channels/app/slashcommands/command_channel_header_test.go
Обычный файл
@@ -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)
|
||||
}
|
||||
97
server/channels/app/slashcommands/command_channel_purpose.go
Обычный файл
97
server/channels/app/slashcommands/command_channel_purpose.go
Обычный файл
@@ -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)
|
||||
}
|
||||
104
server/channels/app/slashcommands/command_channel_rename.go
Обычный файл
104
server/channels/app/slashcommands/command_channel_rename.go
Обычный файл
@@ -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{}
|
||||
}
|
||||
104
server/channels/app/slashcommands/command_channel_rename_test.go
Обычный файл
104
server/channels/app/slashcommands/command_channel_rename_test.go
Обычный файл
@@ -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)
|
||||
}
|
||||
46
server/channels/app/slashcommands/command_code.go
Обычный файл
46
server/channels/app/slashcommands/command_code.go
Обычный файл
@@ -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}
|
||||
}
|
||||
29
server/channels/app/slashcommands/command_code_test.go
Обычный файл
29
server/channels/app/slashcommands/command_code_test.go
Обычный файл
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
140
server/channels/app/slashcommands/command_custom_status.go
Обычный файл
140
server/channels/app/slashcommands/command_custom_status.go
Обычный файл
@@ -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
|
||||
}
|
||||
34
server/channels/app/slashcommands/command_custom_status_test.go
Обычный файл
34
server/channels/app/slashcommands/command_custom_status_test.go
Обычный файл
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
41
server/channels/app/slashcommands/command_dnd.go
Обычный файл
41
server/channels/app/slashcommands/command_dnd.go
Обычный файл
@@ -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")}
|
||||
}
|
||||
98
server/channels/app/slashcommands/command_echo.go
Обычный файл
98
server/channels/app/slashcommands/command_echo.go
Обычный файл
@@ -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{}
|
||||
}
|
||||
95
server/channels/app/slashcommands/command_expand_collapse.go
Обычный файл
95
server/channels/app/slashcommands/command_expand_collapse.go
Обычный файл
@@ -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}
|
||||
}
|
||||
160
server/channels/app/slashcommands/command_groupmsg.go
Обычный файл
160
server/channels/app/slashcommands/command_groupmsg.go
Обычный файл
@@ -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
|
||||
}
|
||||
122
server/channels/app/slashcommands/command_groupmsg_test.go
Обычный файл
122
server/channels/app/slashcommands/command_groupmsg_test.go
Обычный файл
@@ -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)
|
||||
})
|
||||
}
|
||||
50
server/channels/app/slashcommands/command_help.go
Обычный файл
50
server/channels/app/slashcommands/command_help.go
Обычный файл
@@ -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,
|
||||
}),
|
||||
}
|
||||
}
|
||||
213
server/channels/app/slashcommands/command_invite.go
Обычный файл
213
server/channels/app/slashcommands/command_invite.go
Обычный файл
@@ -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 ""
|
||||
}
|
||||
85
server/channels/app/slashcommands/command_invite_people.go
Обычный файл
85
server/channels/app/slashcommands/command_invite_people.go
Обычный файл
@@ -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")}
|
||||
}
|
||||
42
server/channels/app/slashcommands/command_invite_people_test.go
Обычный файл
42
server/channels/app/slashcommands/command_invite_people_test.go
Обычный файл
@@ -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)
|
||||
}
|
||||
266
server/channels/app/slashcommands/command_invite_test.go
Обычный файл
266
server/channels/app/slashcommands/command_invite_test.go
Обычный файл
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
79
server/channels/app/slashcommands/command_join.go
Обычный файл
79
server/channels/app/slashcommands/command_join.go
Обычный файл
@@ -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}
|
||||
}
|
||||
146
server/channels/app/slashcommands/command_join_test.go
Обычный файл
146
server/channels/app/slashcommands/command_join_test.go
Обычный файл
@@ -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)
|
||||
}
|
||||
80
server/channels/app/slashcommands/command_leave.go
Обычный файл
80
server/channels/app/slashcommands/command_leave.go
Обычный файл
@@ -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}
|
||||
}
|
||||
147
server/channels/app/slashcommands/command_leave_test.go
Обычный файл
147
server/channels/app/slashcommands/command_leave_test.go
Обычный файл
@@ -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")
|
||||
})
|
||||
}
|
||||
755
server/channels/app/slashcommands/command_loadtest.go
Обычный файл
755
server/channels/app/slashcommands/command_loadtest.go
Обычный файл
@@ -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
|
||||
}
|
||||
41
server/channels/app/slashcommands/command_logout.go
Обычный файл
41
server/channels/app/slashcommands/command_logout.go
Обычный файл
@@ -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"}
|
||||
}
|
||||
49
server/channels/app/slashcommands/command_marketplace.go
Обычный файл
49
server/channels/app/slashcommands/command_marketplace.go
Обычный файл
@@ -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,
|
||||
}
|
||||
}
|
||||
61
server/channels/app/slashcommands/command_marketplace_test.go
Обычный файл
61
server/channels/app/slashcommands/command_marketplace_test.go
Обычный файл
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
44
server/channels/app/slashcommands/command_me.go
Обычный файл
44
server/channels/app/slashcommands/command_me.go
Обычный файл
@@ -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 + "*",
|
||||
}
|
||||
}
|
||||
27
server/channels/app/slashcommands/command_me_test.go
Обычный файл
27
server/channels/app/slashcommands/command_me_test.go
Обычный файл
@@ -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)
|
||||
}
|
||||
115
server/channels/app/slashcommands/command_msg.go
Обычный файл
115
server/channels/app/slashcommands/command_msg.go
Обычный файл
@@ -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}
|
||||
}
|
||||
94
server/channels/app/slashcommands/command_msg_test.go
Обычный файл
94
server/channels/app/slashcommands/command_msg_test.go
Обычный файл
@@ -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)
|
||||
}
|
||||
82
server/channels/app/slashcommands/command_mute.go
Обычный файл
82
server/channels/app/slashcommands/command_mute.go
Обычный файл
@@ -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}
|
||||
}
|
||||
204
server/channels/app/slashcommands/command_mute_test.go
Обычный файл
204
server/channels/app/slashcommands/command_mute_test.go
Обычный файл
@@ -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])
|
||||
}
|
||||
41
server/channels/app/slashcommands/command_offline.go
Обычный файл
41
server/channels/app/slashcommands/command_offline.go
Обычный файл
@@ -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")}
|
||||
}
|
||||
41
server/channels/app/slashcommands/command_online.go
Обычный файл
41
server/channels/app/slashcommands/command_online.go
Обычный файл
@@ -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")}
|
||||
}
|
||||
33
server/channels/app/slashcommands/command_open.go
Обычный файл
33
server/channels/app/slashcommands/command_open.go
Обычный файл
@@ -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
|
||||
}
|
||||
299
server/channels/app/slashcommands/command_remote.go
Обычный файл
299
server/channels/app/slashcommands/command_remote.go
Обычный файл
@@ -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
|
||||
}
|
||||
153
server/channels/app/slashcommands/command_remove.go
Обычный файл
153
server/channels/app/slashcommands/command_remove.go
Обычный файл
@@ -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{}
|
||||
}
|
||||
124
server/channels/app/slashcommands/command_remove_test.go
Обычный файл
124
server/channels/app/slashcommands/command_remove_test.go
Обычный файл
@@ -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)
|
||||
}
|
||||
44
server/channels/app/slashcommands/command_search.go
Обычный файл
44
server/channels/app/slashcommands/command_search.go
Обычный файл
@@ -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,
|
||||
}
|
||||
}
|
||||
44
server/channels/app/slashcommands/command_settings.go
Обычный файл
44
server/channels/app/slashcommands/command_settings.go
Обычный файл
@@ -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,
|
||||
}
|
||||
}
|
||||
329
server/channels/app/slashcommands/command_share.go
Обычный файл
329
server/channels/app/slashcommands/command_share.go
Обычный файл
@@ -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)
|
||||
}
|
||||
93
server/channels/app/slashcommands/command_share_test.go
Обычный файл
93
server/channels/app/slashcommands/command_share_test.go
Обычный файл
@@ -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)
|
||||
})
|
||||
}
|
||||
44
server/channels/app/slashcommands/command_shortcuts.go
Обычный файл
44
server/channels/app/slashcommands/command_shortcuts.go
Обычный файл
@@ -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,
|
||||
}
|
||||
}
|
||||
45
server/channels/app/slashcommands/command_shrug.go
Обычный файл
45
server/channels/app/slashcommands/command_shrug.go
Обычный файл
@@ -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}
|
||||
}
|
||||
53
server/channels/app/slashcommands/command_templates.go
Обычный файл
53
server/channels/app/slashcommands/command_templates.go
Обычный файл
@@ -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,
|
||||
}
|
||||
}
|
||||
616
server/channels/app/slashcommands/command_test.go
Обычный файл
616
server/channels/app/slashcommands/command_test.go
Обычный файл
@@ -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)
|
||||
}
|
||||
}
|
||||
456
server/channels/app/slashcommands/helper_test.go
Обычный файл
456
server/channels/app/slashcommands/helper_test.go
Обычный файл
@@ -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)
|
||||
}
|
||||
}
|
||||
24
server/channels/app/slashcommands/main_test.go
Обычный файл
24
server/channels/app/slashcommands/main_test.go
Обычный файл
@@ -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
Обычный файл
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
|
||||
}
|
||||
40
server/channels/app/slashcommands/util_test.go
Обычный файл
40
server/channels/app/slashcommands/util_test.go
Обычный файл
@@ -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)
|
||||
}
|
||||
}
|
||||
Ссылка в новой задаче
Block a user