|
|
@ -11,6 +11,7 @@ import ( |
|
|
|
"strconv" |
|
|
|
"strings" |
|
|
|
|
|
|
|
"bytes" |
|
|
|
log "github.com/Sirupsen/logrus" |
|
|
|
gogithub "github.com/google/go-github/github" |
|
|
|
"github.com/matrix-org/go-neb/database" |
|
|
@ -19,6 +20,7 @@ import ( |
|
|
|
"github.com/matrix-org/go-neb/services/github/client" |
|
|
|
"github.com/matrix-org/go-neb/types" |
|
|
|
"github.com/matrix-org/gomatrix" |
|
|
|
"html" |
|
|
|
) |
|
|
|
|
|
|
|
// ServiceType of the Github service
|
|
|
@ -77,14 +79,65 @@ func (s *Service) requireGithubClientFor(userID string) (cli *gogithub.Client, r |
|
|
|
return |
|
|
|
} |
|
|
|
|
|
|
|
const numberGithubSearchSummaries = 3 |
|
|
|
const cmdGithubSearchUsage = `!github create owner/repo "search query"` |
|
|
|
|
|
|
|
func (s *Service) cmdGithubSearch(roomID, userID string, args []string) (interface{}, error) { |
|
|
|
cli := s.githubClientFor(userID, true) |
|
|
|
if len(args) < 2 { |
|
|
|
return &gomatrix.TextMessage{"m.notice", "Usage: " + cmdGithubSearchUsage}, nil |
|
|
|
} |
|
|
|
|
|
|
|
query := fmt.Sprintf("repo:%s %s", args[0], strings.Join(args[1:], " ")) |
|
|
|
searchResult, res, err := cli.Search.Issues(query, nil) |
|
|
|
|
|
|
|
if err != nil { |
|
|
|
log.WithField("err", err).Print("Failed to search") |
|
|
|
if res == nil { |
|
|
|
return nil, fmt.Errorf("Failed to search. Failed to connect to Github") |
|
|
|
} |
|
|
|
return nil, fmt.Errorf("Failed to search. HTTP %d", res.StatusCode) |
|
|
|
} |
|
|
|
|
|
|
|
if searchResult.Total == nil || *searchResult.Total == 0 { |
|
|
|
return &gomatrix.TextMessage{"m.notice", "No results found for your search query!"}, nil |
|
|
|
} |
|
|
|
|
|
|
|
numResults := *searchResult.Total |
|
|
|
var htmlBuffer bytes.Buffer |
|
|
|
var plainBuffer bytes.Buffer |
|
|
|
htmlBuffer.WriteString(fmt.Sprintf("Found %d results, here are the most relevant:<br><ol>", numResults)) |
|
|
|
plainBuffer.WriteString(fmt.Sprintf("Found %d results, here are the most relevant:\n", numResults)) |
|
|
|
for i, issue := range searchResult.Issues { |
|
|
|
if i >= numberGithubSearchSummaries { |
|
|
|
break |
|
|
|
} |
|
|
|
if issue.HTMLURL == nil || issue.User.Login == nil || issue.Title == nil { |
|
|
|
continue |
|
|
|
} |
|
|
|
escapedTitle, escapedUserLogin := html.EscapeString(*issue.Title), html.EscapeString(*issue.User.Login) |
|
|
|
htmlBuffer.WriteString(fmt.Sprintf(`<li><a href="%s" rel="noopener">%s: %s</a></li>`, *issue.HTMLURL, escapedUserLogin, escapedTitle)) |
|
|
|
plainBuffer.WriteString(fmt.Sprintf("%d. %s\n", i+1, *issue.HTMLURL)) |
|
|
|
} |
|
|
|
htmlBuffer.WriteString("</ol>") |
|
|
|
|
|
|
|
return &gomatrix.HTMLMessage{ |
|
|
|
Body: plainBuffer.String(), |
|
|
|
MsgType: "m.notice", |
|
|
|
Format: "org.matrix.custom.html", |
|
|
|
FormattedBody: htmlBuffer.String(), |
|
|
|
}, nil |
|
|
|
} |
|
|
|
|
|
|
|
const cmdGithubCreateUsage = `!github create [owner/repo] "issue title" "description"` |
|
|
|
|
|
|
|
func (s *Service) cmdGithubCreate(roomID, userID string, args []string) (interface{}, error) { |
|
|
|
cli, resp, err := s.requireGithubClientFor(userID) |
|
|
|
if cli == nil { |
|
|
|
return resp, err |
|
|
|
} |
|
|
|
if len(args) == 0 { |
|
|
|
return &gomatrix.TextMessage{"m.notice", |
|
|
|
`Usage: !github create owner/repo "issue title" "description"`}, nil |
|
|
|
return &gomatrix.TextMessage{"m.notice", "Usage: " + cmdGithubCreateUsage}, nil |
|
|
|
} |
|
|
|
|
|
|
|
// We expect the args to look like:
|
|
|
@ -97,14 +150,12 @@ func (s *Service) cmdGithubCreate(roomID, userID string, args []string) (interfa |
|
|
|
// look for a default repo
|
|
|
|
defaultRepo := s.defaultRepo(roomID) |
|
|
|
if defaultRepo == "" { |
|
|
|
return &gomatrix.TextMessage{"m.notice", |
|
|
|
`Usage: !github create owner/repo "issue title" "description"`}, nil |
|
|
|
return &gomatrix.TextMessage{"m.notice", "Need to specify repo. Usage: " + cmdGithubCreateUsage}, nil |
|
|
|
} |
|
|
|
// default repo should pass the regexp
|
|
|
|
ownerRepoGroups = ownerRepoRegex.FindStringSubmatch(defaultRepo) |
|
|
|
if len(ownerRepoGroups) == 0 { |
|
|
|
return &gomatrix.TextMessage{"m.notice", |
|
|
|
`Malformed default repo. Usage: !github create owner/repo "issue title" "description"`}, nil |
|
|
|
return &gomatrix.TextMessage{"m.notice", "Malformed default repo. Usage: " + cmdGithubCreateUsage}, nil |
|
|
|
} |
|
|
|
|
|
|
|
// insert the default as the first arg to reuse the same indices
|
|
|
@ -142,55 +193,87 @@ func (s *Service) cmdGithubCreate(roomID, userID string, args []string) (interfa |
|
|
|
return gomatrix.TextMessage{"m.notice", fmt.Sprintf("Created issue: %s", *issue.HTMLURL)}, nil |
|
|
|
} |
|
|
|
|
|
|
|
func (s *Service) cmdGithubComment(roomID, userID string, args []string) (interface{}, error) { |
|
|
|
var cmdGithubReactAliases = map[string]string{ |
|
|
|
"+1": "+1", |
|
|
|
":+1:": "+1", |
|
|
|
"👍": "+1", |
|
|
|
|
|
|
|
"-1": "-1", |
|
|
|
":-1:": "-1", |
|
|
|
"👎": "-1", |
|
|
|
|
|
|
|
"laugh": "laugh", |
|
|
|
"smile": "laugh", |
|
|
|
":smile:": "laugh", |
|
|
|
"😄": "laugh", |
|
|
|
"grin": "laugh", |
|
|
|
|
|
|
|
"confused": "confused", |
|
|
|
":confused:": "confused", |
|
|
|
"😕": "confused", |
|
|
|
"uncertain": "confused", |
|
|
|
|
|
|
|
"heart": "heart", |
|
|
|
":heart:": "heart", |
|
|
|
"❤": "heart", |
|
|
|
"❤️": "heart", |
|
|
|
|
|
|
|
"hooray": "hooray", |
|
|
|
"tada": "hooray", |
|
|
|
":tada:": "hooray", |
|
|
|
"🎉": "hooray", |
|
|
|
} |
|
|
|
|
|
|
|
const cmdGithubReactUsage = `!github react [owner/repo]#issue (+1|👍|-1|:-1:|laugh|:smile:|confused|uncertain|heart|❤|hooray|:tada:)` |
|
|
|
|
|
|
|
func (s *Service) cmdGithubReact(roomID, userID string, args []string) (interface{}, error) { |
|
|
|
cli, resp, err := s.requireGithubClientFor(userID) |
|
|
|
if cli == nil { |
|
|
|
return resp, err |
|
|
|
} |
|
|
|
if len(args) == 0 { |
|
|
|
return &gomatrix.TextMessage{"m.notice", |
|
|
|
`Usage: !github comment [owner/repo]#issue "comment text"`}, nil |
|
|
|
if len(args) < 2 { |
|
|
|
return &gomatrix.TextMessage{"m.notice", "Usage: " + cmdGithubReactUsage}, nil |
|
|
|
} |
|
|
|
|
|
|
|
// We expect the args to look like:
|
|
|
|
// [ "[owner/repo]#issue", "comment" ]
|
|
|
|
// They can omit the owner/repo if there is a default one set.
|
|
|
|
// Look for a default if the first arg is just an issue number
|
|
|
|
ownerRepoIssueGroups := ownerRepoIssueRegexAnchored.FindStringSubmatch(args[0]) |
|
|
|
reaction, ok := cmdGithubReactAliases[args[1]] |
|
|
|
if !ok { |
|
|
|
return &gomatrix.TextMessage{"m.notice", "Invalid reaction. Usage: " + cmdGithubReactUsage}, nil |
|
|
|
} |
|
|
|
|
|
|
|
if len(ownerRepoIssueGroups) != 5 { |
|
|
|
return &gomatrix.TextMessage{"m.notice", |
|
|
|
`Usage: !github comment [owner/repo]#issue "comment text"`}, nil |
|
|
|
// get owner,repo,issue,resp out of args[0]
|
|
|
|
owner, repo, issueNum, resp := s.getIssueDetailsFor(args[0], roomID, cmdGithubReactUsage) |
|
|
|
if resp != nil { |
|
|
|
return resp, nil |
|
|
|
} |
|
|
|
|
|
|
|
if ownerRepoIssueGroups[1] == "" { |
|
|
|
// issue only match, this only works if there is a default repo
|
|
|
|
defaultRepo := s.defaultRepo(roomID) |
|
|
|
if defaultRepo == "" { |
|
|
|
return &gomatrix.TextMessage{"m.notice", |
|
|
|
`Usage: !github comment [owner/repo]#issue "comment text"`}, nil |
|
|
|
} |
|
|
|
_, res, err := cli.Reactions.CreateIssueReaction(owner, repo, issueNum, reaction) |
|
|
|
|
|
|
|
segs := strings.Split(defaultRepo, "/") |
|
|
|
if len(segs) != 2 { |
|
|
|
return &gomatrix.TextMessage{"m.notice", |
|
|
|
`Malformed default repo. Usage: !github comment [owner/repo]#issue "comment text"`}, nil |
|
|
|
if err != nil { |
|
|
|
log.WithField("err", err).Print("Failed to react to issue") |
|
|
|
if res == nil { |
|
|
|
return nil, fmt.Errorf("Failed to react to issue. Failed to connect to Github") |
|
|
|
} |
|
|
|
return nil, fmt.Errorf("Failed to react to issue. HTTP %d", res.StatusCode) |
|
|
|
} |
|
|
|
|
|
|
|
// Fill in the missing fields in matching groups and fall through into ["foo/bar#11", "foo", "bar", "11"]
|
|
|
|
ownerRepoIssueGroups = []string{ |
|
|
|
defaultRepo + ownerRepoIssueGroups[0], |
|
|
|
defaultRepo, |
|
|
|
segs[0], |
|
|
|
segs[1], |
|
|
|
ownerRepoIssueGroups[4], |
|
|
|
} |
|
|
|
return gomatrix.TextMessage{"m.notice", fmt.Sprintf("Reacted to issue with: %s", args[1])}, nil |
|
|
|
} |
|
|
|
|
|
|
|
const cmdGithubCommentUsage = `!github comment [owner/repo]#issue "comment text"` |
|
|
|
|
|
|
|
func (s *Service) cmdGithubComment(roomID, userID string, args []string) (interface{}, error) { |
|
|
|
cli, resp, err := s.requireGithubClientFor(userID) |
|
|
|
if cli == nil { |
|
|
|
return resp, err |
|
|
|
} |
|
|
|
if len(args) == 0 { |
|
|
|
return &gomatrix.TextMessage{"m.notice", "Usage: " + cmdGithubCommentUsage}, nil |
|
|
|
} |
|
|
|
|
|
|
|
issueNum, err := strconv.Atoi(ownerRepoIssueGroups[4]) |
|
|
|
if err != nil { |
|
|
|
return &gomatrix.TextMessage{"m.notice", |
|
|
|
`Malformed issue number. Usage: !github comment [owner/repo]#issue "comment text"`}, nil |
|
|
|
// get owner,repo,issue,resp out of args[0]
|
|
|
|
owner, repo, issueNum, resp := s.getIssueDetailsFor(args[0], roomID, cmdGithubCommentUsage) |
|
|
|
if resp != nil { |
|
|
|
return resp, nil |
|
|
|
} |
|
|
|
|
|
|
|
var comment *string |
|
|
@ -202,21 +285,127 @@ func (s *Service) cmdGithubComment(roomID, userID string, args []string) (interf |
|
|
|
comment = &joinedComment |
|
|
|
} |
|
|
|
|
|
|
|
issueComment, res, err := cli.Issues.CreateComment(ownerRepoIssueGroups[2], ownerRepoIssueGroups[3], issueNum, &gogithub.IssueComment{ |
|
|
|
issueComment, res, err := cli.Issues.CreateComment(owner, repo, issueNum, &gogithub.IssueComment{ |
|
|
|
Body: comment, |
|
|
|
}) |
|
|
|
|
|
|
|
if err != nil { |
|
|
|
log.WithField("err", err).Print("Failed to create issue") |
|
|
|
log.WithField("err", err).Print("Failed to create issue comment") |
|
|
|
if res == nil { |
|
|
|
return nil, fmt.Errorf("Failed to create issue. Failed to connect to Github") |
|
|
|
return nil, fmt.Errorf("Failed to create issue comment. Failed to connect to Github") |
|
|
|
} |
|
|
|
return nil, fmt.Errorf("Failed to create issue. HTTP %d", res.StatusCode) |
|
|
|
return nil, fmt.Errorf("Failed to create issue comment. HTTP %d", res.StatusCode) |
|
|
|
} |
|
|
|
|
|
|
|
return gomatrix.TextMessage{"m.notice", fmt.Sprintf("Commented on issue: %s", *issueComment.HTMLURL)}, nil |
|
|
|
} |
|
|
|
|
|
|
|
const cmdGithubAssignUsage = `!github assign [owner/repo]#issue username [username] [...]` |
|
|
|
|
|
|
|
func (s *Service) cmdGithubAssign(roomID, userID string, args []string) (interface{}, error) { |
|
|
|
cli, resp, err := s.requireGithubClientFor(userID) |
|
|
|
if cli == nil { |
|
|
|
return resp, err |
|
|
|
} |
|
|
|
if len(args) < 1 { |
|
|
|
return &gomatrix.TextMessage{"m.notice", "Usage: " + cmdGithubAssignUsage}, nil |
|
|
|
} else if len(args) < 2 { |
|
|
|
return &gomatrix.TextMessage{"m.notice", "Needs at least one username. Usage: " + cmdGithubAssignUsage}, nil |
|
|
|
} |
|
|
|
|
|
|
|
// get owner,repo,issue,resp out of args[0]
|
|
|
|
owner, repo, issueNum, resp := s.getIssueDetailsFor(args[0], roomID, cmdGithubAssignUsage) |
|
|
|
if resp != nil { |
|
|
|
return resp, nil |
|
|
|
} |
|
|
|
|
|
|
|
issue, res, err := cli.Issues.AddAssignees(owner, repo, issueNum, args[1:]) |
|
|
|
|
|
|
|
if err != nil { |
|
|
|
log.WithField("err", err).Print("Failed to add issue assignees") |
|
|
|
if res == nil { |
|
|
|
return nil, fmt.Errorf("Failed to add issue assignees. Failed to connect to Github") |
|
|
|
} |
|
|
|
return nil, fmt.Errorf("Failed to add issue assignees. HTTP %d", res.StatusCode) |
|
|
|
} |
|
|
|
|
|
|
|
return gomatrix.TextMessage{"m.notice", fmt.Sprintf("Added assignees to issue: %s", *issue.HTMLURL)}, nil |
|
|
|
} |
|
|
|
|
|
|
|
const cmdGithubCloseUsage = `!github close [owner/repo]#issue` |
|
|
|
|
|
|
|
func (s *Service) cmdGithubClose(roomID, userID string, args []string) (interface{}, error) { |
|
|
|
cli, resp, err := s.requireGithubClientFor(userID) |
|
|
|
if cli == nil { |
|
|
|
return resp, err |
|
|
|
} |
|
|
|
if len(args) == 0 { |
|
|
|
return &gomatrix.TextMessage{"m.notice", "Usage: " + cmdGithubCloseUsage}, nil |
|
|
|
} |
|
|
|
|
|
|
|
// get owner,repo,issue,resp out of args[0]
|
|
|
|
owner, repo, issueNum, resp := s.getIssueDetailsFor(args[0], roomID, cmdGithubCloseUsage) |
|
|
|
if resp != nil { |
|
|
|
return resp, nil |
|
|
|
} |
|
|
|
|
|
|
|
state := "closed" |
|
|
|
issueComment, res, err := cli.Issues.Edit(owner, repo, issueNum, &gogithub.IssueRequest{ |
|
|
|
State: &state, |
|
|
|
}) |
|
|
|
|
|
|
|
if err != nil { |
|
|
|
log.WithField("err", err).Print("Failed to close issue") |
|
|
|
if res == nil { |
|
|
|
return nil, fmt.Errorf("Failed to close issue. Failed to connect to Github") |
|
|
|
} |
|
|
|
return nil, fmt.Errorf("Failed to close issue. HTTP %d", res.StatusCode) |
|
|
|
} |
|
|
|
|
|
|
|
return gomatrix.TextMessage{"m.notice", fmt.Sprintf("Closed issue: %s", *issueComment.HTMLURL)}, nil |
|
|
|
} |
|
|
|
|
|
|
|
func (s *Service) getIssueDetailsFor(input, roomID, usage string) (owner, repo string, issueNum int, resp interface{}) { |
|
|
|
// We expect the input to look like:
|
|
|
|
// "[owner/repo]#issue"
|
|
|
|
// They can omit the owner/repo if there is a default one set.
|
|
|
|
// Look for a default if the first arg is just an issue number
|
|
|
|
ownerRepoIssueGroups := ownerRepoIssueRegexAnchored.FindStringSubmatch(input) |
|
|
|
|
|
|
|
if len(ownerRepoIssueGroups) != 5 { |
|
|
|
resp = &gomatrix.TextMessage{"m.notice", "Usage: " + usage} |
|
|
|
return |
|
|
|
} |
|
|
|
|
|
|
|
owner = ownerRepoIssueGroups[2] |
|
|
|
repo = ownerRepoIssueGroups[3] |
|
|
|
|
|
|
|
var err error |
|
|
|
if issueNum, err = strconv.Atoi(ownerRepoIssueGroups[4]); err != nil { |
|
|
|
resp = &gomatrix.TextMessage{"m.notice", "Malformed issue number. Usage: " + usage} |
|
|
|
return |
|
|
|
} |
|
|
|
|
|
|
|
if ownerRepoIssueGroups[1] == "" { |
|
|
|
// issue only match, this only works if there is a default repo
|
|
|
|
defaultRepo := s.defaultRepo(roomID) |
|
|
|
if defaultRepo == "" { |
|
|
|
resp = &gomatrix.TextMessage{"m.notice", "Need to specify repo. Usage: " + usage} |
|
|
|
return |
|
|
|
} |
|
|
|
|
|
|
|
segs := strings.Split(defaultRepo, "/") |
|
|
|
if len(segs) != 2 { |
|
|
|
resp = &gomatrix.TextMessage{"m.notice", "Malformed default repo. Usage: " + usage} |
|
|
|
return |
|
|
|
} |
|
|
|
|
|
|
|
owner = segs[0] |
|
|
|
repo = segs[1] |
|
|
|
} |
|
|
|
return |
|
|
|
} |
|
|
|
|
|
|
|
func (s *Service) expandIssue(roomID, userID, owner, repo string, issueNum int) interface{} { |
|
|
|
cli := s.githubClientFor(userID, true) |
|
|
|
|
|
|
@ -247,25 +436,54 @@ func (s *Service) expandIssue(roomID, userID, owner, repo string, issueNum int) |
|
|
|
// is no link, it will return a Starter Link instead.
|
|
|
|
func (s *Service) Commands(cli *gomatrix.Client) []types.Command { |
|
|
|
return []types.Command{ |
|
|
|
types.Command{ |
|
|
|
Path: []string{"github", "search"}, |
|
|
|
Command: func(roomID, userID string, args []string) (interface{}, error) { |
|
|
|
return s.cmdGithubSearch(roomID, userID, args) |
|
|
|
}, |
|
|
|
}, |
|
|
|
types.Command{ |
|
|
|
Path: []string{"github", "create"}, |
|
|
|
Command: func(roomID, userID string, args []string) (interface{}, error) { |
|
|
|
return s.cmdGithubCreate(roomID, userID, args) |
|
|
|
}, |
|
|
|
}, |
|
|
|
types.Command{ |
|
|
|
Path: []string{"github", "react"}, |
|
|
|
Command: func(roomID, userID string, args []string) (interface{}, error) { |
|
|
|
return s.cmdGithubReact(roomID, userID, args) |
|
|
|
}, |
|
|
|
}, |
|
|
|
types.Command{ |
|
|
|
Path: []string{"github", "comment"}, |
|
|
|
Command: func(roomID, userID string, args []string) (interface{}, error) { |
|
|
|
return s.cmdGithubComment(roomID, userID, args) |
|
|
|
}, |
|
|
|
}, |
|
|
|
types.Command{ |
|
|
|
Path: []string{"github", "assign"}, |
|
|
|
Command: func(roomID, userID string, args []string) (interface{}, error) { |
|
|
|
return s.cmdGithubAssign(roomID, userID, args) |
|
|
|
}, |
|
|
|
}, |
|
|
|
types.Command{ |
|
|
|
Path: []string{"github", "close"}, |
|
|
|
Command: func(roomID, userID string, args []string) (interface{}, error) { |
|
|
|
return s.cmdGithubClose(roomID, userID, args) |
|
|
|
}, |
|
|
|
}, |
|
|
|
types.Command{ |
|
|
|
Path: []string{"github", "help"}, |
|
|
|
Command: func(roomID, userID string, args []string) (interface{}, error) { |
|
|
|
return &gomatrix.TextMessage{ |
|
|
|
"m.notice", |
|
|
|
fmt.Sprintf(`!github create owner/repo "title text" "description text"` + "\n" + |
|
|
|
`!github comment [owner/repo]#issue "comment text"`), |
|
|
|
strings.Join([]string{ |
|
|
|
cmdGithubCreateUsage, |
|
|
|
cmdGithubReactUsage, |
|
|
|
cmdGithubCommentUsage, |
|
|
|
cmdGithubAssignUsage, |
|
|
|
cmdGithubCloseUsage, |
|
|
|
}, "\n"), |
|
|
|
}, nil |
|
|
|
}, |
|
|
|
}, |
|
|
|