Merge branch 'master' into dependabot/go_modules/github.com/antchfx/xmlquery-1.3.1

This commit is contained in:
Chris Sexton 2023-10-30 13:41:40 -04:00 committed by GitHub
commit 3fe4eda22b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 336 additions and 208 deletions

View File

@ -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() {

View File

@ -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

View File

@ -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

22
bot/nav.html Normal file
View File

@ -0,0 +1,22 @@
<nav class="navbar navbar-expand-lg bg-body-tertiary">
<div class="container-fluid">
<a class="navbar-brand" href="/">catbase</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav">
{{- $currentPage := .CurrentPage -}}
{{range .Items}}
<li class="nav-item">
{{ if (eq $currentPage .Name) }}
<a class="nav-link active" aria-current="page" href="{{.URL}}">{{.Name}}</a>
{{ else }}
<a class="nav-link" href="{{.URL}}">{{.Name}}</a>
{{ end }}
</li>
{{end}}
</ul>
</div>
</div>
</nav>

View File

@ -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())

View File

@ -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 {

View File

@ -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)

View File

@ -1,77 +1,36 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<!-- Load required Bootstrap and BootstrapVue CSS -->
<link type="text/css" rel="stylesheet" href="//unpkg.com/bootstrap/dist/css/bootstrap.min.css"/>
<link type="text/css" rel="stylesheet" href="//unpkg.com/bootstrap-vue@^2/dist/bootstrap-vue.min.css"/>
<!-- Load polyfills to support older browsers -->
<script src="//polyfill.io/v3/polyfill.min.js?features=es2015%2CMutationObserver"></script>
<!-- Load Vue followed by BootstrapVue -->
<script src="//unpkg.com/vue@^2/dist/vue.min.js"></script>
<script src="//unpkg.com/bootstrap-vue@^2/dist/bootstrap-vue.min.js"></script>
<script src="//unpkg.com/axios/dist/axios.min.js"></script>
<meta charset="UTF-8">
<title>Vars</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>vars</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-4bw+/aepP/YC94hEpVNVgiZdgIC5+VKNBQNGCHeKRQN+PtmoHDEXuppvnDJzQIu9" crossorigin="anonymous">
<script src="https://unpkg.com/htmx.org@1.9.4"></script>
</head>
<body>
<div hx-get="/navHTML/Variables" hx-trigger="load" hx-swap="outerHTML"></div>
<div id="app">
<b-navbar>
<b-navbar-brand>Variables</b-navbar-brand>
<b-navbar-nav>
<b-nav-item v-for="item in nav" :href="item.url" :active="item.name === 'Variables'">{{ item.name }}
</b-nav-item>
</b-navbar-nav>
</b-navbar>
<b-alert
dismissable
variant="error"
v-if="err"
@dismissed="err = ''">
{{ err }}
</b-alert>
<b-container>
<b-table
fixed
:items="vars"
:sort-by.sync="sortBy"
:fields="fields"></b-table>
</b-container>
</div>
<script>
var app = new Vue({
el: '#app',
data: {
err: '',
nav: [],
vars: [],
sortBy: 'key',
fields: [
{key: {sortable: true}},
'value'
]
},
mounted() {
this.getData();
axios.get('/nav')
.then(resp => {
this.nav = resp.data;
})
.catch(err => console.log(err))
},
methods: {
getData: function () {
axios.get('/vars/api')
.then(resp => {
this.vars = resp.data;
})
.catch(err => this.err = err);
}
}
})
</script>
<div class="container">
<table class="table-responsive table-striped">
<thead>
<tr>
<th>Key</th>
<th>Value</th>
</tr>
</thead>
<tbody>
{{range .Items}}
<tr>
<td>{{.Key}}</td><td>{{.Value}}</td>
</tr>
{{else}}
<tr>
<td colspan="2">No data</td>
</tr>
{{end}}
</tbody>
</table>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.1/dist/js/bootstrap.bundle.min.js" integrity="sha384-HwwvtgBNo3bZJJLYd8oVXjrBZt8cqVSpeBNS5n7C8IVInixGAoxmnlMuBnhbgrkm" crossorigin="anonymous"></script>
</body>
</html>

View File

@ -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) {

View File

@ -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)

View File

@ -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)

View File

@ -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
}

View File

@ -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<text>.*)`),
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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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
}

View File

@ -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()

146
plugins/twitch/demo/main.go Normal file
View File

@ -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)
}

1
util/stats/main.go Normal file
View File

@ -0,0 +1 @@
package stats