diff --git a/bot/bot.go b/bot/bot.go
index 98a5824..d8a3b51 100644
--- a/bot/bot.go
+++ b/bot/bot.go
@@ -147,6 +147,8 @@ func (b *bot) setupHTTP() {
b.router.HandleFunc("/", b.serveRoot)
b.router.HandleFunc("/nav", b.serveNav)
+ b.router.HandleFunc("/navHTML", b.serveNavHTML)
+ b.router.HandleFunc("/navHTML/{currentPage}", b.serveNavHTML)
}
func (b *bot) ListenAndServe() {
diff --git a/bot/interfaces.go b/bot/interfaces.go
index c574212..c238409 100644
--- a/bot/interfaces.go
+++ b/bot/interfaces.go
@@ -26,6 +26,8 @@ const (
Reply
// Action any /me action
Action
+ // Spoiler is for commented out messages
+ Spoiler
// Reaction Icon reaction if service supports it
Reaction
// Edit message ref'd new message to replace
diff --git a/bot/mock.go b/bot/mock.go
index b1d7ae2..cab54c9 100644
--- a/bot/mock.go
+++ b/bot/mock.go
@@ -37,6 +37,9 @@ func (mb *MockBot) GetPassword() string { return "12345" }
func (mb *MockBot) SetQuiet(bool) {}
func (mb *MockBot) Send(c Connector, kind Kind, args ...any) (string, error) {
switch kind {
+ case Spoiler:
+ mb.Messages = append(mb.Messages, "||"+args[1].(string)+"||")
+ return fmt.Sprintf("m-%d", len(mb.Actions)-1), nil
case Message:
mb.Messages = append(mb.Messages, args[1].(string))
return fmt.Sprintf("m-%d", len(mb.Actions)-1), nil
diff --git a/bot/nav.html b/bot/nav.html
new file mode 100644
index 0000000..c3d9068
--- /dev/null
+++ b/bot/nav.html
@@ -0,0 +1,22 @@
+
\ No newline at end of file
diff --git a/bot/web.go b/bot/web.go
index e32aa23..4d901a6 100644
--- a/bot/web.go
+++ b/bot/web.go
@@ -3,8 +3,12 @@ package bot
import (
"embed"
"encoding/json"
+ "fmt"
+ "github.com/go-chi/chi/v5"
+ "github.com/rs/zerolog/log"
"net/http"
"strings"
+ "text/template"
)
//go:embed *.html
@@ -15,6 +19,19 @@ func (b *bot) serveRoot(w http.ResponseWriter, r *http.Request) {
w.Write(index)
}
+func (b *bot) serveNavHTML(w http.ResponseWriter, r *http.Request) {
+ currentPage := chi.URLParam(r, "currentPage")
+ tpl := template.Must(template.ParseFS(embeddedFS, "nav.html"))
+ if err := tpl.Execute(w, struct {
+ CurrentPage string
+ Items []EndPoint
+ }{currentPage, b.GetWebNavigation()}); err != nil {
+ log.Error().Err(err).Msg("template error")
+ w.WriteHeader(500)
+ fmt.Fprint(w, "Error parsing nav template")
+ }
+}
+
func (b *bot) serveNav(w http.ResponseWriter, r *http.Request) {
enc := json.NewEncoder(w)
err := enc.Encode(b.GetWebNavigation())
diff --git a/connectors/discord/discord.go b/connectors/discord/discord.go
index 6e5109a..66834a6 100644
--- a/connectors/discord/discord.go
+++ b/connectors/discord/discord.go
@@ -74,6 +74,9 @@ func (d Discord) Send(kind bot.Kind, args ...any) (string, error) {
return d.sendMessage(args[0].(string), args[2].(string), false, args...)
case bot.Message:
return d.sendMessage(args[0].(string), args[1].(string), false, args...)
+ case bot.Spoiler:
+ outgoing := "||" + args[1].(string) + "||"
+ return d.sendMessage(args[0].(string), outgoing, false, args...)
case bot.Action:
return d.sendMessage(args[0].(string), args[1].(string), true, args...)
case bot.Edit:
@@ -151,7 +154,27 @@ func (d *Discord) sendMessage(channel, message string, meMessage bool, args ...a
Interface("data", data).
Msg("sending message")
- st, err := d.client.ChannelMessageSendComplex(channel, data)
+ maxLen := 2000
+ chunkSize := maxLen - 100
+ var st *discordgo.Message
+ var err error
+ if len(data.Content) > maxLen {
+ tmp := data.Content
+ data.Content = tmp[:chunkSize]
+ st, err = d.client.ChannelMessageSendComplex(channel, data)
+ if err != nil {
+ return "", err
+ }
+ for i := chunkSize; i < len(data.Content); i += chunkSize {
+ data := &discordgo.MessageSend{Content: tmp[i : i+chunkSize]}
+ st, err = d.client.ChannelMessageSendComplex(channel, data)
+ if err != nil {
+ break
+ }
+ }
+ } else {
+ st, err = d.client.ChannelMessageSendComplex(channel, data)
+ }
//st, err := d.client.ChannelMessageSend(channel, message)
if err != nil {
diff --git a/main.go b/main.go
index b134a0e..296ce88 100644
--- a/main.go
+++ b/main.go
@@ -135,7 +135,6 @@ func main() {
b.AddPlugin(roles.New(b))
b.AddPlugin(twitch.New(b))
b.AddPlugin(pagecomment.New(b))
- b.AddPlugin(gpt.New(b))
b.AddPlugin(secrets.New(b))
b.AddPlugin(mayi.New(b))
b.AddPlugin(giphy.New(b))
@@ -179,8 +178,9 @@ func main() {
b.AddPlugin(cowboy.New(b))
b.AddPlugin(topic.New(b))
b.AddPlugin(talker.New(b))
- // catches anything left, will always return true
b.AddPlugin(fact.New(b))
+ // catches anything left, will always return true
+ b.AddPlugin(gpt.New(b))
if err := client.Serve(); err != nil {
log.Fatal().Err(err)
diff --git a/plugins/admin/vars.html b/plugins/admin/vars.html
index eded1ce..74b0bf5 100644
--- a/plugins/admin/vars.html
+++ b/plugins/admin/vars.html
@@ -1,77 +1,36 @@
-
+
-
-
-
-
-
-
-
-
-
-
-
-
- Vars
+
+
+ vars
+
+
+
-
-
- Variables
-
- {{ item.name }}
-
-
-
-
- {{ err }}
-
-
-
-
-
-
-
+
+
+
+
+ Key |
+ Value |
+
+
+
+ {{range .Items}}
+
+ {{.Key}} | {{.Value}} |
+
+ {{else}}
+
+ No data |
+
+ {{end}}
+
+
+
+
\ No newline at end of file
diff --git a/plugins/admin/web.go b/plugins/admin/web.go
index b20f143..706edd9 100644
--- a/plugins/admin/web.go
+++ b/plugins/admin/web.go
@@ -6,6 +6,7 @@ import (
"embed"
"encoding/json"
"fmt"
+ "html/template"
"io/ioutil"
"net/http"
"strings"
@@ -166,9 +167,32 @@ func writeErr(w http.ResponseWriter, err error) {
fmt.Fprint(w, string(j))
}
+type configEntry struct {
+ Key string `json:"key"`
+ Value string `json:"value"`
+}
+
func (p *AdminPlugin) handleVars(w http.ResponseWriter, r *http.Request) {
- index, _ := embeddedFS.ReadFile("vars.html")
- w.Write(index)
+ tpl := template.Must(template.ParseFS(embeddedFS, "vars.html"))
+ var configEntries []configEntry
+ q := `select key, value from config`
+ err := p.db.Select(&configEntries, q)
+ if err != nil {
+ log.Error().
+ Err(err).
+ Msg("Error getting config entries.")
+ w.WriteHeader(500)
+ fmt.Fprint(w, err)
+ return
+ }
+
+ if err := tpl.Execute(w, struct {
+ Items []configEntry
+ }{configEntries}); err != nil {
+ log.Error().Err(err).Msg("template error")
+ w.WriteHeader(500)
+ fmt.Fprint(w, "Error parsing template")
+ }
}
func (p *AdminPlugin) handleVarsAPI(w http.ResponseWriter, r *http.Request) {
diff --git a/plugins/counter/api.go b/plugins/counter/api.go
index 9d0ba18..35eb4df 100644
--- a/plugins/counter/api.go
+++ b/plugins/counter/api.go
@@ -5,8 +5,9 @@ import (
"encoding/json"
"fmt"
"github.com/velour/catbase/bot/user"
- "io/ioutil"
+ "io"
"net/http"
+ "net/url"
"strconv"
"time"
@@ -29,8 +30,8 @@ func (p *CounterPlugin) registerWeb() {
subrouter.Use(httprate.LimitByIP(requests, dur))
subrouter.HandleFunc("/api/users/{user}/items/{item}/increment/{delta}", p.mkIncrementByNAPI(1))
subrouter.HandleFunc("/api/users/{user}/items/{item}/decrement/{delta}", p.mkIncrementByNAPI(-1))
- subrouter.HandleFunc("/api/users/{user}/items/{item}/increment", p.mkIncrementAPI(1))
- subrouter.HandleFunc("/api/users/{user}/items/{item}/decrement", p.mkIncrementAPI(-1))
+ subrouter.HandleFunc("/api/users/{user}/items/{item}/increment", p.mkIncrementByNAPI(1))
+ subrouter.HandleFunc("/api/users/{user}/items/{item}/decrement", p.mkIncrementByNAPI(-1))
r.Mount("/", subrouter)
r.HandleFunc("/api", p.handleCounterAPI)
r.HandleFunc("/", p.handleCounter)
@@ -39,9 +40,14 @@ func (p *CounterPlugin) registerWeb() {
func (p *CounterPlugin) mkIncrementByNAPI(direction int) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
- userName := chi.URLParam(r, "user")
- itemName := chi.URLParam(r, "item")
- delta, _ := strconv.Atoi(chi.URLParam(r, "delta"))
+ userName, _ := url.QueryUnescape(chi.URLParam(r, "user"))
+ itemName, _ := url.QueryUnescape(chi.URLParam(r, "item"))
+ delta, err := strconv.Atoi(chi.URLParam(r, "delta"))
+ if err != nil || delta == 0 {
+ delta = direction
+ } else {
+ delta = delta * direction
+ }
secret, pass, ok := r.BasicAuth()
if !ok || !p.b.CheckPassword(secret, pass) {
@@ -77,7 +83,7 @@ func (p *CounterPlugin) mkIncrementByNAPI(direction int) func(w http.ResponseWri
return
}
- body, _ := ioutil.ReadAll(r.Body)
+ body, _ := io.ReadAll(r.Body)
postData := map[string]string{}
err = json.Unmarshal(body, &postData)
personalMsg := ""
@@ -85,7 +91,11 @@ func (p *CounterPlugin) mkIncrementByNAPI(direction int) func(w http.ResponseWri
personalMsg = fmt.Sprintf("\nMessage: %s", inputMsg)
}
- chs := p.cfg.GetArray("channels", []string{p.cfg.Get("channels", "none")})
+ chs := p.cfg.GetMap("counter.channelItems", map[string]string{})
+ ch, ok := chs[itemName]
+ if len(chs) == 0 || !ok {
+ return
+ }
req := &bot.Request{
Conn: p.b.DefaultConnector(),
Kind: bot.Message,
@@ -93,7 +103,7 @@ func (p *CounterPlugin) mkIncrementByNAPI(direction int) func(w http.ResponseWri
User: &u,
// Noting here that we're only going to do goals in a "default"
// channel even if it should send updates to others.
- Channel: chs[0],
+ Channel: ch,
Body: fmt.Sprintf("%s += %d", itemName, delta),
Time: time.Now(),
},
@@ -102,7 +112,13 @@ func (p *CounterPlugin) mkIncrementByNAPI(direction int) func(w http.ResponseWri
}
msg := fmt.Sprintf("%s changed their %s counter by %d for a total of %d via the amazing %s API. %s",
userName, itemName, delta, item.Count+delta*direction, p.cfg.Get("nick", "catbase"), personalMsg)
- for _, ch := range chs {
+ if !ok {
+ chs := p.cfg.GetArray("counter.channels", []string{})
+ for _, ch := range chs {
+ p.b.Send(p.b.DefaultConnector(), bot.Message, ch, msg)
+ req.Msg.Channel = ch
+ }
+ } else {
p.b.Send(p.b.DefaultConnector(), bot.Message, ch, msg)
req.Msg.Channel = ch
}
@@ -112,80 +128,6 @@ func (p *CounterPlugin) mkIncrementByNAPI(direction int) func(w http.ResponseWri
}
}
-func (p *CounterPlugin) mkIncrementAPI(delta int) func(w http.ResponseWriter, r *http.Request) {
- return func(w http.ResponseWriter, r *http.Request) {
- userName := chi.URLParam(r, "user")
- itemName := chi.URLParam(r, "item")
-
- secret, pass, ok := r.BasicAuth()
- if !ok || !p.b.CheckPassword(secret, pass) {
- err := fmt.Errorf("unauthorized access")
- log.Error().
- Err(err).
- Msg("error authenticating user")
- w.WriteHeader(401)
- j, _ := json.Marshal(struct {
- Status bool
- Error string
- }{false, err.Error()})
- fmt.Fprint(w, string(j))
- return
- }
-
- // Try to find an ID if possible
- id := ""
- u, err := p.b.DefaultConnector().Profile(userName)
- if err == nil {
- id = u.ID
- }
-
- item, err := GetUserItem(p.db, userName, id, itemName)
- if err != nil {
- log.Error().Err(err).Msg("error finding item")
- w.WriteHeader(400)
- j, _ := json.Marshal(struct {
- Status bool
- Error error
- }{false, err})
- fmt.Fprint(w, string(j))
- return
- }
-
- body, _ := ioutil.ReadAll(r.Body)
- postData := map[string]string{}
- err = json.Unmarshal(body, &postData)
- personalMsg := ""
- if inputMsg, ok := postData["message"]; ok {
- personalMsg = fmt.Sprintf("\nMessage: %s", inputMsg)
- }
-
- chs := p.cfg.GetArray("channels", []string{p.cfg.Get("channels", "none")})
- req := &bot.Request{
- Conn: p.b.DefaultConnector(),
- Kind: bot.Message,
- Msg: msg.Message{
- User: &u,
- // Noting here that we're only going to do goals in a "default"
- // channel even if it should send updates to others.
- Channel: chs[0],
- Body: fmt.Sprintf("%s += %d", itemName, delta),
- Time: time.Now(),
- },
- Values: nil,
- Args: nil,
- }
- msg := fmt.Sprintf("%s changed their %s counter by %d for a total of %d via the amazing %s API. %s",
- userName, itemName, delta, item.Count+delta, p.cfg.Get("nick", "catbase"), personalMsg)
- for _, ch := range chs {
- p.b.Send(p.b.DefaultConnector(), bot.Message, ch, msg)
- req.Msg.Channel = ch
- }
- item.UpdateDelta(req, delta)
- j, _ := json.Marshal(struct{ Status bool }{true})
- fmt.Fprint(w, string(j))
- }
-}
-
func (p *CounterPlugin) handleCounter(w http.ResponseWriter, r *http.Request) {
index, _ := embeddedFS.ReadFile("index.html")
w.Write(index)
diff --git a/plugins/fact/factoid.go b/plugins/fact/factoid.go
index d4098e8..ca1bddf 100644
--- a/plugins/fact/factoid.go
+++ b/plugins/fact/factoid.go
@@ -90,21 +90,8 @@ func New(botInst bot.Bot) *FactoidPlugin {
// findAction simply regexes a string for the action verb
func findAction(message string) string {
- r, err := regexp.Compile("<.+?>")
- if err != nil {
- panic(err)
- }
- action := r.FindString(message)
-
- if action == "" {
- if strings.Contains(message, " is ") {
- return "is"
- } else if strings.Contains(message, " are ") {
- return "are"
- }
- }
-
- return action
+ r := regexp.MustCompile("<.+?>")
+ return r.FindString(message)
}
// learnFact assumes we have a learning situation and inserts a new fact
@@ -157,7 +144,6 @@ func (p *FactoidPlugin) findTrigger(fact string) (bool, *Factoid) {
f, err := GetSingleFact(p.db, fact)
if err != nil {
- log.Error().Err(err).Msg("GetSingleFact")
return findAlias(p.db, fact)
}
return true, f
@@ -485,18 +471,7 @@ func (p *FactoidPlugin) register() {
return true
}
- notFound := p.c.GetArray("fact.notfound", []string{
- "I don't know.",
- "NONONONO",
- "((",
- "*pukes*",
- "NOPE! NOPE! NOPE!",
- "One time, I learned how to jump rope.",
- })
-
- // We didn't find anything, panic!
- p.b.Send(c, bot.Message, message.Channel, notFound[rand.Intn(len(notFound))])
- return true
+ return false
}},
}
p.b.RegisterTable(p, p.handlers)
diff --git a/plugins/gpt/chatgpt.go b/plugins/gpt/chatgpt.go
index f7ea523..9b0f5f5 100644
--- a/plugins/gpt/chatgpt.go
+++ b/plugins/gpt/chatgpt.go
@@ -6,7 +6,7 @@ import (
)
import "github.com/andrewstuart/openai"
-var session *openai.ChatSession
+var session openai.ChatSession
var client *openai.Client
func (p *GPTPlugin) getClient() (*openai.Client, error) {
@@ -14,31 +14,37 @@ func (p *GPTPlugin) getClient() (*openai.Client, error) {
if token == "" {
return nil, fmt.Errorf("no GPT token given")
}
- if client == nil {
- return openai.NewClient(token)
- }
- return client, nil
+ return openai.NewClient(token)
}
func (p *GPTPlugin) chatGPT(request string) (string, error) {
- if session == nil {
- if err := p.setDefaultPrompt(); err != nil {
+ if client == nil {
+ if err := p.setPrompt(p.getDefaultPrompt()); err != nil {
return "", err
}
}
+ if p.chatCount > p.c.GetInt("gpt.maxchats", 10) {
+ p.setPrompt(p.c.Get("gpt3.lastprompt", p.getDefaultPrompt()))
+ p.chatCount = 0
+ }
+ p.chatCount++
return session.Complete(context.Background(), request)
}
-func (p *GPTPlugin) setDefaultPrompt() error {
- return p.setPrompt(p.c.Get("gpt.prompt", ""))
+func (p *GPTPlugin) getDefaultPrompt() string {
+ return p.c.Get("gpt.prompt", "")
}
func (p *GPTPlugin) setPrompt(prompt string) error {
- client, err := p.getClient()
+ var err error
+ client, err = p.getClient()
+ if err != nil {
+ return err
+ }
+ session = client.NewChatSession(prompt)
+ err = p.c.Set("gpt3.lastprompt", prompt)
if err != nil {
return err
}
- sess := client.NewChatSession(prompt)
- session = &sess
return nil
}
diff --git a/plugins/gpt/gpt3.go b/plugins/gpt/gpt3.go
index 10d37c8..764ca53 100644
--- a/plugins/gpt/gpt3.go
+++ b/plugins/gpt/gpt3.go
@@ -23,6 +23,8 @@ type GPTPlugin struct {
b bot.Bot
c *config.Config
h bot.HandlerTable
+
+ chatCount int
}
func New(b bot.Bot) *GPTPlugin {
@@ -54,6 +56,11 @@ func (p *GPTPlugin) register() {
HelpText: "set the ChatGPT prompt",
Handler: p.setPromptMessage,
},
+ {
+ Kind: bot.Message, IsCmd: true,
+ Regex: regexp.MustCompile(`(?P.*)`),
+ Handler: p.chatMessage,
+ },
}
log.Debug().Msg("Registering GPT3 handlers")
p.b.RegisterTable(p, p.h)
@@ -96,7 +103,7 @@ func (p *GPTPlugin) gpt3(stem string) string {
TopP: p.c.GetFloat64("gpt3.top_p", 1),
N: p.c.GetInt("gpt3.n", 1),
Stop: p.c.GetArray("gpt3.stop", []string{"\n"}),
- Echo: true,
+ Echo: p.c.GetBool("gpt3.echo", false),
}
val, err := p.mkRequest(gpt3URL, postStruct)
if err != nil {
diff --git a/plugins/meme/meme.go b/plugins/meme/meme.go
index 178c385..319eabf 100644
--- a/plugins/meme/meme.go
+++ b/plugins/meme/meme.go
@@ -329,7 +329,7 @@ func FindFontSize(c *config.Config, config []string, fontLocation string, w, h i
longestStr, longestW := "", 0.0
for _, s := range config {
- err := m.LoadFontFace(getFont(c, fontLocation), 12)
+ err := m.LoadFontFace(GetFont(c, fontLocation), 12)
if err != nil {
log.Error().Err(err).Msg("could not load font")
return fontSize
@@ -343,7 +343,7 @@ func FindFontSize(c *config.Config, config []string, fontLocation string, w, h i
}
for _, sz := range sizes {
- err := m.LoadFontFace(getFont(c, fontLocation), sz) // problem
+ err := m.LoadFontFace(GetFont(c, fontLocation), sz) // problem
if err != nil {
log.Error().Err(err).Msg("could not load font")
return fontSize
@@ -476,7 +476,7 @@ func (p *MemePlugin) genMeme(spec specification) ([]byte, error) {
if fontLocation == "" {
fontLocation = defaultFont
}
- m.LoadFontFace(getFont(p.c, fontLocation), fontSize)
+ m.LoadFontFace(GetFont(p.c, fontLocation), fontSize)
x := float64(w)*c.XPerc + float64(dx)
y := float64(h)*c.YPerc + float64(dy)
m.DrawStringAnchored(c.Text, x, y, 0.5, 0.5)
@@ -491,7 +491,7 @@ func (p *MemePlugin) genMeme(spec specification) ([]byte, error) {
if fontLocation == "" {
fontLocation = defaultFont
}
- m.LoadFontFace(getFont(p.c, fontLocation), fontSize)
+ m.LoadFontFace(GetFont(p.c, fontLocation), fontSize)
x := float64(w) * c.XPerc
y := float64(h) * c.YPerc
m.DrawStringAnchored(c.Text, x, y, 0.5, 0.5)
@@ -506,7 +506,7 @@ func (p *MemePlugin) genMeme(spec specification) ([]byte, error) {
return p.images[jsonSpec].repr, nil
}
-func getFont(c *config.Config, name string) string {
+func GetFont(c *config.Config, name string) string {
location := c.Get("meme.fontLocation", "")
fontShortcuts := c.GetMap("meme.fontShortcuts", map[string]string{"impact": "impact.ttf"})
if file, ok := fontShortcuts[name]; ok {
diff --git a/plugins/reminder/reminder.go b/plugins/reminder/reminder.go
index f37f37a..31f5523 100644
--- a/plugins/reminder/reminder.go
+++ b/plugins/reminder/reminder.go
@@ -120,7 +120,6 @@ func (p *ReminderPlugin) message(c bot.Connector, kind bot.Kind, message msg.Mes
channel := message.Channel
from := message.User.Name
- message.Body = replaceDuration(p.when, message.Body)
parts := strings.Fields(message.Body)
if len(parts) >= 5 {
diff --git a/plugins/talker/talker.go b/plugins/talker/talker.go
index 5cba8a1..618e078 100644
--- a/plugins/talker/talker.go
+++ b/plugins/talker/talker.go
@@ -104,7 +104,7 @@ func (p *TalkerPlugin) message(c bot.Connector, kind bot.Kind, message msg.Messa
line = strings.Replace(line, "{nick}", nick, 1)
output += line + "\n"
}
- p.bot.Send(c, bot.Message, channel, output)
+ p.bot.Send(c, bot.Spoiler, channel, output)
return true
}
diff --git a/plugins/tappd/image.go b/plugins/tappd/image.go
index 87b1eb1..893ad0f 100644
--- a/plugins/tappd/image.go
+++ b/plugins/tappd/image.go
@@ -67,7 +67,7 @@ func defaultSpec() textSpec {
}
func (p *Tappd) overlay(img image.Image, texts []textSpec) ([]byte, error) {
- font := p.c.Get("meme.font", "impact.ttf")
+ font := meme.GetFont(p.c, p.c.Get("meme.font", "impact.ttf"))
fontSizes := []float64{48, 36, 24, 16, 12}
r := img.Bounds()
w := r.Dx()
diff --git a/plugins/twitch/demo/main.go b/plugins/twitch/demo/main.go
new file mode 100644
index 0000000..e77fdb8
--- /dev/null
+++ b/plugins/twitch/demo/main.go
@@ -0,0 +1,146 @@
+package main
+
+import (
+ "encoding/json"
+ "fmt"
+ "github.com/nicklaw5/helix"
+ "io"
+ "log"
+ "net/http"
+)
+
+func main() {
+ client, err := helix.NewClient(&helix.Options{
+ ClientID: "ptwtiuzl9tcrekpf3d26ey3hb7qsge",
+ ClientSecret: "rpa0w6qemjqp7sgrmidwi4k0kcah82",
+ })
+ if err != nil {
+ log.Printf("Login error: %v", err)
+ return
+ }
+
+ access, err := client.RequestAppAccessToken([]string{"user:read:email"})
+ if err != nil {
+ log.Printf("Login error: %v", err)
+ return
+ }
+
+ fmt.Printf("%+v\n", access)
+
+ // Set the access token on the client
+ client.SetAppAccessToken(access.Data.AccessToken)
+
+ users, err := client.GetUsers(&helix.UsersParams{
+ Logins: []string{"drseabass"},
+ })
+ if err != nil {
+ log.Printf("Error getting users: %v", err)
+ return
+ }
+
+ if users.Error != "" {
+ log.Printf("Users error: %s", users.Error)
+ return
+ }
+
+ log.Printf("drseabass: %+v", users.Data.Users[0])
+ return
+
+ resp, err := client.CreateEventSubSubscription(&helix.EventSubSubscription{
+ Type: helix.EventSubTypeStreamOnline,
+ Version: "1",
+ Condition: helix.EventSubCondition{
+ BroadcasterUserID: users.Data.Users[0].ID,
+ },
+ Transport: helix.EventSubTransport{
+ Method: "webhook",
+ Callback: "https://rathaus.chrissexton.org/live",
+ Secret: "s3cre7w0rd",
+ },
+ })
+ if err != nil {
+ log.Printf("Eventsub error: %v", err)
+ return
+ }
+
+ fmt.Printf("%+v\n", resp)
+
+ resp, err = client.CreateEventSubSubscription(&helix.EventSubSubscription{
+ Type: helix.EventSubTypeStreamOffline,
+ Version: "1",
+ Condition: helix.EventSubCondition{
+ BroadcasterUserID: users.Data.Users[0].ID,
+ },
+ Transport: helix.EventSubTransport{
+ Method: "webhook",
+ Callback: "https://rathaus.chrissexton.org/offline",
+ Secret: "s3cre7w0rd",
+ },
+ })
+ if err != nil {
+ log.Printf("Eventsub error: %v", err)
+ return
+ }
+
+ fmt.Printf("%+v\n", resp)
+
+ http.HandleFunc("/offline", func(w http.ResponseWriter, r *http.Request) {
+ body, err := io.ReadAll(r.Body)
+ if err != nil {
+ log.Println(err)
+ return
+ }
+ defer r.Body.Close()
+ // verify that the notification came from twitch using the secret.
+ if !helix.VerifyEventSubNotification("s3cre7w0rd", r.Header, string(body)) {
+ log.Println("no valid signature on subscription")
+ return
+ } else {
+ log.Println("verified signature for subscription")
+ }
+ var vals map[string]any
+ if err = json.Unmarshal(body, &vals); err != nil {
+ log.Println(err)
+ return
+ }
+
+ if challenge, ok := vals["challenge"]; ok {
+ w.Write([]byte(challenge.(string)))
+ return
+ }
+
+ log.Printf("got offline webhook: %v\n", vals)
+ w.WriteHeader(200)
+ w.Write([]byte("ok"))
+ })
+ http.HandleFunc("/live", func(w http.ResponseWriter, r *http.Request) {
+ body, err := io.ReadAll(r.Body)
+ if err != nil {
+ log.Println(err)
+ return
+ }
+ defer r.Body.Close()
+ // verify that the notification came from twitch using the secret.
+ if !helix.VerifyEventSubNotification("s3cre7w0rd", r.Header, string(body)) {
+ log.Println("no valid signature on subscription")
+ return
+ } else {
+ log.Println("verified signature for subscription")
+ }
+ var vals map[string]any
+ if err = json.Unmarshal(body, &vals); err != nil {
+ log.Println(err)
+ return
+ }
+
+ if challenge, ok := vals["challenge"]; ok {
+ w.Write([]byte(challenge.(string)))
+ return
+ }
+
+ log.Printf("got live webhook: %v\n", vals)
+ w.WriteHeader(200)
+ w.Write([]byte("ok"))
+ })
+ http.ListenAndServe("0.0.0.0:1337", nil)
+}
diff --git a/util/stats/main.go b/util/stats/main.go
new file mode 100644
index 0000000..43b4fd5
--- /dev/null
+++ b/util/stats/main.go
@@ -0,0 +1 @@
+package stats