Compare commits

..

1 Commits

Author SHA1 Message Date
dependabot[bot] 4efa07e783
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-08-01 06:07:58 +00:00
23 changed files with 148 additions and 579 deletions

1
.gitignore vendored
View File

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

View File

@ -106,4 +106,3 @@ 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

View File

@ -4,8 +4,6 @@ package bot
import (
"fmt"
"github.com/go-chi/chi/v5/middleware"
"github.com/go-chi/httprate"
"math/rand"
"net/http"
"os"
@ -16,6 +14,7 @@ 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"
@ -121,34 +120,21 @@ func New(config *config.Config, connector Connector) Bot {
log.Debug().Msgf("created web router")
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 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)
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)
@ -279,7 +265,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().Msgf("%s admin check: passed", u)
log.Info().Msg("%s admin check: passed")
return true
}
}

View File

@ -44,12 +44,6 @@ type EphemeralID string
type UnfurlLinks bool
type EmbedAuthor struct {
ID string
Who string
IconURL string
}
type ImageAttachment struct {
URL string
AltTxt string
@ -232,9 +226,6 @@ 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

View File

@ -201,32 +201,6 @@ 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 {
@ -282,7 +256,7 @@ func ReadConfig(dbpath string) *Config {
value string,
primary key (key)
);`); err != nil {
log.Fatal().Err(err).Msgf("failed to initialize secrets")
log.Fatal().Err(err).Msgf("failed to initialize config")
}
if err := c.RefreshSecrets(); err != nil {

View File

@ -103,32 +103,25 @@ func (d *Discord) sendMessage(channel, message string, meMessage bool, args ...a
message = "_" + message + "_"
}
embeds := []*discordgo.MessageEmbed{}
var 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:
embed := &discordgo.MessageEmbed{}
embed.Description = a.AltTxt
embed.Image = &discordgo.MessageEmbedImage{
//embeds.URL = a.URL
embeds = &discordgo.MessageEmbed{}
embeds.Description = a.AltTxt
embeds.Image = &discordgo.MessageEmbedImage{
URL: a.URL,
Width: a.Width,
Height: a.Height,
}
embeds = append(embeds, embed)
}
}
data := &discordgo.MessageSend{
Content: message,
Embeds: embeds,
Embed: embeds,
}
log.Debug().
@ -247,7 +240,6 @@ func (d *Discord) Serve() error {
d.client.Identify.Intents = discordgo.MakeIntent(
discordgo.IntentsGuilds |
discordgo.IntentsGuildMessages |
discordgo.IntentsDirectMessages |
discordgo.IntentsGuildEmojis |
discordgo.IntentsGuildMessageReactions)
@ -329,9 +321,7 @@ 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, &discordgo.EmojiParams{
emojy, path, defaultRoles,
})
_, err := d.client.GuildEmojiCreate(guildID, emojy, path, defaultRoles)
if err != nil {
return err
}
@ -433,24 +423,3 @@ 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,8 +361,3 @@ 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")
}

View File

@ -751,7 +751,3 @@ func (s *SlackApp) SetRole(userID, roleID string) error {
}
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
github.com/ChimeraCoder/anaconda v2.0.0+incompatible
github.com/PuerkitoBio/goquery v1.8.0
github.com/bwmarrin/discordgo v0.26.1
github.com/bwmarrin/discordgo v0.25.0
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
@ -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-20220829220503-c86fa9a7ed90
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e
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/net v0.0.0-20220624214902-1bab6f366d9e // indirect
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
golang.org/x/sys v0.0.0-20220906165534-d0df966e6959 // indirect
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // 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

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/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.26.1 h1:AIrM+g3cl+iYBr4yBxCBp9tD9jR3K7upEjl0d89FRkE=
github.com/bwmarrin/discordgo v0.26.1/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY=
github.com/bwmarrin/discordgo v0.25.0 h1:NXhdfHRNxtwso6FPdzW2i3uBvvU7UIQTghmV2T4nqAs=
github.com/bwmarrin/discordgo v0.25.0/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=
@ -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-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/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/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=
@ -213,9 +213,8 @@ 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-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=

View File

@ -4,8 +4,6 @@ package main
import (
"flag"
"github.com/velour/catbase/plugins/pagecomment"
"github.com/velour/catbase/plugins/topic"
"io"
"math/rand"
"os"
@ -132,7 +130,6 @@ 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))
@ -176,7 +173,6 @@ 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))

View File

@ -57,7 +57,6 @@ 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()
@ -107,7 +106,6 @@ 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 {
@ -409,17 +407,3 @@ 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
}

View File

@ -140,4 +140,3 @@ 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 }

View File

@ -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: discordgo.MessageFlagsEphemeral,
Flags: uint64(discordgo.MessageFlagsEphemeral),
},
})
}

View File

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

View File

@ -45,10 +45,8 @@
<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>

View File

@ -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+)\s?(?P<comment>.+)?$`)
var bidRegex = regexp.MustCompile(`(?i)^bid (?P<amount>\S+) (?P<url>)\S+$`)
var checkRegex = regexp.MustCompile(`(?i)^check ngate$`)
func (p *NewsBid) balanceCmd(r bot.Request) bool {

View File

@ -1,123 +0,0 @@
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,7 +5,6 @@ package reminder
import (
"errors"
"fmt"
"regexp"
"strconv"
"strings"
"sync"
@ -35,7 +34,6 @@ type ReminderPlugin struct {
timer *time.Timer
config *config.Config
when *when.Parser
lastReminder map[string]*Reminder
}
type Reminder struct {
@ -74,53 +72,33 @@ func New(b bot.Bot) *ReminderPlugin {
timer: timer,
config: b.Config(),
when: w,
lastReminder: map[string]*Reminder{},
}
plugin.queueUpNextReminder()
go plugin.reminderer(b.DefaultConnector())
go reminderer(b.DefaultConnector(), plugin)
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
message.Body = replaceDuration(p.when, message.Body)
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")
}
parts := strings.Fields(message.Body)
if len(parts) >= 5 {
@ -130,7 +108,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
@ -157,7 +135,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'.")
@ -374,15 +352,13 @@ func (p *ReminderPlugin) queueUpNextReminder() {
}
}
func (p *ReminderPlugin) reminderer(c bot.Connector) {
func reminderer(c bot.Connector, p *ReminderPlugin) {
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"

View File

@ -46,6 +46,33 @@ 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 {
@ -103,7 +130,7 @@ func (p *SecretsPlugin) handleRegister(w http.ResponseWriter, r *http.Request) {
return
}
log.Debug().Msgf("Secret: %s", secret)
err = p.c.RegisterSecret(secret.Key, secret.Value)
err = p.registerSecret(secret.Key, secret.Value)
if checkError(err) {
return
}
@ -121,7 +148,7 @@ func (p *SecretsPlugin) handleRemove(w http.ResponseWriter, r *http.Request) {
if checkError(err) {
return
}
err = p.c.RemoveSecret(secret.Key)
err = p.removeSecret(secret.Key)
if checkError(err) {
return
}

View File

@ -1,62 +0,0 @@
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,16 +4,14 @@ 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"
@ -27,10 +25,9 @@ const (
)
type TwitchPlugin struct {
b bot.Bot
c *config.Config
bot bot.Bot
config *config.Config
twitchList map[string]*Twitcher
tbl bot.HandlerTable
}
type Twitcher struct {
@ -64,14 +61,13 @@ type stream struct {
func New(b bot.Bot) *TwitchPlugin {
p := &TwitchPlugin{
b: b,
c: b.Config(),
bot: b,
config: b.Config(),
twitchList: map[string]*Twitcher{},
}
for _, ch := range p.c.GetArray("Twitch.Channels", []string{}) {
for _, twitcherName := range p.c.GetArray("Twitch."+ch+".Users", []string{}) {
twitcherName = strings.ToLower(twitcherName)
for _, ch := range p.config.GetArray("Twitch.Channels", []string{}) {
for _, twitcherName := range p.config.GetArray("Twitch."+ch+".Users", []string{}) {
if _, ok := p.twitchList[twitcherName]; !ok {
p.twitchList[twitcherName] = &Twitcher{
name: twitcherName,
@ -79,12 +75,11 @@ func New(b bot.Bot) *TwitchPlugin {
}
}
}
go p.twitchChannelLoop(b.DefaultConnector(), ch)
go p.twitchLoop(b.DefaultConnector(), ch)
}
go p.twitchAuthLoop(b.DefaultConnector())
p.register()
b.Register(p, bot.Message, p.message)
b.Register(p, bot.Help, p.help)
p.registerWeb()
return p
@ -93,11 +88,11 @@ func New(b bot.Bot) *TwitchPlugin {
func (p *TwitchPlugin) registerWeb() {
r := chi.NewRouter()
r.HandleFunc("/{user}", p.serveStreaming)
p.b.RegisterWeb(r, "/isstreaming")
p.bot.RegisterWeb(r, "/isstreaming")
}
func (p *TwitchPlugin) serveStreaming(w http.ResponseWriter, r *http.Request) {
userName := strings.ToLower(chi.URLParam(r, "user"))
userName := chi.URLParam(r, "user")
if userName == "" {
fmt.Fprint(w, "User not found.")
return
@ -126,68 +121,25 @@ func (p *TwitchPlugin) serveStreaming(w http.ResponseWriter, r *http.Request) {
}
}
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 {
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 {
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")
}
if _, ok := p.twitchList[twitcherName]; ok {
p.checkTwitch(c, channel, p.twitchList[twitcherName], true)
}
}
}
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)
}
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
return false
}
func (p *TwitchPlugin) help(c bot.Connector, kind bot.Kind, message msg.Message, args ...any) bool {
@ -197,57 +149,29 @@ 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.b.Send(c, bot.Message, message.Channel, msg)
p.bot.Send(c, bot.Message, message.Channel, msg)
return true
}
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 auth every %d seconds", frequency)
if err := p.validateCredentials(); err != nil {
log.Error().Err(err).Msgf("error checking twitch validity")
}
for {
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 (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", "") == "" {
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.")
return
}
log.Info().Msgf("Checking channels every %d seconds", frequency)
log.Info().Msgf("Checking 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")
}
for _, twitcherName := range p.config.GetArray("Twitch."+channel+".Users", []string{}) {
p.checkTwitch(c, channel, p.twitchList[twitcherName], false)
}
}
}
func getRequest(url, clientID, token string) ([]byte, int, bool) {
func getRequest(url, clientID, token string) ([]byte, bool) {
bearer := fmt.Sprintf("Bearer %s", token)
var body []byte
var resp *http.Response
@ -269,19 +193,18 @@ func getRequest(url, clientID, token string) ([]byte, int, bool) {
if err != nil {
goto errCase
}
return body, resp.StatusCode, true
return body, true
errCase:
log.Error().Err(err)
return []byte{}, resp.StatusCode, false
return []byte{}, false
}
func (p *TwitchPlugin) checkTwitch(c bot.Connector, channel string, twitcher *Twitcher, alwaysPrintStatus bool) error {
func (p *TwitchPlugin) checkTwitch(c bot.Connector, channel string, twitcher *Twitcher, alwaysPrintStatus bool) {
baseURL, err := url.Parse("https://api.twitch.tv/helix/streams")
if err != nil {
err := fmt.Errorf("error parsing twitch stream URL")
log.Error().Msg(err.Error())
return err
log.Error().Msg("Error parsing twitch stream URL")
return
}
query := baseURL.Query()
@ -289,38 +212,35 @@ func (p *TwitchPlugin) checkTwitch(c bot.Connector, channel string, twitcher *Tw
baseURL.RawQuery = query.Encode()
cid := p.c.Get("twitch.clientid", "")
token := p.c.Get("twitch.token", "")
cid := p.config.Get("twitch.clientid", "")
token := p.config.Get("twitch.token", "")
if cid == token && cid == "" {
log.Info().Msgf("Twitch plugin not enabled.")
return nil
return
}
body, status, ok := getRequest(baseURL.String(), cid, token)
body, ok := getRequest(baseURL.String(), cid, token)
if !ok {
return fmt.Errorf("got status %d: %s", status, string(body))
return
}
var s stream
err = json.Unmarshal(body, &s)
if err != nil {
log.Error().Err(err).Msgf("error reading twitch data")
return err
log.Error().Err(err)
return
}
games := s.Data
gameID, title := "", ""
if len(games) > 0 {
gameID = games[0].GameID
if gameID == "" {
gameID = "unknown"
}
title = games[0].Title
}
notStreamingTpl := p.c.Get("Twitch.NotTpl", notStreamingTplFallback)
isStreamingTpl := p.c.Get("Twitch.IsTpl", isStreamingTplFallback)
stoppedStreamingTpl := p.c.Get("Twitch.StoppedTpl", stoppedStreamingTplFallback)
notStreamingTpl := p.config.Get("Twitch.NotTpl", notStreamingTplFallback)
isStreamingTpl := p.config.Get("Twitch.IsTpl", isStreamingTplFallback)
stoppedStreamingTpl := p.config.Get("Twitch.StoppedTpl", stoppedStreamingTplFallback)
buf := bytes.Buffer{}
info := struct {
@ -338,31 +258,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.b.Send(c, bot.Message, channel, err)
p.bot.Send(c, bot.Message, channel, err)
t = template.Must(template.New("notStreaming").Parse(notStreamingTplFallback))
}
t.Execute(&buf, info)
p.b.Send(c, bot.Message, channel, buf.String())
p.bot.Send(c, bot.Message, channel, buf.String())
} else {
t, err := template.New("isStreaming").Parse(isStreamingTpl)
if err != nil {
log.Error().Err(err)
p.b.Send(c, bot.Message, channel, err)
p.bot.Send(c, bot.Message, channel, err)
t = template.Must(template.New("isStreaming").Parse(isStreamingTplFallback))
}
t.Execute(&buf, info)
p.b.Send(c, bot.Message, channel, buf.String())
p.bot.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.b.Send(c, bot.Message, channel, err)
p.bot.Send(c, bot.Message, channel, err)
t = template.Must(template.New("stoppedStreaming").Parse(stoppedStreamingTplFallback))
}
t.Execute(&buf, info)
p.b.Send(c, bot.Message, channel, buf.String())
p.bot.Send(c, bot.Message, channel, buf.String())
}
twitcher.gameID = ""
} else {
@ -370,55 +290,12 @@ 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.b.Send(c, bot.Message, channel, err)
p.bot.Send(c, bot.Message, channel, err)
t = template.Must(template.New("isStreaming").Parse(isStreamingTplFallback))
}
t.Execute(&buf, info)
p.b.Send(c, bot.Message, channel, buf.String())
p.bot.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)
}

View File

@ -13,17 +13,6 @@ 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 {
@ -41,9 +30,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.secret", "fake")
c.c.SetArray("Twitch.Channels", []string{"test"})
c.c.SetArray("Twitch.test.Users", []string{"drseabass"})
mb.Config().Set("twitch.authorization", "fake")
c.config.SetArray("Twitch.Channels", []string{"test"})
c.config.SetArray("Twitch.test.Users", []string{"drseabass"})
assert.NotNil(t, c)
c.twitchList["drseabass"] = &Twitcher{
@ -56,6 +45,6 @@ func makeTwitchPlugin(t *testing.T) (*TwitchPlugin, *bot.MockBot) {
func TestTwitch(t *testing.T) {
b, mb := makeTwitchPlugin(t)
b.twitchStatus(makeRequest("!twitch status"))
b.message(makeMessage("!twitch status"))
assert.NotEmpty(t, mb.Messages)
}