Compare commits

..

15 Commits

Author SHA1 Message Date
dependabot[bot] f1199a2db2
build(deps): bump github.com/gabriel-vasile/mimetype from 1.4.0 to 1.4.1
Bumps [github.com/gabriel-vasile/mimetype](https://github.com/gabriel-vasile/mimetype) from 1.4.0 to 1.4.1.
- [Release notes](https://github.com/gabriel-vasile/mimetype/releases)
- [Commits](https://github.com/gabriel-vasile/mimetype/compare/v1.4.0...v1.4.1)

---
updated-dependencies:
- dependency-name: github.com/gabriel-vasile/mimetype
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-09-06 21:36:11 +00:00
Chris Sexton b8a199faba pagecomment: add /url command
* Updated discord library
* Added an author embed but it's not useful just yet
2022-09-06 17:26:07 -04:00
Chris Sexton 4617dd84fc first commit 2022-09-06 17:26:07 -04:00
Chris Sexton 9e386cbd70 reminder: add missing line 2022-08-31 14:38:56 -04:00
Chris Sexton 8090d4209a reminder: add snooze functionality 2022-08-31 13:42:18 -04:00
Chris Sexton b59d84b301 topic: fix newline issue 2022-08-16 20:08:39 -04:00
Chris Sexton cf5e52c2b6 topic: add plugin 2022-08-16 19:57:29 -04:00
Chris Sexton e1ccd553f1 discord: add private dm support 2022-08-11 06:29:29 -04:00
Chris Sexton f28026436a twitch: separate into regex commands and add user check 2022-08-09 08:14:36 -04:00
Chris Sexton 6038dd7cf9 git: update ignore 2022-08-09 08:14:36 -04:00
Chris Sexton 37e4dcb5c8 bot: add rate limiting
- emojy: lazy load images so they don't break/spam the server
2022-08-04 09:20:29 -04:00
Chris Sexton 7c0a777737 twitch: add reauthentication
- refactored secrets to be in config
- added missing format string to bot
2022-08-03 21:12:08 -04:00
Chris Sexton 45103cec62 admin: fix nick regex 2022-08-02 13:42:35 -04:00
Chris Sexton 7af94f3473 admin: conditionally require admin for nick change 2022-08-02 13:35:50 -04:00
Chris Sexton e92c89891f bot: add ability to change nick 2022-08-02 13:35:50 -04:00
23 changed files with 582 additions and 151 deletions

1
.gitignore vendored
View File

@ -76,3 +76,4 @@ run.sh
impact.ttf impact.ttf
.env .env
rathaus_discord.sh rathaus_discord.sh
emojy

View File

@ -106,3 +106,4 @@ by issuing a single word command in the form of XdY. "1d20" would roll a single
0. You just DO WHAT THE FUCK YOU WANT TO. 0. You just DO WHAT THE FUCK YOU WANT TO.
``` ```
# c346-34515-fa22-project-rockbottom

View File

@ -4,6 +4,8 @@ package bot
import ( import (
"fmt" "fmt"
"github.com/go-chi/chi/v5/middleware"
"github.com/go-chi/httprate"
"math/rand" "math/rand"
"net/http" "net/http"
"os" "os"
@ -14,7 +16,6 @@ import (
"time" "time"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/velour/catbase/bot/history" "github.com/velour/catbase/bot/history"
@ -120,21 +121,34 @@ func New(config *config.Config, connector Connector) Bot {
log.Debug().Msgf("created web router") log.Debug().Msgf("created web router")
// Make the http logger optional bot.setupHTTP()
// It has never served a purpose in production and with the emojy page, can make a rather noisy log
if bot.Config().GetInt("bot.useLogger", 0) == 1 {
bot.router.Use(middleware.Logger)
}
bot.router.Use(middleware.StripSlashes)
bot.router.HandleFunc("/", bot.serveRoot)
bot.router.HandleFunc("/nav", bot.serveNav)
connector.RegisterEvent(bot.Receive) connector.RegisterEvent(bot.Receive)
return bot return bot
} }
func (b *bot) setupHTTP() {
// Make the http logger optional
// It has never served a purpose in production and with the emojy page, can make a rather noisy log
if b.Config().GetInt("bot.useLogger", 0) == 1 {
b.router.Use(middleware.Logger)
}
reqCount := b.Config().GetInt("bot.httprate.requests", 500)
reqTime := time.Duration(b.Config().GetInt("bot.httprate.seconds", 5))
if reqCount > 0 && reqTime > 0 {
b.router.Use(httprate.LimitByIP(reqCount, reqTime*time.Second))
}
b.router.Use(middleware.RequestID)
b.router.Use(middleware.Recoverer)
b.router.Use(middleware.StripSlashes)
b.router.HandleFunc("/", b.serveRoot)
b.router.HandleFunc("/nav", b.serveNav)
}
func (b *bot) ListenAndServe() { func (b *bot) ListenAndServe() {
addr := b.config.Get("HttpAddr", "127.0.0.1:1337") addr := b.config.Get("HttpAddr", "127.0.0.1:1337")
stop := make(chan os.Signal, 1) stop := make(chan os.Signal, 1)
@ -265,7 +279,7 @@ func (b *bot) CheckAdmin(ID string) bool {
log.Info().Interface("admins", admins).Msgf("Checking admin for %s", ID) log.Info().Interface("admins", admins).Msgf("Checking admin for %s", ID)
for _, u := range admins { for _, u := range admins {
if ID == u { if ID == u {
log.Info().Msg("%s admin check: passed") log.Info().Msgf("%s admin check: passed", u)
return true return true
} }
} }

View File

@ -44,6 +44,12 @@ type EphemeralID string
type UnfurlLinks bool type UnfurlLinks bool
type EmbedAuthor struct {
ID string
Who string
IconURL string
}
type ImageAttachment struct { type ImageAttachment struct {
URL string URL string
AltTxt string AltTxt string
@ -226,6 +232,9 @@ type Connector interface {
// SetRole toggles a role on/off for a user by ID // SetRole toggles a role on/off for a user by ID
SetRole(userID, roleID string) error SetRole(userID, roleID string) error
// Nick sets the username of the bot on the server
Nick(string) error
} }
// Plugin interface used for compatibility with the Plugin interface // Plugin interface used for compatibility with the Plugin interface

View File

@ -201,6 +201,32 @@ func (c *Config) SecretKeys() []string {
return keys return keys
} }
func (c *Config) setSecret(key, value string) error {
q := `insert into secrets (key,value) values (?, ?)
on conflict(key) do update set value=?;`
_, err := c.Exec(q, key, value, value)
if err != nil {
log.Fatal().Err(err).Msgf("secret")
return err
}
return c.RefreshSecrets()
}
// RegisterSecret creates a new secret
func (c *Config) RegisterSecret(key, value string) error {
return c.setSecret(key, value)
}
// RemoveSecret deregisters a secret
func (c *Config) RemoveSecret(key string) error {
q := `delete from secrets where key=?`
_, err := c.Exec(q, key)
if err != nil {
return err
}
return c.RefreshSecrets()
}
func (c *Config) SetMap(key string, values map[string]string) error { func (c *Config) SetMap(key string, values map[string]string) error {
b, err := json.Marshal(values) b, err := json.Marshal(values)
if err != nil { if err != nil {
@ -256,7 +282,7 @@ func ReadConfig(dbpath string) *Config {
value string, value string,
primary key (key) primary key (key)
);`); err != nil { );`); err != nil {
log.Fatal().Err(err).Msgf("failed to initialize config") log.Fatal().Err(err).Msgf("failed to initialize secrets")
} }
if err := c.RefreshSecrets(); err != nil { if err := c.RefreshSecrets(); err != nil {

View File

@ -103,25 +103,32 @@ func (d *Discord) sendMessage(channel, message string, meMessage bool, args ...a
message = "_" + message + "_" message = "_" + message + "_"
} }
var embeds *discordgo.MessageEmbed embeds := []*discordgo.MessageEmbed{}
for _, arg := range args { for _, arg := range args {
switch a := arg.(type) { switch a := arg.(type) {
case bot.EmbedAuthor:
embed := &discordgo.MessageEmbed{}
embed.Author = &discordgo.MessageEmbedAuthor{
Name: a.Who,
IconURL: a.IconURL,
}
embeds = append(embeds, embed)
case bot.ImageAttachment: case bot.ImageAttachment:
//embeds.URL = a.URL embed := &discordgo.MessageEmbed{}
embeds = &discordgo.MessageEmbed{} embed.Description = a.AltTxt
embeds.Description = a.AltTxt embed.Image = &discordgo.MessageEmbedImage{
embeds.Image = &discordgo.MessageEmbedImage{
URL: a.URL, URL: a.URL,
Width: a.Width, Width: a.Width,
Height: a.Height, Height: a.Height,
} }
embeds = append(embeds, embed)
} }
} }
data := &discordgo.MessageSend{ data := &discordgo.MessageSend{
Content: message, Content: message,
Embed: embeds, Embeds: embeds,
} }
log.Debug(). log.Debug().
@ -240,6 +247,7 @@ func (d *Discord) Serve() error {
d.client.Identify.Intents = discordgo.MakeIntent( d.client.Identify.Intents = discordgo.MakeIntent(
discordgo.IntentsGuilds | discordgo.IntentsGuilds |
discordgo.IntentsGuildMessages | discordgo.IntentsGuildMessages |
discordgo.IntentsDirectMessages |
discordgo.IntentsGuildEmojis | discordgo.IntentsGuildEmojis |
discordgo.IntentsGuildMessageReactions) discordgo.IntentsGuildMessageReactions)
@ -321,7 +329,9 @@ func (d *Discord) Emojy(name string) string {
func (d *Discord) UploadEmojy(emojy, path string) error { func (d *Discord) UploadEmojy(emojy, path string) error {
guildID := d.config.Get("discord.guildid", "") guildID := d.config.Get("discord.guildid", "")
defaultRoles := d.config.GetArray("discord.emojyRoles", []string{}) defaultRoles := d.config.GetArray("discord.emojyRoles", []string{})
_, err := d.client.GuildEmojiCreate(guildID, emojy, path, defaultRoles) _, err := d.client.GuildEmojiCreate(guildID, &discordgo.EmojiParams{
emojy, path, defaultRoles,
})
if err != nil { if err != nil {
return err return err
} }
@ -423,3 +433,24 @@ func (d *Discord) Shutdown() {
} }
} }
} }
func (d *Discord) Nick(nick string) error {
guildID := d.config.Get("discord.guildid", "")
return d.client.GuildMemberNickname(guildID, "@me", nick)
}
func (d *Discord) Topic(channelID string) (string, error) {
channel, err := d.client.Channel(channelID)
if err != nil {
return "", err
}
return channel.Topic, nil
}
func (d *Discord) SetTopic(channelID, topic string) error {
ce := &discordgo.ChannelEdit{
Topic: topic,
}
_, err := d.client.ChannelEditComplex(channelID, ce)
return err
}

View File

@ -361,3 +361,8 @@ func (i Irc) SetRole(userID, roleID string) error {
} }
func (i Irc) Shutdown() {} func (i Irc) Shutdown() {}
func (i Irc) Nick(nick string) error {
// Yeah, I could figure this out, but I don't want to test/debug it
return fmt.Errorf("nick changes not supported on irc")
}

View File

@ -751,3 +751,7 @@ func (s *SlackApp) SetRole(userID, roleID string) error {
} }
func (s *SlackApp) Shutdown() {} func (s *SlackApp) Shutdown() {}
func (s *SlackApp) Nick(nick string) error {
return s.api.SetUserRealName(nick)
}

6
go.mod
View File

@ -6,7 +6,7 @@ require (
code.chrissexton.org/cws/getaoc v0.0.0-20191201043947-d5417d4b618d code.chrissexton.org/cws/getaoc v0.0.0-20191201043947-d5417d4b618d
github.com/ChimeraCoder/anaconda v2.0.0+incompatible github.com/ChimeraCoder/anaconda v2.0.0+incompatible
github.com/PuerkitoBio/goquery v1.8.0 github.com/PuerkitoBio/goquery v1.8.0
github.com/bwmarrin/discordgo v0.25.0 github.com/bwmarrin/discordgo v0.26.1
github.com/cdipaolo/goml v0.0.0-20190412180403-e1f51f713598 github.com/cdipaolo/goml v0.0.0-20190412180403-e1f51f713598
github.com/chrissexton/leftpad v0.0.0-20181207133115-1e93189d2fff github.com/chrissexton/leftpad v0.0.0-20181207133115-1e93189d2fff
github.com/chrissexton/sentiment v0.0.0-20190927141846-d69c422ba035 github.com/chrissexton/sentiment v0.0.0-20190927141846-d69c422ba035
@ -30,7 +30,7 @@ require (
github.com/stretchr/testify v1.8.0 github.com/stretchr/testify v1.8.0
github.com/trubitsyn/go-zero-width v1.0.1 github.com/trubitsyn/go-zero-width v1.0.1
github.com/velour/velour v0.0.0-20160303155839-8e090e68d158 github.com/velour/velour v0.0.0-20160303155839-8e090e68d158
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90
gopkg.in/go-playground/webhooks.v5 v5.17.0 gopkg.in/go-playground/webhooks.v5 v5.17.0
) )
@ -83,7 +83,7 @@ require (
golang.org/x/image v0.0.0-20190802002840-cff245a6509b // indirect golang.org/x/image v0.0.0-20190802002840-cff245a6509b // indirect
golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e // indirect golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e // indirect
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect golang.org/x/sys v0.0.0-20220906165534-d0df966e6959 // indirect
golang.org/x/text v0.3.7 // indirect golang.org/x/text v0.3.7 // indirect
gonum.org/v1/gonum v0.6.0 // indirect gonum.org/v1/gonum v0.6.0 // indirect
google.golang.org/appengine v1.6.5 // indirect google.golang.org/appengine v1.6.5 // indirect

11
go.sum
View File

@ -27,8 +27,8 @@ github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI=
github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/azr/backoff v0.0.0-20160115115103-53511d3c7330 h1:ekDALXAVvY/Ub1UtNta3inKQwZ/jMB/zpOtD8rAYh78= github.com/azr/backoff v0.0.0-20160115115103-53511d3c7330 h1:ekDALXAVvY/Ub1UtNta3inKQwZ/jMB/zpOtD8rAYh78=
github.com/azr/backoff v0.0.0-20160115115103-53511d3c7330/go.mod h1:nH+k0SvAt3HeiYyOlJpLLv1HG1p7KWP7qU9QPp2/pCo= github.com/azr/backoff v0.0.0-20160115115103-53511d3c7330/go.mod h1:nH+k0SvAt3HeiYyOlJpLLv1HG1p7KWP7qU9QPp2/pCo=
github.com/bwmarrin/discordgo v0.25.0 h1:NXhdfHRNxtwso6FPdzW2i3uBvvU7UIQTghmV2T4nqAs= github.com/bwmarrin/discordgo v0.26.1 h1:AIrM+g3cl+iYBr4yBxCBp9tD9jR3K7upEjl0d89FRkE=
github.com/bwmarrin/discordgo v0.25.0/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY= github.com/bwmarrin/discordgo v0.26.1/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY=
github.com/cdipaolo/goml v0.0.0-20190412180403-e1f51f713598 h1:j2XRGH5Y5uWtBYXGwmrjKeM/kfu/jh7ZcnrGvyN5Ttk= github.com/cdipaolo/goml v0.0.0-20190412180403-e1f51f713598 h1:j2XRGH5Y5uWtBYXGwmrjKeM/kfu/jh7ZcnrGvyN5Ttk=
github.com/cdipaolo/goml v0.0.0-20190412180403-e1f51f713598/go.mod h1:sduMkaHcXDIWurl/Bd/z0rNEUHw5tr6LUA9IO8E9o0o= github.com/cdipaolo/goml v0.0.0-20190412180403-e1f51f713598/go.mod h1:sduMkaHcXDIWurl/Bd/z0rNEUHw5tr6LUA9IO8E9o0o=
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
@ -178,8 +178,8 @@ github.com/velour/velour v0.0.0-20160303155839-8e090e68d158/go.mod h1:Ojy3BTOiOT
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 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-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e h1:T8NU3HyQ8ClP4SEE+KbFlg6n0NhuTsN4MyznaarGsZM= golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 h1:Y/gsMcFOcR+6S6f3YeMKl5g+dZMEWqcz5Czj/GWYbkM=
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/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-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-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@ -213,8 +213,9 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220906165534-d0df966e6959 h1:qSa+Hg9oBe6UJXrznE+yYvW51V9UbyIj/nj/KpDigo8=
golang.org/x/sys v0.0.0-20220906165534-d0df966e6959/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=

View File

@ -4,6 +4,8 @@ package main
import ( import (
"flag" "flag"
"github.com/velour/catbase/plugins/pagecomment"
"github.com/velour/catbase/plugins/topic"
"io" "io"
"math/rand" "math/rand"
"os" "os"
@ -130,6 +132,7 @@ func main() {
b.AddPlugin(admin.New(b)) b.AddPlugin(admin.New(b))
b.AddPlugin(roles.New(b)) b.AddPlugin(roles.New(b))
b.AddPlugin(pagecomment.New(b))
b.AddPlugin(gpt3.New(b)) b.AddPlugin(gpt3.New(b))
b.AddPlugin(secrets.New(b)) b.AddPlugin(secrets.New(b))
b.AddPlugin(mayi.New(b)) b.AddPlugin(mayi.New(b))
@ -173,6 +176,7 @@ func main() {
b.AddPlugin(quotegame.New(b)) b.AddPlugin(quotegame.New(b))
b.AddPlugin(emojy.New(b)) b.AddPlugin(emojy.New(b))
b.AddPlugin(cowboy.New(b)) b.AddPlugin(cowboy.New(b))
b.AddPlugin(topic.New(b))
// catches anything left, will always return true // catches anything left, will always return true
b.AddPlugin(fact.New(b)) b.AddPlugin(fact.New(b))

View File

@ -57,6 +57,7 @@ func New(b bot.Bot) *AdminPlugin {
b.RegisterRegexCmd(p, bot.Message, pushConfigRegex, p.isAdmin(p.pushConfigCmd)) b.RegisterRegexCmd(p, bot.Message, pushConfigRegex, p.isAdmin(p.pushConfigCmd))
b.RegisterRegexCmd(p, bot.Message, setKeyConfigRegex, p.isAdmin(p.setKeyConfigCmd)) b.RegisterRegexCmd(p, bot.Message, setKeyConfigRegex, p.isAdmin(p.setKeyConfigCmd))
b.RegisterRegexCmd(p, bot.Message, getConfigRegex, p.isAdmin(p.getConfigCmd)) b.RegisterRegexCmd(p, bot.Message, getConfigRegex, p.isAdmin(p.getConfigCmd))
b.RegisterRegexCmd(p, bot.Message, setNickRegex, p.setNick)
b.Register(p, bot.Help, p.help) b.Register(p, bot.Help, p.help)
p.registerWeb() p.registerWeb()
@ -106,6 +107,7 @@ var setConfigRegex = regexp.MustCompile(`(?i)^set (?P<key>\S+) (?P<value>.*)$`)
var pushConfigRegex = regexp.MustCompile(`(?i)^push (?P<key>\S+) (?P<value>.*)$`) var pushConfigRegex = regexp.MustCompile(`(?i)^push (?P<key>\S+) (?P<value>.*)$`)
var setKeyConfigRegex = regexp.MustCompile(`(?i)^setkey (?P<key>\S+) (?P<name>\S+) (?P<value>.*)$`) var setKeyConfigRegex = regexp.MustCompile(`(?i)^setkey (?P<key>\S+) (?P<name>\S+) (?P<value>.*)$`)
var getConfigRegex = regexp.MustCompile(`(?i)^get (?P<key>\S+)$`) var getConfigRegex = regexp.MustCompile(`(?i)^get (?P<key>\S+)$`)
var setNickRegex = regexp.MustCompile(`(?i)^nick (?P<nick>.+)$`)
func (p *AdminPlugin) isAdmin(rh bot.ResponseHandler) bot.ResponseHandler { func (p *AdminPlugin) isAdmin(rh bot.ResponseHandler) bot.ResponseHandler {
return func(r bot.Request) bool { return func(r bot.Request) bool {
@ -407,3 +409,17 @@ func (p *AdminPlugin) modList(query, channel, plugin string) error {
err := fmt.Errorf("unknown plugin named '%s'", plugin) err := fmt.Errorf("unknown plugin named '%s'", plugin)
return err return err
} }
func (p *AdminPlugin) setNick(r bot.Request) bool {
if needAdmin := p.cfg.GetInt("nick.needsadmin", 1); needAdmin == 1 && !p.bot.CheckAdmin(r.Msg.User.ID) {
return false
}
nick := r.Values["nick"]
if err := r.Conn.Nick(nick); err != nil {
log.Error().Err(err).Msg("set nick")
p.bot.Send(r.Conn, bot.Message, r.Msg.Channel, "I can't seem to set a new nick.")
return true
}
p.bot.Send(r.Conn, bot.Message, r.Msg.Channel, fmt.Sprintf("I shall now be known as %s.", nick))
return true
}

View File

@ -140,3 +140,4 @@ func (p *CliPlugin) GetChannelName(id string) string { return id }
func (p *CliPlugin) GetChannelID(name string) string { return name } func (p *CliPlugin) GetChannelID(name string) string { return name }
func (p *CliPlugin) GetRoles() ([]bot.Role, error) { return []bot.Role{}, nil } func (p *CliPlugin) GetRoles() ([]bot.Role, error) { return []bot.Role{}, nil }
func (p *CliPlugin) SetRole(userID, roleID string) error { return nil } func (p *CliPlugin) SetRole(userID, roleID string) error { return nil }
func (p *CliPlugin) Nick(string) error { return nil }

View File

@ -191,7 +191,7 @@ func (p *Cowboy) mkOverlayCB(overlay string) func(s *discordgo.Session, i *disco
Type: discordgo.InteractionResponseChannelMessageWithSource, Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{ Data: &discordgo.InteractionResponseData{
Content: msg, Content: msg,
Flags: uint64(discordgo.MessageFlagsEphemeral), Flags: discordgo.MessageFlagsEphemeral,
}, },
}) })
} }

View File

@ -61,7 +61,7 @@ resp:
Type: discordgo.InteractionResponseChannelMessageWithSource, Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{ Data: &discordgo.InteractionResponseData{
Content: msg, Content: msg,
Flags: uint64(discordgo.MessageFlagsEphemeral), Flags: discordgo.MessageFlagsEphemeral,
}, },
}) })
} }

View File

@ -45,8 +45,10 @@
<div class="row row-cols-5"> <div class="row row-cols-5">
<div class="card text-center" v-for="name in fileKeys" key="name"> <div class="card text-center" v-for="name in fileKeys" key="name">
<img :src="fileList[name]" class="card-img-top mx-auto d-block" :alt="name" style="max-width: 100px">
<div class="card-body"> <div class="card-body">
<span>
<b-img-lazy :src="fileList[name]" class="card-img-top mx-auto d-block" :alt="name" width=100 style="max-width: 100px">
</span>
<h5 class="card-title">{{name}}</h5> <h5 class="card-title">{{name}}</h5>
</div> </div>
</div> </div>

View File

@ -44,7 +44,7 @@ func New(b bot.Bot) *NewsBid {
var balanceRegex = regexp.MustCompile(`(?i)^balance$`) var balanceRegex = regexp.MustCompile(`(?i)^balance$`)
var bidsRegex = regexp.MustCompile(`(?i)^bids$`) var bidsRegex = regexp.MustCompile(`(?i)^bids$`)
var scoresRegex = regexp.MustCompile(`(?i)^scores$`) var scoresRegex = regexp.MustCompile(`(?i)^scores$`)
var bidRegex = regexp.MustCompile(`(?i)^bid (?P<amount>\S+) (?P<url>)\S+$`) var bidRegex = regexp.MustCompile(`(?i)^bid (?P<amount>\S+) (?P<url>\S+)\s?(?P<comment>.+)?$`)
var checkRegex = regexp.MustCompile(`(?i)^check ngate$`) var checkRegex = regexp.MustCompile(`(?i)^check ngate$`)
func (p *NewsBid) balanceCmd(r bot.Request) bool { func (p *NewsBid) balanceCmd(r bot.Request) bool {

View File

@ -0,0 +1,123 @@
package pagecomment
import (
"fmt"
"github.com/PuerkitoBio/goquery"
"github.com/bwmarrin/discordgo"
"github.com/rs/zerolog/log"
"github.com/velour/catbase/bot"
"github.com/velour/catbase/config"
"github.com/velour/catbase/connectors/discord"
"net/http"
"regexp"
"strings"
)
type PageComment struct {
b bot.Bot
c *config.Config
}
func New(b bot.Bot) *PageComment {
p := &PageComment{
b: b,
c: b.Config(),
}
p.register()
return p
}
func (p *PageComment) register() {
p.b.RegisterTable(p, bot.HandlerTable{
{
Kind: bot.Startup, IsCmd: false,
Regex: regexp.MustCompile(`.*`),
Handler: func(r bot.Request) bool {
switch conn := r.Conn.(type) {
case *discord.Discord:
p.registerCmds(conn)
}
return false
},
},
{Kind: bot.Message, IsCmd: true,
Regex: regexp.MustCompile(`(?i)^url (?P<url>\S+) (?P<comment>.+)`),
HelpText: "Comment on a URL", Handler: p.handleURLReq},
})
}
func (p *PageComment) handleURLReq(r bot.Request) bool {
fullText := r.Msg.Body
fullComment := fullText[strings.Index(fullText, r.Values["comment"]):]
u := r.Values["url"]
if strings.HasPrefix(u, "<") && strings.HasSuffix(u, ">") {
u = u[1 : len(u)-1]
}
msg := p.handleURL(u, fullComment, r.Msg.User.Name)
p.b.Send(r.Conn, bot.Delete, r.Msg.Channel, r.Msg.ID)
p.b.Send(r.Conn, bot.Message, r.Msg.Channel, msg)
return true
}
func (p *PageComment) handleURLCmd(conn bot.Connector) func(*discordgo.Session, *discordgo.InteractionCreate) {
return func(s *discordgo.Session, i *discordgo.InteractionCreate) {
u := i.ApplicationCommandData().Options[0].StringValue()
cmt := i.ApplicationCommandData().Options[1].StringValue()
who := i.Member.User.Username
profile, err := conn.Profile(i.Member.User.ID)
if err == nil {
who = profile.Name
}
msg := p.handleURL(u, cmt, who)
err = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: msg,
},
})
if err != nil {
log.Error().Err(err).Msg("")
return
}
}
}
func (p *PageComment) handleURL(u, cmt, who string) string {
req, err := http.Get(u)
if err != nil {
return "Couldn't get that URL"
}
doc, err := goquery.NewDocumentFromReader(req.Body)
if err != nil {
return "Couldn't parse that URL"
}
wait := make(chan string, 1)
doc.Find("title").First().Each(func(i int, s *goquery.Selection) {
wait <- fmt.Sprintf("> %s\n%s: %s\n(<%s>)", s.Text(), who, cmt, u)
})
return <-wait
}
func (p *PageComment) registerCmds(d *discord.Discord) {
cmd := discordgo.ApplicationCommand{
Name: "url",
Description: "comment on a URL with its title",
Options: []*discordgo.ApplicationCommandOption{
{
Type: discordgo.ApplicationCommandOptionString,
Name: "url",
Description: "What URL would you like",
Required: true,
},
{
Type: discordgo.ApplicationCommandOptionString,
Name: "comment",
Description: "Your comment",
Required: true,
},
},
}
if err := d.RegisterSlashCmd(cmd, p.handleURLCmd(d)); err != nil {
log.Error().Err(err).Msg("could not register emojy command")
}
}

View File

@ -5,6 +5,7 @@ package reminder
import ( import (
"errors" "errors"
"fmt" "fmt"
"regexp"
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
@ -28,12 +29,13 @@ const (
) )
type ReminderPlugin struct { type ReminderPlugin struct {
bot bot.Bot bot bot.Bot
db *sqlx.DB db *sqlx.DB
mutex *sync.Mutex mutex *sync.Mutex
timer *time.Timer timer *time.Timer
config *config.Config config *config.Config
when *when.Parser when *when.Parser
lastReminder map[string]*Reminder
} }
type Reminder struct { type Reminder struct {
@ -66,39 +68,59 @@ func New(b bot.Bot) *ReminderPlugin {
w.Add(common.All...) w.Add(common.All...)
plugin := &ReminderPlugin{ plugin := &ReminderPlugin{
bot: b, bot: b,
db: b.DB(), db: b.DB(),
mutex: &sync.Mutex{}, mutex: &sync.Mutex{},
timer: timer, timer: timer,
config: b.Config(), config: b.Config(),
when: w, when: w,
lastReminder: map[string]*Reminder{},
} }
plugin.queueUpNextReminder() plugin.queueUpNextReminder()
go reminderer(b.DefaultConnector(), plugin) go plugin.reminderer(b.DefaultConnector())
b.RegisterRegexCmd(plugin, bot.Message, regexp.MustCompile(`(?i)^snooze (?P<duration>.+)$`), plugin.snooze)
b.Register(plugin, bot.Message, plugin.message) b.Register(plugin, bot.Message, plugin.message)
b.Register(plugin, bot.Help, plugin.help) b.Register(plugin, bot.Help, plugin.help)
return plugin return plugin
} }
func (p *ReminderPlugin) snooze(r bot.Request) bool {
lastReminder := p.lastReminder[r.Msg.Channel]
if lastReminder == nil {
p.bot.Send(r.Conn, bot.Message, r.Msg.Channel, "My memory is too small to contain a snoozed reminder.")
return true
}
durationTxt := replaceDuration(p.when, r.Values["duration"])
dur, err := time.ParseDuration(durationTxt)
if err != nil {
p.bot.Send(r.Conn, bot.Message, r.Msg.Channel, "Whoa, cowboy. I can't parse that time duration.")
return true
}
lastReminder.when = time.Now().UTC().Add(dur)
p.addReminder(lastReminder)
delete(p.lastReminder, r.Msg.Channel)
p.bot.Send(r.Conn, bot.Message, r.Msg.Channel, fmt.Sprintf("Okay, I'll let you know in %s", dur))
p.queueUpNextReminder()
return true
}
func replaceDuration(when *when.Parser, txt string) string {
t, err := when.Parse(txt, time.Now())
if t != nil && err == nil {
return txt[0:t.Index] + t.Time.Sub(time.Now()).String() + txt[t.Index+len(t.Text):]
}
return txt
}
func (p *ReminderPlugin) message(c bot.Connector, kind bot.Kind, message msg.Message, args ...any) bool { func (p *ReminderPlugin) message(c bot.Connector, kind bot.Kind, message msg.Message, args ...any) bool {
channel := message.Channel channel := message.Channel
from := message.User.Name from := message.User.Name
var dur, dur2 time.Duration message.Body = replaceDuration(p.when, message.Body)
t, err := p.when.Parse(message.Body, time.Now())
// Allowing err to fallthrough for other parsing
if t != nil && err == nil {
t2 := t.Time.Sub(time.Now()).String()
message.Body = string(message.Body[0:t.Index]) + t2 + string(message.Body[t.Index+len(t.Text):])
log.Debug().
Str("body", message.Body).
Str("text", t.Text).
Msg("Got time request")
}
parts := strings.Fields(message.Body) parts := strings.Fields(message.Body)
if len(parts) >= 5 { if len(parts) >= 5 {
@ -108,7 +130,7 @@ func (p *ReminderPlugin) message(c bot.Connector, kind bot.Kind, message msg.Mes
who = from who = from
} }
dur, err = time.ParseDuration(parts[3]) dur, err := time.ParseDuration(parts[3])
if err != nil { if err != nil {
p.bot.Send(c, bot.Message, channel, "Easy cowboy, not sure I can parse that duration. Try something like '1.5h' or '2h45m'.") p.bot.Send(c, bot.Message, channel, "Easy cowboy, not sure I can parse that duration. Try something like '1.5h' or '2h45m'.")
return true return true
@ -135,7 +157,7 @@ func (p *ReminderPlugin) message(c bot.Connector, kind bot.Kind, message msg.Mes
} else if operator == "every" && strings.ToLower(parts[4]) == "for" { } else if operator == "every" && strings.ToLower(parts[4]) == "for" {
//batch add, especially for reminding msherms to buy a kit //batch add, especially for reminding msherms to buy a kit
//remind who every dur for dur2 blah //remind who every dur for dur2 blah
dur2, err = time.ParseDuration(parts[5]) dur2, err := time.ParseDuration(parts[5])
if err != nil { if err != nil {
log.Error().Err(err) log.Error().Err(err)
p.bot.Send(c, bot.Message, channel, "Easy cowboy, not sure I can parse that duration. Try something like '1.5h' or '2h45m'.") p.bot.Send(c, bot.Message, channel, "Easy cowboy, not sure I can parse that duration. Try something like '1.5h' or '2h45m'.")
@ -352,13 +374,15 @@ func (p *ReminderPlugin) queueUpNextReminder() {
} }
} }
func reminderer(c bot.Connector, p *ReminderPlugin) { func (p *ReminderPlugin) reminderer(c bot.Connector) {
for { for {
<-p.timer.C <-p.timer.C
reminder := p.getNextReminder() reminder := p.getNextReminder()
if reminder != nil && time.Now().UTC().After(reminder.when) { if reminder != nil && time.Now().UTC().After(reminder.when) {
p.lastReminder[reminder.channel] = reminder
var message string var message string
if reminder.from == reminder.who { if reminder.from == reminder.who {
reminder.from = "you" reminder.from = "you"

View File

@ -46,33 +46,6 @@ func (p *SecretsPlugin) registerWeb() {
p.b.RegisterWebName(r, "/secrets", "Secrets") p.b.RegisterWebName(r, "/secrets", "Secrets")
} }
func (p *SecretsPlugin) registerSecret(key, value string) error {
q := `insert into secrets (key, value) values (?, ?)`
_, err := p.db.Exec(q, key, value)
if err != nil {
return err
}
return p.c.RefreshSecrets()
}
func (p *SecretsPlugin) removeSecret(key string) error {
q := `delete from secrets where key=?`
_, err := p.db.Exec(q, key)
if err != nil {
return err
}
return p.c.RefreshSecrets()
}
func (p *SecretsPlugin) updateSecret(key, value string) error {
q := `update secrets set value=? where key=?`
_, err := p.db.Exec(q, value, key)
if err != nil {
return err
}
return p.c.RefreshSecrets()
}
func mkCheckError(w http.ResponseWriter) func(error) bool { func mkCheckError(w http.ResponseWriter) func(error) bool {
return func(err error) bool { return func(err error) bool {
if err != nil { if err != nil {
@ -130,7 +103,7 @@ func (p *SecretsPlugin) handleRegister(w http.ResponseWriter, r *http.Request) {
return return
} }
log.Debug().Msgf("Secret: %s", secret) log.Debug().Msgf("Secret: %s", secret)
err = p.registerSecret(secret.Key, secret.Value) err = p.c.RegisterSecret(secret.Key, secret.Value)
if checkError(err) { if checkError(err) {
return return
} }
@ -148,7 +121,7 @@ func (p *SecretsPlugin) handleRemove(w http.ResponseWriter, r *http.Request) {
if checkError(err) { if checkError(err) {
return return
} }
err = p.removeSecret(secret.Key) err = p.c.RemoveSecret(secret.Key)
if checkError(err) { if checkError(err) {
return return
} }

62
plugins/topic/topic.go Normal file
View File

@ -0,0 +1,62 @@
package topic
import (
"fmt"
"github.com/rs/zerolog/log"
"github.com/velour/catbase/bot"
"github.com/velour/catbase/config"
"github.com/velour/catbase/connectors/discord"
"regexp"
"strings"
)
type Topic struct {
b bot.Bot
c *config.Config
}
func New(b bot.Bot) *Topic {
t := &Topic{
b: b,
c: b.Config(),
}
t.register()
return t
}
func (p *Topic) register() {
p.b.RegisterRegexCmd(p, bot.Message, regexp.MustCompile(`(?i)^topic$`), func(r bot.Request) bool {
switch conn := r.Conn.(type) {
case *discord.Discord:
topic, err := conn.Topic(r.Msg.Channel)
if err != nil {
log.Error().Err(err).Msg("couldn't get topic")
return false
}
p.b.Send(conn, bot.Message, r.Msg.Channel, fmt.Sprintf("Topic: %s", topic))
return true
}
return false
})
p.b.RegisterRegexCmd(p, bot.Message, regexp.MustCompile(`(?i)^topic (?P<topic>.*)`), func(r bot.Request) bool {
topic := strings.TrimPrefix(r.Msg.Body, "topic ")
switch conn := r.Conn.(type) {
case *discord.Discord:
err := conn.SetTopic(r.Msg.Channel, topic)
if err != nil {
log.Error().Err(err).Msg("couldn't set topic")
return false
}
topic, err := conn.Topic(r.Msg.Channel)
if err != nil {
log.Error().Err(err).Msg("couldn't get topic")
return false
}
p.b.Send(conn, bot.Message, r.Msg.Channel, fmt.Sprintf("Topic: %s", topic))
return true
}
return false
})
}

View File

@ -4,14 +4,16 @@ import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/go-chi/chi/v5"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"net/url" "net/url"
"regexp"
"strings" "strings"
"text/template" "text/template"
"time" "time"
"github.com/go-chi/chi/v5"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/velour/catbase/bot" "github.com/velour/catbase/bot"
"github.com/velour/catbase/bot/msg" "github.com/velour/catbase/bot/msg"
@ -25,9 +27,10 @@ const (
) )
type TwitchPlugin struct { type TwitchPlugin struct {
bot bot.Bot b bot.Bot
config *config.Config c *config.Config
twitchList map[string]*Twitcher twitchList map[string]*Twitcher
tbl bot.HandlerTable
} }
type Twitcher struct { type Twitcher struct {
@ -61,13 +64,14 @@ type stream struct {
func New(b bot.Bot) *TwitchPlugin { func New(b bot.Bot) *TwitchPlugin {
p := &TwitchPlugin{ p := &TwitchPlugin{
bot: b, b: b,
config: b.Config(), c: b.Config(),
twitchList: map[string]*Twitcher{}, twitchList: map[string]*Twitcher{},
} }
for _, ch := range p.config.GetArray("Twitch.Channels", []string{}) { for _, ch := range p.c.GetArray("Twitch.Channels", []string{}) {
for _, twitcherName := range p.config.GetArray("Twitch."+ch+".Users", []string{}) { for _, twitcherName := range p.c.GetArray("Twitch."+ch+".Users", []string{}) {
twitcherName = strings.ToLower(twitcherName)
if _, ok := p.twitchList[twitcherName]; !ok { if _, ok := p.twitchList[twitcherName]; !ok {
p.twitchList[twitcherName] = &Twitcher{ p.twitchList[twitcherName] = &Twitcher{
name: twitcherName, name: twitcherName,
@ -75,11 +79,12 @@ func New(b bot.Bot) *TwitchPlugin {
} }
} }
} }
go p.twitchLoop(b.DefaultConnector(), ch) go p.twitchChannelLoop(b.DefaultConnector(), ch)
} }
b.Register(p, bot.Message, p.message) go p.twitchAuthLoop(b.DefaultConnector())
b.Register(p, bot.Help, p.help)
p.register()
p.registerWeb() p.registerWeb()
return p return p
@ -88,11 +93,11 @@ func New(b bot.Bot) *TwitchPlugin {
func (p *TwitchPlugin) registerWeb() { func (p *TwitchPlugin) registerWeb() {
r := chi.NewRouter() r := chi.NewRouter()
r.HandleFunc("/{user}", p.serveStreaming) r.HandleFunc("/{user}", p.serveStreaming)
p.bot.RegisterWeb(r, "/isstreaming") p.b.RegisterWeb(r, "/isstreaming")
} }
func (p *TwitchPlugin) serveStreaming(w http.ResponseWriter, r *http.Request) { func (p *TwitchPlugin) serveStreaming(w http.ResponseWriter, r *http.Request) {
userName := chi.URLParam(r, "user") userName := strings.ToLower(chi.URLParam(r, "user"))
if userName == "" { if userName == "" {
fmt.Fprint(w, "User not found.") fmt.Fprint(w, "User not found.")
return return
@ -121,25 +126,68 @@ func (p *TwitchPlugin) serveStreaming(w http.ResponseWriter, r *http.Request) {
} }
} }
func (p *TwitchPlugin) message(c bot.Connector, kind bot.Kind, message msg.Message, args ...any) bool { func (p *TwitchPlugin) register() {
body := strings.ToLower(message.Body) p.tbl = bot.HandlerTable{
if body == "twitch status" { {
channel := message.Channel Kind: bot.Message, IsCmd: true,
if users := p.config.GetArray("Twitch."+channel+".Users", []string{}); len(users) > 0 { Regex: regexp.MustCompile(`(?i)^twitch status$`),
for _, twitcherName := range users { HelpText: "Get status of all twitchers",
if _, ok := p.twitchList[twitcherName]; ok { Handler: p.twitchStatus,
p.checkTwitch(c, channel, p.twitchList[twitcherName], true) },
{
Kind: bot.Message, IsCmd: false,
Regex: regexp.MustCompile(`(?i)^is (?P<who>.+) streaming\??$`),
HelpText: "Check if a specific twitcher is streaming",
Handler: p.twitchUserStatus,
},
{
Kind: bot.Message, IsCmd: true,
Regex: regexp.MustCompile(`(?i)^reset twitch$`),
HelpText: "Reset the twitch templates",
Handler: p.resetTwitch,
},
}
p.b.Register(p, bot.Help, p.help)
p.b.RegisterTable(p, p.tbl)
}
func (p *TwitchPlugin) twitchStatus(r bot.Request) bool {
channel := r.Msg.Channel
if users := p.c.GetArray("Twitch."+channel+".Users", []string{}); len(users) > 0 {
for _, twitcherName := range users {
twitcherName = strings.ToLower(twitcherName)
// we could re-add them here instead of needing to restart the bot.
if t, ok := p.twitchList[twitcherName]; ok {
err := p.checkTwitch(r.Conn, channel, t, true)
if err != nil {
log.Error().Err(err).Msgf("error in checking twitch")
} }
} }
} }
return true
} else if body == "reset twitch" {
p.config.Set("twitch.istpl", isStreamingTplFallback)
p.config.Set("twitch.nottpl", notStreamingTplFallback)
p.config.Set("twitch.stoppedtpl", stoppedStreamingTplFallback)
} }
return true
}
return false func (p *TwitchPlugin) twitchUserStatus(r bot.Request) bool {
who := strings.ToLower(r.Values["who"])
if t, ok := p.twitchList[who]; ok {
err := p.checkTwitch(r.Conn, r.Msg.Channel, t, true)
if err != nil {
log.Error().Err(err).Msgf("error in checking twitch")
p.b.Send(r.Conn, bot.Message, r.Msg.Channel, "I had trouble with that.")
}
} else {
p.b.Send(r.Conn, bot.Message, r.Msg.Channel, "I don't know who that is.")
}
return true
}
func (p *TwitchPlugin) resetTwitch(r bot.Request) bool {
p.c.Set("twitch.istpl", isStreamingTplFallback)
p.c.Set("twitch.nottpl", notStreamingTplFallback)
p.c.Set("twitch.stoppedtpl", stoppedStreamingTplFallback)
p.b.Send(r.Conn, bot.Message, r.Msg.Channel, "The Twitch templates have been reset.")
return true
} }
func (p *TwitchPlugin) help(c bot.Connector, kind bot.Kind, message msg.Message, args ...any) bool { func (p *TwitchPlugin) help(c bot.Connector, kind bot.Kind, message msg.Message, args ...any) bool {
@ -149,29 +197,57 @@ func (p *TwitchPlugin) help(c bot.Connector, kind bot.Kind, message msg.Message,
msg += fmt.Sprintf("twitch.stoppedtpl (default: %s)\n", stoppedStreamingTplFallback) msg += fmt.Sprintf("twitch.stoppedtpl (default: %s)\n", stoppedStreamingTplFallback)
msg += "You can reset all messages with `!reset twitch`" msg += "You can reset all messages with `!reset twitch`"
msg += "And you can ask who is streaming with `!twitch status`" msg += "And you can ask who is streaming with `!twitch status`"
p.bot.Send(c, bot.Message, message.Channel, msg) p.b.Send(c, bot.Message, message.Channel, msg)
return true return true
} }
func (p *TwitchPlugin) twitchLoop(c bot.Connector, channel string) { func (p *TwitchPlugin) twitchAuthLoop(c bot.Connector) {
frequency := p.config.GetInt("Twitch.Freq", 60) frequency := p.c.GetInt("Twitch.AuthFreq", 60*60)
if p.config.Get("twitch.clientid", "") == "" || p.config.Get("twitch.token", "") == "" { cid := p.c.Get("twitch.clientid", "")
log.Info().Msgf("Disabling twitch autochecking.") secret := p.c.Get("twitch.secret", "")
if cid == "" || secret == "" {
log.Info().Msgf("Disabling twitch autoauth.")
return return
} }
log.Info().Msgf("Checking every %d seconds", frequency) log.Info().Msgf("Checking auth every %d seconds", frequency)
if err := p.validateCredentials(); err != nil {
log.Error().Err(err).Msgf("error checking twitch validity")
}
for { for {
time.Sleep(time.Duration(frequency) * time.Second) select {
case <-time.After(time.Duration(frequency) * time.Second):
for _, twitcherName := range p.config.GetArray("Twitch."+channel+".Users", []string{}) { if err := p.validateCredentials(); err != nil {
p.checkTwitch(c, channel, p.twitchList[twitcherName], false) log.Error().Err(err).Msgf("error checking twitch validity")
}
} }
} }
} }
func getRequest(url, clientID, token string) ([]byte, bool) { func (p *TwitchPlugin) twitchChannelLoop(c bot.Connector, channel string) {
frequency := p.c.GetInt("Twitch.Freq", 60)
if p.c.Get("twitch.clientid", "") == "" || p.c.Get("twitch.secret", "") == "" {
log.Info().Msgf("Disabling twitch autochecking.")
return
}
log.Info().Msgf("Checking channels every %d seconds", frequency)
for {
time.Sleep(time.Duration(frequency) * time.Second)
for _, twitcherName := range p.c.GetArray("Twitch."+channel+".Users", []string{}) {
twitcherName = strings.ToLower(twitcherName)
if err := p.checkTwitch(c, channel, p.twitchList[twitcherName], false); err != nil {
log.Error().Err(err).Msgf("error in twitch loop")
}
}
}
}
func getRequest(url, clientID, token string) ([]byte, int, bool) {
bearer := fmt.Sprintf("Bearer %s", token) bearer := fmt.Sprintf("Bearer %s", token)
var body []byte var body []byte
var resp *http.Response var resp *http.Response
@ -193,18 +269,19 @@ func getRequest(url, clientID, token string) ([]byte, bool) {
if err != nil { if err != nil {
goto errCase goto errCase
} }
return body, true return body, resp.StatusCode, true
errCase: errCase:
log.Error().Err(err) log.Error().Err(err)
return []byte{}, false return []byte{}, resp.StatusCode, false
} }
func (p *TwitchPlugin) checkTwitch(c bot.Connector, channel string, twitcher *Twitcher, alwaysPrintStatus bool) { func (p *TwitchPlugin) checkTwitch(c bot.Connector, channel string, twitcher *Twitcher, alwaysPrintStatus bool) error {
baseURL, err := url.Parse("https://api.twitch.tv/helix/streams") baseURL, err := url.Parse("https://api.twitch.tv/helix/streams")
if err != nil { if err != nil {
log.Error().Msg("Error parsing twitch stream URL") err := fmt.Errorf("error parsing twitch stream URL")
return log.Error().Msg(err.Error())
return err
} }
query := baseURL.Query() query := baseURL.Query()
@ -212,35 +289,38 @@ func (p *TwitchPlugin) checkTwitch(c bot.Connector, channel string, twitcher *Tw
baseURL.RawQuery = query.Encode() baseURL.RawQuery = query.Encode()
cid := p.config.Get("twitch.clientid", "") cid := p.c.Get("twitch.clientid", "")
token := p.config.Get("twitch.token", "") token := p.c.Get("twitch.token", "")
if cid == token && cid == "" { if cid == token && cid == "" {
log.Info().Msgf("Twitch plugin not enabled.") log.Info().Msgf("Twitch plugin not enabled.")
return return nil
} }
body, ok := getRequest(baseURL.String(), cid, token) body, status, ok := getRequest(baseURL.String(), cid, token)
if !ok { if !ok {
return return fmt.Errorf("got status %d: %s", status, string(body))
} }
var s stream var s stream
err = json.Unmarshal(body, &s) err = json.Unmarshal(body, &s)
if err != nil { if err != nil {
log.Error().Err(err) log.Error().Err(err).Msgf("error reading twitch data")
return return err
} }
games := s.Data games := s.Data
gameID, title := "", "" gameID, title := "", ""
if len(games) > 0 { if len(games) > 0 {
gameID = games[0].GameID gameID = games[0].GameID
if gameID == "" {
gameID = "unknown"
}
title = games[0].Title title = games[0].Title
} }
notStreamingTpl := p.config.Get("Twitch.NotTpl", notStreamingTplFallback) notStreamingTpl := p.c.Get("Twitch.NotTpl", notStreamingTplFallback)
isStreamingTpl := p.config.Get("Twitch.IsTpl", isStreamingTplFallback) isStreamingTpl := p.c.Get("Twitch.IsTpl", isStreamingTplFallback)
stoppedStreamingTpl := p.config.Get("Twitch.StoppedTpl", stoppedStreamingTplFallback) stoppedStreamingTpl := p.c.Get("Twitch.StoppedTpl", stoppedStreamingTplFallback)
buf := bytes.Buffer{} buf := bytes.Buffer{}
info := struct { info := struct {
@ -258,31 +338,31 @@ func (p *TwitchPlugin) checkTwitch(c bot.Connector, channel string, twitcher *Tw
t, err := template.New("notStreaming").Parse(notStreamingTpl) t, err := template.New("notStreaming").Parse(notStreamingTpl)
if err != nil { if err != nil {
log.Error().Err(err) log.Error().Err(err)
p.bot.Send(c, bot.Message, channel, err) p.b.Send(c, bot.Message, channel, err)
t = template.Must(template.New("notStreaming").Parse(notStreamingTplFallback)) t = template.Must(template.New("notStreaming").Parse(notStreamingTplFallback))
} }
t.Execute(&buf, info) t.Execute(&buf, info)
p.bot.Send(c, bot.Message, channel, buf.String()) p.b.Send(c, bot.Message, channel, buf.String())
} else { } else {
t, err := template.New("isStreaming").Parse(isStreamingTpl) t, err := template.New("isStreaming").Parse(isStreamingTpl)
if err != nil { if err != nil {
log.Error().Err(err) log.Error().Err(err)
p.bot.Send(c, bot.Message, channel, err) p.b.Send(c, bot.Message, channel, err)
t = template.Must(template.New("isStreaming").Parse(isStreamingTplFallback)) t = template.Must(template.New("isStreaming").Parse(isStreamingTplFallback))
} }
t.Execute(&buf, info) t.Execute(&buf, info)
p.bot.Send(c, bot.Message, channel, buf.String()) p.b.Send(c, bot.Message, channel, buf.String())
} }
} else if gameID == "" { } else if gameID == "" {
if twitcher.gameID != "" { if twitcher.gameID != "" {
t, err := template.New("stoppedStreaming").Parse(stoppedStreamingTpl) t, err := template.New("stoppedStreaming").Parse(stoppedStreamingTpl)
if err != nil { if err != nil {
log.Error().Err(err) log.Error().Err(err)
p.bot.Send(c, bot.Message, channel, err) p.b.Send(c, bot.Message, channel, err)
t = template.Must(template.New("stoppedStreaming").Parse(stoppedStreamingTplFallback)) t = template.Must(template.New("stoppedStreaming").Parse(stoppedStreamingTplFallback))
} }
t.Execute(&buf, info) t.Execute(&buf, info)
p.bot.Send(c, bot.Message, channel, buf.String()) p.b.Send(c, bot.Message, channel, buf.String())
} }
twitcher.gameID = "" twitcher.gameID = ""
} else { } else {
@ -290,12 +370,55 @@ func (p *TwitchPlugin) checkTwitch(c bot.Connector, channel string, twitcher *Tw
t, err := template.New("isStreaming").Parse(isStreamingTpl) t, err := template.New("isStreaming").Parse(isStreamingTpl)
if err != nil { if err != nil {
log.Error().Err(err) log.Error().Err(err)
p.bot.Send(c, bot.Message, channel, err) p.b.Send(c, bot.Message, channel, err)
t = template.Must(template.New("isStreaming").Parse(isStreamingTplFallback)) t = template.Must(template.New("isStreaming").Parse(isStreamingTplFallback))
} }
t.Execute(&buf, info) t.Execute(&buf, info)
p.bot.Send(c, bot.Message, channel, buf.String()) p.b.Send(c, bot.Message, channel, buf.String())
} }
twitcher.gameID = gameID twitcher.gameID = gameID
} }
return nil
}
func (p *TwitchPlugin) validateCredentials() error {
cid := p.c.Get("twitch.clientid", "")
token := p.c.Get("twitch.token", "")
if token == "" {
return p.reAuthenticate()
}
_, status, ok := getRequest("https://id.twitch.tv/oauth2/validate", cid, token)
if !ok || status == http.StatusUnauthorized {
return p.reAuthenticate()
}
log.Debug().Msgf("checked credentials and they were valid")
return nil
}
func (p *TwitchPlugin) reAuthenticate() error {
cid := p.c.Get("twitch.clientid", "")
secret := p.c.Get("twitch.secret", "")
if cid == "" || secret == "" {
return fmt.Errorf("could not request a new token without config values set")
}
resp, err := http.PostForm("https://id.twitch.tv/oauth2/token", url.Values{
"client_id": {cid},
"client_secret": {secret},
"grant_type": {"client_credentials"},
})
if err != nil {
return err
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}
credentials := struct {
AccessToken string `json:"access_token"`
ExpiresIn int `json:"expires_in"`
TokenType string `json:"token_type"`
}{}
err = json.Unmarshal(body, &credentials)
log.Debug().Int("expires", credentials.ExpiresIn).Msgf("setting new twitch token")
return p.c.RegisterSecret("twitch.token", credentials.AccessToken)
} }

View File

@ -13,6 +13,17 @@ import (
"github.com/velour/catbase/bot/user" "github.com/velour/catbase/bot/user"
) )
func makeRequest(payload string) bot.Request {
c, k, m := makeMessage(payload)
return bot.Request{
Conn: c,
Kind: k,
Msg: m,
Values: nil,
Args: nil,
}
}
func makeMessage(payload string) (bot.Connector, bot.Kind, msg.Message) { func makeMessage(payload string) (bot.Connector, bot.Kind, msg.Message) {
isCmd := strings.HasPrefix(payload, "!") isCmd := strings.HasPrefix(payload, "!")
if isCmd { if isCmd {
@ -30,9 +41,9 @@ func makeTwitchPlugin(t *testing.T) (*TwitchPlugin, *bot.MockBot) {
mb := bot.NewMockBot() mb := bot.NewMockBot()
c := New(mb) c := New(mb)
mb.Config().Set("twitch.clientid", "fake") mb.Config().Set("twitch.clientid", "fake")
mb.Config().Set("twitch.authorization", "fake") mb.Config().Set("twitch.secret", "fake")
c.config.SetArray("Twitch.Channels", []string{"test"}) c.c.SetArray("Twitch.Channels", []string{"test"})
c.config.SetArray("Twitch.test.Users", []string{"drseabass"}) c.c.SetArray("Twitch.test.Users", []string{"drseabass"})
assert.NotNil(t, c) assert.NotNil(t, c)
c.twitchList["drseabass"] = &Twitcher{ c.twitchList["drseabass"] = &Twitcher{
@ -45,6 +56,6 @@ func makeTwitchPlugin(t *testing.T) (*TwitchPlugin, *bot.MockBot) {
func TestTwitch(t *testing.T) { func TestTwitch(t *testing.T) {
b, mb := makeTwitchPlugin(t) b, mb := makeTwitchPlugin(t)
b.message(makeMessage("!twitch status")) b.twitchStatus(makeRequest("!twitch status"))
assert.NotEmpty(t, mb.Messages) assert.NotEmpty(t, mb.Messages)
} }