diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 5f1cea0..01c304b 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -27,7 +27,7 @@ jobs: - name: Setup Go uses: actions/setup-go@v2 with: - go-version: '~1.16.4' + go-version: '~1.18.0' - name: Install libolm run: sudo apt-get -y install libolm3 libolm-dev - name: Install linters diff --git a/clients/clients.go b/clients/clients.go index 0e5816c..79dc37b 100644 --- a/clients/clients.go +++ b/clients/clients.go @@ -287,11 +287,12 @@ func (c *Clients) onBotOptionsEvent(client *mautrix.Client, event *mevt.Event) { return } // these options fully clobber what was there previously. + opts := types.BotOptions{ UserID: client.UserID, RoomID: event.RoomID, SetByUserID: event.Sender, - Options: event.Content.Raw, + Options: event.Content.Parsed.(*types.BotOptionsContent), } if _, err := c.db.StoreBotOptions(opts); err != nil { log.WithFields(log.Fields{ @@ -328,6 +329,8 @@ func (c *Clients) onRoomMemberEvent(client *mautrix.Client, event *mevt.Event) { } } +var StateBotOptionsEvent = mevt.Type{Type: "m.room.bot.options", Class: mevt.StateEventType} + func (c *Clients) initClient(botClient *BotClient) error { config := botClient.config client, err := mautrix.NewClient(config.HomeserverURL, config.UserID, config.AccessToken) @@ -344,6 +347,10 @@ func (c *Clients) initClient(botClient *BotClient) error { botClient.verificationSAS = &sync.Map{} syncer := client.Syncer.(*mautrix.DefaultSyncer) + syncer.ParseEventContent = true + + // Add m.room.bot.options to mautrix's TypeMap so that it parses it as a valid event + mevt.TypeMap[StateBotOptionsEvent] = reflect.TypeOf(types.BotOptionsContent{}) nebStore := &matrix.NEBStore{ InMemoryStore: *mautrix.NewInMemoryStore(), @@ -366,7 +373,7 @@ func (c *Clients) initClient(botClient *BotClient) error { c.onMessageEvent(botClient, event) }) - syncer.OnEventType(mevt.Type{Type: "m.room.bot.options", Class: mevt.UnknownEventType}, func(_ mautrix.EventSource, event *mevt.Event) { + syncer.OnEventType(StateBotOptionsEvent, func(_ mautrix.EventSource, event *mevt.Event) { c.onBotOptionsEvent(botClient.Client, event) }) diff --git a/go.mod b/go.mod index 32aa1eb..8daa9d5 100644 --- a/go.mod +++ b/go.mod @@ -33,6 +33,7 @@ require ( github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect golang.org/x/net v0.0.0-20210220033124-5f55cee0dc0d golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d + golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f // indirect gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect gopkg.in/yaml.v2 v2.3.0 maunium.net/go/mautrix v0.9.12 diff --git a/go.sum b/go.sum index 2eec917..a9af9f4 100644 --- a/go.sum +++ b/go.sum @@ -143,6 +143,8 @@ golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f h1:8w7RhxzTVgUzw/AH/9mUV5q0vMgy40SQRursCcfmkCw= +golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/services/github/github.go b/services/github/github.go index 91b1515..772c362 100644 --- a/services/github/github.go +++ b/services/github/github.go @@ -7,6 +7,7 @@ package github import ( "context" "database/sql" + "errors" "fmt" "regexp" "strconv" @@ -50,17 +51,20 @@ var ownerRepoRegex = regexp.MustCompile(`^([A-z0-9-_.]+)/([A-z0-9-_.]+)$`) // // Before you can set up a Github Service, you need to set up a Github Realm. // -// You can set a "default repository" for a Matrix room by sending a `m.room.bot.options` state event +// You can set optional config for a Matrix room by sending a `m.room.bot.options` state event // which has the following `content`: // // { // "github": { -// "default_repo": "owner/repo" +// // The default repository to use for this room; this allows "owner/repo" to be omitted +// // when creating/expanding issues. +// "default_repo": "owner/repo", +// +// // Array of Github labels to attach to any issue created by this bot in this room. +// "new_issue_labels": ["bot-label-1", "bot-label-2"] // } // } // -// This will allow the "owner/repo" to be omitted when creating/expanding issues. -// // Example request: // { // "RealmID": "github-realm-id" @@ -167,9 +171,18 @@ func (s *Service) cmdGithubCreate(roomID id.RoomID, userID id.UserID, args []str // Look for a default if the first arg doesn't look like an owner/repo ownerRepoGroups := ownerRepoRegex.FindStringSubmatch(args[0]) + logger := log.WithFields(log.Fields{ + "room_id": roomID, + "bot_user_id": s.ServiceUserID(), + }) + options, err := s.loadBotOptions(roomID, logger) + if err != nil { + return nil, err + } + if len(ownerRepoGroups) == 0 { // look for a default repo - defaultRepo := s.defaultRepo(roomID) + defaultRepo := options.DefaultRepo if defaultRepo == "" { return &mevt.MessageEventContent{ MsgType: mevt.MsgNotice, @@ -204,8 +217,9 @@ func (s *Service) cmdGithubCreate(roomID id.RoomID, userID id.UserID, args []str } issue, res, err := cli.Issues.Create(context.Background(), ownerRepoGroups[1], ownerRepoGroups[2], &gogithub.IssueRequest{ - Title: title, - Body: desc, + Title: title, + Body: desc, + Labels: &options.NewIssueLabels, }) if err != nil { log.WithField("err", err).Print("Failed to create issue") @@ -735,34 +749,37 @@ func (s *Service) Register(oldService types.Service, client types.MatrixClient) return nil } +func (s *Service) loadBotOptions(roomID id.RoomID, logger *log.Entry) (result types.GithubOptions, err error) { + opts, err := database.GetServiceDB().LoadBotOptions(s.ServiceUserID(), roomID) + if err != nil { + if err == sql.ErrNoRows { + logger.Info("no bot options specified - using defaults") + return types.GithubOptions{}, nil + } else { + err := errors.New("Failed to load bot options") + logger.WithError(err).Error(err) + return types.GithubOptions{}, err + } + } + // Expect opts to look like: + // { + // github: { + // default_repo: $OWNER_REPO, + // new_issue_labels: [ "label1", .. ] + // } + // } + return opts.Options.Github, nil +} + // defaultRepo returns the default repo for the given room, or an empty string. func (s *Service) defaultRepo(roomID id.RoomID) string { logger := log.WithFields(log.Fields{ "room_id": roomID, "bot_user_id": s.ServiceUserID(), }) - opts, err := database.GetServiceDB().LoadBotOptions(s.ServiceUserID(), roomID) - if err != nil { - if err != sql.ErrNoRows { - logger.WithError(err).Error("Failed to load bot options") - } - return "" - } - // Expect opts to look like: - // { github: { default_repo: $OWNER_REPO } } - ghOpts, ok := opts.Options["github"].(map[string]interface{}) - if !ok { - logger.WithField("options", opts.Options).Error("Failed to cast bot options as github options") - return "" - } - defaultRepo, ok := ghOpts["default_repo"].(string) - if !ok { - logger.WithField("default_repo", ghOpts["default_repo"]).Error( - "Failed to cast default repo as a string", - ) - return "" - } - return defaultRepo + // ignore any errors, we treat it the same as no options and log inside the method + ghOpts, _ := s.loadBotOptions(roomID, logger) + return ghOpts.DefaultRepo } func (s *Service) githubClientFor(userID id.UserID, allowUnauth bool) *gogithub.Client { diff --git a/types/service.go b/types/service.go index 5f208d0..7726017 100644 --- a/types/service.go +++ b/types/service.go @@ -13,12 +13,21 @@ import ( "maunium.net/go/mautrix/id" ) +type GithubOptions struct { + DefaultRepo string `json:"default_repo,omitempty"` + NewIssueLabels []string `json:"new_issue_labels,omitempty"` +} + +type BotOptionsContent struct { + Github GithubOptions `json:"github"` +} + // BotOptions for a given bot user in a given room type BotOptions struct { RoomID id.RoomID UserID id.UserID SetByUserID id.UserID - Options map[string]interface{} + Options *BotOptionsContent } // Poller represents a thing which can poll. Services should implement this method signature to support polling.