diff --git a/bot/interfaces.go b/bot/interfaces.go index 92e6cd3..57ca2a8 100644 --- a/bot/interfaces.go +++ b/bot/interfaces.go @@ -197,22 +197,33 @@ type Connector interface { // Profile returns a user's information given an ID Profile(string) (user.User, error) - // URL Format utility + // URLFormat utility URLFormat(title, url string) string - // Translate emojy to/from services + // Emojy translates emojy to/from services Emojy(string) string // GetChannelName returns the human-friendly name for an ID (if possible) GetChannelName(id string) string - // GetChannelName returns the channel ID for a human-friendly name (if possible) + // GetChannelID returns the channel ID for a human-friendly name (if possible) GetChannelID(id string) string - // Get any web handlers the connector exposes + // GetRouter gets any web handlers the connector exposes GetRouter() (http.Handler, string) + + // GetRoleNames returns list of names and IDs of roles + GetRoles() ([]Role, error) + + // SetRole toggles a role on/off for a user by ID + SetRole(userID, roleID string) error } // Plugin interface used for compatibility with the Plugin interface // Uhh it turned empty, but we're still using it to ID plugins type Plugin interface{} + +type Role struct { + ID string `json:"id"` + Name string `json:"name"` +} diff --git a/connectors/discord/discord.go b/connectors/discord/discord.go index 012b963..6479f43 100644 --- a/connectors/discord/discord.go +++ b/connectors/discord/discord.go @@ -296,3 +296,39 @@ func (d *Discord) GetChannelName(id string) string { } return ch.Name } + +func (d *Discord) GetRoles() ([]bot.Role, error) { + ret := []bot.Role{} + + guildID := d.config.Get("discord.guildid", "") + if guildID == "" { + return nil, errors.New("no guildID set") + } + roles, err := d.client.GuildRoles(guildID) + if err != nil { + return nil, err + } + + for _, r := range roles { + ret = append(ret, bot.Role{ + ID: r.ID, + Name: r.Name, + }) + } + + return ret, nil +} + +func (d *Discord) SetRole(userID, roleID string) error { + guildID := d.config.Get("discord.guildid", "") + member, err := d.client.GuildMember(guildID, userID) + if err != nil { + return err + } + for _, r := range member.Roles { + if r == roleID { + return d.client.GuildMemberRoleRemove(guildID, userID, roleID) + } + } + return d.client.GuildMemberRoleAdd(guildID, userID, roleID) +} diff --git a/connectors/irc/irc.go b/connectors/irc/irc.go index c329a27..ab2166e 100644 --- a/connectors/irc/irc.go +++ b/connectors/irc/irc.go @@ -341,3 +341,11 @@ func (i Irc) GetChannelID(name string) string { func (i Irc) GetChannelName(id string) string { return id } + +func (i Irc) GetRoles() ([]bot.Role, error) { + return []bot.Role{}, nil +} + +func (i Irc) SetRole(userID, roleID string) error { + return nil +} diff --git a/connectors/slackapp/slackApp.go b/connectors/slackapp/slackApp.go index 8a12ab2..7509998 100644 --- a/connectors/slackapp/slackApp.go +++ b/connectors/slackapp/slackApp.go @@ -729,3 +729,11 @@ func (s *SlackApp) Emojy(name string) string { func (s *SlackApp) URLFormat(title, url string) string { return fmt.Sprintf("<%s|%s>", url, title) } + +func (s *SlackApp) GetRoles() ([]bot.Role, error) { + return []bot.Role{}, nil +} + +func (s *SlackApp) SetRole(userID, roleID string) error { + return nil +} diff --git a/go.mod b/go.mod index 9f0af8a..c5be57f 100644 --- a/go.mod +++ b/go.mod @@ -59,13 +59,15 @@ require ( github.com/ttacon/builder v0.0.0-20170518171403-c099f663e1c2 // indirect github.com/ttacon/libphonenumber v1.1.0 // indirect github.com/velour/velour v0.0.0-20160303155839-8e090e68d158 - golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 + golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871 golang.org/x/exp v0.0.0-20191014171548-69215a2ee97e // indirect + golang.org/x/net v0.0.0-20211118161319-6a13c67c3ce4 // indirect golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 // indirect - golang.org/x/sys v0.0.0-20211020174200-9d6173849985 // indirect + golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1 // indirect + golang.org/x/text v0.3.7 // indirect gonum.org/v1/gonum v0.6.0 // indirect google.golang.org/appengine v1.6.5 // indirect gopkg.in/go-playground/webhooks.v5 v5.13.0 gopkg.in/sourcemap.v1 v1.0.5 // indirect - gopkg.in/yaml.v2 v2.2.4 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/go.sum b/go.sum index e50a07e..78a74b7 100644 --- a/go.sum +++ b/go.sum @@ -154,8 +154,8 @@ github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxt golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871 h1:/pEO3GD/ABYAjuakUS6xSEmmlyVS4kxBNkeA9tLJiTI= +golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -174,8 +174,9 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211118161319-6a13c67c3ce4 h1:DZshvxDdVoeKIbudAdFEKi+f70l51luSy/7b76ibTY0= +golang.org/x/net v0.0.0-20211118161319-6a13c67c3ce4/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 h1:SQFwaSi55rU7vdNs9Yr0Z324VNlrF+0wMqRXT4St8ck= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -187,14 +188,16 @@ golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210301091718-77cc2087c03b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211020174200-9d6173849985 h1:LOlKVhfDyahgmqa97awczplwkjzNaELFg3zRIJ13RYo= -golang.org/x/sys v0.0.0-20211020174200-9d6173849985/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1 h1:kwrAHlwJ0DUBZwQ238v+Uod/3eZ8B2K5rYsUHBQvzmI= +golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 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= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -217,7 +220,7 @@ gopkg.in/go-playground/webhooks.v5 v5.13.0/go.mod h1:LZbya/qLVdbqDR1aKrGuWV6qbia gopkg.in/sourcemap.v1 v1.0.5 h1:inv58fC9f9J3TK2Y2R1NPntXEn3/wjWHkonhIUODNTI= gopkg.in/sourcemap.v1 v1.0.5/go.mod h1:2RlvNNSMglmRrcvhfuzp4hQHwOtjxlbjX7UPY/GXb78= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= -gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/main.go b/main.go index fd96a77..5b30dae 100644 --- a/main.go +++ b/main.go @@ -17,6 +17,7 @@ import ( "github.com/velour/catbase/plugins/mayi" "github.com/velour/catbase/plugins/quotegame" "github.com/velour/catbase/plugins/rest" + "github.com/velour/catbase/plugins/roles" "github.com/velour/catbase/plugins/secrets" "github.com/velour/catbase/plugins/achievements" @@ -125,6 +126,7 @@ func main() { } b.AddPlugin(admin.New(b)) + b.AddPlugin(roles.New(b)) b.AddPlugin(gpt3.New(b)) b.AddPlugin(secrets.New(b)) b.AddPlugin(mayi.New(b)) diff --git a/plugins/cli/cli.go b/plugins/cli/cli.go index 3e387a8..8b9220e 100644 --- a/plugins/cli/cli.go +++ b/plugins/cli/cli.go @@ -132,5 +132,7 @@ func (p *CliPlugin) Emojy(name string) string { return name } func (p *CliPlugin) URLFormat(title, url string) string { return fmt.Sprintf("%s (%s)", title, url) } -func (p *CliPlugin) GetChannelName(id string) string { return id } -func (p *CliPlugin) GetChannelID(name string) string { return name } +func (p *CliPlugin) GetChannelName(id string) string { return id } +func (p *CliPlugin) GetChannelID(name string) string { return name } +func (p *CliPlugin) GetRoles() ([]bot.Role, error) { return []bot.Role{}, nil } +func (p *CliPlugin) SetRole(userID, roleID string) error { return nil } diff --git a/plugins/roles/role.go b/plugins/roles/role.go new file mode 100644 index 0000000..1ab7ccf --- /dev/null +++ b/plugins/roles/role.go @@ -0,0 +1,66 @@ +package roles + +import ( + "github.com/jmoiron/sqlx" + "github.com/rs/zerolog/log" +) + +type Role struct { + ID int64 `json:"id"` + Name string `json:"name"` + Offering string `json:"offering"` + + *sqlx.DB +} + +func NewRole(db *sqlx.DB, name, offering string) *Role { + return &Role{ + Name: name, + Offering: offering, + DB: db, + } +} + +func (r *Role) Save() error { + q := `insert or replace into roles (name, offering) values (?, ?)` + res, err := r.Exec(q, r.Name, r.Offering) + if checkErr(err) { + return err + } + id, err := res.LastInsertId() + if checkErr(err) { + return err + } + r.ID = id + return nil +} + +func getRolesByOffering(db *sqlx.DB, offering string) ([]*Role, error) { + roles := []*Role{} + err := db.Select(&roles, `select * from roles where offering=?`, offering) + if checkErr(err) { + return nil, err + } + for _, r := range roles { + r.DB = db + } + return roles, nil +} + +func getRole(db *sqlx.DB, name, offering string) (*Role, error) { + role := &Role{} + err := db.Get(role, `select * from roles where role=? and offering=?`, role, offering) + if checkErr(err) { + return nil, err + } + role.DB = db + return role, nil +} + +func checkErr(err error) bool { + if err != nil { + log.Error().Err(err).Msg("") + return true + } + return false +} diff --git a/plugins/roles/roles.go b/plugins/roles/roles.go new file mode 100644 index 0000000..9dbcb7f --- /dev/null +++ b/plugins/roles/roles.go @@ -0,0 +1,129 @@ +package roles + +import ( + "fmt" + "regexp" + "strings" + + "github.com/jmoiron/sqlx" + "github.com/rs/zerolog/log" + "github.com/velour/catbase/bot" + "github.com/velour/catbase/config" +) + +type RolesPlugin struct { + b bot.Bot + c *config.Config + db *sqlx.DB + h bot.HandlerTable +} + +func New(b bot.Bot) *RolesPlugin { + p := &RolesPlugin{ + b: b, + c: b.Config(), + db: b.DB(), + } + p.RegisterWeb() + p.Register() + return p +} + +func (p *RolesPlugin) Register() { + p.h = bot.HandlerTable{ + { + Kind: bot.Message, IsCmd: true, + Regex: regexp.MustCompile(`(?i)^role (?P.+)$`), + HelpText: "Toggles a role on or off for you. Role must be of the current offerings", + Handler: p.toggleRole, + }, + { + Kind: bot.Message, IsCmd: true, + Regex: regexp.MustCompile(`(?i)^lsroles\s?(?P.*)`), + HelpText: "Lists roles with an optional offering set specifier", + Handler: p.lsRoles, + }, + { + Kind: bot.Message, IsCmd: true, + Regex: regexp.MustCompile(`(?i)^offering (?P.+)`), + HelpText: "Changes the current offering set", + Handler: p.setOffering, + }, + } + p.b.RegisterTable(p, p.h) +} + +func (p *RolesPlugin) toggleRole(r bot.Request) bool { + role := r.Values["role"] + offering := p.c.Get("roles.currentoffering", "") + if !strings.HasSuffix(role, offering) { + role = fmt.Sprintf("%s-%s", role, offering) + } + roles, err := r.Conn.GetRoles() + if err != nil { + log.Error().Err(err).Msg("getRoles") + p.b.Send(r.Conn, bot.Message, r.Msg.Channel, "I couldn't get the roles for some reason.") + return true + } + for _, rr := range roles { + if rr.Name == role { + if err = r.Conn.SetRole(r.Msg.User.ID, rr.ID); err != nil { + log.Error().Err(err).Msg("setRole") + p.b.Send(r.Conn, bot.Message, r.Msg.Channel, "I couldn't set that role.") + return true + } + p.b.Send(r.Conn, bot.Message, r.Msg.Channel, fmt.Sprintf("I have toggled your role, %s.", rr.Name)) + return true + } + } + p.b.Send(r.Conn, bot.Message, r.Msg.Channel, "I couldn't find that role.") + return true +} + +func (p *RolesPlugin) lsRoles(r bot.Request) bool { + offering := r.Values["offering"] + if offering == "" { + // This would be all if we hit the fallback + offering = p.c.Get("roles.currentOffering", "") + } + roles, err := r.Conn.GetRoles() + if err != nil { + log.Error().Err(err).Msg("getRoles") + p.b.Send(r.Conn, bot.Message, r.Msg.Channel, "I couldn't get the roles for some reason.") + return true + } + msg := "Available roles: " + i := 0 + for _, r := range roles { + if !strings.HasSuffix(r.Name, offering) || r.Name == "@everyone" { + continue + } + if i > 0 { + msg += ", " + } + msg += fmt.Sprintf("%s", strings.TrimSuffix(r.Name, "-"+offering)) + i++ + } + msg += "\n\nUse `!role [rolename]` to toggle your membership in one of these roles." + if i == 0 { + msg = "I found no matching roles." + } + p.b.Send(r.Conn, bot.Message, r.Msg.Channel, msg) + return true +} + +func (p *RolesPlugin) setOffering(r bot.Request) bool { + if !p.b.CheckAdmin(r.Msg.User.Name) { + p.b.Send(r.Conn, bot.Message, r.Msg.Channel, "You must be an admin to set an offering.") + return true + } + offering := r.Values["offering"] + err := p.c.Set("roles.currentOffering", offering) + if err != nil { + log.Error().Err(err).Msg("setOffering") + p.b.Send(r.Conn, bot.Message, r.Msg.Channel, "Could not set that offering") + return true + } + p.b.Send(r.Conn, bot.Message, r.Msg.Channel, fmt.Sprintf("New offering set to %s", offering)) + return true +} diff --git a/plugins/roles/web.go b/plugins/roles/web.go new file mode 100644 index 0000000..60589fc --- /dev/null +++ b/plugins/roles/web.go @@ -0,0 +1,4 @@ +package roles + +func (p *RolesPlugin) RegisterWeb() { +}