Browse Source

Merge pull request #47 from matrix-org/kegan/bot-options

Support bot options and implement github bot options for default repos
kegan/giphy
Kegsay 8 years ago
committed by GitHub
parent
commit
16e7bf3957
  1. 25
      src/github.com/matrix-org/go-neb/clients/clients.go
  2. 27
      src/github.com/matrix-org/go-neb/database/db.go
  3. 55
      src/github.com/matrix-org/go-neb/database/schema.go
  4. 113
      src/github.com/matrix-org/go-neb/services/github/github.go
  5. 8
      src/github.com/matrix-org/go-neb/types/types.go

25
src/github.com/matrix-org/go-neb/clients/clients.go

@ -7,6 +7,7 @@ import (
"github.com/matrix-org/go-neb/plugin"
"github.com/matrix-org/go-neb/types"
"net/url"
"strings"
"sync"
)
@ -155,6 +156,30 @@ func (c *Clients) newClient(config types.ClientConfig) (*matrix.Client, error) {
plugin.OnMessage(plugins, client, event)
})
client.Worker.OnEventType("m.room.bot.options", func(event *matrix.Event) {
// see if these options are for us. The state key is the user ID with a leading _
// to get around restrictions in the HS about having user IDs as state keys.
targetUserID := strings.TrimPrefix(event.StateKey, "_")
if targetUserID != client.UserID {
return
}
// these options fully clobber what was there previously.
opts := types.BotOptions{
UserID: client.UserID,
RoomID: event.RoomID,
SetByUserID: event.Sender,
Options: event.Content,
}
if _, err := c.db.StoreBotOptions(opts); err != nil {
log.WithFields(log.Fields{
log.ErrorKey: err,
"room_id": event.RoomID,
"bot_user_id": client.UserID,
"set_by_user_id": event.Sender,
}).Error("Failed to persist bot options")
}
})
if config.AutoJoinRooms {
client.Worker.OnEventType("m.room.member", func(event *matrix.Event) {
if event.StateKey != config.UserID {

27
src/github.com/matrix-org/go-neb/database/db.go

@ -194,6 +194,33 @@ func (d *ServiceDB) LoadAuthSessionByID(realmID, sessionID string) (session type
return
}
// LoadBotOptions loads bot options from the database.
// Returns sql.ErrNoRows if the bot options isn't in the database.
func (d *ServiceDB) LoadBotOptions(userID, roomID string) (opts types.BotOptions, err error) {
err = runTransaction(d.db, func(txn *sql.Tx) error {
opts, err = selectBotOptionsTxn(txn, userID, roomID)
return err
})
return
}
// StoreBotOptions stores a BotOptions into the database either by inserting a new
// bot options or updating an existing bot options. Returns the old bot options if there
// was one.
func (d *ServiceDB) StoreBotOptions(opts types.BotOptions) (oldOpts types.BotOptions, err error) {
err = runTransaction(d.db, func(txn *sql.Tx) error {
oldOpts, err = selectBotOptionsTxn(txn, opts.UserID, opts.RoomID)
if err == sql.ErrNoRows {
return insertBotOptionsTxn(txn, time.Now(), opts)
} else if err != nil {
return err
} else {
return updateBotOptionsTxn(txn, time.Now(), opts)
}
})
return
}
func runTransaction(db *sql.DB, fn func(txn *sql.Tx) error) (err error) {
txn, err := db.Begin()
if err != nil {

55
src/github.com/matrix-org/go-neb/database/schema.go

@ -48,6 +48,16 @@ CREATE TABLE IF NOT EXISTS auth_sessions (
UNIQUE(realm_id, user_id),
UNIQUE(realm_id, session_id)
);
CREATE TABLE IF NOT EXISTS bot_options (
user_id TEXT NOT NULL,
room_id TEXT NOT NULL,
set_by_user_id TEXT NOT NULL,
bot_options_json TEXT NOT NULL,
time_added_ms BIGINT NOT NULL,
time_updated_ms BIGINT NOT NULL,
UNIQUE(user_id, room_id)
);
`
const selectMatrixClientConfigSQL = `
@ -370,3 +380,48 @@ func updateAuthSessionTxn(txn *sql.Tx, now time.Time, session types.AuthSession)
)
return err
}
const selectBotOptionsSQL = `
SELECT bot_options_json, set_by_user_id FROM bot_options WHERE user_id = $1 AND room_id = $2
`
func selectBotOptionsTxn(txn *sql.Tx, userID, roomID string) (opts types.BotOptions, err error) {
var optionsJSON []byte
err = txn.QueryRow(selectBotOptionsSQL, userID, roomID).Scan(&optionsJSON, &opts.SetByUserID)
if err != nil {
return
}
err = json.Unmarshal(optionsJSON, &opts.Options)
return
}
const insertBotOptionsSQL = `
INSERT INTO bot_options(
user_id, room_id, bot_options_json, set_by_user_id, time_added_ms, time_updated_ms
) VALUES ($1, $2, $3, $4, $5, $6)
`
func insertBotOptionsTxn(txn *sql.Tx, now time.Time, opts types.BotOptions) error {
t := now.UnixNano() / 1000000
optsJSON, err := json.Marshal(&opts.Options)
if err != nil {
return err
}
_, err = txn.Exec(insertBotOptionsSQL, opts.UserID, opts.RoomID, optsJSON, opts.SetByUserID, t, t)
return err
}
const updateBotOptionsSQL = `
UPDATE bot_options SET bot_options_json = $1, set_by_user_id = $2, time_updated_ms = $3
WHERE user_id = $4 AND room_id = $5
`
func updateBotOptionsTxn(txn *sql.Tx, now time.Time, opts types.BotOptions) error {
t := now.UnixNano() / 1000000
optsJSON, err := json.Marshal(&opts.Options)
if err != nil {
return err
}
_, err = txn.Exec(updateBotOptionsSQL, optsJSON, opts.SetByUserID, t, opts.UserID, opts.RoomID)
return err
}

113
src/github.com/matrix-org/go-neb/services/github/github.go

@ -1,6 +1,7 @@
package services
import (
"database/sql"
"fmt"
log "github.com/Sirupsen/logrus"
"github.com/google/go-github/github"
@ -21,7 +22,7 @@ import (
// Matches alphanumeric then a /, then more alphanumeric then a #, then a number.
// E.g. owner/repo#11 (issue/PR numbers) - Captured groups for owner/repo/number
var ownerRepoIssueRegex = regexp.MustCompile("([A-z0-9-_]+)/([A-z0-9-_]+)#([0-9]+)")
var ownerRepoIssueRegex = regexp.MustCompile(`(([A-z0-9-_]+)/([A-z0-9-_]+))?#([0-9]+)`)
type githubService struct {
id string
@ -62,9 +63,19 @@ func (s *githubService) cmdGithubCreate(roomID, userID string, args []string) (i
}, nil
}
if len(args) < 2 {
return &matrix.TextMessage{"m.notice",
`Usage: !github create owner/repo "issue title" "description"`}, nil
// We expect the args to look like:
// [ "owner/repo", "title text", "desc text" ]
// They can omit the owner/repo if there is a default one set.
if len(args) < 2 || strings.Count(args[0], "/") != 1 {
// look for a default repo
defaultRepo := s.defaultRepo(roomID)
if defaultRepo == "" {
return &matrix.TextMessage{"m.notice",
`Usage: !github create owner/repo "issue title" "description"`}, nil
}
// insert the default as the first arg to reuse the same code path
args = append([]string{defaultRepo}, args...)
}
var (
@ -101,31 +112,15 @@ func (s *githubService) cmdGithubCreate(roomID, userID string, args []string) (i
return matrix.TextMessage{"m.notice", fmt.Sprintf("Created issue: %s", *issue.HTMLURL)}, nil
}
func (s *githubService) expandIssue(roomID, userID string, matchingGroups []string) interface{} {
if !s.HandleExpansions {
return nil
}
// matchingGroups => ["foo/bar#11", "foo", "bar", "11"]
if len(matchingGroups) != 4 {
log.WithField("groups", matchingGroups).Print("Unexpected number of groups")
return nil
}
num, err := strconv.Atoi(matchingGroups[3])
if err != nil {
log.WithField("issue_number", matchingGroups[3]).Print("Bad issue number")
return nil
}
owner := matchingGroups[1]
repo := matchingGroups[2]
func (s *githubService) expandIssue(roomID, userID, owner, repo string, issueNum int) interface{} {
cli := s.githubClientFor(userID, true)
i, _, err := cli.Issues.Get(owner, repo, num)
i, _, err := cli.Issues.Get(owner, repo, issueNum)
if err != nil {
log.WithError(err).WithFields(log.Fields{
"owner": owner,
"repo": repo,
"number": num,
"number": issueNum,
}).Print("Failed to fetch issue")
return nil
}
@ -150,7 +145,47 @@ func (s *githubService) Plugin(roomID string) plugin.Plugin {
plugin.Expansion{
Regexp: ownerRepoIssueRegex,
Expand: func(roomID, userID string, matchingGroups []string) interface{} {
return s.expandIssue(roomID, userID, matchingGroups)
if !s.HandleExpansions {
return nil
}
// There's an optional group in the regex so matchingGroups can look like:
// [foo/bar#55 foo/bar foo bar 55]
// [#55 55]
if len(matchingGroups) != 5 {
log.WithField("groups", matchingGroups).WithField("len", len(matchingGroups)).Print(
"Unexpected number of groups",
)
return nil
}
if matchingGroups[1] == "" && matchingGroups[2] == "" && matchingGroups[3] == "" {
// issue only match, this only works if there is a default repo
defaultRepo := s.defaultRepo(roomID)
if defaultRepo == "" {
return nil
}
segs := strings.Split(defaultRepo, "/")
if len(segs) != 2 {
log.WithFields(log.Fields{
"room_id": roomID,
"default_repo": defaultRepo,
}).Error("Default repo is malformed")
return nil
}
// Fill in the missing fields in matching groups and fall through into ["foo/bar#11", "foo", "bar", "11"]
matchingGroups = []string{
defaultRepo + matchingGroups[0],
defaultRepo,
segs[0],
segs[1],
matchingGroups[4],
}
}
num, err := strconv.Atoi(matchingGroups[4])
if err != nil {
log.WithField("issue_number", matchingGroups[4]).Print("Bad issue number")
return nil
}
return s.expandIssue(roomID, userID, matchingGroups[2], matchingGroups[3], num)
},
},
},
@ -285,6 +320,36 @@ func (s *githubService) Register(oldService types.Service, client *matrix.Client
return nil
}
// defaultRepo returns the default repo for the given room, or an empty string.
func (s *githubService) defaultRepo(roomID string) 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
}
func (s *githubService) joinWebhookRooms(client *matrix.Client) error {
for roomID := range s.Rooms {
if _, err := client.JoinRoom(roomID, "", ""); err != nil {

8
src/github.com/matrix-org/go-neb/types/types.go

@ -31,6 +31,14 @@ func (c *ClientConfig) Check() error {
return nil
}
// BotOptions for a given bot user in a given room
type BotOptions struct {
RoomID string
UserID string
SetByUserID string
Options map[string]interface{}
}
// A Service is the configuration for a bot service.
type Service interface {
ServiceUserID() string

Loading…
Cancel
Save