func (s *Service) Commands(cli types.MatrixClient) []types.Command {
	return []types.Command{
		{
			Path: []string{"github", "search"},
			Command: func(roomID id.RoomID, userID id.UserID, args []string) (interface{}, error) {
				return s.cmdGithubSearch(roomID, userID, args)
			},
		},
		{
			Path: []string{"github", "create"},
			Command: func(roomID id.RoomID, userID id.UserID, args []string) (interface{}, error) {
				return s.cmdGithubCreate(roomID, userID, args)
			},
		},
		{
			Path: []string{"github", "react"},
			Command: func(roomID id.RoomID, userID id.UserID, args []string) (interface{}, error) {
				return s.cmdGithubReact(roomID, userID, args)
			},
		},
		{
			Path: []string{"github", "comment"},
			Command: func(roomID id.RoomID, userID id.UserID, args []string) (interface{}, error) {
				return s.cmdGithubComment(roomID, userID, args)
			},
		},
		{
			Path: []string{"github", "assign"},
			Command: func(roomID id.RoomID, userID id.UserID, args []string) (interface{}, error) {
				return s.cmdGithubAssign(roomID, userID, args)
			},
		},
		{
			Path: []string{"github", "close"},
			Command: func(roomID id.RoomID, userID id.UserID, args []string) (interface{}, error) {
				return s.cmdGithubClose(roomID, userID, args)
			},
		},
		{
			Path: []string{"github", "reopen"},
			Command: func(roomID id.RoomID, userID id.UserID, args []string) (interface{}, error) {
				return s.cmdGithubReopen(roomID, userID, args)
			},
		},
		{
			Path: []string{"github", "help"},
			Command: func(roomID id.RoomID, userID id.UserID, args []string) (interface{}, error) {
				return &mevt.MessageEventContent{
					MsgType: mevt.MsgNotice,
					Body: strings.Join([]string{
						cmdGithubCreateUsage,
						cmdGithubReactUsage,
						cmdGithubCommentUsage,
						cmdGithubAssignUsage,
						cmdGithubCloseUsage,
						cmdGithubReopenUsage,
					}, "\n"),
				}, nil
			},
		},
	}
}
Commands supported:
!github create owner/repo "issue title" "optional issue description"
Responds with the outcome of the issue creation request. This command requires
a Github account to be linked to the Matrix user ID issuing the command. If there
is no link, it will return a Starter Link instead.
!github comment [owner/repo]#issue "comment"
Responds with the outcome of the issue comment creation request. This command requires
a Github account to be linked to the Matrix user ID issuing the command. If there
is no link, it will return a Starter Link instead.
package github
import (
	"context"
	"database/sql"
	"fmt"
	"regexp"
	"strconv"
	"strings"
	"bytes"
	"html"
	gogithub "github.com/google/go-github/github"
	"github.com/matrix-org/go-neb/database"
	"github.com/matrix-org/go-neb/matrix"
	"github.com/matrix-org/go-neb/realms/github"
	"github.com/matrix-org/go-neb/services/github/client"
	"github.com/matrix-org/go-neb/types"
	log "github.com/sirupsen/logrus"
	mevt "maunium.net/go/mautrix/event"
	"maunium.net/go/mautrix/id"
)
const ServiceType = "github"
var ownerRepoBaseRegex = `(?:(?:([A-z0-9-_.]+)/([A-z0-9-_.]+))|\B)`
var ownerRepoIssueRegex = regexp.MustCompile(ownerRepoBaseRegex + `#([0-9]+)\b`)
var ownerRepoCommitRegex = regexp.MustCompile(ownerRepoBaseRegex + `@([0-9a-fA-F]+)\b`)
var ownerRepoIssueRegexAnchored = regexp.MustCompile(`^(([A-z0-9-_.]+)/([A-z0-9-_.]+))?#([0-9]+)$`)
var ownerRepoRegex = regexp.MustCompile(`^([A-z0-9-_.]+)/([A-z0-9-_.]+)$`)
type Service struct {
	types.DefaultService
	RealmID string
}
func (s *Service) requireGithubClientFor(userID id.UserID) (cli *gogithub.Client, resp interface{}, err error) {
	cli = s.githubClientFor(userID, false)
	if cli == nil {
		var r types.AuthRealm
		if r, err = database.GetServiceDB().LoadAuthRealm(s.RealmID); err != nil {
			return
		}
		if ghRealm, ok := r.(*github.Realm); ok {
			resp = matrix.StarterLinkMessage{
				Body: "You need to log into Github before you can create issues.",
				Link: ghRealm.StarterLink,
			}
		} else {
			err = fmt.Errorf("Failed to cast realm %s into a GithubRealm", s.RealmID)
		}
	}
	return
}
const numberGithubSearchSummaries = 3
const cmdGithubSearchUsage = `!github search "search query"`
func (s *Service) cmdGithubSearch(roomID id.RoomID, userID id.UserID, args []string) (interface{}, error) {
	cli := s.githubClientFor(userID, true)
	if len(args) < 2 {
		return &mevt.MessageEventContent{
			MsgType: mevt.MsgNotice,
			Body:    "Usage: " + cmdGithubSearchUsage,
		}, nil
	}
	query := strings.Join(args, " ")
	searchResult, res, err := cli.Search.Issues(context.Background(), 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 &mevt.MessageEventContent{
			MsgType: mevt.MsgNotice,
			Body:    "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 &mevt.MessageEventContent{
		Body:          plainBuffer.String(),
		MsgType:       mevt.MsgNotice,
		Format:        "org.matrix.custom.html",
		FormattedBody: htmlBuffer.String(),
	}, nil
}
const cmdGithubCreateUsage = `!github create [owner/repo] "issue title" "description"`
func (s *Service) cmdGithubCreate(roomID id.RoomID, userID id.UserID, args []string) (interface{}, error) {
	cli, resp, err := s.requireGithubClientFor(userID)
	if cli == nil {
		return resp, err
	}
	if len(args) == 0 {
		return &mevt.MessageEventContent{
			MsgType: mevt.MsgNotice,
			Body:    "Usage: " + cmdGithubCreateUsage,
		}, nil
	}
	ownerRepoGroups := ownerRepoRegex.FindStringSubmatch(args[0])
	if len(ownerRepoGroups) == 0 {
		defaultRepo := s.defaultRepo(roomID)
		if defaultRepo == "" {
			return &mevt.MessageEventContent{
				MsgType: mevt.MsgNotice,
				Body:    "Need to specify repo. Usage: " + cmdGithubCreateUsage,
			}, nil
		}
		ownerRepoGroups = ownerRepoRegex.FindStringSubmatch(defaultRepo)
		if len(ownerRepoGroups) == 0 {
			return &mevt.MessageEventContent{
				MsgType: mevt.MsgNotice, Body: "Malformed default repo. Usage: " + cmdGithubCreateUsage}, nil
		}
		args = append([]string{defaultRepo}, args...)
	}
	var (
		title *string
		desc  *string
	)
	if len(args) == 2 {
		title = &args[1]
	} else if len(args) == 3 {
		title = &args[1]
		desc = &args[2]
	} else {  
		joinedTitle := strings.Join(args[1:], " ")
		title = &joinedTitle
	}
	issue, res, err := cli.Issues.Create(context.Background(), ownerRepoGroups[1], ownerRepoGroups[2], &gogithub.IssueRequest{
		Title: title,
		Body:  desc,
	})
	if err != nil {
		log.WithField("err", err).Print("Failed to create issue")
		if res == nil {
			return nil, fmt.Errorf("Failed to create issue. Failed to connect to Github")
		}
		return nil, fmt.Errorf("Failed to create issue. HTTP %d", res.StatusCode)
	}
	return mevt.MessageEventContent{
		MsgType: mevt.MsgNotice, Body: fmt.Sprintf("Created issue: %s", *issue.HTMLURL)}, nil
}
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 id.RoomID, userID id.UserID, args []string) (interface{}, error) {
	cli, resp, err := s.requireGithubClientFor(userID)
	if cli == nil {
		return resp, err
	}
	if len(args) < 2 {
		return &mevt.MessageEventContent{
			MsgType: mevt.MsgNotice, Body: "Usage: " + cmdGithubReactUsage,
		}, nil
	}
	reaction, ok := cmdGithubReactAliases[args[1]]
	if !ok {
		return &mevt.MessageEventContent{
			MsgType: mevt.MsgNotice,
			Body:    "Invalid reaction. Usage: " + cmdGithubReactUsage,
		}, nil
	}
	owner, repo, issueNum, resp := s.getIssueDetailsFor(args[0], roomID, cmdGithubReactUsage)
	if resp != nil {
		return resp, nil
	}
	_, res, err := cli.Reactions.CreateIssueReaction(context.Background(), owner, repo, issueNum, reaction)
	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)
	}
	return mevt.MessageEventContent{
		MsgType: mevt.MsgNotice,
		Body:    fmt.Sprintf("Reacted to issue with: %s", args[1]),
	}, nil
}
const cmdGithubCommentUsage = `!github comment [owner/repo]#issue "comment text"`
func (s *Service) cmdGithubComment(roomID id.RoomID, userID id.UserID, args []string) (interface{}, error) {
	cli, resp, err := s.requireGithubClientFor(userID)
	if cli == nil {
		return resp, err
	}
	if len(args) == 0 {
		return &mevt.MessageEventContent{
			MsgType: mevt.MsgNotice,
			Body:    "Usage: " + cmdGithubCommentUsage,
		}, nil
	}
	owner, repo, issueNum, resp := s.getIssueDetailsFor(args[0], roomID, cmdGithubCommentUsage)
	if resp != nil {
		return resp, nil
	}
	var comment *string
	if len(args) == 2 {
		comment = &args[1]
	} else {  
		joinedComment := strings.Join(args[1:], " ")
		comment = &joinedComment
	}
	issueComment, res, err := cli.Issues.CreateComment(context.Background(), owner, repo, issueNum, &gogithub.IssueComment{
		Body: comment,
	})
	if err != nil {
		log.WithField("err", err).Print("Failed to create issue comment")
		if res == nil {
			return nil, fmt.Errorf("Failed to create issue comment. Failed to connect to Github")
		}
		return nil, fmt.Errorf("Failed to create issue comment. HTTP %d", res.StatusCode)
	}
	return mevt.MessageEventContent{
		MsgType: mevt.MsgNotice,
		Body:    fmt.Sprintf("Commented on issue: %s", *issueComment.HTMLURL),
	}, nil
}
const cmdGithubAssignUsage = `!github assign [owner/repo]#issue username [username] [...]`
func (s *Service) cmdGithubAssign(roomID id.RoomID, userID id.UserID, args []string) (interface{}, error) {
	cli, resp, err := s.requireGithubClientFor(userID)
	if cli == nil {
		return resp, err
	}
	if len(args) < 1 {
		return &mevt.MessageEventContent{
			MsgType: mevt.MsgNotice,
			Body:    "Usage: " + cmdGithubAssignUsage,
		}, nil
	} else if len(args) < 2 {
		return &mevt.MessageEventContent{
			MsgType: mevt.MsgNotice,
			Body:    "Needs at least one username. Usage: " + cmdGithubAssignUsage,
		}, nil
	}
	owner, repo, issueNum, resp := s.getIssueDetailsFor(args[0], roomID, cmdGithubAssignUsage)
	if resp != nil {
		return resp, nil
	}
	issue, res, err := cli.Issues.AddAssignees(context.Background(), 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 mevt.MessageEventContent{
		MsgType: mevt.MsgNotice,
		Body:    fmt.Sprintf("Added assignees to issue: %s", *issue.HTMLURL),
	}, nil
}
func (s *Service) githubIssueCloseReopen(roomID id.RoomID, userID id.UserID, args []string, state, verb, help string) (interface{}, error) {
	cli, resp, err := s.requireGithubClientFor(userID)
	if cli == nil {
		return resp, err
	}
	if len(args) == 0 {
		return &mevt.MessageEventContent{
			MsgType: mevt.MsgNotice,
			Body:    "Usage: " + help,
		}, nil
	}
	owner, repo, issueNum, resp := s.getIssueDetailsFor(args[0], roomID, help)
	if resp != nil {
		return resp, nil
	}
	issueComment, res, err := cli.Issues.Edit(context.Background(), owner, repo, issueNum, &gogithub.IssueRequest{
		State: &state,
	})
	if err != nil {
		log.WithField("err", err).Printf("Failed to %s issue", verb)
		if res == nil {
			return nil, fmt.Errorf("Failed to %s issue. Failed to connect to Github", verb)
		}
		return nil, fmt.Errorf("Failed to %s issue. HTTP %d", verb, res.StatusCode)
	}
	return mevt.MessageEventContent{
		MsgType: mevt.MsgNotice,
		Body:    fmt.Sprintf("Closed issue: %s", *issueComment.HTMLURL),
	}, nil
}
const cmdGithubCloseUsage = `!github close [owner/repo]#issue`
func (s *Service) cmdGithubClose(roomID id.RoomID, userID id.UserID, args []string) (interface{}, error) {
	return s.githubIssueCloseReopen(roomID, userID, args, "closed", "close", cmdGithubCloseUsage)
}
const cmdGithubReopenUsage = `!github reopen [owner/repo]#issue`
func (s *Service) cmdGithubReopen(roomID id.RoomID, userID id.UserID, args []string) (interface{}, error) {
	return s.githubIssueCloseReopen(roomID, userID, args, "open", "open", cmdGithubCloseUsage)
}
func (s *Service) getIssueDetailsFor(input string, roomID id.RoomID, usage string) (owner, repo string, issueNum int, resp interface{}) {
	ownerRepoIssueGroups := ownerRepoIssueRegexAnchored.FindStringSubmatch(input)
	if len(ownerRepoIssueGroups) != 5 {
		resp = &mevt.MessageEventContent{
			MsgType: mevt.MsgNotice,
			Body:    "Usage: " + usage,
		}
		return
	}
	owner = ownerRepoIssueGroups[2]
	repo = ownerRepoIssueGroups[3]
	var err error
	if issueNum, err = strconv.Atoi(ownerRepoIssueGroups[4]); err != nil {
		resp = &mevt.MessageEventContent{
			MsgType: mevt.MsgNotice,
			Body:    "Malformed issue number. Usage: " + usage,
		}
		return
	}
	if ownerRepoIssueGroups[1] == "" {
		defaultRepo := s.defaultRepo(roomID)
		if defaultRepo == "" {
			resp = &mevt.MessageEventContent{
				MsgType: mevt.MsgNotice,
				Body:    "Need to specify repo. Usage: " + usage,
			}
			return
		}
		segs := strings.Split(defaultRepo, "/")
		if len(segs) != 2 {
			resp = &mevt.MessageEventContent{
				MsgType: mevt.MsgNotice,
				Body:    "Malformed default repo. Usage: " + usage,
			}
			return
		}
		owner = segs[0]
		repo = segs[1]
	}
	return
}
func (s *Service) expandIssue(roomID id.RoomID, userID id.UserID, owner, repo string, issueNum int) interface{} {
	cli := s.githubClientFor(userID, true)
	i, _, err := cli.Issues.Get(context.Background(), owner, repo, issueNum)
	if err != nil {
		log.WithError(err).WithFields(log.Fields{
			"owner":  owner,
			"repo":   repo,
			"number": issueNum,
		}).Print("Failed to fetch issue")
		return nil
	}
	return &mevt.MessageEventContent{
		MsgType: mevt.MsgNotice,
		Body:    fmt.Sprintf("%s : %s", *i.HTMLURL, *i.Title),
	}
}
func (s *Service) expandCommit(roomID id.RoomID, userID id.UserID, owner, repo, sha string) interface{} {
	cli := s.githubClientFor(userID, true)
	c, _, err := cli.Repositories.GetCommit(context.Background(), owner, repo, sha)
	if err != nil {
		log.WithError(err).WithFields(log.Fields{
			"owner": owner,
			"repo":  repo,
			"sha":   sha,
		}).Print("Failed to fetch commit")
		return nil
	}
	commit := c.Commit
	var htmlBuffer bytes.Buffer
	var plainBuffer bytes.Buffer
	shortURL := strings.TrimSuffix(*c.HTMLURL, *c.SHA) + sha
	htmlBuffer.WriteString(fmt.Sprintf("<a href=\"%s\">%s</a><br />", *c.HTMLURL, shortURL))
	plainBuffer.WriteString(fmt.Sprintf("%s\n", shortURL))
	if c.Stats != nil {
		htmlBuffer.WriteString(fmt.Sprintf("[<strong><font color='#1cc3ed'>~%d</font>, <font color='#30bf2b'>+%d</font>, <font color='#fc3a25'>-%d</font></strong>] ", len(c.Files), *c.Stats.Additions, *c.Stats.Deletions))
		plainBuffer.WriteString(fmt.Sprintf("[~%d, +%d, -%d] ", len(c.Files), *c.Stats.Additions, *c.Stats.Deletions))
	}
	if commit.Author != nil {
		authorName := ""
		if commit.Author.Name != nil {
			authorName = *commit.Author.Name
		} else if commit.Author.Login != nil {
			authorName = *commit.Author.Login
		}
		htmlBuffer.WriteString(fmt.Sprintf("%s: ", authorName))
		plainBuffer.WriteString(fmt.Sprintf("%s: ", authorName))
	}
	if commit.Message != nil {
		segs := strings.SplitN(*commit.Message, "\n", 2)
		htmlBuffer.WriteString(segs[0])
		plainBuffer.WriteString(segs[0])
	}
	return &mevt.MessageEventContent{
		Body:          plainBuffer.String(),
		MsgType:       mevt.MsgNotice,
		Format:        mevt.FormatHTML,
		FormattedBody: htmlBuffer.String(),
	}
}