mirror of https://github.com/velour/catbase.git
Compare commits
15 Commits
4efa07e783
...
f1199a2db2
Author | SHA1 | Date |
---|---|---|
dependabot[bot] | f1199a2db2 | |
Chris Sexton | b8a199faba | |
Chris Sexton | 4617dd84fc | |
Chris Sexton | 9e386cbd70 | |
Chris Sexton | 8090d4209a | |
Chris Sexton | b59d84b301 | |
Chris Sexton | cf5e52c2b6 | |
Chris Sexton | e1ccd553f1 | |
Chris Sexton | f28026436a | |
Chris Sexton | 6038dd7cf9 | |
Chris Sexton | 37e4dcb5c8 | |
Chris Sexton | 7c0a777737 | |
Chris Sexton | 45103cec62 | |
Chris Sexton | 7af94f3473 | |
Chris Sexton | e92c89891f |
|
@ -76,3 +76,4 @@ run.sh
|
|||
impact.ttf
|
||||
.env
|
||||
rathaus_discord.sh
|
||||
emojy
|
||||
|
|
|
@ -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.
|
||||
```
|
||||
# c346-34515-fa22-project-rockbottom
|
||||
|
|
36
bot/bot.go
36
bot/bot.go
|
@ -4,6 +4,8 @@ package bot
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
"github.com/go-chi/httprate"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"os"
|
||||
|
@ -14,7 +16,6 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/velour/catbase/bot/history"
|
||||
|
@ -120,21 +121,34 @@ func New(config *config.Config, connector Connector) Bot {
|
|||
|
||||
log.Debug().Msgf("created web router")
|
||||
|
||||
// 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 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)
|
||||
bot.setupHTTP()
|
||||
|
||||
connector.RegisterEvent(bot.Receive)
|
||||
|
||||
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() {
|
||||
addr := b.config.Get("HttpAddr", "127.0.0.1:1337")
|
||||
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)
|
||||
for _, u := range admins {
|
||||
if ID == u {
|
||||
log.Info().Msg("%s admin check: passed")
|
||||
log.Info().Msgf("%s admin check: passed", u)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -44,6 +44,12 @@ type EphemeralID string
|
|||
|
||||
type UnfurlLinks bool
|
||||
|
||||
type EmbedAuthor struct {
|
||||
ID string
|
||||
Who string
|
||||
IconURL string
|
||||
}
|
||||
|
||||
type ImageAttachment struct {
|
||||
URL string
|
||||
AltTxt string
|
||||
|
@ -226,6 +232,9 @@ type Connector interface {
|
|||
|
||||
// SetRole toggles a role on/off for a user by ID
|
||||
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
|
||||
|
|
|
@ -201,6 +201,32 @@ func (c *Config) SecretKeys() []string {
|
|||
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 {
|
||||
b, err := json.Marshal(values)
|
||||
if err != nil {
|
||||
|
@ -256,7 +282,7 @@ func ReadConfig(dbpath string) *Config {
|
|||
value string,
|
||||
primary key (key)
|
||||
);`); 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 {
|
||||
|
|
|
@ -103,25 +103,32 @@ func (d *Discord) sendMessage(channel, message string, meMessage bool, args ...a
|
|||
message = "_" + message + "_"
|
||||
}
|
||||
|
||||
var embeds *discordgo.MessageEmbed
|
||||
embeds := []*discordgo.MessageEmbed{}
|
||||
|
||||
for _, arg := range args {
|
||||
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:
|
||||
//embeds.URL = a.URL
|
||||
embeds = &discordgo.MessageEmbed{}
|
||||
embeds.Description = a.AltTxt
|
||||
embeds.Image = &discordgo.MessageEmbedImage{
|
||||
embed := &discordgo.MessageEmbed{}
|
||||
embed.Description = a.AltTxt
|
||||
embed.Image = &discordgo.MessageEmbedImage{
|
||||
URL: a.URL,
|
||||
Width: a.Width,
|
||||
Height: a.Height,
|
||||
}
|
||||
embeds = append(embeds, embed)
|
||||
}
|
||||
}
|
||||
|
||||
data := &discordgo.MessageSend{
|
||||
Content: message,
|
||||
Embed: embeds,
|
||||
Embeds: embeds,
|
||||
}
|
||||
|
||||
log.Debug().
|
||||
|
@ -240,6 +247,7 @@ func (d *Discord) Serve() error {
|
|||
d.client.Identify.Intents = discordgo.MakeIntent(
|
||||
discordgo.IntentsGuilds |
|
||||
discordgo.IntentsGuildMessages |
|
||||
discordgo.IntentsDirectMessages |
|
||||
discordgo.IntentsGuildEmojis |
|
||||
discordgo.IntentsGuildMessageReactions)
|
||||
|
||||
|
@ -321,7 +329,9 @@ func (d *Discord) Emojy(name string) string {
|
|||
func (d *Discord) UploadEmojy(emojy, path string) error {
|
||||
guildID := d.config.Get("discord.guildid", "")
|
||||
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 {
|
||||
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
|
||||
}
|
||||
|
|
|
@ -361,3 +361,8 @@ func (i Irc) SetRole(userID, roleID string) error {
|
|||
}
|
||||
|
||||
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")
|
||||
}
|
||||
|
|
|
@ -751,3 +751,7 @@ func (s *SlackApp) SetRole(userID, roleID string) error {
|
|||
}
|
||||
|
||||
func (s *SlackApp) Shutdown() {}
|
||||
|
||||
func (s *SlackApp) Nick(nick string) error {
|
||||
return s.api.SetUserRealName(nick)
|
||||
}
|
||||
|
|
10
go.mod
10
go.mod
|
@ -6,13 +6,13 @@ require (
|
|||
code.chrissexton.org/cws/getaoc v0.0.0-20191201043947-d5417d4b618d
|
||||
github.com/ChimeraCoder/anaconda v2.0.0+incompatible
|
||||
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/chrissexton/leftpad v0.0.0-20181207133115-1e93189d2fff
|
||||
github.com/chrissexton/sentiment v0.0.0-20190927141846-d69c422ba035
|
||||
github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90
|
||||
github.com/forPelevin/gomoji v1.1.4
|
||||
github.com/gabriel-vasile/mimetype v1.4.0
|
||||
github.com/gabriel-vasile/mimetype v1.4.1
|
||||
github.com/go-chi/chi/v5 v5.0.7
|
||||
github.com/go-chi/httprate v0.5.3
|
||||
github.com/gocolly/colly v1.2.0
|
||||
|
@ -30,7 +30,7 @@ require (
|
|||
github.com/stretchr/testify v1.8.0
|
||||
github.com/trubitsyn/go-zero-width v1.0.1
|
||||
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
|
||||
)
|
||||
|
||||
|
@ -81,9 +81,9 @@ require (
|
|||
github.com/ttacon/libphonenumber v1.1.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20191014171548-69215a2ee97e // indirect
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b // indirect
|
||||
golang.org/x/net v0.0.0-20211118161319-6a13c67c3ce4 // indirect
|
||||
golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e // 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
|
||||
gonum.org/v1/gonum v0.6.0 // indirect
|
||||
google.golang.org/appengine v1.6.5 // indirect
|
||||
|
|
22
go.sum
22
go.sum
|
@ -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/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/bwmarrin/discordgo v0.25.0 h1:NXhdfHRNxtwso6FPdzW2i3uBvvU7UIQTghmV2T4nqAs=
|
||||
github.com/bwmarrin/discordgo v0.25.0/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY=
|
||||
github.com/bwmarrin/discordgo v0.26.1 h1:AIrM+g3cl+iYBr4yBxCBp9tD9jR3K7upEjl0d89FRkE=
|
||||
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/go.mod h1:sduMkaHcXDIWurl/Bd/z0rNEUHw5tr6LUA9IO8E9o0o=
|
||||
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
|
||||
|
@ -50,8 +50,8 @@ github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90 h1:WXb3TSNmHp2vHoCro
|
|||
github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
|
||||
github.com/forPelevin/gomoji v1.1.4 h1:mlxsZQgTO7v1qnpUUoS8kk0Lf/rEvxZYgYxuVUX7edg=
|
||||
github.com/forPelevin/gomoji v1.1.4/go.mod h1:ypB7Kz3Fsp+LVR7KoT7mEFOioYBuTuAtaAT4RGl+ASY=
|
||||
github.com/gabriel-vasile/mimetype v1.4.0 h1:Cn9dkdYsMIu56tGho+fqzh7XmvY2YyGU0FnbhiOsEro=
|
||||
github.com/gabriel-vasile/mimetype v1.4.0/go.mod h1:fA8fi6KUiG7MgQQ+mEWotXoEOvmxRtOJlERCzSmRvr8=
|
||||
github.com/gabriel-vasile/mimetype v1.4.1 h1:TRWk7se+TOjCYgRth7+1/OYLNiRNIotknkFtf/dnN7Q=
|
||||
github.com/gabriel-vasile/mimetype v1.4.1/go.mod h1:05Vi0w3Y9c/lNvJOdmIwvrrAhX3rYhfQQCaf9VJcv7M=
|
||||
github.com/garyburd/go-oauth v0.0.0-20180319155456-bca2e7f09a17 h1:GOfMz6cRgTJ9jWV0qAezv642OhPnKEG7gtUjJSdStHE=
|
||||
github.com/garyburd/go-oauth v0.0.0-20180319155456-bca2e7f09a17/go.mod h1:HfkOCN6fkKKaPSAeNq/er3xObxTW4VLeY6UUK895gLQ=
|
||||
github.com/go-chi/chi/v5 v5.0.7 h1:rDTPXLDHGATaeHvVlLcR4Qe0zftYethFucbjVQ1PxU8=
|
||||
|
@ -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-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-20220525230936-793ad666bf5e h1:T8NU3HyQ8ClP4SEE+KbFlg6n0NhuTsN4MyznaarGsZM=
|
||||
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 h1:Y/gsMcFOcR+6S6f3YeMKl5g+dZMEWqcz5Czj/GWYbkM=
|
||||
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-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
|
@ -199,10 +199,9 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL
|
|||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/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/net v0.0.0-20220624214902-1bab6f366d9e h1:TsQ7F31D3bUCLeqPT0u+yjp1guoArKaNKmCr22PYgTQ=
|
||||
golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
|
@ -211,11 +210,14 @@ golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/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-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-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-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-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.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
|
|
4
main.go
4
main.go
|
@ -4,6 +4,8 @@ package main
|
|||
|
||||
import (
|
||||
"flag"
|
||||
"github.com/velour/catbase/plugins/pagecomment"
|
||||
"github.com/velour/catbase/plugins/topic"
|
||||
"io"
|
||||
"math/rand"
|
||||
"os"
|
||||
|
@ -130,6 +132,7 @@ func main() {
|
|||
|
||||
b.AddPlugin(admin.New(b))
|
||||
b.AddPlugin(roles.New(b))
|
||||
b.AddPlugin(pagecomment.New(b))
|
||||
b.AddPlugin(gpt3.New(b))
|
||||
b.AddPlugin(secrets.New(b))
|
||||
b.AddPlugin(mayi.New(b))
|
||||
|
@ -173,6 +176,7 @@ func main() {
|
|||
b.AddPlugin(quotegame.New(b))
|
||||
b.AddPlugin(emojy.New(b))
|
||||
b.AddPlugin(cowboy.New(b))
|
||||
b.AddPlugin(topic.New(b))
|
||||
// catches anything left, will always return true
|
||||
b.AddPlugin(fact.New(b))
|
||||
|
||||
|
|
|
@ -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, setKeyConfigRegex, p.isAdmin(p.setKeyConfigCmd))
|
||||
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)
|
||||
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 setKeyConfigRegex = regexp.MustCompile(`(?i)^setkey (?P<key>\S+) (?P<name>\S+) (?P<value>.*)$`)
|
||||
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 {
|
||||
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)
|
||||
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
|
||||
}
|
||||
|
|
|
@ -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) GetRoles() ([]bot.Role, error) { return []bot.Role{}, nil }
|
||||
func (p *CliPlugin) SetRole(userID, roleID string) error { return nil }
|
||||
func (p *CliPlugin) Nick(string) error { return nil }
|
||||
|
|
|
@ -191,7 +191,7 @@ func (p *Cowboy) mkOverlayCB(overlay string) func(s *discordgo.Session, i *disco
|
|||
Type: discordgo.InteractionResponseChannelMessageWithSource,
|
||||
Data: &discordgo.InteractionResponseData{
|
||||
Content: msg,
|
||||
Flags: uint64(discordgo.MessageFlagsEphemeral),
|
||||
Flags: discordgo.MessageFlagsEphemeral,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
|
@ -61,7 +61,7 @@ resp:
|
|||
Type: discordgo.InteractionResponseChannelMessageWithSource,
|
||||
Data: &discordgo.InteractionResponseData{
|
||||
Content: msg,
|
||||
Flags: uint64(discordgo.MessageFlagsEphemeral),
|
||||
Flags: discordgo.MessageFlagsEphemeral,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
|
@ -45,8 +45,10 @@
|
|||
|
||||
<div class="row row-cols-5">
|
||||
<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">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -44,7 +44,7 @@ func New(b bot.Bot) *NewsBid {
|
|||
var balanceRegex = regexp.MustCompile(`(?i)^balance$`)
|
||||
var bidsRegex = regexp.MustCompile(`(?i)^bids$`)
|
||||
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$`)
|
||||
|
||||
func (p *NewsBid) balanceCmd(r bot.Request) bool {
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
|
@ -5,6 +5,7 @@ package reminder
|
|||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
@ -28,12 +29,13 @@ const (
|
|||
)
|
||||
|
||||
type ReminderPlugin struct {
|
||||
bot bot.Bot
|
||||
db *sqlx.DB
|
||||
mutex *sync.Mutex
|
||||
timer *time.Timer
|
||||
config *config.Config
|
||||
when *when.Parser
|
||||
bot bot.Bot
|
||||
db *sqlx.DB
|
||||
mutex *sync.Mutex
|
||||
timer *time.Timer
|
||||
config *config.Config
|
||||
when *when.Parser
|
||||
lastReminder map[string]*Reminder
|
||||
}
|
||||
|
||||
type Reminder struct {
|
||||
|
@ -66,39 +68,59 @@ func New(b bot.Bot) *ReminderPlugin {
|
|||
w.Add(common.All...)
|
||||
|
||||
plugin := &ReminderPlugin{
|
||||
bot: b,
|
||||
db: b.DB(),
|
||||
mutex: &sync.Mutex{},
|
||||
timer: timer,
|
||||
config: b.Config(),
|
||||
when: w,
|
||||
bot: b,
|
||||
db: b.DB(),
|
||||
mutex: &sync.Mutex{},
|
||||
timer: timer,
|
||||
config: b.Config(),
|
||||
when: w,
|
||||
lastReminder: map[string]*Reminder{},
|
||||
}
|
||||
|
||||
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.Help, plugin.help)
|
||||
|
||||
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 {
|
||||
channel := message.Channel
|
||||
from := message.User.Name
|
||||
|
||||
var dur, dur2 time.Duration
|
||||
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")
|
||||
}
|
||||
message.Body = replaceDuration(p.when, message.Body)
|
||||
parts := strings.Fields(message.Body)
|
||||
|
||||
if len(parts) >= 5 {
|
||||
|
@ -108,7 +130,7 @@ func (p *ReminderPlugin) message(c bot.Connector, kind bot.Kind, message msg.Mes
|
|||
who = from
|
||||
}
|
||||
|
||||
dur, err = time.ParseDuration(parts[3])
|
||||
dur, err := time.ParseDuration(parts[3])
|
||||
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'.")
|
||||
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" {
|
||||
//batch add, especially for reminding msherms to buy a kit
|
||||
//remind who every dur for dur2 blah
|
||||
dur2, err = time.ParseDuration(parts[5])
|
||||
dur2, err := time.ParseDuration(parts[5])
|
||||
if err != nil {
|
||||
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'.")
|
||||
|
@ -352,13 +374,15 @@ func (p *ReminderPlugin) queueUpNextReminder() {
|
|||
}
|
||||
}
|
||||
|
||||
func reminderer(c bot.Connector, p *ReminderPlugin) {
|
||||
func (p *ReminderPlugin) reminderer(c bot.Connector) {
|
||||
|
||||
for {
|
||||
<-p.timer.C
|
||||
|
||||
reminder := p.getNextReminder()
|
||||
|
||||
if reminder != nil && time.Now().UTC().After(reminder.when) {
|
||||
p.lastReminder[reminder.channel] = reminder
|
||||
var message string
|
||||
if reminder.from == reminder.who {
|
||||
reminder.from = "you"
|
||||
|
|
|
@ -46,33 +46,6 @@ func (p *SecretsPlugin) registerWeb() {
|
|||
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 {
|
||||
return func(err error) bool {
|
||||
if err != nil {
|
||||
|
@ -130,7 +103,7 @@ func (p *SecretsPlugin) handleRegister(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
log.Debug().Msgf("Secret: %s", secret)
|
||||
err = p.registerSecret(secret.Key, secret.Value)
|
||||
err = p.c.RegisterSecret(secret.Key, secret.Value)
|
||||
if checkError(err) {
|
||||
return
|
||||
}
|
||||
|
@ -148,7 +121,7 @@ func (p *SecretsPlugin) handleRemove(w http.ResponseWriter, r *http.Request) {
|
|||
if checkError(err) {
|
||||
return
|
||||
}
|
||||
err = p.removeSecret(secret.Key)
|
||||
err = p.c.RemoveSecret(secret.Key)
|
||||
if checkError(err) {
|
||||
return
|
||||
}
|
||||
|
|
|
@ -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
|
||||
})
|
||||
}
|
|
@ -4,14 +4,16 @@ import (
|
|||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/velour/catbase/bot"
|
||||
"github.com/velour/catbase/bot/msg"
|
||||
|
@ -25,9 +27,10 @@ const (
|
|||
)
|
||||
|
||||
type TwitchPlugin struct {
|
||||
bot bot.Bot
|
||||
config *config.Config
|
||||
b bot.Bot
|
||||
c *config.Config
|
||||
twitchList map[string]*Twitcher
|
||||
tbl bot.HandlerTable
|
||||
}
|
||||
|
||||
type Twitcher struct {
|
||||
|
@ -61,13 +64,14 @@ type stream struct {
|
|||
|
||||
func New(b bot.Bot) *TwitchPlugin {
|
||||
p := &TwitchPlugin{
|
||||
bot: b,
|
||||
config: b.Config(),
|
||||
b: b,
|
||||
c: b.Config(),
|
||||
twitchList: map[string]*Twitcher{},
|
||||
}
|
||||
|
||||
for _, ch := range p.config.GetArray("Twitch.Channels", []string{}) {
|
||||
for _, twitcherName := range p.config.GetArray("Twitch."+ch+".Users", []string{}) {
|
||||
for _, ch := range p.c.GetArray("Twitch.Channels", []string{}) {
|
||||
for _, twitcherName := range p.c.GetArray("Twitch."+ch+".Users", []string{}) {
|
||||
twitcherName = strings.ToLower(twitcherName)
|
||||
if _, ok := p.twitchList[twitcherName]; !ok {
|
||||
p.twitchList[twitcherName] = &Twitcher{
|
||||
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)
|
||||
b.Register(p, bot.Help, p.help)
|
||||
go p.twitchAuthLoop(b.DefaultConnector())
|
||||
|
||||
p.register()
|
||||
p.registerWeb()
|
||||
|
||||
return p
|
||||
|
@ -88,11 +93,11 @@ func New(b bot.Bot) *TwitchPlugin {
|
|||
func (p *TwitchPlugin) registerWeb() {
|
||||
r := chi.NewRouter()
|
||||
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) {
|
||||
userName := chi.URLParam(r, "user")
|
||||
userName := strings.ToLower(chi.URLParam(r, "user"))
|
||||
if userName == "" {
|
||||
fmt.Fprint(w, "User not found.")
|
||||
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 {
|
||||
body := strings.ToLower(message.Body)
|
||||
if body == "twitch status" {
|
||||
channel := message.Channel
|
||||
if users := p.config.GetArray("Twitch."+channel+".Users", []string{}); len(users) > 0 {
|
||||
for _, twitcherName := range users {
|
||||
if _, ok := p.twitchList[twitcherName]; ok {
|
||||
p.checkTwitch(c, channel, p.twitchList[twitcherName], true)
|
||||
func (p *TwitchPlugin) register() {
|
||||
p.tbl = bot.HandlerTable{
|
||||
{
|
||||
Kind: bot.Message, IsCmd: true,
|
||||
Regex: regexp.MustCompile(`(?i)^twitch status$`),
|
||||
HelpText: "Get status of all twitchers",
|
||||
Handler: p.twitchStatus,
|
||||
},
|
||||
{
|
||||
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 {
|
||||
|
@ -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 += "You can reset all messages with `!reset twitch`"
|
||||
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
|
||||
}
|
||||
|
||||
func (p *TwitchPlugin) twitchLoop(c bot.Connector, channel string) {
|
||||
frequency := p.config.GetInt("Twitch.Freq", 60)
|
||||
if p.config.Get("twitch.clientid", "") == "" || p.config.Get("twitch.token", "") == "" {
|
||||
log.Info().Msgf("Disabling twitch autochecking.")
|
||||
func (p *TwitchPlugin) twitchAuthLoop(c bot.Connector) {
|
||||
frequency := p.c.GetInt("Twitch.AuthFreq", 60*60)
|
||||
cid := p.c.Get("twitch.clientid", "")
|
||||
secret := p.c.Get("twitch.secret", "")
|
||||
if cid == "" || secret == "" {
|
||||
log.Info().Msgf("Disabling twitch autoauth.")
|
||||
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 {
|
||||
time.Sleep(time.Duration(frequency) * time.Second)
|
||||
|
||||
for _, twitcherName := range p.config.GetArray("Twitch."+channel+".Users", []string{}) {
|
||||
p.checkTwitch(c, channel, p.twitchList[twitcherName], false)
|
||||
select {
|
||||
case <-time.After(time.Duration(frequency) * time.Second):
|
||||
if err := p.validateCredentials(); err != nil {
|
||||
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)
|
||||
var body []byte
|
||||
var resp *http.Response
|
||||
|
@ -193,18 +269,19 @@ func getRequest(url, clientID, token string) ([]byte, bool) {
|
|||
if err != nil {
|
||||
goto errCase
|
||||
}
|
||||
return body, true
|
||||
return body, resp.StatusCode, true
|
||||
|
||||
errCase:
|
||||
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")
|
||||
if err != nil {
|
||||
log.Error().Msg("Error parsing twitch stream URL")
|
||||
return
|
||||
err := fmt.Errorf("error parsing twitch stream URL")
|
||||
log.Error().Msg(err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
query := baseURL.Query()
|
||||
|
@ -212,35 +289,38 @@ func (p *TwitchPlugin) checkTwitch(c bot.Connector, channel string, twitcher *Tw
|
|||
|
||||
baseURL.RawQuery = query.Encode()
|
||||
|
||||
cid := p.config.Get("twitch.clientid", "")
|
||||
token := p.config.Get("twitch.token", "")
|
||||
cid := p.c.Get("twitch.clientid", "")
|
||||
token := p.c.Get("twitch.token", "")
|
||||
if cid == token && cid == "" {
|
||||
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 {
|
||||
return
|
||||
return fmt.Errorf("got status %d: %s", status, string(body))
|
||||
}
|
||||
|
||||
var s stream
|
||||
err = json.Unmarshal(body, &s)
|
||||
if err != nil {
|
||||
log.Error().Err(err)
|
||||
return
|
||||
log.Error().Err(err).Msgf("error reading twitch data")
|
||||
return err
|
||||
}
|
||||
|
||||
games := s.Data
|
||||
gameID, title := "", ""
|
||||
if len(games) > 0 {
|
||||
gameID = games[0].GameID
|
||||
if gameID == "" {
|
||||
gameID = "unknown"
|
||||
}
|
||||
title = games[0].Title
|
||||
}
|
||||
|
||||
notStreamingTpl := p.config.Get("Twitch.NotTpl", notStreamingTplFallback)
|
||||
isStreamingTpl := p.config.Get("Twitch.IsTpl", isStreamingTplFallback)
|
||||
stoppedStreamingTpl := p.config.Get("Twitch.StoppedTpl", stoppedStreamingTplFallback)
|
||||
notStreamingTpl := p.c.Get("Twitch.NotTpl", notStreamingTplFallback)
|
||||
isStreamingTpl := p.c.Get("Twitch.IsTpl", isStreamingTplFallback)
|
||||
stoppedStreamingTpl := p.c.Get("Twitch.StoppedTpl", stoppedStreamingTplFallback)
|
||||
buf := bytes.Buffer{}
|
||||
|
||||
info := struct {
|
||||
|
@ -258,31 +338,31 @@ func (p *TwitchPlugin) checkTwitch(c bot.Connector, channel string, twitcher *Tw
|
|||
t, err := template.New("notStreaming").Parse(notStreamingTpl)
|
||||
if err != nil {
|
||||
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.Execute(&buf, info)
|
||||
p.bot.Send(c, bot.Message, channel, buf.String())
|
||||
p.b.Send(c, bot.Message, channel, buf.String())
|
||||
} else {
|
||||
t, err := template.New("isStreaming").Parse(isStreamingTpl)
|
||||
if err != nil {
|
||||
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.Execute(&buf, info)
|
||||
p.bot.Send(c, bot.Message, channel, buf.String())
|
||||
p.b.Send(c, bot.Message, channel, buf.String())
|
||||
}
|
||||
} else if gameID == "" {
|
||||
if twitcher.gameID != "" {
|
||||
t, err := template.New("stoppedStreaming").Parse(stoppedStreamingTpl)
|
||||
if err != nil {
|
||||
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.Execute(&buf, info)
|
||||
p.bot.Send(c, bot.Message, channel, buf.String())
|
||||
p.b.Send(c, bot.Message, channel, buf.String())
|
||||
}
|
||||
twitcher.gameID = ""
|
||||
} else {
|
||||
|
@ -290,12 +370,55 @@ func (p *TwitchPlugin) checkTwitch(c bot.Connector, channel string, twitcher *Tw
|
|||
t, err := template.New("isStreaming").Parse(isStreamingTpl)
|
||||
if err != nil {
|
||||
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.Execute(&buf, info)
|
||||
p.bot.Send(c, bot.Message, channel, buf.String())
|
||||
p.b.Send(c, bot.Message, channel, buf.String())
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -13,6 +13,17 @@ import (
|
|||
"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) {
|
||||
isCmd := strings.HasPrefix(payload, "!")
|
||||
if isCmd {
|
||||
|
@ -30,9 +41,9 @@ func makeTwitchPlugin(t *testing.T) (*TwitchPlugin, *bot.MockBot) {
|
|||
mb := bot.NewMockBot()
|
||||
c := New(mb)
|
||||
mb.Config().Set("twitch.clientid", "fake")
|
||||
mb.Config().Set("twitch.authorization", "fake")
|
||||
c.config.SetArray("Twitch.Channels", []string{"test"})
|
||||
c.config.SetArray("Twitch.test.Users", []string{"drseabass"})
|
||||
mb.Config().Set("twitch.secret", "fake")
|
||||
c.c.SetArray("Twitch.Channels", []string{"test"})
|
||||
c.c.SetArray("Twitch.test.Users", []string{"drseabass"})
|
||||
assert.NotNil(t, c)
|
||||
|
||||
c.twitchList["drseabass"] = &Twitcher{
|
||||
|
@ -45,6 +56,6 @@ func makeTwitchPlugin(t *testing.T) (*TwitchPlugin, *bot.MockBot) {
|
|||
|
||||
func TestTwitch(t *testing.T) {
|
||||
b, mb := makeTwitchPlugin(t)
|
||||
b.message(makeMessage("!twitch status"))
|
||||
b.twitchStatus(makeRequest("!twitch status"))
|
||||
assert.NotEmpty(t, mb.Messages)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue