Skip to content

Commit d6b9ff6

Browse files
Merge branch 'master' of github.com:mattermost/mattermost-plugin-github into cyrusjc/adding-mentions-to-sidebar
2 parents 520b6e2 + 041c9e0 commit d6b9ff6

File tree

43 files changed

+21597
-27984
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+21597
-27984
lines changed

CODEOWNERS

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
* @wiggin77
1+
* @mattermost/deployment-engineering

Makefile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,11 @@ ifneq ($(HAS_WEBAPP),)
246246
mkdir -p dist/$(PLUGIN_ID)/webapp
247247
cp -r webapp/dist dist/$(PLUGIN_ID)/webapp/
248248
endif
249+
ifeq ($(shell uname),Darwin)
250+
cd dist && tar --disable-copyfile -cvzf $(BUNDLE_NAME) $(PLUGIN_ID)
251+
else
249252
cd dist && tar -cvzf $(BUNDLE_NAME) $(PLUGIN_ID)
253+
endif
250254

251255
@echo plugin built at: dist/$(BUNDLE_NAME)
252256

server/mocks/mock_KvStore.go

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

server/plugin/api.go

Lines changed: 75 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ const (
3737
requestTimeout = 30 * time.Second
3838
oauthCompleteTimeout = 2 * time.Minute
3939

40-
channelIDParam = "channelId"
40+
channelIDParam = "channelId"
41+
organisationParam = "organization"
4142
)
4243

4344
type OAuthState struct {
@@ -1443,59 +1444,99 @@ func (p *Plugin) getReposByOrg(c *UserContext, w http.ResponseWriter, r *http.Re
14431444

14441445
opt := github.ListOptions{PerPage: 50}
14451446

1446-
org := r.URL.Query().Get("organization")
1447+
orgString := r.URL.Query().Get(organisationParam)
14471448

1448-
if org == "" {
1449+
if orgString == "" {
14491450
c.Log.Warnf("Organization query param is empty")
1450-
p.writeAPIError(w, &APIErrorResponse{Message: "Organization query is empty, must include organization name ", StatusCode: http.StatusBadRequest})
1451+
p.writeAPIError(w, &APIErrorResponse{Message: "Organization query parameter is empty, must include organization name ", StatusCode: http.StatusBadRequest})
1452+
return
1453+
}
1454+
1455+
channelIDString := r.URL.Query().Get(channelIDParam)
1456+
if channelIDString == "" {
1457+
c.Log.Warnf("Channel ID query param is empty")
1458+
p.writeAPIError(w, &APIErrorResponse{Message: "ChannelId query parameter is empty, must include Channel ID ", StatusCode: http.StatusBadRequest})
14511459
return
14521460
}
14531461

1462+
orgList := strings.Split(orgString, ",")
14541463
var allRepos []*github.Repository
1455-
var err error
1456-
var statusCode int
14571464

1458-
// If an organization is the username of an authenticated user then return repos where the authenticated user is the owner
1459-
if org == c.GHInfo.GitHubUsername {
1460-
allRepos, err = p.getRepositoryList(c.Ctx, c.GHInfo, "", githubClient, opt)
1461-
if err != nil {
1462-
c.Log.WithError(err).Errorf("Failed to list repositories")
1463-
p.writeAPIError(w, &APIErrorResponse{Message: "Failed to fetch repositories", StatusCode: http.StatusInternalServerError})
1464-
return
1465+
for _, org := range orgList {
1466+
org = strings.TrimSpace(org)
1467+
if org == "" {
1468+
continue
14651469
}
1466-
} else {
1467-
allRepos, statusCode, err = p.getRepositoryListByOrg(c.Ctx, c.GHInfo, org, githubClient, opt)
1468-
if err != nil {
1469-
if statusCode == http.StatusNotFound {
1470-
allRepos, err = p.getRepositoryList(c.Ctx, c.GHInfo, org, githubClient, opt)
1471-
if err != nil {
1472-
c.Log.WithError(err).Errorf("Failed to list repositories")
1473-
p.writeAPIError(w, &APIErrorResponse{Message: "Failed to fetch repositories", StatusCode: http.StatusInternalServerError})
1474-
return
1470+
1471+
var repos []*github.Repository
1472+
var err error
1473+
var statusCode int
1474+
1475+
// If an organization is the username of an authenticated user then return repos where the authenticated user is the owner
1476+
if org == c.GHInfo.GitHubUsername {
1477+
repos, err = p.getRepositoryList(c.Ctx, c.GHInfo, "", githubClient, opt)
1478+
if err != nil {
1479+
c.Log.WithError(err).Errorf("Failed to list repositories for user %s", org)
1480+
continue
1481+
}
1482+
} else {
1483+
repos, statusCode, err = p.getRepositoryListByOrg(c.Ctx, c.GHInfo, org, githubClient, opt)
1484+
if err != nil {
1485+
if statusCode == http.StatusNotFound {
1486+
repos, err = p.getRepositoryList(c.Ctx, c.GHInfo, org, githubClient, opt)
1487+
if err != nil {
1488+
c.Log.WithError(err).Errorf("Failed to list repositories for org/user %s", org)
1489+
continue
1490+
}
1491+
} else {
1492+
c.Log.WithError(err).Warnf("Failed to list repositories for org %s", org)
1493+
continue
14751494
}
1476-
} else {
1477-
c.Log.WithError(err).Warnf("Failed to list repositories")
1478-
p.writeAPIError(w, &APIErrorResponse{Message: "Failed to fetch repositories", StatusCode: statusCode})
1479-
return
14801495
}
14811496
}
1497+
1498+
allRepos = append(allRepos, repos...)
14821499
}
1483-
// Only send repositories which are part of the requested organization
1484-
type RepositoryResponse struct {
1485-
Name string `json:"name,omitempty"`
1486-
FullName string `json:"full_name,omitempty"`
1487-
Permissions map[string]bool `json:"permissions,omitempty"`
1488-
}
14891500

1490-
resp := make([]*RepositoryResponse, len(allRepos))
1501+
// Only send repositories which are part of the requested organization(s)
1502+
1503+
repoResp := make([]RepoResponse, len(allRepos))
14911504
for i, r := range allRepos {
1492-
resp[i] = &RepositoryResponse{
1505+
repoResp[i] = RepoResponse{
14931506
Name: r.GetName(),
14941507
FullName: r.GetFullName(),
14951508
Permissions: r.GetPermissions(),
14961509
}
14971510
}
14981511

1512+
resp := RepositoryResponse{
1513+
Repos: repoResp,
1514+
}
1515+
1516+
// Add default repo if available
1517+
defaultRepo, dErr := p.GetDefaultRepo(c.GHInfo.UserID, channelIDString)
1518+
if dErr != nil {
1519+
c.Log.WithError(dErr).Warnf("Failed to get the default repo for the channel. UserID: %s. ChannelID: %s", c.GHInfo.UserID, channelIDString)
1520+
}
1521+
1522+
if defaultRepo != "" {
1523+
config := p.getConfiguration()
1524+
baseURL := config.getBaseURL()
1525+
owner, repo := parseOwnerAndRepo(defaultRepo, baseURL)
1526+
defaultRepository, err := getRepository(c.Ctx, owner, repo, githubClient)
1527+
if err != nil {
1528+
c.Log.WithError(err).Warnf("Failed to get the default repo %s/%s", owner, repo)
1529+
}
1530+
1531+
if defaultRepository != nil {
1532+
resp.DefaultRepo = RepoResponse{
1533+
Name: defaultRepository.GetName(),
1534+
FullName: defaultRepository.GetFullName(),
1535+
Permissions: defaultRepository.Permissions,
1536+
}
1537+
}
1538+
}
1539+
14991540
p.writeJSON(w, resp)
15001541
}
15011542

server/plugin/command.go

Lines changed: 94 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package plugin
66
import (
77
"context"
88
"fmt"
9+
"net/http"
910
"strings"
1011
"unicode"
1112

@@ -41,6 +42,8 @@ const (
4142
PerPageValue = 50
4243
)
4344

45+
var ErrNotFound = errors.New("github user not found")
46+
4447
const DefaultRepoKey string = "%s_%s-default-repo"
4548

4649
var validFeatures = map[string]bool{
@@ -187,6 +190,36 @@ func contains(s []string, e string) bool {
187190
return false
188191
}
189192

193+
func (p *Plugin) isValidGitHubUsername(username string, userInfo *GitHubUserInfo) (bool, error) {
194+
githubClient := p.githubConnectUser(context.Background(), userInfo)
195+
196+
if cErr := p.useGitHubClient(userInfo, func(userInfo *GitHubUserInfo, token *oauth2.Token) error {
197+
ghUser, _, err := githubClient.Users.Get(context.Background(), username)
198+
if err != nil {
199+
if gErr, ok := err.(*github.ErrorResponse); ok && gErr.Response.StatusCode == http.StatusNotFound {
200+
return ErrNotFound
201+
}
202+
203+
return err
204+
}
205+
206+
if ghUser == nil {
207+
return ErrNotFound
208+
}
209+
210+
return nil
211+
}); cErr != nil {
212+
if errors.Is(cErr, ErrNotFound) {
213+
return false, nil
214+
}
215+
216+
p.client.Log.Warn("Failed to fetch user", "error", cErr.Error())
217+
return false, errors.New("Failed to fetch user")
218+
}
219+
220+
return true, nil
221+
}
222+
190223
func (p *Plugin) handleMuteAdd(_ *model.CommandArgs, username string, userInfo *GitHubUserInfo) string {
191224
mutedUsernames, err := p.getMutedUsernames(userInfo)
192225
if err != nil {
@@ -198,7 +231,12 @@ func (p *Plugin) handleMuteAdd(_ *model.CommandArgs, username string, userInfo *
198231
return username + " is already muted"
199232
}
200233

201-
if strings.Contains(username, ",") {
234+
isValidUsername, err := p.isValidGitHubUsername(username, userInfo)
235+
if err != nil {
236+
return "Error occurred validating username"
237+
}
238+
239+
if strings.Contains(username, ",") || !isValidUsername {
202240
return "Invalid username provided"
203241
}
204242

@@ -226,7 +264,10 @@ func (p *Plugin) handleUnmute(_ *model.CommandArgs, username string, userInfo *G
226264
}
227265

228266
userToMute := []string{username}
229-
newMutedList := arrayDifference(mutedUsernames, userToMute)
267+
newMutedList, removed := arrayDifference(mutedUsernames, userToMute)
268+
if !removed {
269+
return username + " is not muted"
270+
}
230271

231272
_, err = p.store.Set(userInfo.UserID+"-muted-users", []byte(strings.Join(newMutedList, ",")))
232273
if err != nil {
@@ -237,7 +278,17 @@ func (p *Plugin) handleUnmute(_ *model.CommandArgs, username string, userInfo *G
237278
}
238279

239280
func (p *Plugin) handleUnmuteAll(_ *model.CommandArgs, userInfo *GitHubUserInfo) string {
240-
_, err := p.store.Set(userInfo.UserID+"-muted-users", []byte(""))
281+
mutedUsernames, err := p.getMutedUsernames(userInfo)
282+
if err != nil {
283+
p.client.Log.Error("error occurred getting muted users.", "UserID", userInfo.UserID, "Error", err)
284+
return "An error occurred getting muted users. Please try again later"
285+
}
286+
287+
if len(mutedUsernames) == 0 {
288+
return "You have no muted users"
289+
}
290+
291+
_, err = p.store.Set(userInfo.UserID+"-muted-users", []byte(""))
241292
if err != nil {
242293
return "Error occurred unmuting users"
243294
}
@@ -273,18 +324,22 @@ func (p *Plugin) handleMuteCommand(_ *plugin.Context, args *model.CommandArgs, p
273324
}
274325

275326
// Returns the elements in a, that are not in b
276-
func arrayDifference(a, b []string) []string {
327+
func arrayDifference(a, b []string) ([]string, bool) {
277328
mb := make(map[string]struct{}, len(b))
278329
for _, x := range b {
279330
mb[x] = struct{}{}
280331
}
332+
281333
var diff []string
334+
removed := false
282335
for _, x := range a {
283336
if _, found := mb[x]; !found {
284337
diff = append(diff, x)
338+
} else {
339+
removed = true
285340
}
286341
}
287-
return diff
342+
return diff, removed
288343
}
289344

290345
func (p *Plugin) handleSubscribe(c *plugin.Context, args *model.CommandArgs, parameters []string, userInfo *GitHubUserInfo) string {
@@ -599,8 +654,12 @@ func (p *Plugin) handleUnsubscribe(_ *plugin.Context, args *model.CommandArgs, p
599654

600655
owner = strings.ToLower(owner)
601656
repo = strings.ToLower(repo)
602-
if err := p.Unsubscribe(args.ChannelId, repo, owner); err != nil {
603-
p.client.Log.Warn("Failed to unsubscribe", "repo", repo, "error", err.Error())
657+
if sErr := p.Unsubscribe(args.ChannelId, repo, owner); sErr != nil {
658+
if sErr.Code == SubscriptionNotFound {
659+
return sErr.Error.Error()
660+
}
661+
662+
p.client.Log.Warn("Failed to unsubscribe", "repo", repo, "error", sErr.Error.Error())
604663
return "Encountered an error trying to unsubscribe. Please try again."
605664
}
606665

@@ -798,8 +857,8 @@ func (p *Plugin) handleSetDefaultRepo(args *model.CommandArgs, parameters []stri
798857
owner = strings.ToLower(owner)
799858
repo = strings.ToLower(repo)
800859

801-
if config.GitHubOrg != "" && strings.ToLower(config.GitHubOrg) != owner {
802-
return fmt.Sprintf("Repository is not part of the locked Github organization. Locked Github organization: %s", config.GitHubOrg)
860+
if config.GitHubOrg != "" && !p.isOrgInLockedOrgs(config.GitHubOrg, owner) {
861+
return fmt.Sprintf("Repository is not part of the locked Github organization. Locked Github organizations: %s", config.GitHubOrg)
803862
}
804863

805864
ctx := context.Background()
@@ -866,6 +925,21 @@ func (p *Plugin) handleUnSetDefaultRepo(args *model.CommandArgs, userInfo *GitHu
866925
return "The default repository has been unset successfully"
867926
}
868927

928+
func (p *Plugin) isOrgInLockedOrgs(configuredOrgs, owner string) bool {
929+
if configuredOrgs == "" {
930+
return true
931+
}
932+
933+
orgs := strings.Split(configuredOrgs, ",")
934+
for _, org := range orgs {
935+
if strings.EqualFold(strings.TrimSpace(org), strings.TrimSpace(owner)) {
936+
return true
937+
}
938+
}
939+
940+
return false
941+
}
942+
869943
func (p *Plugin) handleSetup(_ *plugin.Context, args *model.CommandArgs, parameters []string) string {
870944
userID := args.UserId
871945
isSysAdmin, err := p.isAuthorizedSysAdmin(userID)
@@ -944,6 +1018,14 @@ func (p *Plugin) ExecuteCommand(c *plugin.Context, args *model.CommandArgs) (*mo
9441018
return &model.CommandResponse{}, nil
9451019
}
9461020

1021+
if action == "help" {
1022+
message := p.handleHelp(c, args, parameters, nil)
1023+
if message != "" {
1024+
p.postCommandResponse(args, message)
1025+
}
1026+
return &model.CommandResponse{}, nil
1027+
}
1028+
9471029
config := p.getConfiguration()
9481030

9491031
if validationErr := config.IsValid(); validationErr != nil {
@@ -1032,6 +1114,9 @@ func getAutocompleteData(config *Configuration) *model.AutocompleteData {
10321114
about := command.BuildInfoAutocomplete("about")
10331115
github.AddCommand(about)
10341116

1117+
help := model.NewAutocompleteData("help", "", "Display Slash Command help text")
1118+
github.AddCommand(help)
1119+
10351120
return github
10361121
}
10371122

0 commit comments

Comments
 (0)