mirror of https://github.com/velour/catbase.git
Compare commits
99 Commits
09b90cd7c1
...
95ae28aa0f
Author | SHA1 | Date |
---|---|---|
Chris Sexton | 95ae28aa0f | |
Chris Sexton | 25f9171223 | |
Chris Sexton | 07470c5127 | |
Chris Sexton | 61b71362b9 | |
Chris Sexton | 8ba0ec076e | |
Chris Sexton | ec008a8884 | |
Chris Sexton | 0a775b78b7 | |
Chris Sexton | 718ee32165 | |
Chris Sexton | a01e52b4d4 | |
Chris Sexton | bd20d4001b | |
Chris Sexton | 8e2c55f8bd | |
Chris Sexton | 0ce8a21e0e | |
Chris Sexton | 344bcb9f64 | |
Chris Sexton | 41e8241cb5 | |
Chris Sexton | b66038354a | |
Chris Sexton | b20da607bc | |
Chris Sexton | a5e919733c | |
Chris Sexton | 2d06fd6be8 | |
Chris Sexton | b4f9f902ce | |
Chris Sexton | f83cc32788 | |
Chris Sexton | 3e3cc3cf95 | |
Chris Sexton | f6b1712eda | |
Chris Sexton | b8e6e0595d | |
Chris Sexton | e668fbe688 | |
Chris Sexton | 12f4e51ba5 | |
Chris Sexton | 00352a1512 | |
Chris Sexton | c089a80ffc | |
Chris Sexton | 3ff95d3c85 | |
Chris Sexton | 1743b65242 | |
Chris Sexton | 0397fa2897 | |
Chris Sexton | 852239e89d | |
Chris Sexton | 5acf14b0ae | |
Chris Sexton | f8f18acacb | |
Chris Sexton | 1a066ce979 | |
Chris Sexton | 494c9e87d6 | |
Chris Sexton | 448ae768ba | |
Chris Sexton | 0b787a65a1 | |
Chris Sexton | 9c4673fb40 | |
Chris Sexton | 230733f4ff | |
dependabot[bot] | 9bb89d5711 | |
Chris Sexton | c44ada3061 | |
Chris Sexton | f18154be5b | |
Chris Sexton | 9cecccfcdd | |
Chris Sexton | f6cfec477f | |
Chris Sexton | f2153bf0b4 | |
Chris Sexton | 5de82d96e4 | |
Chris Sexton | bfd50a346d | |
Chris Sexton | c32738f444 | |
Chris Sexton | 3dc8c77505 | |
Chris Sexton | c91fdcdf29 | |
Chris Sexton | b63b317dfc | |
Chris Sexton | 21ce66fc15 | |
Chris Sexton | 295d9fef77 | |
Chris Sexton | 6707902caf | |
Chris Sexton | d1986be68a | |
Chris Sexton | 4626d0270c | |
Chris Sexton | 7d5cf3909d | |
Chris Sexton | 8b8ac7b244 | |
Chris Sexton | 92ce4979b4 | |
Chris Sexton | 43eda811eb | |
Chris Sexton | 68738f847b | |
Chris Sexton | eb67d1a35e | |
Chris Sexton | 91d21c1076 | |
Chris Sexton | 690fd01fd2 | |
Chris Sexton | e2c55fab00 | |
Chris Sexton | 0c94d71960 | |
Chris Sexton | 7eba55f236 | |
Chris Sexton | f70eb46c5d | |
Chris Sexton | c171d4ba10 | |
Chris Sexton | 12543c569c | |
Chris Sexton | 3bedaf2ec0 | |
Chris Sexton | 9569acf4b0 | |
Chris Sexton | f6dd52a222 | |
dependabot[bot] | 7b39ebf534 | |
Chris Sexton | ca1be52e2f | |
Chris Sexton | da780a7a92 | |
Chris Sexton | e8ca86d008 | |
Chris Sexton | 04fecf1987 | |
Chris Sexton | bf54f421fe | |
Chris Sexton | c147e65497 | |
Chris Sexton | df9db4e6fd | |
Chris Sexton | 435f45fa7c | |
Chris Sexton | e93b6d07ab | |
Chris Sexton | 866b947f42 | |
dependabot[bot] | 2457d6769e | |
Chris Sexton | 3c6e69a0fc | |
Chris Sexton | a7b2830b46 | |
Chris Sexton | 7ca4a30c14 | |
Chris Sexton | dfa6302757 | |
Chris Sexton | dd262f524e | |
dependabot[bot] | bc710219ed | |
dependabot[bot] | c431d9ccc0 | |
dependabot[bot] | c9981bc5f5 | |
dependabot[bot] | 1852b35150 | |
dependabot[bot] | 11d8c576e4 | |
Chris Sexton | a9a4c9274c | |
Chris Sexton | b670ecc647 | |
Chris Sexton | b8a199faba | |
Chris Sexton | 4617dd84fc |
|
@ -7,10 +7,10 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
|
|
||||||
- name: Set up Go 1.18
|
- name: Set up Go 1.21
|
||||||
uses: actions/setup-go@v1
|
uses: actions/setup-go@v1
|
||||||
with:
|
with:
|
||||||
go-version: 1.18.x
|
go-version: 1.21.x
|
||||||
id: go
|
id: go
|
||||||
|
|
||||||
- name: Check out code into the Go module directory
|
- name: Check out code into the Go module directory
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
name: Go Tidy
|
||||||
|
on: [push]
|
||||||
|
jobs:
|
||||||
|
|
||||||
|
build:
|
||||||
|
name: Go mod tidy check
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: katexochen/go-tidy-check@v1
|
||||||
|
with:
|
||||||
|
# (Optional) The path to the root of each modules, space separated. Default is the current directory.
|
||||||
|
modules: . ./...
|
|
@ -20,7 +20,6 @@ _cgo_export.*
|
||||||
_testmain.go
|
_testmain.go
|
||||||
|
|
||||||
*.exe
|
*.exe
|
||||||
catbase
|
|
||||||
config.json
|
config.json
|
||||||
*.db
|
*.db
|
||||||
vendor
|
vendor
|
||||||
|
@ -77,3 +76,4 @@ impact.ttf
|
||||||
.env
|
.env
|
||||||
rathaus_discord.sh
|
rathaus_discord.sh
|
||||||
emojy
|
emojy
|
||||||
|
catbase
|
||||||
|
|
|
@ -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.
|
0. You just DO WHAT THE FUCK YOU WANT TO.
|
||||||
```
|
```
|
||||||
# c346-34515-fa22-project-rockbottom
|
|
||||||
|
|
62
bot/bot.go
62
bot/bot.go
|
@ -4,10 +4,9 @@ package bot
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/go-chi/chi/v5/middleware"
|
"github.com/velour/catbase/bot/stats"
|
||||||
"github.com/go-chi/httprate"
|
"github.com/velour/catbase/bot/web"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"net/http"
|
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
@ -15,7 +14,6 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/go-chi/chi/v5"
|
|
||||||
"github.com/jmoiron/sqlx"
|
"github.com/jmoiron/sqlx"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"github.com/velour/catbase/bot/history"
|
"github.com/velour/catbase/bot/history"
|
||||||
|
@ -53,8 +51,7 @@ type bot struct {
|
||||||
|
|
||||||
version string
|
version string
|
||||||
|
|
||||||
// The entries to the bot's HTTP interface
|
web *web.Web
|
||||||
httpEndPoints []EndPoint
|
|
||||||
|
|
||||||
// filters registered by plugins
|
// filters registered by plugins
|
||||||
filters map[string]func(string) string
|
filters map[string]func(string) string
|
||||||
|
@ -66,14 +63,8 @@ type bot struct {
|
||||||
|
|
||||||
quiet bool
|
quiet bool
|
||||||
|
|
||||||
router *chi.Mux
|
|
||||||
|
|
||||||
history *history.History
|
history *history.History
|
||||||
}
|
stats *stats.Stats
|
||||||
|
|
||||||
type EndPoint struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
URL string `json:"url"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Variable represents a $var replacement
|
// Variable represents a $var replacement
|
||||||
|
@ -107,11 +98,10 @@ func New(config *config.Config, connector Connector) Bot {
|
||||||
me: users[0],
|
me: users[0],
|
||||||
logIn: logIn,
|
logIn: logIn,
|
||||||
logOut: logOut,
|
logOut: logOut,
|
||||||
httpEndPoints: make([]EndPoint, 0),
|
|
||||||
filters: make(map[string]func(string) string),
|
filters: make(map[string]func(string) string),
|
||||||
callbacks: make(CallbackMap),
|
callbacks: make(CallbackMap),
|
||||||
router: chi.NewRouter(),
|
|
||||||
history: history.New(historySz),
|
history: history.New(historySz),
|
||||||
|
stats: stats.New(),
|
||||||
}
|
}
|
||||||
|
|
||||||
bot.migrateDB()
|
bot.migrateDB()
|
||||||
|
@ -119,55 +109,23 @@ func New(config *config.Config, connector Connector) Bot {
|
||||||
bot.RefreshPluginBlacklist()
|
bot.RefreshPluginBlacklist()
|
||||||
bot.RefreshPluginWhitelist()
|
bot.RefreshPluginWhitelist()
|
||||||
|
|
||||||
log.Debug().Msgf("created web router")
|
bot.web = web.New(bot.config, bot.stats)
|
||||||
|
|
||||||
bot.setupHTTP()
|
|
||||||
|
|
||||||
connector.RegisterEvent(bot.Receive)
|
connector.RegisterEvent(bot.Receive)
|
||||||
|
|
||||||
return bot
|
return bot
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *bot) setupHTTP() {
|
|
||||||
// Make the http logger optional
|
|
||||||
// It has never served a purpose in production and with the emojy page, can make a rather noisy log
|
|
||||||
if b.Config().GetInt("bot.useLogger", 0) == 1 {
|
|
||||||
b.router.Use(middleware.Logger)
|
|
||||||
}
|
|
||||||
|
|
||||||
reqCount := b.Config().GetInt("bot.httprate.requests", 500)
|
|
||||||
reqTime := time.Duration(b.Config().GetInt("bot.httprate.seconds", 5))
|
|
||||||
if reqCount > 0 && reqTime > 0 {
|
|
||||||
b.router.Use(httprate.LimitByIP(reqCount, reqTime*time.Second))
|
|
||||||
}
|
|
||||||
|
|
||||||
b.router.Use(middleware.RequestID)
|
|
||||||
b.router.Use(middleware.Recoverer)
|
|
||||||
b.router.Use(middleware.StripSlashes)
|
|
||||||
|
|
||||||
b.router.HandleFunc("/", b.serveRoot)
|
|
||||||
b.router.HandleFunc("/nav", b.serveNav)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *bot) ListenAndServe() {
|
func (b *bot) ListenAndServe() {
|
||||||
addr := b.config.Get("HttpAddr", "127.0.0.1:1337")
|
addr := b.config.Get("HttpAddr", "127.0.0.1:1337")
|
||||||
stop := make(chan os.Signal, 1)
|
stop := make(chan os.Signal, 1)
|
||||||
signal.Notify(stop, os.Interrupt)
|
signal.Notify(stop, os.Interrupt)
|
||||||
go func() {
|
go func() {
|
||||||
log.Debug().Msgf("starting web service at %s", addr)
|
b.web.ListenAndServe(addr)
|
||||||
log.Fatal().Err(http.ListenAndServe(addr, b.router)).Msg("bot killed")
|
|
||||||
}()
|
}()
|
||||||
<-stop
|
<-stop
|
||||||
b.DefaultConnector().Shutdown()
|
b.DefaultConnector().Shutdown()
|
||||||
}
|
b.Receive(b.DefaultConnector(), Shutdown, msg.Message{})
|
||||||
|
|
||||||
func (b *bot) RegisterWeb(r http.Handler, root string) {
|
|
||||||
b.router.Mount(root, r)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *bot) RegisterWebName(r http.Handler, root, name string) {
|
|
||||||
b.httpEndPoints = append(b.httpEndPoints, EndPoint{name, root})
|
|
||||||
b.router.Mount(root, r)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultConnector is the main connector used for the bot
|
// DefaultConnector is the main connector used for the bot
|
||||||
|
@ -455,3 +413,7 @@ func (b *bot) CheckPassword(secret, password string) bool {
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *bot) GetWeb() *web.Web {
|
||||||
|
return b.web
|
||||||
|
}
|
||||||
|
|
|
@ -25,6 +25,8 @@ func (b *bot) Receive(conn Connector, kind Kind, msg msg.Message, args ...any) b
|
||||||
// msg := b.buildMessage(client, inMsg)
|
// msg := b.buildMessage(client, inMsg)
|
||||||
// do need to look up user and fix it
|
// do need to look up user and fix it
|
||||||
|
|
||||||
|
b.stats.MessagesRcv++
|
||||||
|
|
||||||
if kind == Edit {
|
if kind == Edit {
|
||||||
b.history.Edit(msg.ID, &msg)
|
b.history.Edit(msg.ID, &msg)
|
||||||
} else {
|
} else {
|
||||||
|
@ -88,6 +90,7 @@ func (b *bot) Send(conn Connector, kind Kind, args ...any) (string, error) {
|
||||||
if b.quiet {
|
if b.quiet {
|
||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
|
b.stats.MessagesSent++
|
||||||
return conn.Send(kind, args...)
|
return conn.Send(kind, args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,47 +0,0 @@
|
||||||
<!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="https://unpkg.com/vue-router"></script>
|
|
||||||
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<title>catbase</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<div id="app">
|
|
||||||
<b-navbar>
|
|
||||||
<b-navbar-brand>catbase</b-navbar-brand>
|
|
||||||
<b-navbar-nav>
|
|
||||||
<b-nav-item v-for="item in nav" :href="item.url">{{ item.name }}</b-nav-item>
|
|
||||||
</b-navbar-nav>
|
|
||||||
</b-navbar>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
var app = new Vue({
|
|
||||||
el: '#app',
|
|
||||||
data: {
|
|
||||||
err: '',
|
|
||||||
nav: [],
|
|
||||||
},
|
|
||||||
mounted: function() {
|
|
||||||
axios.get('/nav')
|
|
||||||
.then(resp => {
|
|
||||||
this.nav = resp.data;
|
|
||||||
})
|
|
||||||
.catch(err => console.log(err))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
|
@ -3,8 +3,11 @@
|
||||||
package bot
|
package bot
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/gabriel-vasile/mimetype"
|
||||||
|
"github.com/velour/catbase/bot/web"
|
||||||
"net/http"
|
"net/http"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/jmoiron/sqlx"
|
"github.com/jmoiron/sqlx"
|
||||||
|
|
||||||
|
@ -24,6 +27,8 @@ const (
|
||||||
Reply
|
Reply
|
||||||
// Action any /me action
|
// Action any /me action
|
||||||
Action
|
Action
|
||||||
|
// Spoiler is for commented out messages
|
||||||
|
Spoiler
|
||||||
// Reaction Icon reaction if service supports it
|
// Reaction Icon reaction if service supports it
|
||||||
Reaction
|
Reaction
|
||||||
// Edit message ref'd new message to replace
|
// Edit message ref'd new message to replace
|
||||||
|
@ -38,12 +43,42 @@ const (
|
||||||
Delete
|
Delete
|
||||||
// Startup is triggered after the connector has run the Serve function
|
// Startup is triggered after the connector has run the Serve function
|
||||||
Startup
|
Startup
|
||||||
|
// Shutdown is triggered after an OS interrupt
|
||||||
|
Shutdown
|
||||||
)
|
)
|
||||||
|
|
||||||
type EphemeralID string
|
type EphemeralID string
|
||||||
|
|
||||||
type UnfurlLinks bool
|
type UnfurlLinks bool
|
||||||
|
|
||||||
|
type EmbedAuthor struct {
|
||||||
|
ID string
|
||||||
|
Who string
|
||||||
|
IconURL string
|
||||||
|
}
|
||||||
|
|
||||||
|
type File struct {
|
||||||
|
Description string
|
||||||
|
Data []byte
|
||||||
|
mime *mimetype.MIME
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f File) Mime() *mimetype.MIME {
|
||||||
|
if f.mime == nil {
|
||||||
|
f.mime = mimetype.Detect(f.Data)
|
||||||
|
}
|
||||||
|
return f.mime
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f File) ContentType() string {
|
||||||
|
return f.Mime().String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f File) FileName() string {
|
||||||
|
ext := f.Mime().Extension()
|
||||||
|
return strings.ReplaceAll(f.Description, " ", "-") + ext
|
||||||
|
}
|
||||||
|
|
||||||
type ImageAttachment struct {
|
type ImageAttachment struct {
|
||||||
URL string
|
URL string
|
||||||
AltTxt string
|
AltTxt string
|
||||||
|
@ -134,20 +169,14 @@ type Bot interface {
|
||||||
// RegisterFilter creates a filter function for message processing
|
// RegisterFilter creates a filter function for message processing
|
||||||
RegisterFilter(string, func(string) string)
|
RegisterFilter(string, func(string) string)
|
||||||
|
|
||||||
// RegisterWeb records a web endpoint for the UI
|
|
||||||
RegisterWebName(http.Handler, string, string)
|
|
||||||
|
|
||||||
// RegisterWeb records a web endpoint for the API
|
|
||||||
RegisterWeb(http.Handler, string)
|
|
||||||
|
|
||||||
// Start the HTTP service
|
// Start the HTTP service
|
||||||
ListenAndServe()
|
ListenAndServe()
|
||||||
|
|
||||||
// DefaultConnector returns the base connector, which may not be the only connector
|
// DefaultConnector returns the base connector, which may not be the only connector
|
||||||
DefaultConnector() Connector
|
DefaultConnector() Connector
|
||||||
|
|
||||||
// GetWebNavigation returns the current known web endpoints
|
// GetWeb returns the bot's webserver structure
|
||||||
GetWebNavigation() []EndPoint
|
GetWeb() *web.Web
|
||||||
|
|
||||||
// GetPassword generates a unique password for modification commands on the public website
|
// GetPassword generates a unique password for modification commands on the public website
|
||||||
GetPassword() string
|
GetPassword() string
|
||||||
|
|
12
bot/mock.go
12
bot/mock.go
|
@ -4,6 +4,8 @@ package bot
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/velour/catbase/bot/stats"
|
||||||
|
"github.com/velour/catbase/bot/web"
|
||||||
"net/http"
|
"net/http"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
@ -26,6 +28,8 @@ type MockBot struct {
|
||||||
Messages []string
|
Messages []string
|
||||||
Actions []string
|
Actions []string
|
||||||
Reactions []string
|
Reactions []string
|
||||||
|
|
||||||
|
web *web.Web
|
||||||
}
|
}
|
||||||
|
|
||||||
func (mb *MockBot) Config() *config.Config { return mb.Cfg }
|
func (mb *MockBot) Config() *config.Config { return mb.Cfg }
|
||||||
|
@ -37,6 +41,9 @@ func (mb *MockBot) GetPassword() string { return "12345" }
|
||||||
func (mb *MockBot) SetQuiet(bool) {}
|
func (mb *MockBot) SetQuiet(bool) {}
|
||||||
func (mb *MockBot) Send(c Connector, kind Kind, args ...any) (string, error) {
|
func (mb *MockBot) Send(c Connector, kind Kind, args ...any) (string, error) {
|
||||||
switch kind {
|
switch kind {
|
||||||
|
case Spoiler:
|
||||||
|
mb.Messages = append(mb.Messages, "||"+args[1].(string)+"||")
|
||||||
|
return fmt.Sprintf("m-%d", len(mb.Actions)-1), nil
|
||||||
case Message:
|
case Message:
|
||||||
mb.Messages = append(mb.Messages, args[1].(string))
|
mb.Messages = append(mb.Messages, args[1].(string))
|
||||||
return fmt.Sprintf("m-%d", len(mb.Actions)-1), nil
|
return fmt.Sprintf("m-%d", len(mb.Actions)-1), nil
|
||||||
|
@ -57,9 +64,7 @@ func (mb *MockBot) Register(p Plugin, kind Kind, cb Callback)
|
||||||
func (mb *MockBot) RegisterTable(p Plugin, hs HandlerTable) {}
|
func (mb *MockBot) RegisterTable(p Plugin, hs HandlerTable) {}
|
||||||
func (mb *MockBot) RegisterRegex(p Plugin, kind Kind, r *regexp.Regexp, h ResponseHandler) {}
|
func (mb *MockBot) RegisterRegex(p Plugin, kind Kind, r *regexp.Regexp, h ResponseHandler) {}
|
||||||
func (mb *MockBot) RegisterRegexCmd(p Plugin, kind Kind, r *regexp.Regexp, h ResponseHandler) {}
|
func (mb *MockBot) RegisterRegexCmd(p Plugin, kind Kind, r *regexp.Regexp, h ResponseHandler) {}
|
||||||
func (mb *MockBot) RegisterWebName(_ http.Handler, _, _ string) {}
|
func (mb *MockBot) GetWeb() *web.Web { return mb.web }
|
||||||
func (mb *MockBot) RegisterWeb(_ http.Handler, _ string) {}
|
|
||||||
func (mb *MockBot) GetWebNavigation() []EndPoint { return nil }
|
|
||||||
func (mb *MockBot) Receive(c Connector, kind Kind, msg msg.Message, args ...any) bool {
|
func (mb *MockBot) Receive(c Connector, kind Kind, msg msg.Message, args ...any) bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -115,6 +120,7 @@ func NewMockBot() *MockBot {
|
||||||
Messages: make([]string, 0),
|
Messages: make([]string, 0),
|
||||||
Actions: make([]string, 0),
|
Actions: make([]string, 0),
|
||||||
}
|
}
|
||||||
|
b.web = web.New(cfg, stats.New())
|
||||||
// If any plugin registered a route, we need to reset those before any new test
|
// If any plugin registered a route, we need to reset those before any new test
|
||||||
http.DefaultServeMux = new(http.ServeMux)
|
http.DefaultServeMux = new(http.ServeMux)
|
||||||
return &b
|
return &b
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
package stats
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Stats struct {
|
||||||
|
MessagesSent int
|
||||||
|
MessagesRcv int
|
||||||
|
startTime time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func New() *Stats {
|
||||||
|
return &Stats{startTime: time.Now()}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Stats) Uptime() string {
|
||||||
|
return time.Now().Sub(s.startTime).Truncate(time.Second).String()
|
||||||
|
}
|
42
bot/web.go
42
bot/web.go
|
@ -1,42 +0,0 @@
|
||||||
package bot
|
|
||||||
|
|
||||||
import (
|
|
||||||
"embed"
|
|
||||||
"encoding/json"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
//go:embed *.html
|
|
||||||
var embeddedFS embed.FS
|
|
||||||
|
|
||||||
func (b *bot) serveRoot(w http.ResponseWriter, r *http.Request) {
|
|
||||||
index, _ := embeddedFS.ReadFile("index.html")
|
|
||||||
w.Write(index)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *bot) serveNav(w http.ResponseWriter, r *http.Request) {
|
|
||||||
enc := json.NewEncoder(w)
|
|
||||||
err := enc.Encode(b.GetWebNavigation())
|
|
||||||
if err != nil {
|
|
||||||
jsonErr, _ := json.Marshal(err)
|
|
||||||
w.WriteHeader(500)
|
|
||||||
w.Write(jsonErr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetWebNavigation returns a list of bootstrap-vue <b-nav-item> links
|
|
||||||
// The parent <nav> is not included so each page may display it as
|
|
||||||
// best fits
|
|
||||||
func (b *bot) GetWebNavigation() []EndPoint {
|
|
||||||
endpoints := b.httpEndPoints
|
|
||||||
moreEndpoints := b.config.GetArray("bot.links", []string{})
|
|
||||||
for _, e := range moreEndpoints {
|
|
||||||
link := strings.SplitN(e, ":", 2)
|
|
||||||
if len(link) != 2 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
endpoints = append(endpoints, EndPoint{link[0], link[1]})
|
|
||||||
}
|
|
||||||
return endpoints
|
|
||||||
}
|
|
|
@ -0,0 +1,84 @@
|
||||||
|
package web
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
templ (w *Web) Header(title string) {
|
||||||
|
<head>
|
||||||
|
<link rel="stylesheet" href="//cdn.jsdelivr.net/npm/foundation-sites@6.8.1/dist/css/foundation.min.css" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
if title != "" {
|
||||||
|
<title>{ w.botName() } - { title }</title>
|
||||||
|
} else {
|
||||||
|
<title>{ w.botName() }</title>
|
||||||
|
}
|
||||||
|
</head>
|
||||||
|
}
|
||||||
|
|
||||||
|
templ (w *Web) Footer() {
|
||||||
|
<script src="//unpkg.com/htmx.org@1.9.10" integrity="sha384-D1Kt99CQMDuVetoL1lrYwg5t+9QdHe7NLX/SoJYkXDFfX37iInKRy5xLSi8nO7UC" crossorigin="anonymous"></script>
|
||||||
|
<script src="//cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js"></script>
|
||||||
|
<script src="//cdn.jsdelivr.net/npm/foundation-sites@6.8.1/dist/js/foundation.min.js"></script>
|
||||||
|
}
|
||||||
|
|
||||||
|
templ (w *Web) Index(title string, contents templ.Component) {
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en" class="no-js">
|
||||||
|
@w.Header(title)
|
||||||
|
<body>
|
||||||
|
|
||||||
|
@w.Nav(title)
|
||||||
|
|
||||||
|
if contents != nil {
|
||||||
|
@contents
|
||||||
|
}
|
||||||
|
|
||||||
|
@w.Footer()
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
}
|
||||||
|
|
||||||
|
templ (w *Web) Nav(currentPage string) {
|
||||||
|
|
||||||
|
<div class="top-bar">
|
||||||
|
<div class="top-bar-left">
|
||||||
|
<ul class="menu">
|
||||||
|
<li><a style="color: black; font-weight: bold;" href="/">{ w.botName() }</a></li>
|
||||||
|
for _, item := range w.GetWebNavigation() {
|
||||||
|
<li>
|
||||||
|
if currentPage == item.Name {
|
||||||
|
<a class="is-active" aria-current="page" href={ templ.URL(item.URL) }>{ item.Name }</a>
|
||||||
|
} else {
|
||||||
|
<a href={ templ.URL(item.URL) }>{ item.Name }</a>
|
||||||
|
}
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
templ (w *Web) showStats() {
|
||||||
|
<div class="grid-container">
|
||||||
|
<div class="cell">
|
||||||
|
<h2>Stats</h2>
|
||||||
|
</div>
|
||||||
|
<div class="cell">
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<td>Messages Seen</td>
|
||||||
|
<td>{ fmt.Sprintf("%d", w.stats.MessagesRcv) }</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Messages Sent</td>
|
||||||
|
<td>{ fmt.Sprintf("%d", w.stats.MessagesSent) }</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Uptime</td>
|
||||||
|
<td>{ w.stats.Uptime() }</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
|
@ -0,0 +1,334 @@
|
||||||
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
|
// templ: version: v0.2.543
|
||||||
|
package web
|
||||||
|
|
||||||
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|
||||||
|
import "github.com/a-h/templ"
|
||||||
|
import "context"
|
||||||
|
import "io"
|
||||||
|
import "bytes"
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
func (w *Web) Header(title string) templ.Component {
|
||||||
|
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
templ_7745c5c3_Buffer = templ.GetBuffer()
|
||||||
|
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var1 == nil {
|
||||||
|
templ_7745c5c3_Var1 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<head><link rel=\"stylesheet\" href=\"//cdn.jsdelivr.net/npm/foundation-sites@6.8.1/dist/css/foundation.min.css\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"><meta charset=\"UTF-8\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if title != "" {
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<title>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var2 string
|
||||||
|
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(w.botName())
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `bot/web/index.templ`, Line: 10, Col: 32}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" - ")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var3 string
|
||||||
|
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(title)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `bot/web/index.templ`, Line: 10, Col: 44}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</title>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<title>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var4 string
|
||||||
|
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(w.botName())
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `bot/web/index.templ`, Line: 12, Col: 32}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</title>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</head>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
|
||||||
|
}
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Web) Footer() templ.Component {
|
||||||
|
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
templ_7745c5c3_Buffer = templ.GetBuffer()
|
||||||
|
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var5 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var5 == nil {
|
||||||
|
templ_7745c5c3_Var5 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<script src=\"//unpkg.com/htmx.org@1.9.10\" integrity=\"sha384-D1Kt99CQMDuVetoL1lrYwg5t+9QdHe7NLX/SoJYkXDFfX37iInKRy5xLSi8nO7UC\" crossorigin=\"anonymous\"></script><script src=\"//cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js\"></script><script src=\"//cdn.jsdelivr.net/npm/foundation-sites@6.8.1/dist/js/foundation.min.js\"></script>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
|
||||||
|
}
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Web) Index(title string, contents templ.Component) templ.Component {
|
||||||
|
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
templ_7745c5c3_Buffer = templ.GetBuffer()
|
||||||
|
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var6 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var6 == nil {
|
||||||
|
templ_7745c5c3_Var6 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<!doctype html><html lang=\"en\" class=\"no-js\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = w.Header(title).Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<body>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = w.Nav(title).Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if contents != nil {
|
||||||
|
templ_7745c5c3_Err = contents.Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = w.Footer().Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</body></html>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
|
||||||
|
}
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Web) Nav(currentPage string) templ.Component {
|
||||||
|
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
templ_7745c5c3_Buffer = templ.GetBuffer()
|
||||||
|
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var7 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var7 == nil {
|
||||||
|
templ_7745c5c3_Var7 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"top-bar\"><div class=\"top-bar-left\"><ul class=\"menu\"><li><a style=\"color: black; font-weight: bold;\" href=\"/\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var8 string
|
||||||
|
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(w.botName())
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `bot/web/index.templ`, Line: 45, Col: 86}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</a></li>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
for _, item := range w.GetWebNavigation() {
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<li>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if currentPage == item.Name {
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<a class=\"is-active\" aria-current=\"page\" href=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var9 templ.SafeURL = templ.URL(item.URL)
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var9)))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var10 string
|
||||||
|
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(item.Name)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `bot/web/index.templ`, Line: 49, Col: 109}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</a>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<a href=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var11 templ.SafeURL = templ.URL(item.URL)
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var11)))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var12 string
|
||||||
|
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(item.Name)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `bot/web/index.templ`, Line: 51, Col: 71}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</a>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</li>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</ul></div></div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
|
||||||
|
}
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Web) showStats() templ.Component {
|
||||||
|
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
templ_7745c5c3_Buffer = templ.GetBuffer()
|
||||||
|
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var13 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var13 == nil {
|
||||||
|
templ_7745c5c3_Var13 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"grid-container\"><div class=\"cell\"><h2>Stats</h2></div><div class=\"cell\"><table><tr><td>Messages Seen</td><td>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var14 string
|
||||||
|
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", w.stats.MessagesRcv))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `bot/web/index.templ`, Line: 70, Col: 56}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</td></tr><tr><td>Messages Sent</td><td>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var15 string
|
||||||
|
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", w.stats.MessagesSent))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `bot/web/index.templ`, Line: 74, Col: 57}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</td></tr><tr><td>Uptime</td><td>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var16 string
|
||||||
|
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(w.stats.Uptime())
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `bot/web/index.templ`, Line: 78, Col: 34}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</td></tr></table></div></div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
|
||||||
|
}
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,112 @@
|
||||||
|
package web
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
"github.com/go-chi/chi/v5/middleware"
|
||||||
|
"github.com/go-chi/httprate"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"github.com/velour/catbase/bot/stats"
|
||||||
|
"github.com/velour/catbase/config"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Web struct {
|
||||||
|
config *config.Config
|
||||||
|
router *chi.Mux
|
||||||
|
httpEndPoints []EndPoint
|
||||||
|
stats *stats.Stats
|
||||||
|
}
|
||||||
|
|
||||||
|
type EndPoint struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetWebNavigation returns a list of bootstrap-vue <b-nav-item> links
|
||||||
|
// The parent <nav> is not included so each page may display it as
|
||||||
|
// best fits
|
||||||
|
func (ws *Web) GetWebNavigation() []EndPoint {
|
||||||
|
endpoints := ws.httpEndPoints
|
||||||
|
moreEndpoints := ws.config.GetArray("bot.links", []string{})
|
||||||
|
for _, e := range moreEndpoints {
|
||||||
|
link := strings.SplitN(e, ":", 2)
|
||||||
|
if len(link) != 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
endpoints = append(endpoints, EndPoint{link[0], link[1]})
|
||||||
|
}
|
||||||
|
return endpoints
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ws *Web) serveRoot(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ws.Index("Home", ws.showStats()).Render(r.Context(), w)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ws *Web) serveNavHTML(w http.ResponseWriter, r *http.Request) {
|
||||||
|
currentPage := chi.URLParam(r, "currentPage")
|
||||||
|
ws.Nav(currentPage).Render(r.Context(), w)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ws *Web) serveNav(w http.ResponseWriter, r *http.Request) {
|
||||||
|
enc := json.NewEncoder(w)
|
||||||
|
err := enc.Encode(ws.GetWebNavigation())
|
||||||
|
if err != nil {
|
||||||
|
jsonErr, _ := json.Marshal(err)
|
||||||
|
w.WriteHeader(500)
|
||||||
|
w.Write(jsonErr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ws *Web) 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 ws.config.GetInt("bot.useLogger", 0) == 1 {
|
||||||
|
ws.router.Use(middleware.Logger)
|
||||||
|
}
|
||||||
|
|
||||||
|
reqCount := ws.config.GetInt("bot.httprate.requests", 500)
|
||||||
|
reqTime := time.Duration(ws.config.GetInt("bot.httprate.seconds", 5))
|
||||||
|
if reqCount > 0 && reqTime > 0 {
|
||||||
|
ws.router.Use(httprate.LimitByIP(reqCount, reqTime*time.Second))
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.router.Use(middleware.RequestID)
|
||||||
|
ws.router.Use(middleware.Recoverer)
|
||||||
|
ws.router.Use(middleware.StripSlashes)
|
||||||
|
|
||||||
|
ws.router.HandleFunc("/", ws.serveRoot)
|
||||||
|
ws.router.HandleFunc("/nav", ws.serveNav)
|
||||||
|
ws.router.HandleFunc("/navHTML", ws.serveNavHTML)
|
||||||
|
ws.router.HandleFunc("/navHTML/{currentPage}", ws.serveNavHTML)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ws *Web) RegisterWeb(r http.Handler, root string) {
|
||||||
|
ws.router.Mount(root, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ws *Web) RegisterWebName(r http.Handler, root, name string) {
|
||||||
|
ws.httpEndPoints = append(ws.httpEndPoints, EndPoint{name, root})
|
||||||
|
ws.router.Mount(root, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ws *Web) ListenAndServe(addr string) {
|
||||||
|
log.Debug().Msgf("starting web service at %s", addr)
|
||||||
|
log.Fatal().Err(http.ListenAndServe(addr, ws.router)).Msg("bot killed")
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(config *config.Config, s *stats.Stats) *Web {
|
||||||
|
w := &Web{
|
||||||
|
config: config,
|
||||||
|
router: chi.NewRouter(),
|
||||||
|
stats: s,
|
||||||
|
}
|
||||||
|
w.setupHTTP()
|
||||||
|
return w
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ws *Web) botName() string {
|
||||||
|
return ws.config.Get("nick", "catbase")
|
||||||
|
}
|
|
@ -3,15 +3,13 @@
|
||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"regexp"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
sqlite3 "github.com/mattn/go-sqlite3"
|
_ "github.com/mattn/go-sqlite3"
|
||||||
|
|
||||||
"github.com/jmoiron/sqlx"
|
"github.com/jmoiron/sqlx"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
|
@ -73,6 +71,13 @@ func (c *Config) GetInt(key string, fallback int) int {
|
||||||
return i
|
return i
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetBool returns true or false for config key
|
||||||
|
// It will assume false for any string except "true"
|
||||||
|
func (c *Config) GetBool(key string, fallback bool) bool {
|
||||||
|
val := c.GetString(key, strconv.FormatBool(fallback))
|
||||||
|
return val == "true"
|
||||||
|
}
|
||||||
|
|
||||||
// Get is a shortcut for GetString
|
// Get is a shortcut for GetString
|
||||||
func (c *Config) Get(key, fallback string) string {
|
func (c *Config) Get(key, fallback string) string {
|
||||||
return c.GetString(key, fallback)
|
return c.GetString(key, fallback)
|
||||||
|
@ -101,7 +106,7 @@ func (c *Config) GetString(key, fallback string) string {
|
||||||
q := `select value from config where key=?`
|
q := `select value from config where key=?`
|
||||||
err := c.DB.Get(&configValue, q, key)
|
err := c.DB.Get(&configValue, q, key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Debug().Msgf("WARN: Key %s is empty", key)
|
log.Info().Msgf("WARN: Key %s is empty", key)
|
||||||
return fallback
|
return fallback
|
||||||
}
|
}
|
||||||
return configValue
|
return configValue
|
||||||
|
@ -240,18 +245,6 @@ func (c *Config) SetArray(key string, values []string) error {
|
||||||
return c.Set(key, vals)
|
return c.Set(key, vals)
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
|
||||||
regex := func(re, s string) (bool, error) {
|
|
||||||
return regexp.MatchString(re, s)
|
|
||||||
}
|
|
||||||
sql.Register("sqlite3_custom",
|
|
||||||
&sqlite3.SQLiteDriver{
|
|
||||||
ConnectHook: func(conn *sqlite3.SQLiteConn) error {
|
|
||||||
return conn.RegisterFunc("REGEXP", regex, true)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Readconfig loads the config data out of a JSON file located in cfile
|
// Readconfig loads the config data out of a JSON file located in cfile
|
||||||
func ReadConfig(dbpath string) *Config {
|
func ReadConfig(dbpath string) *Config {
|
||||||
if dbpath == "" {
|
if dbpath == "" {
|
||||||
|
@ -259,7 +252,7 @@ func ReadConfig(dbpath string) *Config {
|
||||||
}
|
}
|
||||||
log.Info().Msgf("Using %s as database file.\n", dbpath)
|
log.Info().Msgf("Using %s as database file.\n", dbpath)
|
||||||
|
|
||||||
sqlDB, err := sqlx.Open("sqlite3_custom", dbpath)
|
sqlDB, err := sqlx.Open("sqlite3", dbpath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal().Err(err)
|
log.Fatal().Err(err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package discord
|
package discord
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
@ -30,6 +31,8 @@ type Discord struct {
|
||||||
|
|
||||||
registeredCmds []*discordgo.ApplicationCommand
|
registeredCmds []*discordgo.ApplicationCommand
|
||||||
cmdHandlers map[string]CmdHandler
|
cmdHandlers map[string]CmdHandler
|
||||||
|
|
||||||
|
guildID string
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(config *config.Config) *Discord {
|
func New(config *config.Config) *Discord {
|
||||||
|
@ -37,12 +40,17 @@ func New(config *config.Config) *Discord {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal().Err(err).Msg("Could not connect to Discord")
|
log.Fatal().Err(err).Msg("Could not connect to Discord")
|
||||||
}
|
}
|
||||||
|
guildID := config.Get("discord.guildid", "")
|
||||||
|
if guildID == "" {
|
||||||
|
log.Fatal().Msgf("You must set either DISCORD_GUILDID env or discord.guildid db config")
|
||||||
|
}
|
||||||
d := &Discord{
|
d := &Discord{
|
||||||
config: config,
|
config: config,
|
||||||
client: client,
|
client: client,
|
||||||
uidCache: map[string]string{},
|
uidCache: map[string]string{},
|
||||||
registeredCmds: []*discordgo.ApplicationCommand{},
|
registeredCmds: []*discordgo.ApplicationCommand{},
|
||||||
cmdHandlers: map[string]CmdHandler{},
|
cmdHandlers: map[string]CmdHandler{},
|
||||||
|
guildID: guildID,
|
||||||
}
|
}
|
||||||
d.client.AddHandler(func(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
d.client.AddHandler(func(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
||||||
if h, ok := d.cmdHandlers[i.ApplicationCommandData().Name]; ok {
|
if h, ok := d.cmdHandlers[i.ApplicationCommandData().Name]; ok {
|
||||||
|
@ -66,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...)
|
return d.sendMessage(args[0].(string), args[2].(string), false, args...)
|
||||||
case bot.Message:
|
case bot.Message:
|
||||||
return d.sendMessage(args[0].(string), args[1].(string), false, args...)
|
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:
|
case bot.Action:
|
||||||
return d.sendMessage(args[0].(string), args[1].(string), true, args...)
|
return d.sendMessage(args[0].(string), args[1].(string), true, args...)
|
||||||
case bot.Edit:
|
case bot.Edit:
|
||||||
|
@ -103,33 +114,67 @@ func (d *Discord) sendMessage(channel, message string, meMessage bool, args ...a
|
||||||
message = "_" + message + "_"
|
message = "_" + message + "_"
|
||||||
}
|
}
|
||||||
|
|
||||||
var embeds *discordgo.MessageEmbed
|
embeds := []*discordgo.MessageEmbed{}
|
||||||
|
files := []*discordgo.File{}
|
||||||
|
|
||||||
for _, arg := range args {
|
for _, arg := range args {
|
||||||
switch a := arg.(type) {
|
switch a := arg.(type) {
|
||||||
|
case bot.EmbedAuthor:
|
||||||
|
embed := &discordgo.MessageEmbed{}
|
||||||
|
embed.Author = &discordgo.MessageEmbedAuthor{
|
||||||
|
Name: a.Who,
|
||||||
|
IconURL: a.IconURL,
|
||||||
|
}
|
||||||
|
embeds = append(embeds, embed)
|
||||||
case bot.ImageAttachment:
|
case bot.ImageAttachment:
|
||||||
//embeds.URL = a.URL
|
embed := &discordgo.MessageEmbed{}
|
||||||
embeds = &discordgo.MessageEmbed{}
|
embed.Description = a.AltTxt
|
||||||
embeds.Description = a.AltTxt
|
embed.Image = &discordgo.MessageEmbedImage{
|
||||||
embeds.Image = &discordgo.MessageEmbedImage{
|
|
||||||
URL: a.URL,
|
URL: a.URL,
|
||||||
Width: a.Width,
|
Width: a.Width,
|
||||||
Height: a.Height,
|
Height: a.Height,
|
||||||
}
|
}
|
||||||
|
embeds = append(embeds, embed)
|
||||||
|
case bot.File:
|
||||||
|
files = append(files, &discordgo.File{
|
||||||
|
Name: a.FileName(),
|
||||||
|
ContentType: a.ContentType(),
|
||||||
|
Reader: bytes.NewBuffer(a.Data),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data := &discordgo.MessageSend{
|
data := &discordgo.MessageSend{
|
||||||
Content: message,
|
Content: message,
|
||||||
Embed: embeds,
|
Embeds: embeds,
|
||||||
|
Files: files,
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debug().
|
log.Debug().
|
||||||
Interface("data", data).
|
Interface("data", data).
|
||||||
Interface("args", args).
|
|
||||||
Msg("sending message")
|
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)
|
//st, err := d.client.ChannelMessageSend(channel, message)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -148,12 +193,7 @@ func (d *Discord) GetEmojiList(force bool) map[string]string {
|
||||||
if d.emojiCache != nil && !force {
|
if d.emojiCache != nil && !force {
|
||||||
return d.emojiCache
|
return d.emojiCache
|
||||||
}
|
}
|
||||||
guildID := d.config.Get("discord.guildid", "")
|
e, err := d.client.GuildEmojis(d.guildID)
|
||||||
if guildID == "" {
|
|
||||||
log.Error().Msg("no guild ID set")
|
|
||||||
return map[string]string{}
|
|
||||||
}
|
|
||||||
e, err := d.client.GuildEmojis(guildID)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("could not retrieve emojis")
|
log.Error().Err(err).Msg("could not retrieve emojis")
|
||||||
return map[string]string{}
|
return map[string]string{}
|
||||||
|
@ -213,16 +253,11 @@ func (d *Discord) convertUser(u *discordgo.User) *user.User {
|
||||||
}
|
}
|
||||||
nick := u.Username
|
nick := u.Username
|
||||||
|
|
||||||
guildID := d.config.Get("discord.guildid", "")
|
mem, err := d.client.GuildMember(d.guildID, u.ID)
|
||||||
if guildID == "" {
|
if err != nil {
|
||||||
log.Error().Msg("no guild ID set")
|
log.Error().Err(err).Msg("could not get guild member")
|
||||||
} else {
|
} else if mem.Nick != "" {
|
||||||
mem, err := d.client.GuildMember(guildID, u.ID)
|
nick = mem.Nick
|
||||||
if err != nil {
|
|
||||||
log.Error().Err(err).Msg("could not get guild member")
|
|
||||||
} else if mem.Nick != "" {
|
|
||||||
nick = mem.Nick
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return &user.User{
|
return &user.User{
|
||||||
|
@ -320,9 +355,10 @@ func (d *Discord) Emojy(name string) string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Discord) UploadEmojy(emojy, path string) error {
|
func (d *Discord) UploadEmojy(emojy, path string) error {
|
||||||
guildID := d.config.Get("discord.guildid", "")
|
|
||||||
defaultRoles := d.config.GetArray("discord.emojyRoles", []string{})
|
defaultRoles := d.config.GetArray("discord.emojyRoles", []string{})
|
||||||
_, err := d.client.GuildEmojiCreate(guildID, emojy, path, defaultRoles)
|
_, err := d.client.GuildEmojiCreate(d.guildID, &discordgo.EmojiParams{
|
||||||
|
emojy, path, defaultRoles,
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -330,9 +366,8 @@ func (d *Discord) UploadEmojy(emojy, path string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Discord) DeleteEmojy(emojy string) error {
|
func (d *Discord) DeleteEmojy(emojy string) error {
|
||||||
guildID := d.config.Get("discord.guildid", "")
|
|
||||||
emojyID := d.GetEmojySnowflake(emojy)
|
emojyID := d.GetEmojySnowflake(emojy)
|
||||||
return d.client.GuildEmojiDelete(guildID, emojyID)
|
return d.client.GuildEmojiDelete(d.guildID, emojyID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Discord) URLFormat(title, url string) string {
|
func (d *Discord) URLFormat(title, url string) string {
|
||||||
|
@ -341,11 +376,7 @@ func (d *Discord) URLFormat(title, url string) string {
|
||||||
|
|
||||||
// GetChannelName returns the channel ID for a human-friendly name (if possible)
|
// GetChannelName returns the channel ID for a human-friendly name (if possible)
|
||||||
func (d *Discord) GetChannelID(name string) string {
|
func (d *Discord) GetChannelID(name string) string {
|
||||||
guildID := d.config.Get("discord.guildid", "")
|
chs, err := d.client.GuildChannels(d.guildID)
|
||||||
if guildID == "" {
|
|
||||||
return name
|
|
||||||
}
|
|
||||||
chs, err := d.client.GuildChannels(guildID)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return name
|
return name
|
||||||
}
|
}
|
||||||
|
@ -369,11 +400,7 @@ func (d *Discord) GetChannelName(id string) string {
|
||||||
func (d *Discord) GetRoles() ([]bot.Role, error) {
|
func (d *Discord) GetRoles() ([]bot.Role, error) {
|
||||||
ret := []bot.Role{}
|
ret := []bot.Role{}
|
||||||
|
|
||||||
guildID := d.config.Get("discord.guildid", "")
|
roles, err := d.client.GuildRoles(d.guildID)
|
||||||
if guildID == "" {
|
|
||||||
return nil, errors.New("no guildID set")
|
|
||||||
}
|
|
||||||
roles, err := d.client.GuildRoles(guildID)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -389,24 +416,25 @@ func (d *Discord) GetRoles() ([]bot.Role, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Discord) SetRole(userID, roleID string) error {
|
func (d *Discord) SetRole(userID, roleID string) error {
|
||||||
guildID := d.config.Get("discord.guildid", "")
|
member, err := d.client.GuildMember(d.guildID, userID)
|
||||||
member, err := d.client.GuildMember(guildID, userID)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for _, r := range member.Roles {
|
for _, r := range member.Roles {
|
||||||
if r == roleID {
|
if r == roleID {
|
||||||
return d.client.GuildMemberRoleRemove(guildID, userID, roleID)
|
return d.client.GuildMemberRoleRemove(d.guildID, userID, roleID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return d.client.GuildMemberRoleAdd(guildID, userID, roleID)
|
return d.client.GuildMemberRoleAdd(d.guildID, userID, roleID)
|
||||||
}
|
}
|
||||||
|
|
||||||
type CmdHandler func(s *discordgo.Session, i *discordgo.InteractionCreate)
|
type CmdHandler func(s *discordgo.Session, i *discordgo.InteractionCreate)
|
||||||
|
|
||||||
func (d *Discord) RegisterSlashCmd(c discordgo.ApplicationCommand, handler CmdHandler) error {
|
func (d *Discord) RegisterSlashCmd(c discordgo.ApplicationCommand, handler CmdHandler) error {
|
||||||
guildID := d.config.Get("discord.guildid", "")
|
if !d.config.GetBool("registerSlash", true) {
|
||||||
cmd, err := d.client.ApplicationCommandCreate(d.client.State.User.ID, guildID, &c)
|
return nil
|
||||||
|
}
|
||||||
|
cmd, err := d.client.ApplicationCommandCreate(d.client.State.User.ID, d.guildID, &c)
|
||||||
d.cmdHandlers[c.Name] = handler
|
d.cmdHandlers[c.Name] = handler
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -417,17 +445,15 @@ func (d *Discord) RegisterSlashCmd(c discordgo.ApplicationCommand, handler CmdHa
|
||||||
|
|
||||||
func (d *Discord) Shutdown() {
|
func (d *Discord) Shutdown() {
|
||||||
log.Debug().Msgf("Shutting down and deleting %d slash commands", len(d.registeredCmds))
|
log.Debug().Msgf("Shutting down and deleting %d slash commands", len(d.registeredCmds))
|
||||||
guildID := d.config.Get("discord.guildid", "")
|
|
||||||
for _, c := range d.registeredCmds {
|
for _, c := range d.registeredCmds {
|
||||||
if err := d.client.ApplicationCommandDelete(d.client.State.User.ID, guildID, c.ID); err != nil {
|
if err := d.client.ApplicationCommandDelete(d.client.State.User.ID, d.guildID, c.ID); err != nil {
|
||||||
log.Error().Err(err).Msgf("could not delete command %s", c.Name)
|
log.Error().Err(err).Msgf("could not delete command %s", c.Name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Discord) Nick(nick string) error {
|
func (d *Discord) Nick(nick string) error {
|
||||||
guildID := d.config.Get("discord.guildid", "")
|
return d.client.GuildMemberNickname(d.guildID, "@me", nick)
|
||||||
return d.client.GuildMemberNickname(guildID, "@me", nick)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Discord) Topic(channelID string) (string, error) {
|
func (d *Discord) Topic(channelID string) (string, error) {
|
||||||
|
@ -445,3 +471,34 @@ func (d *Discord) SetTopic(channelID, topic string) error {
|
||||||
_, err := d.client.ChannelEditComplex(channelID, ce)
|
_, err := d.client.ChannelEditComplex(channelID, ce)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ThreadStart struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
AutoArchiveDuration int `json:"auto_archive_duration,omitempty"`
|
||||||
|
RateLimitPerUser int `json:"rate_limit_per_user,omitempty"`
|
||||||
|
AppliedTags []string `json:"applied_tags,omitempty"`
|
||||||
|
Message ForumMessageData `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ForumMessageData struct {
|
||||||
|
Content string `json:"content"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Discord) CreateRoom(name, message, parent string, duration int) (string, error) {
|
||||||
|
data := &ThreadStart{
|
||||||
|
Name: name,
|
||||||
|
AutoArchiveDuration: duration,
|
||||||
|
Message: ForumMessageData{message},
|
||||||
|
}
|
||||||
|
ch := &discordgo.Channel{}
|
||||||
|
endpoint := discordgo.EndpointChannelThreads(parent)
|
||||||
|
body, err := d.client.RequestWithBucketID("POST", endpoint, data, endpoint)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = discordgo.Unmarshal(body, &ch); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return ch.ID, nil
|
||||||
|
}
|
||||||
|
|
84
go.mod
84
go.mod
|
@ -3,90 +3,116 @@ module github.com/velour/catbase
|
||||||
go 1.18
|
go 1.18
|
||||||
|
|
||||||
require (
|
require (
|
||||||
code.chrissexton.org/cws/getaoc v0.0.0-20191201043947-d5417d4b618d
|
code.chrissexton.org/cws/getaoc v0.0.0-20231202052842-1b2a337b799d
|
||||||
github.com/ChimeraCoder/anaconda v2.0.0+incompatible
|
github.com/ChimeraCoder/anaconda v2.0.0+incompatible
|
||||||
github.com/PuerkitoBio/goquery v1.8.0
|
github.com/PuerkitoBio/goquery v1.8.1
|
||||||
github.com/bwmarrin/discordgo v0.25.0
|
github.com/a-h/templ v0.2.543
|
||||||
|
github.com/andrewstuart/openai v0.8.0
|
||||||
|
github.com/bwmarrin/discordgo v0.26.1
|
||||||
github.com/cdipaolo/goml v0.0.0-20190412180403-e1f51f713598
|
github.com/cdipaolo/goml v0.0.0-20190412180403-e1f51f713598
|
||||||
|
github.com/cenkalti/backoff/v4 v4.2.1
|
||||||
github.com/chrissexton/leftpad v0.0.0-20181207133115-1e93189d2fff
|
github.com/chrissexton/leftpad v0.0.0-20181207133115-1e93189d2fff
|
||||||
github.com/chrissexton/sentiment v0.0.0-20190927141846-d69c422ba035
|
github.com/chrissexton/sentiment v0.0.0-20190927141846-d69c422ba035
|
||||||
|
github.com/disintegration/imageorient v0.0.0-20180920195336-8147d86e83ec
|
||||||
github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90
|
github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90
|
||||||
github.com/forPelevin/gomoji v1.1.4
|
github.com/forPelevin/gomoji v1.1.6
|
||||||
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/chi/v5 v5.0.7
|
||||||
github.com/go-chi/httprate v0.5.3
|
github.com/go-chi/httprate v0.7.0
|
||||||
github.com/gocolly/colly v1.2.0
|
github.com/gocolly/colly v1.2.0
|
||||||
github.com/google/uuid v1.3.0
|
github.com/google/uuid v1.3.0
|
||||||
github.com/itchyny/gojq v0.12.8
|
github.com/itchyny/gojq v0.12.9
|
||||||
github.com/james-bowman/nlp v0.0.0-20191016091239-d9dbfaff30c6
|
github.com/james-bowman/nlp v0.0.0-20191016091239-d9dbfaff30c6
|
||||||
github.com/jmoiron/sqlx v1.3.5
|
github.com/jmoiron/sqlx v1.3.5
|
||||||
github.com/kevinburke/twilio-go v0.0.0-20200424172635-4f0b2357b852
|
github.com/kevinburke/twilio-go v0.0.0-20200424172635-4f0b2357b852
|
||||||
github.com/mattn/go-sqlite3 v2.0.3+incompatible
|
github.com/mattn/go-sqlite3 v2.0.3+incompatible
|
||||||
github.com/mmcdole/gofeed v1.1.3
|
github.com/mmcdole/gofeed v1.1.3
|
||||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646
|
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646
|
||||||
|
github.com/nicklaw5/helix v1.25.0
|
||||||
github.com/olebedev/when v0.0.0-20190311101825-c3b538a97254
|
github.com/olebedev/when v0.0.0-20190311101825-c3b538a97254
|
||||||
github.com/rs/zerolog v1.27.0
|
github.com/rs/zerolog v1.28.0
|
||||||
github.com/slack-go/slack v0.11.0
|
github.com/slack-go/slack v0.11.3
|
||||||
github.com/stretchr/testify v1.8.0
|
github.com/stretchr/testify v1.8.2
|
||||||
github.com/trubitsyn/go-zero-width v1.0.1
|
github.com/trubitsyn/go-zero-width v1.0.1
|
||||||
github.com/velour/velour v0.0.0-20160303155839-8e090e68d158
|
github.com/velour/velour v0.0.0-20160303155839-8e090e68d158
|
||||||
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e
|
golang.org/x/crypto v0.14.0
|
||||||
gopkg.in/go-playground/webhooks.v5 v5.17.0
|
gopkg.in/go-playground/webhooks.v5 v5.17.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
git.stuart.fun/andrew/rester/v2 v2.2.0 // indirect
|
||||||
github.com/AlekSi/pointer v1.1.0 // indirect
|
github.com/AlekSi/pointer v1.1.0 // indirect
|
||||||
github.com/ChimeraCoder/tokenbucket v0.0.0-20131201223612-c5a927568de7 // indirect
|
github.com/ChimeraCoder/tokenbucket v0.0.0-20131201223612-c5a927568de7 // indirect
|
||||||
github.com/andybalholm/cascadia v1.3.1 // indirect
|
github.com/andybalholm/cascadia v1.3.1 // indirect
|
||||||
github.com/antchfx/htmlquery v1.2.0 // indirect
|
github.com/antchfx/htmlquery v1.2.0 // indirect
|
||||||
github.com/antchfx/xmlquery v1.2.0 // indirect
|
github.com/antchfx/xmlquery v1.3.1 // indirect
|
||||||
github.com/antchfx/xpath v1.1.1 // indirect
|
github.com/antchfx/xpath v1.1.10 // indirect
|
||||||
github.com/armon/go-radix v1.0.0 // indirect
|
github.com/armon/go-radix v1.0.0 // indirect
|
||||||
github.com/azr/backoff v0.0.0-20160115115103-53511d3c7330 // indirect
|
github.com/azr/backoff v0.0.0-20160115115103-53511d3c7330 // indirect
|
||||||
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
|
github.com/cenkalti/backoff v2.2.1+incompatible // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/disintegration/gift v1.1.2 // indirect
|
||||||
github.com/dustin/go-jsonpointer v0.0.0-20160814072949-ba0abeacc3dc // indirect
|
github.com/dustin/go-jsonpointer v0.0.0-20160814072949-ba0abeacc3dc // indirect
|
||||||
github.com/dustin/gojson v0.0.0-20160307161227-2e71ec9dd5ad // indirect
|
github.com/dustin/gojson v0.0.0-20160307161227-2e71ec9dd5ad // indirect
|
||||||
github.com/garyburd/go-oauth v0.0.0-20180319155456-bca2e7f09a17 // indirect
|
github.com/garyburd/go-oauth v0.0.0-20180319155456-bca2e7f09a17 // indirect
|
||||||
github.com/go-stack/stack v1.8.0 // indirect
|
github.com/go-stack/stack v1.8.0 // indirect
|
||||||
github.com/gobwas/glob v0.2.3 // indirect
|
github.com/gobwas/glob v0.2.3 // indirect
|
||||||
|
github.com/golang-jwt/jwt v3.2.1+incompatible // indirect
|
||||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
|
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
|
||||||
github.com/golang/groupcache v0.0.0-20191027212112-611e8accdfc9 // indirect
|
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect
|
||||||
github.com/golang/protobuf v1.3.1 // indirect
|
github.com/golang/protobuf v1.5.2 // indirect
|
||||||
github.com/gonum/floats v0.0.0-20181209220543-c233463c7e82 // indirect
|
github.com/gonum/floats v0.0.0-20181209220543-c233463c7e82 // indirect
|
||||||
github.com/gonum/internal v0.0.0-20181124074243-f884aa714029 // indirect
|
github.com/gonum/internal v0.0.0-20181124074243-f884aa714029 // indirect
|
||||||
github.com/gorilla/websocket v1.5.0 // indirect
|
github.com/gorilla/websocket v1.5.0 // indirect
|
||||||
|
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect
|
||||||
|
github.com/hashicorp/errwrap v1.0.0 // indirect
|
||||||
|
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||||
|
github.com/improbable-eng/go-httpwares v0.0.0-20200609095714-edc8019f93cc // indirect
|
||||||
github.com/inconshreveable/log15 v0.0.0-20200109203555-b30bc20e4fd1 // indirect
|
github.com/inconshreveable/log15 v0.0.0-20200109203555-b30bc20e4fd1 // indirect
|
||||||
github.com/itchyny/timefmt-go v0.1.3 // indirect
|
github.com/itchyny/timefmt-go v0.1.4 // indirect
|
||||||
github.com/james-bowman/sparse v0.0.0-20190423065201-80c6877364c7 // indirect
|
github.com/james-bowman/sparse v0.0.0-20190423065201-80c6877364c7 // indirect
|
||||||
github.com/json-iterator/go v1.1.10 // indirect
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
github.com/kennygrant/sanitize v1.2.4 // indirect
|
github.com/kennygrant/sanitize v1.2.4 // indirect
|
||||||
github.com/kevinburke/go-types v0.0.0-20200309064045-f2d4aea18a7a // indirect
|
github.com/kevinburke/go-types v0.0.0-20200309064045-f2d4aea18a7a // indirect
|
||||||
github.com/kevinburke/go.uuid v1.2.0 // indirect
|
github.com/kevinburke/go.uuid v1.2.0 // indirect
|
||||||
github.com/kevinburke/rest v0.0.0-20200429221318-0d2892b400f8 // indirect
|
github.com/kevinburke/rest v0.0.0-20200429221318-0d2892b400f8 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.12 // indirect
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.14 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
|
||||||
github.com/mmcdole/goxpp v0.0.0-20181012175147-0068e33feabf // indirect
|
github.com/mmcdole/goxpp v0.0.0-20181012175147-0068e33feabf // indirect
|
||||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
|
github.com/opentracing/opentracing-go v1.2.0 // indirect
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/rivo/uniseg v0.2.0 // indirect
|
github.com/prometheus/client_golang v1.14.0 // indirect
|
||||||
|
github.com/prometheus/client_model v0.3.0 // indirect
|
||||||
|
github.com/prometheus/common v0.37.0 // indirect
|
||||||
|
github.com/prometheus/procfs v0.8.0 // indirect
|
||||||
|
github.com/rivo/uniseg v0.3.4 // indirect
|
||||||
github.com/robertkrimen/otto v0.0.0-20180617131154-15f95af6e78d // indirect
|
github.com/robertkrimen/otto v0.0.0-20180617131154-15f95af6e78d // indirect
|
||||||
github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca // indirect
|
github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca // indirect
|
||||||
|
github.com/sirupsen/logrus v1.9.0 // indirect
|
||||||
github.com/spaolacci/murmur3 v1.1.0 // indirect
|
github.com/spaolacci/murmur3 v1.1.0 // indirect
|
||||||
github.com/stretchr/objx v0.4.0 // indirect
|
github.com/stretchr/objx v0.5.0 // indirect
|
||||||
github.com/temoto/robotstxt v1.1.1 // indirect
|
github.com/temoto/robotstxt v1.1.1 // indirect
|
||||||
github.com/ttacon/builder v0.0.0-20170518171403-c099f663e1c2 // indirect
|
github.com/ttacon/builder v0.0.0-20170518171403-c099f663e1c2 // indirect
|
||||||
github.com/ttacon/libphonenumber v1.1.0 // indirect
|
github.com/ttacon/libphonenumber v1.1.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20191014171548-69215a2ee97e // indirect
|
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6 // indirect
|
||||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b // indirect
|
golang.org/x/image v0.0.0-20190802002840-cff245a6509b // indirect
|
||||||
golang.org/x/net v0.0.0-20211118161319-6a13c67c3ce4 // indirect
|
golang.org/x/net v0.17.0 // indirect
|
||||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 // indirect
|
||||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect
|
golang.org/x/sys v0.14.0 // indirect
|
||||||
golang.org/x/text v0.3.7 // indirect
|
golang.org/x/text v0.13.0 // indirect
|
||||||
gonum.org/v1/gonum v0.6.0 // indirect
|
gonum.org/v1/gonum v0.6.0 // indirect
|
||||||
google.golang.org/appengine v1.6.5 // indirect
|
google.golang.org/appengine v1.6.7 // indirect
|
||||||
|
google.golang.org/genproto v0.0.0-20221227171554-f9683d7f8bef // indirect
|
||||||
|
google.golang.org/grpc v1.52.0 // indirect
|
||||||
|
google.golang.org/protobuf v1.28.1 // indirect
|
||||||
gopkg.in/sourcemap.v1 v1.0.5 // indirect
|
gopkg.in/sourcemap.v1 v1.0.5 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
|
replace github.com/mattn/go-sqlite3 => github.com/leso-kn/go-sqlite3 v0.0.0-20230710125852-03158dc838ed
|
||||||
|
|
602
go.sum
602
go.sum
|
@ -1,6 +1,40 @@
|
||||||
code.chrissexton.org/cws/getaoc v0.0.0-20191201043947-d5417d4b618d h1:XS13tP+cMAvXYHQiYqcst64wQ854pueMRZSU4+6puU4=
|
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||||
code.chrissexton.org/cws/getaoc v0.0.0-20191201043947-d5417d4b618d/go.mod h1:rEpfJR9MplF2TUj2Oy+u4XAaLve2kwB8I2zlzeIQxl8=
|
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||||
|
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
|
||||||
|
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
|
||||||
|
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
|
||||||
|
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
|
||||||
|
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
|
||||||
|
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
|
||||||
|
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
|
||||||
|
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
|
||||||
|
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
|
||||||
|
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
|
||||||
|
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
|
||||||
|
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
|
||||||
|
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
|
||||||
|
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
|
||||||
|
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
|
||||||
|
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
|
||||||
|
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
|
||||||
|
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
|
||||||
|
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
|
||||||
|
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
|
||||||
|
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
|
||||||
|
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
|
||||||
|
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
|
||||||
|
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
|
||||||
|
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
|
||||||
|
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
|
||||||
|
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
|
||||||
|
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
|
||||||
|
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
|
||||||
|
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
|
||||||
|
code.chrissexton.org/cws/getaoc v0.0.0-20231202052842-1b2a337b799d h1:s2OEp4YDwfdKVZVkt2hrN/tZlbpHtC2GPNRYWgtqMDw=
|
||||||
|
code.chrissexton.org/cws/getaoc v0.0.0-20231202052842-1b2a337b799d/go.mod h1:rEpfJR9MplF2TUj2Oy+u4XAaLve2kwB8I2zlzeIQxl8=
|
||||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||||
|
git.stuart.fun/andrew/rester/v2 v2.2.0 h1:h8VSZC1MKmYQdpbpbDRYg5HrPaqyW5lbU69C+433BAs=
|
||||||
|
git.stuart.fun/andrew/rester/v2 v2.2.0/go.mod h1:Uc4e4vUP/Y7bSmTE4U+eSqbREaoudpKOrHyhcSGWeHk=
|
||||||
github.com/AlekSi/pointer v1.0.0/go.mod h1:1kjywbfcPFCmncIxtk6fIEub6LKrfMz3gc5QKVOSOA8=
|
github.com/AlekSi/pointer v1.0.0/go.mod h1:1kjywbfcPFCmncIxtk6fIEub6LKrfMz3gc5QKVOSOA8=
|
||||||
github.com/AlekSi/pointer v1.1.0 h1:SSDMPcXD9jSl8FPy9cRzoRaMJtm9g9ggGTxecRUbQoI=
|
github.com/AlekSi/pointer v1.1.0 h1:SSDMPcXD9jSl8FPy9cRzoRaMJtm9g9ggGTxecRUbQoI=
|
||||||
github.com/AlekSi/pointer v1.1.0/go.mod h1:y7BvfRI3wXPWKXEBhU71nbnIEEZX0QTSB2Bj48UJIZE=
|
github.com/AlekSi/pointer v1.1.0/go.mod h1:y7BvfRI3wXPWKXEBhU71nbnIEEZX0QTSB2Bj48UJIZE=
|
||||||
|
@ -11,54 +45,97 @@ github.com/ChimeraCoder/anaconda v2.0.0+incompatible/go.mod h1:TCt3MijIq3Qqo9SBt
|
||||||
github.com/ChimeraCoder/tokenbucket v0.0.0-20131201223612-c5a927568de7 h1:r+EmXjfPosKO4wfiMLe1XQictsIlhErTufbWUsjOTZs=
|
github.com/ChimeraCoder/tokenbucket v0.0.0-20131201223612-c5a927568de7 h1:r+EmXjfPosKO4wfiMLe1XQictsIlhErTufbWUsjOTZs=
|
||||||
github.com/ChimeraCoder/tokenbucket v0.0.0-20131201223612-c5a927568de7/go.mod h1:b2EuEMLSG9q3bZ95ql1+8oVqzzrTNSiOQqSXWFBzxeI=
|
github.com/ChimeraCoder/tokenbucket v0.0.0-20131201223612-c5a927568de7/go.mod h1:b2EuEMLSG9q3bZ95ql1+8oVqzzrTNSiOQqSXWFBzxeI=
|
||||||
github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
|
github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
|
||||||
github.com/PuerkitoBio/goquery v1.8.0 h1:PJTF7AmFCFKk1N6V6jmKfrNH9tV5pNE6lZMkG0gta/U=
|
github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM=
|
||||||
github.com/PuerkitoBio/goquery v1.8.0/go.mod h1:ypIiRMtY7COPGk+I/YbZLbxsxn9g5ejnI2HSMtkjZvI=
|
github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ=
|
||||||
|
github.com/a-h/templ v0.2.543 h1:8YyLvyUtf0/IE2nIwZ62Z/m2o2NqwhnMynzOL78Lzbk=
|
||||||
|
github.com/a-h/templ v0.2.543/go.mod h1:jP908DQCwI08IrnTalhzSEH9WJqG/Q94+EODQcJGFUA=
|
||||||
github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw=
|
github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw=
|
||||||
|
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||||
|
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||||
|
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||||
|
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||||
|
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
|
||||||
|
github.com/andrewstuart/openai v0.8.0 h1:pN5MwD/2+gQ3y89fquU/z/rHCUx9+AP5b8BYypcFkF8=
|
||||||
|
github.com/andrewstuart/openai v0.8.0/go.mod h1:Gi+pjULfqujtYCtMPhbN4bWZItytFEhiZUroAxq2IPg=
|
||||||
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
|
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
|
||||||
github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c=
|
github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c=
|
||||||
github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
|
github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
|
||||||
github.com/antchfx/htmlquery v1.2.0 h1:oKShnsGlnOHX6t4uj5OHgLKkABcJoqnXpqnscoi9Lpw=
|
github.com/antchfx/htmlquery v1.2.0 h1:oKShnsGlnOHX6t4uj5OHgLKkABcJoqnXpqnscoi9Lpw=
|
||||||
github.com/antchfx/htmlquery v1.2.0/go.mod h1:MS9yksVSQXls00iXkiMqXr0J+umL/AmxXKuP28SUJM8=
|
github.com/antchfx/htmlquery v1.2.0/go.mod h1:MS9yksVSQXls00iXkiMqXr0J+umL/AmxXKuP28SUJM8=
|
||||||
github.com/antchfx/xmlquery v1.2.0 h1:1nrzsSN5mFrlqFWSK9byiq/qXKE7O2vivYzhv1Ksnfw=
|
github.com/antchfx/xmlquery v1.3.1 h1:nIKWdtnhrXtj0/IRUAAw2I7TfpHUa3zMnHvNmPXFg+w=
|
||||||
github.com/antchfx/xmlquery v1.2.0/go.mod h1:/+CnyD/DzHRnv2eRxrVbieRU/FIF6N0C+7oTtyUtCKk=
|
github.com/antchfx/xmlquery v1.3.1/go.mod h1:64w0Xesg2sTaawIdNqMB+7qaW/bSqkQm+ssPaCMWNnc=
|
||||||
github.com/antchfx/xpath v1.1.1 h1:mqGYmd5pioPu06+REIf8j3y6O3S1UpVNVoCameZHotg=
|
github.com/antchfx/xpath v1.1.10 h1:cJ0pOvEdN/WvYXxvRrzQH9x5QWKpzHacYO8qzCcDYAg=
|
||||||
github.com/antchfx/xpath v1.1.1/go.mod h1:Yee4kTMuNiPYJ7nSNorELQMr1J33uOpXDMByNYhvtNk=
|
github.com/antchfx/xpath v1.1.10/go.mod h1:Yee4kTMuNiPYJ7nSNorELQMr1J33uOpXDMByNYhvtNk=
|
||||||
github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI=
|
github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI=
|
||||||
github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||||
github.com/azr/backoff v0.0.0-20160115115103-53511d3c7330 h1:ekDALXAVvY/Ub1UtNta3inKQwZ/jMB/zpOtD8rAYh78=
|
github.com/azr/backoff v0.0.0-20160115115103-53511d3c7330 h1:ekDALXAVvY/Ub1UtNta3inKQwZ/jMB/zpOtD8rAYh78=
|
||||||
github.com/azr/backoff v0.0.0-20160115115103-53511d3c7330/go.mod h1:nH+k0SvAt3HeiYyOlJpLLv1HG1p7KWP7qU9QPp2/pCo=
|
github.com/azr/backoff v0.0.0-20160115115103-53511d3c7330/go.mod h1:nH+k0SvAt3HeiYyOlJpLLv1HG1p7KWP7qU9QPp2/pCo=
|
||||||
github.com/bwmarrin/discordgo v0.25.0 h1:NXhdfHRNxtwso6FPdzW2i3uBvvU7UIQTghmV2T4nqAs=
|
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||||
github.com/bwmarrin/discordgo v0.25.0/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY=
|
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||||
|
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||||
|
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||||
|
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 h1:j2XRGH5Y5uWtBYXGwmrjKeM/kfu/jh7ZcnrGvyN5Ttk=
|
||||||
github.com/cdipaolo/goml v0.0.0-20190412180403-e1f51f713598/go.mod h1:sduMkaHcXDIWurl/Bd/z0rNEUHw5tr6LUA9IO8E9o0o=
|
github.com/cdipaolo/goml v0.0.0-20190412180403-e1f51f713598/go.mod h1:sduMkaHcXDIWurl/Bd/z0rNEUHw5tr6LUA9IO8E9o0o=
|
||||||
|
github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
|
||||||
|
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
|
||||||
|
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
|
||||||
|
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||||
|
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||||
|
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
|
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
|
||||||
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/chrissexton/leftpad v0.0.0-20181207133115-1e93189d2fff h1:+TEqaP0eO1unI7XHHFeMDhsxhLDIb0x8KYuZbqbAmxA=
|
github.com/chrissexton/leftpad v0.0.0-20181207133115-1e93189d2fff h1:+TEqaP0eO1unI7XHHFeMDhsxhLDIb0x8KYuZbqbAmxA=
|
||||||
github.com/chrissexton/leftpad v0.0.0-20181207133115-1e93189d2fff/go.mod h1:QCRjR0b4qiJiNjuP7RFM89bh4UExGJalcWmYeSvlnRc=
|
github.com/chrissexton/leftpad v0.0.0-20181207133115-1e93189d2fff/go.mod h1:QCRjR0b4qiJiNjuP7RFM89bh4UExGJalcWmYeSvlnRc=
|
||||||
github.com/chrissexton/sentiment v0.0.0-20190927141846-d69c422ba035 h1:3+eJGFTbUgOMDCpa8PTmJABs1Z3EDHRrcz6d3oXfZm0=
|
github.com/chrissexton/sentiment v0.0.0-20190927141846-d69c422ba035 h1:3+eJGFTbUgOMDCpa8PTmJABs1Z3EDHRrcz6d3oXfZm0=
|
||||||
github.com/chrissexton/sentiment v0.0.0-20190927141846-d69c422ba035/go.mod h1:5V55omeg+mdO+zAi38c3S9I1m5IZgdNPqiSKSXIdo88=
|
github.com/chrissexton/sentiment v0.0.0-20190927141846-d69c422ba035/go.mod h1:5V55omeg+mdO+zAi38c3S9I1m5IZgdNPqiSKSXIdo88=
|
||||||
|
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||||
|
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||||
|
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||||
|
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||||
|
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||||
github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/disintegration/gift v1.1.2 h1:9ZyHJr+kPamiH10FX3Pynt1AxFUob812bU9Wt4GMzhs=
|
||||||
|
github.com/disintegration/gift v1.1.2/go.mod h1:Jh2i7f7Q2BM7Ezno3PhfezbR1xpUg9dUg3/RlKGr4HI=
|
||||||
|
github.com/disintegration/imageorient v0.0.0-20180920195336-8147d86e83ec h1:YrB6aVr9touOt75I9O1SiancmR2GMg45U9UYf0gtgWg=
|
||||||
|
github.com/disintegration/imageorient v0.0.0-20180920195336-8147d86e83ec/go.mod h1:K0KBFIr1gWu/C1Gp10nFAcAE4hsB7JxE6OgLijrJ8Sk=
|
||||||
github.com/dustin/go-jsonpointer v0.0.0-20160814072949-ba0abeacc3dc h1:tP7tkU+vIsEOKiK+l/NSLN4uUtkyuxc6hgYpQeCWAeI=
|
github.com/dustin/go-jsonpointer v0.0.0-20160814072949-ba0abeacc3dc h1:tP7tkU+vIsEOKiK+l/NSLN4uUtkyuxc6hgYpQeCWAeI=
|
||||||
github.com/dustin/go-jsonpointer v0.0.0-20160814072949-ba0abeacc3dc/go.mod h1:ORH5Qp2bskd9NzSfKqAF7tKfONsEkCarTE5ESr/RVBw=
|
github.com/dustin/go-jsonpointer v0.0.0-20160814072949-ba0abeacc3dc/go.mod h1:ORH5Qp2bskd9NzSfKqAF7tKfONsEkCarTE5ESr/RVBw=
|
||||||
github.com/dustin/gojson v0.0.0-20160307161227-2e71ec9dd5ad h1:Qk76DOWdOp+GlyDKBAG3Klr9cn7N+LcYc82AZ2S7+cA=
|
github.com/dustin/gojson v0.0.0-20160307161227-2e71ec9dd5ad h1:Qk76DOWdOp+GlyDKBAG3Klr9cn7N+LcYc82AZ2S7+cA=
|
||||||
github.com/dustin/gojson v0.0.0-20160307161227-2e71ec9dd5ad/go.mod h1:mPKfmRa823oBIgl2r20LeMSpTAteW5j7FLkc0vjmzyQ=
|
github.com/dustin/gojson v0.0.0-20160307161227-2e71ec9dd5ad/go.mod h1:mPKfmRa823oBIgl2r20LeMSpTAteW5j7FLkc0vjmzyQ=
|
||||||
|
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||||
|
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||||
|
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||||
|
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||||
github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90 h1:WXb3TSNmHp2vHoCroCIB1foO/yQ36swABL8aOVeDpgg=
|
github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90 h1:WXb3TSNmHp2vHoCroCIB1foO/yQ36swABL8aOVeDpgg=
|
||||||
github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
|
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.6 h1:mSIGhjyMiywuGFHR/6CLL/L6HwwDiQmYGdl1R9a/05w=
|
||||||
github.com/forPelevin/gomoji v1.1.4/go.mod h1:ypB7Kz3Fsp+LVR7KoT7mEFOioYBuTuAtaAT4RGl+ASY=
|
github.com/forPelevin/gomoji v1.1.6/go.mod h1:h31zCiwG8nIto/c9RmijODA1xgN2JSvwKfU7l65xeTk=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.0 h1:Cn9dkdYsMIu56tGho+fqzh7XmvY2YyGU0FnbhiOsEro=
|
github.com/gabriel-vasile/mimetype v1.4.1 h1:TRWk7se+TOjCYgRth7+1/OYLNiRNIotknkFtf/dnN7Q=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.0/go.mod h1:fA8fi6KUiG7MgQQ+mEWotXoEOvmxRtOJlERCzSmRvr8=
|
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 h1:GOfMz6cRgTJ9jWV0qAezv642OhPnKEG7gtUjJSdStHE=
|
||||||
github.com/garyburd/go-oauth v0.0.0-20180319155456-bca2e7f09a17/go.mod h1:HfkOCN6fkKKaPSAeNq/er3xObxTW4VLeY6UUK895gLQ=
|
github.com/garyburd/go-oauth v0.0.0-20180319155456-bca2e7f09a17/go.mod h1:HfkOCN6fkKKaPSAeNq/er3xObxTW4VLeY6UUK895gLQ=
|
||||||
|
github.com/go-chi/chi v1.5.4 h1:QHdzF2szwjqVV4wmByUnTcsbIg7UGaQ0tPF2t5GcAIs=
|
||||||
github.com/go-chi/chi/v5 v5.0.7 h1:rDTPXLDHGATaeHvVlLcR4Qe0zftYethFucbjVQ1PxU8=
|
github.com/go-chi/chi/v5 v5.0.7 h1:rDTPXLDHGATaeHvVlLcR4Qe0zftYethFucbjVQ1PxU8=
|
||||||
github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||||
github.com/go-chi/httprate v0.5.3 h1:5HPWb0N6ymIiuotMtCfOGpQKiKeqXVzMexHh1W1yXPc=
|
github.com/go-chi/httprate v0.7.0 h1:8W0dF7Xa2Duz2p8ncGaehIphrxQGNlOtoGY0+NRRfjQ=
|
||||||
github.com/go-chi/httprate v0.5.3/go.mod h1:kYR4lorHX3It9tTh4eTdHhcF2bzrYnCrRNlv5+IBm2M=
|
github.com/go-chi/httprate v0.7.0/go.mod h1:6GOYBSwnpra4CQfAKXu8sQZg+nZ0M1g9QnyFvxrAB8A=
|
||||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||||
|
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||||
|
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||||
|
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||||
|
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||||
|
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
|
||||||
|
github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0=
|
||||||
|
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||||
|
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||||
|
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
|
||||||
|
github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
|
||||||
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
|
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
|
||||||
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||||
github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
|
github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
|
||||||
|
@ -70,39 +147,110 @@ github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJA
|
||||||
github.com/gocolly/colly v1.2.0 h1:qRz9YAn8FIH0qzgNUw+HT9UN7wm1oF9OBAilwEWpyrI=
|
github.com/gocolly/colly v1.2.0 h1:qRz9YAn8FIH0qzgNUw+HT9UN7wm1oF9OBAilwEWpyrI=
|
||||||
github.com/gocolly/colly v1.2.0/go.mod h1:Hof5T3ZswNVsOHYmba1u03W65HDWgpV5HifSuueE0EA=
|
github.com/gocolly/colly v1.2.0/go.mod h1:Hof5T3ZswNVsOHYmba1u03W65HDWgpV5HifSuueE0EA=
|
||||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
|
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||||
|
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||||
|
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||||
|
github.com/golang-jwt/jwt v3.2.1+incompatible h1:73Z+4BJcrTC+KczS6WvTPvRGOp1WmfEP4Q1lOd9Z/+c=
|
||||||
|
github.com/golang-jwt/jwt v3.2.1+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
|
||||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
|
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
|
||||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
|
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
|
||||||
github.com/golang/groupcache v0.0.0-20191027212112-611e8accdfc9 h1:uHTyIjqVhYRhLbJ8nIiOJHkEZZ+5YoOsAbD3sk82NiE=
|
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||||
github.com/golang/groupcache v0.0.0-20191027212112-611e8accdfc9/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||||
github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg=
|
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||||
|
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY=
|
||||||
|
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||||
|
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||||
|
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||||
|
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
|
||||||
|
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||||
|
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||||
|
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||||
|
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
|
||||||
|
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||||
|
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||||
|
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
|
||||||
|
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||||
|
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||||
|
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||||
|
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||||
|
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||||
|
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||||
|
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||||
|
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||||
|
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||||
|
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
|
||||||
|
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||||
github.com/gonum/floats v0.0.0-20181209220543-c233463c7e82 h1:EvokxLQsaaQjcWVWSV38221VAK7qc2zhaO17bKys/18=
|
github.com/gonum/floats v0.0.0-20181209220543-c233463c7e82 h1:EvokxLQsaaQjcWVWSV38221VAK7qc2zhaO17bKys/18=
|
||||||
github.com/gonum/floats v0.0.0-20181209220543-c233463c7e82/go.mod h1:PxC8OnwL11+aosOB5+iEPoV3picfs8tUpkVd0pDo+Kg=
|
github.com/gonum/floats v0.0.0-20181209220543-c233463c7e82/go.mod h1:PxC8OnwL11+aosOB5+iEPoV3picfs8tUpkVd0pDo+Kg=
|
||||||
github.com/gonum/internal v0.0.0-20181124074243-f884aa714029 h1:8jtTdc+Nfj9AR+0soOeia9UZSvYBvETVHZrugUowJ7M=
|
github.com/gonum/internal v0.0.0-20181124074243-f884aa714029 h1:8jtTdc+Nfj9AR+0soOeia9UZSvYBvETVHZrugUowJ7M=
|
||||||
github.com/gonum/internal v0.0.0-20181124074243-f884aa714029/go.mod h1:Pu4dmpkhSyOzRwuXkOgAvijx4o+4YMUJJo9OvPYMkks=
|
github.com/gonum/internal v0.0.0-20181124074243-f884aa714029/go.mod h1:Pu4dmpkhSyOzRwuXkOgAvijx4o+4YMUJJo9OvPYMkks=
|
||||||
|
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||||
|
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||||
|
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||||
|
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
|
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
|
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o=
|
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
|
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
|
||||||
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
|
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||||
|
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||||
|
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||||
|
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||||
|
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||||
|
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||||
|
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||||
|
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||||
|
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||||
|
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||||
|
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
|
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 h1:+9834+KizmvFV7pXQGSXQTsaWhq2GjuNUt0aUU0YBYw=
|
||||||
|
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y=
|
||||||
|
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
|
||||||
|
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||||
|
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
||||||
|
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||||
|
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||||
|
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||||
|
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||||
|
github.com/improbable-eng/go-httpwares v0.0.0-20200609095714-edc8019f93cc h1:jPofYCdWojUaUhjlAe5yM/H4PFDfrZ6ldrlqoVv5YDM=
|
||||||
|
github.com/improbable-eng/go-httpwares v0.0.0-20200609095714-edc8019f93cc/go.mod h1:LE9Hs6fsYQ7RoDuFUQlYmlRAku9vUlSlO++jWNj+D0I=
|
||||||
github.com/inconshreveable/log15 v0.0.0-20200109203555-b30bc20e4fd1 h1:KUDFlmBg2buRWNzIcwLlKvfcnujcHQRQ1As1LoaCLAM=
|
github.com/inconshreveable/log15 v0.0.0-20200109203555-b30bc20e4fd1 h1:KUDFlmBg2buRWNzIcwLlKvfcnujcHQRQ1As1LoaCLAM=
|
||||||
github.com/inconshreveable/log15 v0.0.0-20200109203555-b30bc20e4fd1/go.mod h1:cOaXtrgN4ScfRrD9Bre7U1thNq5RtJ8ZoP4iXVGRj6o=
|
github.com/inconshreveable/log15 v0.0.0-20200109203555-b30bc20e4fd1/go.mod h1:cOaXtrgN4ScfRrD9Bre7U1thNq5RtJ8ZoP4iXVGRj6o=
|
||||||
github.com/itchyny/gojq v0.12.8 h1:Zxcwq8w4IeR8JJYEtoG2MWJZUv0RGY6QqJcO1cqV8+A=
|
github.com/itchyny/gojq v0.12.9 h1:biKpbKwMxVYhCU1d6mR7qMr3f0Hn9F5k5YykCVb3gmM=
|
||||||
github.com/itchyny/gojq v0.12.8/go.mod h1:gE2kZ9fVRU0+JAksaTzjIlgnCa2akU+a1V0WXgJQN5c=
|
github.com/itchyny/gojq v0.12.9/go.mod h1:T4Ip7AETUXeGpD+436m+UEl3m3tokRgajd5pRfsR5oE=
|
||||||
github.com/itchyny/timefmt-go v0.1.3 h1:7M3LGVDsqcd0VZH2U+x393obrzZisp7C0uEe921iRkU=
|
github.com/itchyny/timefmt-go v0.1.4 h1:hFEfWVdwsEi+CY8xY2FtgWHGQaBaC3JeHd+cve0ynVM=
|
||||||
github.com/itchyny/timefmt-go v0.1.3/go.mod h1:0osSSCQSASBJMsIZnhAaF1C2fCBTJZXrnj37mG8/c+A=
|
github.com/itchyny/timefmt-go v0.1.4/go.mod h1:nEP7L+2YmAbT2kZ2HfSs1d8Xtw9LY8D2stDBckWakZ8=
|
||||||
github.com/james-bowman/nlp v0.0.0-20191016091239-d9dbfaff30c6 h1:k8+n5sfvxlixRNVkbelPGzEYjbGIKaBnRzRlx2NCtYA=
|
github.com/james-bowman/nlp v0.0.0-20191016091239-d9dbfaff30c6 h1:k8+n5sfvxlixRNVkbelPGzEYjbGIKaBnRzRlx2NCtYA=
|
||||||
github.com/james-bowman/nlp v0.0.0-20191016091239-d9dbfaff30c6/go.mod h1:kixuaexEqWB+mHZNysgnb6mqgGIT25WvD1/tFRRt0J0=
|
github.com/james-bowman/nlp v0.0.0-20191016091239-d9dbfaff30c6/go.mod h1:kixuaexEqWB+mHZNysgnb6mqgGIT25WvD1/tFRRt0J0=
|
||||||
github.com/james-bowman/sparse v0.0.0-20190423065201-80c6877364c7 h1:ph/BDQQDL41apnHSN48I5GyNOQXXAlc79HwGqDSXCss=
|
github.com/james-bowman/sparse v0.0.0-20190423065201-80c6877364c7 h1:ph/BDQQDL41apnHSN48I5GyNOQXXAlc79HwGqDSXCss=
|
||||||
github.com/james-bowman/sparse v0.0.0-20190423065201-80c6877364c7/go.mod h1:G6EcQnwZKsWtItoaQHd+FHPPk6bDeYVJSeeSP9Sge+I=
|
github.com/james-bowman/sparse v0.0.0-20190423065201-80c6877364c7/go.mod h1:G6EcQnwZKsWtItoaQHd+FHPPk6bDeYVJSeeSP9Sge+I=
|
||||||
github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g=
|
github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g=
|
||||||
github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ=
|
github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ=
|
||||||
github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68=
|
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
|
||||||
|
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||||
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||||
|
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||||
|
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
|
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
|
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||||
|
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
|
||||||
|
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||||
|
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
|
||||||
github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
|
github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
|
||||||
github.com/kennygrant/sanitize v1.2.4 h1:gN25/otpP5vAsO2djbMhF/LQX6R7+O1TB4yv8NzpJ3o=
|
github.com/kennygrant/sanitize v1.2.4 h1:gN25/otpP5vAsO2djbMhF/LQX6R7+O1TB4yv8NzpJ3o=
|
||||||
github.com/kennygrant/sanitize v1.2.4/go.mod h1:LGsjYYtgxbetdg5owWB2mpgUL6e2nfw2eObZ0u0qvak=
|
github.com/kennygrant/sanitize v1.2.4/go.mod h1:LGsjYYtgxbetdg5owWB2mpgUL6e2nfw2eObZ0u0qvak=
|
||||||
|
@ -114,56 +262,118 @@ github.com/kevinburke/rest v0.0.0-20200429221318-0d2892b400f8 h1:KpuDJTaTPQAyWqE
|
||||||
github.com/kevinburke/rest v0.0.0-20200429221318-0d2892b400f8/go.mod h1:pD+iEcdAGVXld5foVN4e24zb/6fnb60tgZPZ3P/3T/I=
|
github.com/kevinburke/rest v0.0.0-20200429221318-0d2892b400f8/go.mod h1:pD+iEcdAGVXld5foVN4e24zb/6fnb60tgZPZ3P/3T/I=
|
||||||
github.com/kevinburke/twilio-go v0.0.0-20200424172635-4f0b2357b852 h1:wJMykIkD7A4tlwQNzqBJ23hkLlKtRKYeNNt+n8ASqWE=
|
github.com/kevinburke/twilio-go v0.0.0-20200424172635-4f0b2357b852 h1:wJMykIkD7A4tlwQNzqBJ23hkLlKtRKYeNNt+n8ASqWE=
|
||||||
github.com/kevinburke/twilio-go v0.0.0-20200424172635-4f0b2357b852/go.mod h1:Fm9alkN1/LPVY1eqD/psyMwPWE4VWl4P01/nTYZKzBk=
|
github.com/kevinburke/twilio-go v0.0.0-20200424172635-4f0b2357b852/go.mod h1:Fm9alkN1/LPVY1eqD/psyMwPWE4VWl4P01/nTYZKzBk=
|
||||||
|
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||||
|
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||||
|
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
|
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
|
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||||
|
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||||
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
|
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||||
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
|
github.com/leso-kn/go-sqlite3 v0.0.0-20230710125852-03158dc838ed h1:lM1oz49yOQhEQsJh3lRnQ/voNTO+Lurx8fRy2Gmb2c8=
|
||||||
|
github.com/leso-kn/go-sqlite3 v0.0.0-20230710125852-03158dc838ed/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||||
github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0=
|
github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0=
|
||||||
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||||
github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
|
|
||||||
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||||
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
|
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||||
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||||
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
|
||||||
|
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||||
github.com/mmcdole/gofeed v1.1.3 h1:pdrvMb18jMSLidGp8j0pLvc9IGziX4vbmvVqmLH6z8o=
|
github.com/mmcdole/gofeed v1.1.3 h1:pdrvMb18jMSLidGp8j0pLvc9IGziX4vbmvVqmLH6z8o=
|
||||||
github.com/mmcdole/gofeed v1.1.3/go.mod h1:QQO3maftbOu+hiVOGOZDRLymqGQCos4zxbA4j89gMrE=
|
github.com/mmcdole/gofeed v1.1.3/go.mod h1:QQO3maftbOu+hiVOGOZDRLymqGQCos4zxbA4j89gMrE=
|
||||||
github.com/mmcdole/goxpp v0.0.0-20181012175147-0068e33feabf h1:sWGE2v+hO0Nd4yFU/S/mDBM5plIU8v/Qhfz41hkDIAI=
|
github.com/mmcdole/goxpp v0.0.0-20181012175147-0068e33feabf h1:sWGE2v+hO0Nd4yFU/S/mDBM5plIU8v/Qhfz41hkDIAI=
|
||||||
github.com/mmcdole/goxpp v0.0.0-20181012175147-0068e33feabf/go.mod h1:pasqhqstspkosTneA62Nc+2p9SOBBYAPbnmRRWPQ0V8=
|
github.com/mmcdole/goxpp v0.0.0-20181012175147-0068e33feabf/go.mod h1:pasqhqstspkosTneA62Nc+2p9SOBBYAPbnmRRWPQ0V8=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
|
|
||||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||||
|
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||||
|
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||||
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
|
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||||
|
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU=
|
||||||
|
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
|
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
|
||||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
|
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
|
||||||
|
github.com/nicklaw5/helix v1.25.0 h1:Mrz537izZVsGdM3I46uGAAlslj61frgkhS/9xQqyT/M=
|
||||||
|
github.com/nicklaw5/helix v1.25.0/go.mod h1:yvXZFapT6afIoxnAvlWiJiUMsYnoHl7tNs+t0bloAMw=
|
||||||
github.com/olebedev/when v0.0.0-20190311101825-c3b538a97254 h1:JYoQR67E1vv1WGoeW8DkdFs7vrIEe/5wP+qJItd5tUE=
|
github.com/olebedev/when v0.0.0-20190311101825-c3b538a97254 h1:JYoQR67E1vv1WGoeW8DkdFs7vrIEe/5wP+qJItd5tUE=
|
||||||
github.com/olebedev/when v0.0.0-20190311101825-c3b538a97254/go.mod h1:DPucAeQGDPUzYUt+NaWw6qsF5SFapWWToxEiVDh2aV0=
|
github.com/olebedev/when v0.0.0-20190311101825-c3b538a97254/go.mod h1:DPucAeQGDPUzYUt+NaWw6qsF5SFapWWToxEiVDh2aV0=
|
||||||
|
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
|
||||||
|
github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs=
|
||||||
|
github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
|
||||||
|
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
||||||
|
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
|
||||||
|
github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
|
||||||
|
github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY=
|
||||||
|
github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw=
|
||||||
|
github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y=
|
||||||
|
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||||
|
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||||
|
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||||
|
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||||
|
github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4=
|
||||||
|
github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w=
|
||||||
|
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||||
|
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
|
||||||
|
github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
|
||||||
|
github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=
|
||||||
|
github.com/prometheus/common v0.37.0 h1:ccBbHCgIiT9uSoFY0vX8H3zsNR5eLt17/RQLUvn8pXE=
|
||||||
|
github.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA=
|
||||||
|
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||||
|
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||||
|
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
|
||||||
|
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
|
||||||
|
github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
|
||||||
|
github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo=
|
||||||
|
github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4=
|
||||||
|
github.com/rivo/uniseg v0.3.4 h1:3Z3Eu6FGHZWSfNKJTOUiPatWwfc7DzJRU04jFUqJODw=
|
||||||
|
github.com/rivo/uniseg v0.3.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
github.com/robertkrimen/otto v0.0.0-20180617131154-15f95af6e78d h1:1VUlQbCfkoSGv7qP7Y+ro3ap1P1pPZxgdGVqiTVy5C4=
|
github.com/robertkrimen/otto v0.0.0-20180617131154-15f95af6e78d h1:1VUlQbCfkoSGv7qP7Y+ro3ap1P1pPZxgdGVqiTVy5C4=
|
||||||
github.com/robertkrimen/otto v0.0.0-20180617131154-15f95af6e78d/go.mod h1:xvqspoSXJTIpemEonrMDFq6XzwHYYgToXWj5eRX1OtY=
|
github.com/robertkrimen/otto v0.0.0-20180617131154-15f95af6e78d/go.mod h1:xvqspoSXJTIpemEonrMDFq6XzwHYYgToXWj5eRX1OtY=
|
||||||
github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||||
github.com/rs/zerolog v1.27.0 h1:1T7qCieN22GVc8S4Q2yuexzBb1EqjbgjSH9RohbMjKs=
|
github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||||
github.com/rs/zerolog v1.27.0/go.mod h1:7frBqO0oezxmnO7GF86FY++uy8I0Tk/If5ni1G9Qc0U=
|
github.com/rs/zerolog v1.28.0 h1:MirSo27VyNi7RJYP3078AA1+Cyzd2GB66qy3aUHvsWY=
|
||||||
|
github.com/rs/zerolog v1.28.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0=
|
||||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca h1:NugYot0LIVPxTvN8n+Kvkn6TrbMyxQiuvKdEwFdR9vI=
|
github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca h1:NugYot0LIVPxTvN8n+Kvkn6TrbMyxQiuvKdEwFdR9vI=
|
||||||
github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU=
|
github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU=
|
||||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||||
github.com/slack-go/slack v0.11.0 h1:sBBjQz8LY++6eeWhGJNZpRm5jvLRNnWBFZ/cAq58a6k=
|
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||||
github.com/slack-go/slack v0.11.0/go.mod h1:hlGi5oXA+Gt+yWTPP0plCdRKmjsDxecdHxYQdlMQKOw=
|
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||||
|
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
|
||||||
|
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
|
||||||
|
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||||
|
github.com/slack-go/slack v0.11.3 h1:GN7revxEMax4amCc3El9a+9SGnjmBvSUobs0QnO6ZO8=
|
||||||
|
github.com/slack-go/slack v0.11.3/go.mod h1:hlGi5oXA+Gt+yWTPP0plCdRKmjsDxecdHxYQdlMQKOw=
|
||||||
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
|
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
|
||||||
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.4.0 h1:M2gUjqZET1qApGOWNSnZ49BAIMX4F/1plDv3+l31EJ4=
|
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
|
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
|
||||||
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
|
|
||||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
|
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
|
||||||
|
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
github.com/temoto/robotstxt v1.1.1 h1:Gh8RCs8ouX3hRSxxK7B1mO5RFByQ4CmJZDwgom++JaA=
|
github.com/temoto/robotstxt v1.1.1 h1:Gh8RCs8ouX3hRSxxK7B1mO5RFByQ4CmJZDwgom++JaA=
|
||||||
github.com/temoto/robotstxt v1.1.1/go.mod h1:+1AmkuG3IYkh1kv0d2qEB9Le88ehNO0zwOr3ujewlOo=
|
github.com/temoto/robotstxt v1.1.1/go.mod h1:+1AmkuG3IYkh1kv0d2qEB9Le88ehNO0zwOr3ujewlOo=
|
||||||
github.com/trubitsyn/go-zero-width v1.0.1 h1:AAZhtyGXW79T5BouAF0R9FtDhGcp7IGbLZo2Id3N+m8=
|
github.com/trubitsyn/go-zero-width v1.0.1 h1:AAZhtyGXW79T5BouAF0R9FtDhGcp7IGbLZo2Id3N+m8=
|
||||||
|
@ -175,75 +385,357 @@ github.com/ttacon/libphonenumber v1.1.0/go.mod h1:E0TpmdVMq5dyVlQ7oenAkhsLu86OkU
|
||||||
github.com/urfave/cli v1.22.3/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
github.com/urfave/cli v1.22.3/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
||||||
github.com/velour/velour v0.0.0-20160303155839-8e090e68d158 h1:p3rTUXxzuKsBOsHlkly7+rj9wagFBKeIsCDKkDII9sw=
|
github.com/velour/velour v0.0.0-20160303155839-8e090e68d158 h1:p3rTUXxzuKsBOsHlkly7+rj9wagFBKeIsCDKkDII9sw=
|
||||||
github.com/velour/velour v0.0.0-20160303155839-8e090e68d158/go.mod h1:Ojy3BTOiOTwpHpw7/HNi+TVTFppao191PQs+Qc3sqpE=
|
github.com/velour/velour v0.0.0-20160303155839-8e090e68d158/go.mod h1:Ojy3BTOiOTwpHpw7/HNi+TVTFppao191PQs+Qc3sqpE=
|
||||||
|
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
|
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
|
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
|
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
|
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||||
|
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||||
|
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||||
|
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||||
|
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||||
|
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||||
|
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||||
|
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||||
|
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||||
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e h1:T8NU3HyQ8ClP4SEE+KbFlg6n0NhuTsN4MyznaarGsZM=
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
|
||||||
|
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
||||||
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
|
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
golang.org/x/exp v0.0.0-20191014171548-69215a2ee97e h1:ewBcnrlKhy0GKnQ31tXkOC/G7/jHC4ogar1TiIfANC4=
|
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||||
golang.org/x/exp v0.0.0-20191014171548-69215a2ee97e/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
|
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
|
||||||
|
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
|
||||||
|
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||||
|
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||||
|
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||||
|
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
|
||||||
|
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6 h1:QE6XYQK6naiK1EPAe1g/ILLxN5RBoH5xkJk3CqlMI/Y=
|
||||||
|
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
|
||||||
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
|
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
|
||||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b h1:+qEpEAPhDZ1o0x3tHzZTQDArnOixOzGD9HUJfcg0mb4=
|
golang.org/x/image v0.0.0-20190802002840-cff245a6509b h1:+qEpEAPhDZ1o0x3tHzZTQDArnOixOzGD9HUJfcg0mb4=
|
||||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||||
|
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||||
|
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||||
|
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||||
|
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
|
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
|
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
|
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
|
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
|
||||||
|
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||||
|
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||||
|
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
||||||
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||||
|
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||||
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
||||||
|
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||||
|
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||||
|
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
|
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||||
|
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/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-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||||
|
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||||
|
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||||
|
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||||
|
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||||
|
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||||
|
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||||
|
golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||||
|
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||||
|
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
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-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/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-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||||
golang.org/x/net v0.0.0-20211118161319-6a13c67c3ce4/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||||
|
golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
|
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
|
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
|
||||||
|
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||||
|
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
|
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
|
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
|
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
|
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
|
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||||
|
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
|
||||||
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
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-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw=
|
||||||
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k=
|
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q=
|
||||||
|
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
|
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/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.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
|
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
|
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
|
||||||
|
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||||
|
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
|
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
|
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||||
|
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
|
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
|
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
|
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||||
|
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||||
|
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||||
|
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||||
|
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||||
|
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||||
|
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||||
|
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||||
|
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||||
|
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||||
|
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||||
|
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||||
|
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||||
|
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||||
|
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||||
|
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||||
|
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
|
||||||
|
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
|
||||||
|
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
|
||||||
|
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||||
|
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||||
|
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||||
|
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||||
|
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||||
|
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||||
|
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||||
|
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||||
|
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||||
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo=
|
gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo=
|
||||||
gonum.org/v1/gonum v0.6.0 h1:DJy6UzXbahnGUf1ujUNkh/NEtK14qMo2nvlBPs4U5yw=
|
gonum.org/v1/gonum v0.6.0 h1:DJy6UzXbahnGUf1ujUNkh/NEtK14qMo2nvlBPs4U5yw=
|
||||||
gonum.org/v1/gonum v0.6.0/go.mod h1:9mxDZsDKxgMAuccQkewq682L+0eCu4dCN2yonUJTCLU=
|
gonum.org/v1/gonum v0.6.0/go.mod h1:9mxDZsDKxgMAuccQkewq682L+0eCu4dCN2yonUJTCLU=
|
||||||
gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0 h1:OE9mWmgKkjJyEmDAAtGMPjXu+YNeGvK9VTSHY6+Qihc=
|
gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0 h1:OE9mWmgKkjJyEmDAAtGMPjXu+YNeGvK9VTSHY6+Qihc=
|
||||||
gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw=
|
gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw=
|
||||||
gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc=
|
gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc=
|
||||||
google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM=
|
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||||
|
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
|
||||||
|
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||||
|
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||||
|
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||||
|
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||||
|
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||||
|
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||||
|
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||||
|
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||||
|
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||||
|
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||||
|
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
|
||||||
|
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
|
||||||
|
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
|
||||||
|
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
|
||||||
|
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||||
|
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||||
|
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||||
|
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
|
||||||
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||||
|
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
|
||||||
|
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||||
|
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||||
|
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||||
|
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||||
|
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||||
|
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||||
|
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||||
|
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||||
|
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
|
||||||
|
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||||
|
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||||
|
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||||
|
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||||
|
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||||
|
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||||
|
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
|
||||||
|
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||||
|
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||||
|
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||||
|
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||||
|
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||||
|
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||||
|
google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||||
|
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||||
|
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||||
|
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
|
||||||
|
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||||
|
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
|
||||||
|
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||||
|
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||||
|
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||||
|
google.golang.org/genproto v0.0.0-20221227171554-f9683d7f8bef h1:uQ2vjV/sHTsWSqdKeLqmwitzgvjMl7o4IdtHwUDXSJY=
|
||||||
|
google.golang.org/genproto v0.0.0-20221227171554-f9683d7f8bef/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM=
|
||||||
|
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||||
|
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||||
|
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||||
|
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||||
|
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||||
|
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||||
|
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||||
|
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||||
|
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
|
||||||
|
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
|
||||||
|
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||||
|
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||||
|
google.golang.org/grpc v1.52.0 h1:kd48UiU7EHsV4rnLyOJRuP/Il/UHE7gdDAQ+SZI7nZk=
|
||||||
|
google.golang.org/grpc v1.52.0/go.mod h1:pu6fVzoFb+NBYNAvQL08ic+lvB2IojljRYuun5vorUY=
|
||||||
|
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||||
|
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||||
|
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||||
|
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||||
|
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||||
|
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||||
|
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||||
|
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||||
|
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
|
||||||
|
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||||
|
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||||
|
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||||
|
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
|
||||||
|
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||||
|
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||||
gopkg.in/go-playground/webhooks.v5 v5.17.0 h1:truBced5ZmkiNKK47cM8bMe86wUSjNks7SFMuNKwzlc=
|
gopkg.in/go-playground/webhooks.v5 v5.17.0 h1:truBced5ZmkiNKK47cM8bMe86wUSjNks7SFMuNKwzlc=
|
||||||
gopkg.in/go-playground/webhooks.v5 v5.17.0/go.mod h1:LZbya/qLVdbqDR1aKrGuWV6qbia2zCYSR5dpom2SInQ=
|
gopkg.in/go-playground/webhooks.v5 v5.17.0/go.mod h1:LZbya/qLVdbqDR1aKrGuWV6qbia2zCYSR5dpom2SInQ=
|
||||||
gopkg.in/sourcemap.v1 v1.0.5 h1:inv58fC9f9J3TK2Y2R1NPntXEn3/wjWHkonhIUODNTI=
|
gopkg.in/sourcemap.v1 v1.0.5 h1:inv58fC9f9J3TK2Y2R1NPntXEn3/wjWHkonhIUODNTI=
|
||||||
gopkg.in/sourcemap.v1 v1.0.5/go.mod h1:2RlvNNSMglmRrcvhfuzp4hQHwOtjxlbjX7UPY/GXb78=
|
gopkg.in/sourcemap.v1 v1.0.5/go.mod h1:2RlvNNSMglmRrcvhfuzp4hQHwOtjxlbjX7UPY/GXb78=
|
||||||
|
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
|
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
|
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
|
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
|
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||||
|
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||||
|
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||||
|
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||||
|
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||||
|
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
||||||
|
|
22
main.go
22
main.go
|
@ -2,8 +2,14 @@
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
|
//go:generate templ generate
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"flag"
|
"flag"
|
||||||
|
"github.com/velour/catbase/plugins/gpt"
|
||||||
|
"github.com/velour/catbase/plugins/pagecomment"
|
||||||
|
"github.com/velour/catbase/plugins/talker"
|
||||||
|
"github.com/velour/catbase/plugins/tappd"
|
||||||
"github.com/velour/catbase/plugins/topic"
|
"github.com/velour/catbase/plugins/topic"
|
||||||
"io"
|
"io"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
|
@ -16,7 +22,6 @@ import (
|
||||||
"github.com/velour/catbase/bot/msg"
|
"github.com/velour/catbase/bot/msg"
|
||||||
"github.com/velour/catbase/connectors/discord"
|
"github.com/velour/catbase/connectors/discord"
|
||||||
"github.com/velour/catbase/plugins/giphy"
|
"github.com/velour/catbase/plugins/giphy"
|
||||||
"github.com/velour/catbase/plugins/gpt3"
|
|
||||||
"github.com/velour/catbase/plugins/last"
|
"github.com/velour/catbase/plugins/last"
|
||||||
"github.com/velour/catbase/plugins/mayi"
|
"github.com/velour/catbase/plugins/mayi"
|
||||||
"github.com/velour/catbase/plugins/quotegame"
|
"github.com/velour/catbase/plugins/quotegame"
|
||||||
|
@ -42,7 +47,6 @@ import (
|
||||||
"github.com/velour/catbase/plugins/admin"
|
"github.com/velour/catbase/plugins/admin"
|
||||||
"github.com/velour/catbase/plugins/babbler"
|
"github.com/velour/catbase/plugins/babbler"
|
||||||
"github.com/velour/catbase/plugins/beers"
|
"github.com/velour/catbase/plugins/beers"
|
||||||
"github.com/velour/catbase/plugins/cli"
|
|
||||||
"github.com/velour/catbase/plugins/couldashouldawoulda"
|
"github.com/velour/catbase/plugins/couldashouldawoulda"
|
||||||
"github.com/velour/catbase/plugins/counter"
|
"github.com/velour/catbase/plugins/counter"
|
||||||
"github.com/velour/catbase/plugins/dice"
|
"github.com/velour/catbase/plugins/dice"
|
||||||
|
@ -63,7 +67,6 @@ import (
|
||||||
"github.com/velour/catbase/plugins/rss"
|
"github.com/velour/catbase/plugins/rss"
|
||||||
"github.com/velour/catbase/plugins/sisyphus"
|
"github.com/velour/catbase/plugins/sisyphus"
|
||||||
"github.com/velour/catbase/plugins/stock"
|
"github.com/velour/catbase/plugins/stock"
|
||||||
"github.com/velour/catbase/plugins/talker"
|
|
||||||
"github.com/velour/catbase/plugins/tell"
|
"github.com/velour/catbase/plugins/tell"
|
||||||
"github.com/velour/catbase/plugins/tldr"
|
"github.com/velour/catbase/plugins/tldr"
|
||||||
"github.com/velour/catbase/plugins/twitch"
|
"github.com/velour/catbase/plugins/twitch"
|
||||||
|
@ -126,12 +129,13 @@ func main() {
|
||||||
b := bot.New(c, client)
|
b := bot.New(c, client)
|
||||||
|
|
||||||
if r, path := client.GetRouter(); r != nil {
|
if r, path := client.GetRouter(); r != nil {
|
||||||
b.RegisterWeb(r, path)
|
b.GetWeb().RegisterWeb(r, path)
|
||||||
}
|
}
|
||||||
|
|
||||||
b.AddPlugin(admin.New(b))
|
b.AddPlugin(admin.New(b))
|
||||||
b.AddPlugin(roles.New(b))
|
b.AddPlugin(roles.New(b))
|
||||||
b.AddPlugin(gpt3.New(b))
|
b.AddPlugin(twitch.New(b))
|
||||||
|
b.AddPlugin(pagecomment.New(b))
|
||||||
b.AddPlugin(secrets.New(b))
|
b.AddPlugin(secrets.New(b))
|
||||||
b.AddPlugin(mayi.New(b))
|
b.AddPlugin(mayi.New(b))
|
||||||
b.AddPlugin(giphy.New(b))
|
b.AddPlugin(giphy.New(b))
|
||||||
|
@ -139,9 +143,9 @@ func main() {
|
||||||
b.AddPlugin(last.New(b))
|
b.AddPlugin(last.New(b))
|
||||||
b.AddPlugin(first.New(b))
|
b.AddPlugin(first.New(b))
|
||||||
b.AddPlugin(leftpad.New(b))
|
b.AddPlugin(leftpad.New(b))
|
||||||
b.AddPlugin(talker.New(b))
|
|
||||||
b.AddPlugin(dice.New(b))
|
b.AddPlugin(dice.New(b))
|
||||||
b.AddPlugin(picker.New(b))
|
b.AddPlugin(picker.New(b))
|
||||||
|
b.AddPlugin(tappd.New(b))
|
||||||
b.AddPlugin(beers.New(b))
|
b.AddPlugin(beers.New(b))
|
||||||
b.AddPlugin(remember.New(b))
|
b.AddPlugin(remember.New(b))
|
||||||
b.AddPlugin(your.New(b))
|
b.AddPlugin(your.New(b))
|
||||||
|
@ -151,7 +155,6 @@ func main() {
|
||||||
b.AddPlugin(babbler.New(b))
|
b.AddPlugin(babbler.New(b))
|
||||||
b.AddPlugin(rss.New(b))
|
b.AddPlugin(rss.New(b))
|
||||||
b.AddPlugin(reaction.New(b))
|
b.AddPlugin(reaction.New(b))
|
||||||
b.AddPlugin(twitch.New(b))
|
|
||||||
b.AddPlugin(inventory.New(b))
|
b.AddPlugin(inventory.New(b))
|
||||||
b.AddPlugin(rpgORdie.New(b))
|
b.AddPlugin(rpgORdie.New(b))
|
||||||
b.AddPlugin(sisyphus.New(b))
|
b.AddPlugin(sisyphus.New(b))
|
||||||
|
@ -164,7 +167,6 @@ func main() {
|
||||||
b.AddPlugin(twitter.New(b))
|
b.AddPlugin(twitter.New(b))
|
||||||
b.AddPlugin(git.New(b))
|
b.AddPlugin(git.New(b))
|
||||||
b.AddPlugin(impossible.New(b))
|
b.AddPlugin(impossible.New(b))
|
||||||
b.AddPlugin(cli.New(b))
|
|
||||||
b.AddPlugin(aoc.New(b))
|
b.AddPlugin(aoc.New(b))
|
||||||
b.AddPlugin(meme.New(b))
|
b.AddPlugin(meme.New(b))
|
||||||
b.AddPlugin(achievements.New(b))
|
b.AddPlugin(achievements.New(b))
|
||||||
|
@ -175,8 +177,10 @@ func main() {
|
||||||
b.AddPlugin(emojy.New(b))
|
b.AddPlugin(emojy.New(b))
|
||||||
b.AddPlugin(cowboy.New(b))
|
b.AddPlugin(cowboy.New(b))
|
||||||
b.AddPlugin(topic.New(b))
|
b.AddPlugin(topic.New(b))
|
||||||
// catches anything left, will always return true
|
b.AddPlugin(talker.New(b))
|
||||||
b.AddPlugin(fact.New(b))
|
b.AddPlugin(fact.New(b))
|
||||||
|
// catches anything left, will always return true
|
||||||
|
b.AddPlugin(gpt.New(b))
|
||||||
|
|
||||||
if err := client.Serve(); err != nil {
|
if err := client.Serve(); err != nil {
|
||||||
log.Fatal().Err(err)
|
log.Fatal().Err(err)
|
||||||
|
|
|
@ -0,0 +1,91 @@
|
||||||
|
package admin
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
templ (a *AdminPlugin) page() {
|
||||||
|
<div class="grid-container">
|
||||||
|
<form>
|
||||||
|
<div class="grid-x grid-margin-x align-bottom">
|
||||||
|
<h2>App Pass</h2>
|
||||||
|
</div>
|
||||||
|
<div class="grid-x grid-margin-x align-bottom">
|
||||||
|
<div class="cell auto">
|
||||||
|
<label for="password">Password:
|
||||||
|
<input type="text" name="password"></input>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="cell auto">
|
||||||
|
<label for="secret">Secret:
|
||||||
|
<input type="text" name="secret"></input>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="cell auto">
|
||||||
|
<button hx-post="/apppass/api" hx-target="#data" class="button">List</button>
|
||||||
|
<button hx-put="/apppass/api" hx-target="#data" class="submit success button">New</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="grid-x">
|
||||||
|
<div class="cell" id="data"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
templ (a *AdminPlugin) showPassword(entry PassEntry) {
|
||||||
|
<div><span>ID</span><span>{ fmt.Sprintf("%d", entry.ID) }</span></div>
|
||||||
|
<div><span>Password</span><span>{ entry.Secret }:{ entry.Pass }</span></div>
|
||||||
|
}
|
||||||
|
|
||||||
|
templ (a *AdminPlugin) entries(items []PassEntry) {
|
||||||
|
<div>
|
||||||
|
if len(items) == 0 {
|
||||||
|
<span>No items</span>
|
||||||
|
}
|
||||||
|
<ul>
|
||||||
|
for _, entry := range items {
|
||||||
|
<li>
|
||||||
|
<button href="#"
|
||||||
|
class="button alert tiny"
|
||||||
|
style="vertical-align: baseline"
|
||||||
|
hx-delete="/apppass/api"
|
||||||
|
hx-confirm={ fmt.Sprintf("Are you sure you want to delete %d?", entry.ID) }
|
||||||
|
hx-target="#data"
|
||||||
|
hx-include="this,[name='password'],[name='secret']"
|
||||||
|
name="id" value={ fmt.Sprintf("%d", entry.ID) }>X</button>
|
||||||
|
{ fmt.Sprintf("%d", entry.ID) }
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
templ renderError(err error) {
|
||||||
|
<div>{ err.Error() }</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
templ vars(items []configEntry) {
|
||||||
|
<div class="container">
|
||||||
|
<h2>Variables</h2>
|
||||||
|
<table class="hover striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Key</th>
|
||||||
|
<th>Value</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
for _, item := range items {
|
||||||
|
<tr>
|
||||||
|
<td>{ item.Key }</td><td>{ item.Value }</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
if len(items) == 0 {
|
||||||
|
<tr>
|
||||||
|
<td colspan="2">No data</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
}
|
|
@ -0,0 +1,276 @@
|
||||||
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
|
// templ: version: v0.2.543
|
||||||
|
package admin
|
||||||
|
|
||||||
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|
||||||
|
import "github.com/a-h/templ"
|
||||||
|
import "context"
|
||||||
|
import "io"
|
||||||
|
import "bytes"
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
func (a *AdminPlugin) page() templ.Component {
|
||||||
|
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
templ_7745c5c3_Buffer = templ.GetBuffer()
|
||||||
|
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var1 == nil {
|
||||||
|
templ_7745c5c3_Var1 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"grid-container\"><form><div class=\"grid-x grid-margin-x align-bottom\"><h2>App Pass</h2></div><div class=\"grid-x grid-margin-x align-bottom\"><div class=\"cell auto\"><label for=\"password\">Password: <input type=\"text\" name=\"password\"></label></div><div class=\"cell auto\"><label for=\"secret\">Secret: <input type=\"text\" name=\"secret\"></label></div><div class=\"cell auto\"><button hx-post=\"/apppass/api\" hx-target=\"#data\" class=\"button\">List</button> <button hx-put=\"/apppass/api\" hx-target=\"#data\" class=\"submit success button\">New</button></div></div></form><div class=\"grid-x\"><div class=\"cell\" id=\"data\"></div></div></div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
|
||||||
|
}
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AdminPlugin) showPassword(entry PassEntry) templ.Component {
|
||||||
|
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
templ_7745c5c3_Buffer = templ.GetBuffer()
|
||||||
|
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var2 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var2 == nil {
|
||||||
|
templ_7745c5c3_Var2 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div><span>ID</span><span>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var3 string
|
||||||
|
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", entry.ID))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `plugins/admin/admin.templ`, Line: 35, Col: 59}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</span></div><div><span>Password</span><span>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var4 string
|
||||||
|
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(entry.Secret)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `plugins/admin/admin.templ`, Line: 36, Col: 50}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(":")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var5 string
|
||||||
|
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(entry.Pass)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `plugins/admin/admin.templ`, Line: 36, Col: 65}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</span></div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
|
||||||
|
}
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AdminPlugin) entries(items []PassEntry) templ.Component {
|
||||||
|
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
templ_7745c5c3_Buffer = templ.GetBuffer()
|
||||||
|
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var6 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var6 == nil {
|
||||||
|
templ_7745c5c3_Var6 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if len(items) == 0 {
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<span>No items</span>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<ul>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
for _, entry := range items {
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<li><button href=\"#\" class=\"button alert tiny\" style=\"vertical-align: baseline\" hx-delete=\"/apppass/api\" hx-confirm=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(fmt.Sprintf("Are you sure you want to delete %d?", entry.ID)))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\" hx-target=\"#data\" hx-include=\"this,[name='password'],[name='secret']\" name=\"id\" value=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(fmt.Sprintf("%d", entry.ID)))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\">X</button> ")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var7 string
|
||||||
|
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", entry.ID))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `plugins/admin/admin.templ`, Line: 55, Col: 57}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</li>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</ul></div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
|
||||||
|
}
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderError(err error) templ.Component {
|
||||||
|
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
templ_7745c5c3_Buffer = templ.GetBuffer()
|
||||||
|
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var8 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var8 == nil {
|
||||||
|
templ_7745c5c3_Var8 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var9 string
|
||||||
|
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(err.Error())
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `plugins/admin/admin.templ`, Line: 63, Col: 22}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
|
||||||
|
}
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func vars(items []configEntry) templ.Component {
|
||||||
|
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
templ_7745c5c3_Buffer = templ.GetBuffer()
|
||||||
|
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var10 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var10 == nil {
|
||||||
|
templ_7745c5c3_Var10 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"container\"><h2>Variables</h2><table class=\"hover striped\"><thead><tr><th>Key</th><th>Value</th></tr></thead> <tbody>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
for _, item := range items {
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<tr><td>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var11 string
|
||||||
|
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(item.Key)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `plugins/admin/admin.templ`, Line: 79, Col: 38}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</td><td>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var12 string
|
||||||
|
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(item.Value)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `plugins/admin/admin.templ`, Line: 79, Col: 61}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</td></tr>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(items) == 0 {
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<tr><td colspan=\"2\">No data</td></tr>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</tbody></table></div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
|
||||||
|
}
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
})
|
||||||
|
}
|
|
@ -5,8 +5,6 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/velour/catbase/plugins/cli"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/velour/catbase/bot"
|
"github.com/velour/catbase/bot"
|
||||||
"github.com/velour/catbase/bot/msg"
|
"github.com/velour/catbase/bot/msg"
|
||||||
|
@ -34,10 +32,8 @@ func makeMessage(payload string, r *regexp.Regexp) bot.Request {
|
||||||
if isCmd {
|
if isCmd {
|
||||||
payload = payload[1:]
|
payload = payload[1:]
|
||||||
}
|
}
|
||||||
c := cli.CliPlugin{}
|
|
||||||
values := bot.ParseValues(r, payload)
|
values := bot.ParseValues(r, payload)
|
||||||
return bot.Request{
|
return bot.Request{
|
||||||
Conn: &c,
|
|
||||||
Kind: bot.Message,
|
Kind: bot.Message,
|
||||||
Values: values,
|
Values: values,
|
||||||
Msg: msg.Message{
|
Msg: msg.Message{
|
||||||
|
|
|
@ -1,160 +0,0 @@
|
||||||
<!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>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<div id="app">
|
|
||||||
<b-navbar>
|
|
||||||
<b-navbar-brand>App Pass</b-navbar-brand>
|
|
||||||
<b-navbar-nav>
|
|
||||||
<b-nav-item v-for="item in nav" :href="item.url" :active="item.name === 'App Pass'">{{ item.name }}
|
|
||||||
</b-nav-item>
|
|
||||||
</b-navbar-nav>
|
|
||||||
</b-navbar>
|
|
||||||
<div class="alert alert-warning alert-dismissible fade show" role="alert" v-if="err != ''">
|
|
||||||
<b-button type="link" class="close" data-dismiss="alert" aria-label="Close" @click="err = ''">
|
|
||||||
<span aria-hidden="true">×</span>
|
|
||||||
</b-button>
|
|
||||||
{{ err }}
|
|
||||||
</div>
|
|
||||||
<b-form>
|
|
||||||
<b-container>
|
|
||||||
<b-row>
|
|
||||||
<b-col cols="5">Password:</b-col>
|
|
||||||
<b-col>
|
|
||||||
<b-input v-model="password"/>
|
|
||||||
</b-col>
|
|
||||||
</b-row>
|
|
||||||
<b-row>
|
|
||||||
<b-col cols="5">Secret:</b-col>
|
|
||||||
<b-col>
|
|
||||||
<b-input v-model="entry.secret"/>
|
|
||||||
</b-col>
|
|
||||||
</b-row>
|
|
||||||
<b-row>
|
|
||||||
<b-col>
|
|
||||||
<b-button @click="list">List</b-button>
|
|
||||||
</b-col>
|
|
||||||
<b-col>
|
|
||||||
<b-button @click="newPass">New</b-button>
|
|
||||||
</b-col>
|
|
||||||
</b-row>
|
|
||||||
</b-container>
|
|
||||||
</b-form>
|
|
||||||
<b-container v-show="showPassword" style="padding: 2em">
|
|
||||||
<b-row align-h="center">
|
|
||||||
<b-col align-self="center" cols="1">ID:</b-col>
|
|
||||||
<b-col align-self="center" cols="3">{{ entry.id }}</b-col>
|
|
||||||
</b-row>
|
|
||||||
<b-row align-h="center">
|
|
||||||
<b-col align-self="center" cols="1">Password:</b-col>
|
|
||||||
<b-col align-self="center" cols="3">{{ entry.secret }}:{{ showPassword }}</b-col>
|
|
||||||
</b-row>
|
|
||||||
<b-row align-h="center">
|
|
||||||
<b-col align-self="center" class="text-center" cols="6">Note: this password will only be displayed once. For
|
|
||||||
single-field entry passwords, use the secret:password format.
|
|
||||||
</b-col>
|
|
||||||
</b-row>
|
|
||||||
</b-container>
|
|
||||||
<b-container>
|
|
||||||
<b-row style="padding-top: 2em;">
|
|
||||||
<b-col>
|
|
||||||
<ul>
|
|
||||||
<li v-for="entry in entries" key="id">
|
|
||||||
<a @click="rm(entry)" href="#">X</a> {{entry.id}}
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</b-col>
|
|
||||||
</b-row>
|
|
||||||
</b-container>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
var app = new Vue({
|
|
||||||
el: '#app',
|
|
||||||
data: {
|
|
||||||
err: '',
|
|
||||||
entry: {
|
|
||||||
secret: '',
|
|
||||||
},
|
|
||||||
password: '',
|
|
||||||
showPassword: '',
|
|
||||||
nav: [],
|
|
||||||
entries: [],
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
axios.get('/nav')
|
|
||||||
.then(resp => {
|
|
||||||
this.nav = resp.data;
|
|
||||||
})
|
|
||||||
.catch(err => console.log(err))
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
rm: function (data) {
|
|
||||||
this.showPassword = '';
|
|
||||||
this.entry.id = data.id
|
|
||||||
axios.delete('/apppass/api', {
|
|
||||||
data: {
|
|
||||||
password: this.password,
|
|
||||||
passEntry: this.entry
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
this.getData()
|
|
||||||
})
|
|
||||||
.catch(({response}) => {
|
|
||||||
console.log('error: ' + response.data.err)
|
|
||||||
this.err = response.data.err
|
|
||||||
})
|
|
||||||
},
|
|
||||||
list: function () {
|
|
||||||
this.showPassword = '';
|
|
||||||
this.getData();
|
|
||||||
},
|
|
||||||
newPass: function () {
|
|
||||||
axios.put('/apppass/api', {
|
|
||||||
password: this.password,
|
|
||||||
passEntry: this.entry
|
|
||||||
})
|
|
||||||
.then(resp => {
|
|
||||||
this.getData()
|
|
||||||
this.showPassword = resp.data.pass
|
|
||||||
this.entry.id = resp.data.id
|
|
||||||
})
|
|
||||||
.catch(({response}) => {
|
|
||||||
console.log('error: ' + response.data.err)
|
|
||||||
this.err = response.data.err
|
|
||||||
})
|
|
||||||
},
|
|
||||||
getData: function () {
|
|
||||||
axios.post('/apppass/api', {
|
|
||||||
password: this.password,
|
|
||||||
passEntry: this.entry
|
|
||||||
})
|
|
||||||
.then(resp => {
|
|
||||||
this.entries = resp.data;
|
|
||||||
})
|
|
||||||
.catch(({response}) => {
|
|
||||||
console.log('error: ' + response.data.err)
|
|
||||||
this.err = response.data.err
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
|
@ -1,77 +0,0 @@
|
||||||
<!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>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<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>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
|
@ -1,38 +1,34 @@
|
||||||
package admin
|
package admin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"crypto/md5"
|
"crypto/md5"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"embed"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed *.html
|
|
||||||
var embeddedFS embed.FS
|
|
||||||
|
|
||||||
func (p *AdminPlugin) registerWeb() {
|
func (p *AdminPlugin) registerWeb() {
|
||||||
r := chi.NewRouter()
|
r := chi.NewRouter()
|
||||||
r.HandleFunc("/api", p.handleVarsAPI)
|
|
||||||
r.HandleFunc("/", p.handleVars)
|
r.HandleFunc("/", p.handleVars)
|
||||||
p.bot.RegisterWebName(r, "/vars", "Variables")
|
p.bot.GetWeb().RegisterWebName(r, "/vars", "Variables")
|
||||||
r = chi.NewRouter()
|
r = chi.NewRouter()
|
||||||
r.HandleFunc("/verify", p.handleAppPassCheck)
|
r.HandleFunc("/verify", p.handleAppPassCheck)
|
||||||
r.HandleFunc("/api", p.handleAppPassAPI)
|
r.HandleFunc("/api", p.handleAppPassAPI)
|
||||||
r.HandleFunc("/", p.handleAppPass)
|
r.HandleFunc("/", p.handleAppPass)
|
||||||
p.bot.RegisterWebName(r, "/apppass", "App Pass")
|
p.bot.GetWeb().RegisterWebName(r, "/apppass", "App Pass")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *AdminPlugin) handleAppPass(w http.ResponseWriter, r *http.Request) {
|
func (p *AdminPlugin) handleAppPass(w http.ResponseWriter, r *http.Request) {
|
||||||
index, _ := embeddedFS.ReadFile("apppass.html")
|
p.bot.GetWeb().Index("App Pass", p.page()).Render(r.Context(), w)
|
||||||
w.Write(index)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type PassEntry struct {
|
type PassEntry struct {
|
||||||
|
@ -76,26 +72,41 @@ func (p *AdminPlugin) handleAppPassCheck(w http.ResponseWriter, r *http.Request)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *AdminPlugin) handleAppPassAPI(w http.ResponseWriter, r *http.Request) {
|
func (p *AdminPlugin) handleAppPassAPI(w http.ResponseWriter, r *http.Request) {
|
||||||
|
r.ParseForm()
|
||||||
|
b, _ := io.ReadAll(r.Body)
|
||||||
|
query, _ := url.ParseQuery(string(b))
|
||||||
|
secret := r.FormValue("secret")
|
||||||
|
password := r.FormValue("password")
|
||||||
|
id, _ := strconv.ParseInt(r.FormValue("id"), 10, 64)
|
||||||
|
if !r.Form.Has("secret") && query.Has("secret") {
|
||||||
|
secret = query.Get("secret")
|
||||||
|
}
|
||||||
|
if !r.Form.Has("password") && query.Has("password") {
|
||||||
|
password = query.Get("password")
|
||||||
|
}
|
||||||
|
if !r.Form.Has("id") && query.Has("id") {
|
||||||
|
id, _ = strconv.ParseInt(query.Get("id"), 10, 64)
|
||||||
|
}
|
||||||
req := struct {
|
req := struct {
|
||||||
Password string `json:"password"`
|
Password string `json:"password"`
|
||||||
PassEntry PassEntry `json:"passEntry"`
|
PassEntry PassEntry `json:"passEntry"`
|
||||||
}{}
|
}{
|
||||||
body, _ := ioutil.ReadAll(r.Body)
|
password,
|
||||||
_ = json.Unmarshal(body, &req)
|
PassEntry{
|
||||||
|
ID: id,
|
||||||
|
Secret: secret,
|
||||||
|
},
|
||||||
|
}
|
||||||
if req.PassEntry.Secret == "" {
|
if req.PassEntry.Secret == "" {
|
||||||
writeErr(w, fmt.Errorf("missing secret"))
|
writeErr(r.Context(), w, fmt.Errorf("missing secret"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if req.Password == "" || !p.bot.CheckPassword(req.PassEntry.Secret, req.Password) {
|
if req.Password == "" || !p.bot.CheckPassword(req.PassEntry.Secret, req.Password) {
|
||||||
writeErr(w, fmt.Errorf("missing or incorrect password"))
|
writeErr(r.Context(), w, fmt.Errorf("missing or incorrect password"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
switch r.Method {
|
switch r.Method {
|
||||||
case http.MethodPut:
|
case http.MethodPut:
|
||||||
if req.PassEntry.Secret == "" {
|
|
||||||
writeErr(w, fmt.Errorf("missing secret"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if string(req.PassEntry.Pass) == "" {
|
if string(req.PassEntry.Pass) == "" {
|
||||||
c := 10
|
c := 10
|
||||||
b := make([]byte, c)
|
b := make([]byte, c)
|
||||||
|
@ -120,27 +131,27 @@ func (p *AdminPlugin) handleAppPassAPI(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
res, err := p.db.Exec(q, req.PassEntry.Secret, req.PassEntry.encodedPass, req.PassEntry.Cost)
|
res, err := p.db.Exec(q, req.PassEntry.Secret, req.PassEntry.encodedPass, req.PassEntry.Cost)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErr(w, err)
|
writeErr(r.Context(), w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
id, err := res.LastInsertId()
|
id, err := res.LastInsertId()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErr(w, err)
|
writeErr(r.Context(), w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
req.PassEntry.ID = id
|
req.PassEntry.ID = id
|
||||||
j, _ := json.Marshal(req.PassEntry)
|
p.showPassword(req.PassEntry).Render(r.Context(), w)
|
||||||
fmt.Fprint(w, string(j))
|
|
||||||
return
|
return
|
||||||
case http.MethodDelete:
|
case http.MethodDelete:
|
||||||
|
|
||||||
if req.PassEntry.ID <= 0 {
|
if req.PassEntry.ID <= 0 {
|
||||||
writeErr(w, fmt.Errorf("missing ID"))
|
writeErr(r.Context(), w, fmt.Errorf("missing ID"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
q := `delete from apppass where id = ?`
|
q := `delete from apppass where id = ?`
|
||||||
_, err := p.db.Exec(q, req.PassEntry.ID)
|
_, err := p.db.Exec(q, req.PassEntry.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErr(w, err)
|
writeErr(r.Context(), w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -148,34 +159,24 @@ func (p *AdminPlugin) handleAppPassAPI(w http.ResponseWriter, r *http.Request) {
|
||||||
passEntries := []PassEntry{}
|
passEntries := []PassEntry{}
|
||||||
err := p.db.Select(&passEntries, q, req.PassEntry.Secret)
|
err := p.db.Select(&passEntries, q, req.PassEntry.Secret)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErr(w, err)
|
writeErr(r.Context(), w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
j, _ := json.Marshal(passEntries)
|
p.entries(passEntries).Render(r.Context(), w)
|
||||||
_, _ = fmt.Fprint(w, string(j))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func writeErr(w http.ResponseWriter, err error) {
|
func writeErr(ctx context.Context, w http.ResponseWriter, err error) {
|
||||||
log.Error().Err(err).Msg("apppass error")
|
log.Error().Err(err).Msg("apppass error")
|
||||||
j, _ := json.Marshal(struct {
|
renderError(err).Render(ctx, w)
|
||||||
Err string `json:"err"`
|
}
|
||||||
}{
|
|
||||||
err.Error(),
|
type configEntry struct {
|
||||||
})
|
Key string `json:"key"`
|
||||||
w.WriteHeader(400)
|
Value string `json:"value"`
|
||||||
fmt.Fprint(w, string(j))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *AdminPlugin) handleVars(w http.ResponseWriter, r *http.Request) {
|
func (p *AdminPlugin) handleVars(w http.ResponseWriter, r *http.Request) {
|
||||||
index, _ := embeddedFS.ReadFile("vars.html")
|
var configEntries []configEntry
|
||||||
w.Write(index)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *AdminPlugin) handleVarsAPI(w http.ResponseWriter, r *http.Request) {
|
|
||||||
var configEntries []struct {
|
|
||||||
Key string `json:"key"`
|
|
||||||
Value string `json:"value"`
|
|
||||||
}
|
|
||||||
q := `select key, value from config`
|
q := `select key, value from config`
|
||||||
err := p.db.Select(&configEntries, q)
|
err := p.db.Select(&configEntries, q)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -186,13 +187,6 @@ func (p *AdminPlugin) handleVarsAPI(w http.ResponseWriter, r *http.Request) {
|
||||||
fmt.Fprint(w, err)
|
fmt.Fprint(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
for i, e := range configEntries {
|
|
||||||
if strings.Contains(e.Value, ";;") {
|
p.bot.GetWeb().Index("Variables", vars(configEntries)).Render(r.Context(), w)
|
||||||
e.Value = strings.ReplaceAll(e.Value, ";;", ", ")
|
|
||||||
e.Value = fmt.Sprintf("[%s]", e.Value)
|
|
||||||
configEntries[i] = e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
j, _ := json.Marshal(configEntries)
|
|
||||||
fmt.Fprintf(w, "%s", j)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -65,7 +65,7 @@ func (p *AOC) aocCmd(r bot.Request) bool {
|
||||||
})
|
})
|
||||||
|
|
||||||
gold, silver, bronze := -1, -1, -1
|
gold, silver, bronze := -1, -1, -1
|
||||||
goldID, silverID, bronzeID := "", "", ""
|
goldID, silverID, bronzeID := -1, -1, -1
|
||||||
for _, m := range members {
|
for _, m := range members {
|
||||||
if m.LocalScore > gold {
|
if m.LocalScore > gold {
|
||||||
gold = m.LocalScore
|
gold = m.LocalScore
|
||||||
|
|
|
@ -7,8 +7,6 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/velour/catbase/plugins/cli"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/velour/catbase/bot"
|
"github.com/velour/catbase/bot"
|
||||||
"github.com/velour/catbase/bot/msg"
|
"github.com/velour/catbase/bot/msg"
|
||||||
|
@ -16,13 +14,11 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func makeMessage(payload string, r *regexp.Regexp) bot.Request {
|
func makeMessage(payload string, r *regexp.Regexp) bot.Request {
|
||||||
c := &cli.CliPlugin{}
|
|
||||||
isCmd := strings.HasPrefix(payload, "!")
|
isCmd := strings.HasPrefix(payload, "!")
|
||||||
if isCmd {
|
if isCmd {
|
||||||
payload = payload[1:]
|
payload = payload[1:]
|
||||||
}
|
}
|
||||||
return bot.Request{
|
return bot.Request{
|
||||||
Conn: c,
|
|
||||||
Kind: bot.Message,
|
Kind: bot.Message,
|
||||||
Values: bot.ParseValues(r, payload),
|
Values: bot.ParseValues(r, payload),
|
||||||
Msg: msg.Message{
|
Msg: msg.Message{
|
||||||
|
@ -82,7 +78,7 @@ func TestBabblerNothingSaid(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBabbler(t *testing.T) {
|
func testBabbler(t *testing.T) {
|
||||||
mb := bot.NewMockBot()
|
mb := bot.NewMockBot()
|
||||||
bp := newBabblerPlugin(mb)
|
bp := newBabblerPlugin(mb)
|
||||||
assert.NotNil(t, bp)
|
assert.NotNil(t, bp)
|
||||||
|
@ -272,7 +268,6 @@ func TestHelp(t *testing.T) {
|
||||||
mb := bot.NewMockBot()
|
mb := bot.NewMockBot()
|
||||||
bp := newBabblerPlugin(mb)
|
bp := newBabblerPlugin(mb)
|
||||||
assert.NotNil(t, bp)
|
assert.NotNil(t, bp)
|
||||||
c := &cli.CliPlugin{}
|
bp.help(nil, bot.Help, msg.Message{Channel: "channel"}, []string{})
|
||||||
bp.help(c, bot.Help, msg.Message{Channel: "channel"}, []string{})
|
|
||||||
assert.Len(t, mb.Messages, 1)
|
assert.Len(t, mb.Messages, 1)
|
||||||
}
|
}
|
||||||
|
|
|
@ -598,7 +598,7 @@ func (p *BeersPlugin) untappdLoop(c bot.Connector, channel string) {
|
||||||
func (p *BeersPlugin) registerWeb() {
|
func (p *BeersPlugin) registerWeb() {
|
||||||
r := chi.NewRouter()
|
r := chi.NewRouter()
|
||||||
r.HandleFunc("/img/{id}", p.img)
|
r.HandleFunc("/img/{id}", p.img)
|
||||||
p.b.RegisterWeb(r, "/beers")
|
p.b.GetWeb().RegisterWeb(r, "/beers")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *BeersPlugin) img(w http.ResponseWriter, r *http.Request) {
|
func (p *BeersPlugin) img(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
|
@ -7,8 +7,6 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/velour/catbase/plugins/cli"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/velour/catbase/bot"
|
"github.com/velour/catbase/bot"
|
||||||
"github.com/velour/catbase/bot/msg"
|
"github.com/velour/catbase/bot/msg"
|
||||||
|
@ -21,10 +19,8 @@ func makeMessage(payload string, r *regexp.Regexp) bot.Request {
|
||||||
if isCmd {
|
if isCmd {
|
||||||
payload = payload[1:]
|
payload = payload[1:]
|
||||||
}
|
}
|
||||||
c := &cli.CliPlugin{}
|
|
||||||
values := bot.ParseValues(r, payload)
|
values := bot.ParseValues(r, payload)
|
||||||
return bot.Request{
|
return bot.Request{
|
||||||
Conn: c,
|
|
||||||
Kind: bot.Message,
|
Kind: bot.Message,
|
||||||
Values: values,
|
Values: values,
|
||||||
Msg: msg.Message{
|
Msg: msg.Message{
|
||||||
|
@ -136,6 +132,6 @@ func TestBeersReport(t *testing.T) {
|
||||||
|
|
||||||
func TestHelp(t *testing.T) {
|
func TestHelp(t *testing.T) {
|
||||||
b, mb := makeBeersPlugin(t)
|
b, mb := makeBeersPlugin(t)
|
||||||
b.help(&cli.CliPlugin{}, bot.Help, msg.Message{Channel: "channel"}, []string{})
|
b.help(nil, bot.Help, msg.Message{Channel: "channel"}, []string{})
|
||||||
assert.Len(t, mb.Messages, 1)
|
assert.Len(t, mb.Messages, 1)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,143 +0,0 @@
|
||||||
// © 2013 the CatBase Authors under the WTFPL. See AUTHORS for the list of authors.
|
|
||||||
|
|
||||||
package cli
|
|
||||||
|
|
||||||
import (
|
|
||||||
"embed"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/go-chi/chi/v5"
|
|
||||||
"github.com/jmoiron/sqlx"
|
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
|
|
||||||
"github.com/velour/catbase/bot"
|
|
||||||
"github.com/velour/catbase/bot/msg"
|
|
||||||
"github.com/velour/catbase/bot/user"
|
|
||||||
)
|
|
||||||
|
|
||||||
//go:embed *.html
|
|
||||||
var embeddedFS embed.FS
|
|
||||||
|
|
||||||
type CliPlugin struct {
|
|
||||||
bot bot.Bot
|
|
||||||
db *sqlx.DB
|
|
||||||
cache string
|
|
||||||
counter int
|
|
||||||
}
|
|
||||||
|
|
||||||
func New(b bot.Bot) *CliPlugin {
|
|
||||||
cp := &CliPlugin{
|
|
||||||
bot: b,
|
|
||||||
}
|
|
||||||
cp.registerWeb()
|
|
||||||
return cp
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *CliPlugin) registerWeb() {
|
|
||||||
r := chi.NewRouter()
|
|
||||||
r.HandleFunc("/api", p.handleWebAPI)
|
|
||||||
r.HandleFunc("/", p.handleWeb)
|
|
||||||
p.bot.RegisterWebName(r, "/cli", "CLI")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *CliPlugin) Shutdown() {}
|
|
||||||
|
|
||||||
func (p *CliPlugin) GetRouter() (http.Handler, string) {
|
|
||||||
return nil, ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *CliPlugin) handleWebAPI(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if r.Method != http.MethodPost {
|
|
||||||
fmt.Fprintf(w, "Incorrect HTTP method")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
info := struct {
|
|
||||||
User string `json:"user"`
|
|
||||||
Payload string `json:"payload"`
|
|
||||||
Password string `json:"password"`
|
|
||||||
}{}
|
|
||||||
decoder := json.NewDecoder(r.Body)
|
|
||||||
err := decoder.Decode(&info)
|
|
||||||
if err != nil {
|
|
||||||
w.WriteHeader(500)
|
|
||||||
fmt.Fprint(w, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
log.Debug().
|
|
||||||
Interface("postbody", info).
|
|
||||||
Msg("Got a POST")
|
|
||||||
if !p.bot.CheckPassword("", info.Password) {
|
|
||||||
w.WriteHeader(http.StatusForbidden)
|
|
||||||
j, _ := json.Marshal(struct{ Err string }{Err: "Invalid Password"})
|
|
||||||
w.Write(j)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
p.bot.Receive(p, bot.Message, msg.Message{
|
|
||||||
User: &user.User{
|
|
||||||
ID: info.User,
|
|
||||||
Name: info.User,
|
|
||||||
Admin: false,
|
|
||||||
},
|
|
||||||
Channel: "web",
|
|
||||||
Body: info.Payload,
|
|
||||||
Raw: info.Payload,
|
|
||||||
Command: true,
|
|
||||||
Time: time.Now(),
|
|
||||||
})
|
|
||||||
|
|
||||||
info.User = p.bot.WhoAmI()
|
|
||||||
info.Payload = p.cache
|
|
||||||
p.cache = ""
|
|
||||||
|
|
||||||
data, err := json.Marshal(info)
|
|
||||||
if err != nil {
|
|
||||||
w.WriteHeader(500)
|
|
||||||
fmt.Fprint(w, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.Write(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *CliPlugin) handleWeb(w http.ResponseWriter, r *http.Request) {
|
|
||||||
index, _ := embeddedFS.ReadFile("index.html")
|
|
||||||
w.Write(index)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Completing the Connector interface, but will not actually be a connector
|
|
||||||
func (p *CliPlugin) RegisterEvent(cb bot.Callback) {}
|
|
||||||
func (p *CliPlugin) Send(kind bot.Kind, args ...any) (string, error) {
|
|
||||||
switch kind {
|
|
||||||
case bot.Message:
|
|
||||||
fallthrough
|
|
||||||
case bot.Action:
|
|
||||||
fallthrough
|
|
||||||
case bot.Reply:
|
|
||||||
fallthrough
|
|
||||||
case bot.Reaction:
|
|
||||||
p.cache += args[1].(string) + "\n"
|
|
||||||
}
|
|
||||||
id := fmt.Sprintf("%d", p.counter)
|
|
||||||
p.counter++
|
|
||||||
return id, nil
|
|
||||||
}
|
|
||||||
func (p *CliPlugin) GetEmojiList(bool) map[string]string { return nil }
|
|
||||||
func (p *CliPlugin) Serve() error { return nil }
|
|
||||||
func (p *CliPlugin) Who(s string) []string { return nil }
|
|
||||||
func (p *CliPlugin) Profile(name string) (user.User, error) {
|
|
||||||
return user.User{}, fmt.Errorf("unimplemented")
|
|
||||||
}
|
|
||||||
func (p *CliPlugin) Emojy(name string) string { return name }
|
|
||||||
func (p *CliPlugin) DeleteEmojy(name string) error { return nil }
|
|
||||||
func (p *CliPlugin) UploadEmojy(emojy, path string) error { return nil }
|
|
||||||
func (p *CliPlugin) URLFormat(title, url string) string {
|
|
||||||
return fmt.Sprintf("%s (%s)", title, url)
|
|
||||||
}
|
|
||||||
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 }
|
|
|
@ -1,132 +0,0 @@
|
||||||
<!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="https://unpkg.com/axios/dist/axios.min.js"></script>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<title>CLI</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<div id="app">
|
|
||||||
<b-navbar>
|
|
||||||
<b-navbar-brand>CLI</b-navbar-brand>
|
|
||||||
<b-navbar-nav>
|
|
||||||
<b-nav-item v-for="item in nav" :href="item.url" :active="item.name === 'CLI'">{{ item.name }}</b-nav-item>
|
|
||||||
</b-navbar-nav>
|
|
||||||
</b-navbar>
|
|
||||||
<b-alert
|
|
||||||
dismissable
|
|
||||||
variant="error"
|
|
||||||
:show="err">
|
|
||||||
{{ err }}
|
|
||||||
</b-alert>
|
|
||||||
<b-container>
|
|
||||||
<b-row>
|
|
||||||
<b-col cols="5">Password:</b-col>
|
|
||||||
<b-col><b-input v-model="answer"></b-col>
|
|
||||||
</b-row>
|
|
||||||
<b-row>
|
|
||||||
<b-form-textarea
|
|
||||||
v-sticky-scroll
|
|
||||||
disabled
|
|
||||||
id="textarea"
|
|
||||||
v-model="text"
|
|
||||||
placeholder="The bot will respond here..."
|
|
||||||
rows="10"
|
|
||||||
max-rows="10"
|
|
||||||
no-resize
|
|
||||||
></b-form-textarea>
|
|
||||||
</b-row>
|
|
||||||
<b-form
|
|
||||||
@submit="send">
|
|
||||||
<b-row>
|
|
||||||
<b-col>
|
|
||||||
<b-form-input
|
|
||||||
type="text"
|
|
||||||
placeholder="Username"
|
|
||||||
v-model="user"></b-form-input>
|
|
||||||
</b-col>
|
|
||||||
<b-col>
|
|
||||||
<b-form-input
|
|
||||||
type="text"
|
|
||||||
placeholder="Enter something to send to the bot"
|
|
||||||
v-model="input"
|
|
||||||
autocomplete="off"
|
|
||||||
></b-form-input>
|
|
||||||
</b-col>
|
|
||||||
<b-col>
|
|
||||||
<b-button type="submit" :disabled="!authenticated">Send</b-button>
|
|
||||||
</b-col>
|
|
||||||
</b-row>
|
|
||||||
</b-form>
|
|
||||||
</b-container>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
var app = new Vue({
|
|
||||||
el: '#app',
|
|
||||||
data: {
|
|
||||||
err: '',
|
|
||||||
nav: [],
|
|
||||||
answer: '',
|
|
||||||
correct: 0,
|
|
||||||
textarea: [],
|
|
||||||
user: '',
|
|
||||||
input: '',
|
|
||||||
},
|
|
||||||
mounted: function() {
|
|
||||||
axios.get('/nav')
|
|
||||||
.then(resp => {
|
|
||||||
this.nav = resp.data;
|
|
||||||
})
|
|
||||||
.catch(err => console.log(err))
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
authenticated: function() {
|
|
||||||
if (this.user !== '')
|
|
||||||
return true;
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
text: function() {
|
|
||||||
return this.textarea.join('\n');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
addText(user, text) {
|
|
||||||
this.textarea.push(user + ": " + text);
|
|
||||||
const len = this.textarea.length;
|
|
||||||
if (this.textarea.length > 10)
|
|
||||||
this.textarea = this.textarea.slice(len-10, len);
|
|
||||||
},
|
|
||||||
send(evt) {
|
|
||||||
evt.preventDefault();
|
|
||||||
evt.stopPropagation()
|
|
||||||
if (!this.authenticated) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const payload = {user: this.user, payload: this.input, password: this.answer};
|
|
||||||
this.addText(this.user, this.input);
|
|
||||||
this.input = "";
|
|
||||||
axios.post('/cli/api', payload)
|
|
||||||
.then(resp => {
|
|
||||||
const data = resp.data;
|
|
||||||
this.addText(data.user, data.payload.trim());
|
|
||||||
this.err = '';
|
|
||||||
})
|
|
||||||
.catch(err => (this.err = err));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
|
@ -6,8 +6,6 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/velour/catbase/plugins/cli"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
"github.com/velour/catbase/bot"
|
"github.com/velour/catbase/bot"
|
||||||
|
@ -21,7 +19,6 @@ func makeMessage(payload string) bot.Request {
|
||||||
payload = payload[1:]
|
payload = payload[1:]
|
||||||
}
|
}
|
||||||
return bot.Request{
|
return bot.Request{
|
||||||
Conn: &cli.CliPlugin{},
|
|
||||||
Kind: bot.Message,
|
Kind: bot.Message,
|
||||||
Msg: msg.Message{
|
Msg: msg.Message{
|
||||||
User: &user.User{Name: "tester"},
|
User: &user.User{Name: "tester"},
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
package counter
|
package counter
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"embed"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/velour/catbase/bot/user"
|
"io"
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -17,9 +16,6 @@ import (
|
||||||
"github.com/velour/catbase/bot/msg"
|
"github.com/velour/catbase/bot/msg"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed *.html
|
|
||||||
var embeddedFS embed.FS
|
|
||||||
|
|
||||||
func (p *CounterPlugin) registerWeb() {
|
func (p *CounterPlugin) registerWeb() {
|
||||||
r := chi.NewRouter()
|
r := chi.NewRouter()
|
||||||
requests := p.cfg.GetInt("counter.requestsPer", 1)
|
requests := p.cfg.GetInt("counter.requestsPer", 1)
|
||||||
|
@ -29,19 +25,92 @@ func (p *CounterPlugin) registerWeb() {
|
||||||
subrouter.Use(httprate.LimitByIP(requests, dur))
|
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}/increment/{delta}", p.mkIncrementByNAPI(1))
|
||||||
subrouter.HandleFunc("/api/users/{user}/items/{item}/decrement/{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}/increment", p.mkIncrementByNAPI(1))
|
||||||
subrouter.HandleFunc("/api/users/{user}/items/{item}/decrement", p.mkIncrementAPI(-1))
|
subrouter.HandleFunc("/api/users/{user}/items/{item}/decrement", p.mkIncrementByNAPI(-1))
|
||||||
r.Mount("/", subrouter)
|
r.Mount("/", subrouter)
|
||||||
r.HandleFunc("/api", p.handleCounterAPI)
|
r.HandleFunc("/users/{user}/items/{item}/increment", p.incHandler(1))
|
||||||
|
r.HandleFunc("/users/{user}/items/{item}/decrement", p.incHandler(-1))
|
||||||
r.HandleFunc("/", p.handleCounter)
|
r.HandleFunc("/", p.handleCounter)
|
||||||
p.b.RegisterWebName(r, "/counter", "Counter")
|
p.b.GetWeb().RegisterWebName(r, "/counter", "Counter")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *CounterPlugin) incHandler(delta int) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
userName, _ := url.QueryUnescape(chi.URLParam(r, "user"))
|
||||||
|
itemName, _ := url.QueryUnescape(chi.URLParam(r, "item"))
|
||||||
|
pass := r.FormValue("password")
|
||||||
|
if !p.b.CheckPassword("", pass) {
|
||||||
|
w.WriteHeader(401)
|
||||||
|
fmt.Fprintf(w, "error")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
item, err := p.delta(userName, itemName, "", delta)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(500)
|
||||||
|
fmt.Fprintf(w, "error")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
p.renderItem(userName, item).Render(r.Context(), w)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *CounterPlugin) delta(userName, itemName, personalMessage string, delta int) (Item, error) {
|
||||||
|
// 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 {
|
||||||
|
return item, err
|
||||||
|
}
|
||||||
|
|
||||||
|
chs := p.cfg.GetMap("counter.channelItems", map[string]string{})
|
||||||
|
ch, ok := chs[itemName]
|
||||||
|
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: ch,
|
||||||
|
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"), personalMessage)
|
||||||
|
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
|
||||||
|
}
|
||||||
|
if err := item.UpdateDelta(req, delta); err != nil {
|
||||||
|
return item, err
|
||||||
|
}
|
||||||
|
return item, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *CounterPlugin) mkIncrementByNAPI(direction int) func(w http.ResponseWriter, r *http.Request) {
|
func (p *CounterPlugin) mkIncrementByNAPI(direction int) func(w http.ResponseWriter, r *http.Request) {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
userName := chi.URLParam(r, "user")
|
userName, _ := url.QueryUnescape(chi.URLParam(r, "user"))
|
||||||
itemName := chi.URLParam(r, "item")
|
itemName, _ := url.QueryUnescape(chi.URLParam(r, "item"))
|
||||||
delta, _ := strconv.Atoi(chi.URLParam(r, "delta"))
|
delta, err := strconv.Atoi(chi.URLParam(r, "delta"))
|
||||||
|
if err != nil || delta == 0 {
|
||||||
|
delta = direction
|
||||||
|
} else {
|
||||||
|
delta = delta * direction
|
||||||
|
}
|
||||||
|
|
||||||
secret, pass, ok := r.BasicAuth()
|
secret, pass, ok := r.BasicAuth()
|
||||||
if !ok || !p.b.CheckPassword(secret, pass) {
|
if !ok || !p.b.CheckPassword(secret, pass) {
|
||||||
|
@ -58,15 +127,15 @@ func (p *CounterPlugin) mkIncrementByNAPI(direction int) func(w http.ResponseWri
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to find an ID if possible
|
body, _ := io.ReadAll(r.Body)
|
||||||
id := ""
|
postData := map[string]string{}
|
||||||
u, err := p.b.DefaultConnector().Profile(userName)
|
err = json.Unmarshal(body, &postData)
|
||||||
if err == nil {
|
personalMsg := ""
|
||||||
id = u.ID
|
if inputMsg, ok := postData["message"]; ok {
|
||||||
|
personalMsg = fmt.Sprintf("\nMessage: %s", inputMsg)
|
||||||
}
|
}
|
||||||
|
|
||||||
item, err := GetUserItem(p.db, userName, id, itemName)
|
if _, err := p.delta(userName, itemName, personalMsg, delta*direction); err != nil {
|
||||||
if err != nil {
|
|
||||||
log.Error().Err(err).Msg("error finding item")
|
log.Error().Err(err).Msg("error finding item")
|
||||||
w.WriteHeader(400)
|
w.WriteHeader(400)
|
||||||
j, _ := json.Marshal(struct {
|
j, _ := json.Marshal(struct {
|
||||||
|
@ -77,194 +146,13 @@ func (p *CounterPlugin) mkIncrementByNAPI(direction int) func(w http.ResponseWri
|
||||||
return
|
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*direction, 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*direction)
|
|
||||||
j, _ := json.Marshal(struct{ Status bool }{true})
|
|
||||||
fmt.Fprint(w, string(j))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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})
|
j, _ := json.Marshal(struct{ Status bool }{true})
|
||||||
fmt.Fprint(w, string(j))
|
fmt.Fprint(w, string(j))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *CounterPlugin) handleCounter(w http.ResponseWriter, r *http.Request) {
|
func (p *CounterPlugin) handleCounter(w http.ResponseWriter, r *http.Request) {
|
||||||
index, _ := embeddedFS.ReadFile("index.html")
|
p.b.GetWeb().Index("Counter", p.index()).Render(r.Context(), w)
|
||||||
w.Write(index)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *CounterPlugin) handleCounterAPI(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if r.Method == http.MethodPost {
|
|
||||||
info := struct {
|
|
||||||
User string
|
|
||||||
Thing string
|
|
||||||
Action string
|
|
||||||
Password string
|
|
||||||
}{}
|
|
||||||
decoder := json.NewDecoder(r.Body)
|
|
||||||
err := decoder.Decode(&info)
|
|
||||||
if err != nil {
|
|
||||||
w.WriteHeader(500)
|
|
||||||
fmt.Fprint(w, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
log.Debug().
|
|
||||||
Interface("postbody", info).
|
|
||||||
Msg("Got a POST")
|
|
||||||
if !p.b.CheckPassword("", info.Password) {
|
|
||||||
w.WriteHeader(http.StatusForbidden)
|
|
||||||
j, _ := json.Marshal(struct{ Err string }{Err: "Invalid Password"})
|
|
||||||
w.Write(j)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
req := bot.Request{
|
|
||||||
Conn: p.b.DefaultConnector(),
|
|
||||||
Kind: bot.Message,
|
|
||||||
Msg: msg.Message{
|
|
||||||
User: &user.User{
|
|
||||||
ID: "",
|
|
||||||
Name: info.User,
|
|
||||||
Admin: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
// resolveUser requires a "full" request object so we are faking it
|
|
||||||
nick, id := p.resolveUser(req, info.User)
|
|
||||||
item, err := GetUserItem(p.db, nick, id, info.Thing)
|
|
||||||
if err != nil {
|
|
||||||
log.Error().
|
|
||||||
Err(err).
|
|
||||||
Str("subject", info.User).
|
|
||||||
Str("itemName", info.Thing).
|
|
||||||
Msg("error finding item")
|
|
||||||
w.WriteHeader(404)
|
|
||||||
fmt.Fprint(w, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if info.Action == "++" {
|
|
||||||
item.UpdateDelta(nil, 1)
|
|
||||||
} else if info.Action == "--" {
|
|
||||||
item.UpdateDelta(nil, -1)
|
|
||||||
} else {
|
|
||||||
w.WriteHeader(400)
|
|
||||||
fmt.Fprint(w, "Invalid increment")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
all, err := GetAllItems(p.db)
|
|
||||||
if err != nil {
|
|
||||||
w.WriteHeader(500)
|
|
||||||
fmt.Fprint(w, err)
|
|
||||||
log.Error().Err(err).Msg("Error getting items")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
data, err := json.Marshal(all)
|
|
||||||
if err != nil {
|
|
||||||
w.WriteHeader(500)
|
|
||||||
fmt.Fprint(w, err)
|
|
||||||
log.Error().Err(err).Msg("Error marshaling items")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fmt.Fprint(w, string(data))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update represents a change that gets sent off to other plugins such as goals
|
// Update represents a change that gets sent off to other plugins such as goals
|
||||||
|
|
|
@ -45,7 +45,7 @@ type alias struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetItems returns all counters
|
// GetItems returns all counters
|
||||||
func GetAllItems(db *sqlx.DB) ([]Item, error) {
|
func GetAllItemsByUser(db *sqlx.DB) (map[string][]Item, error) {
|
||||||
var items []Item
|
var items []Item
|
||||||
err := db.Select(&items, `select * from counter`)
|
err := db.Select(&items, `select * from counter`)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -55,7 +55,11 @@ func GetAllItems(db *sqlx.DB) ([]Item, error) {
|
||||||
for i := range items {
|
for i := range items {
|
||||||
items[i].DB = db
|
items[i].DB = db
|
||||||
}
|
}
|
||||||
return items, nil
|
out := map[string][]Item{}
|
||||||
|
for _, it := range items {
|
||||||
|
out[it.Nick] = append(out[it.Nick], it)
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetItems returns all counters for a subject
|
// GetItems returns all counters for a subject
|
||||||
|
|
|
@ -0,0 +1,66 @@
|
||||||
|
package counter
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
func urlFor(who, what, dir string) string {
|
||||||
|
return fmt.Sprintf("/counter/users/%s/items/%s/%s", who, what, dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *CounterPlugin) allItems() map[string][]Item {
|
||||||
|
items, err := GetAllItemsByUser(p.db)
|
||||||
|
if err != nil {
|
||||||
|
return map[string][]Item{"error": []Item{}}
|
||||||
|
}
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
|
||||||
|
templ (p *CounterPlugin) index() {
|
||||||
|
<div class="grid-container">
|
||||||
|
<div class="grid-x">
|
||||||
|
<h2>Counter</h2>
|
||||||
|
</div>
|
||||||
|
<div class="grid-x">
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-label">Password</span>
|
||||||
|
<input class="input-group-field" type="text" name="password" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<table>
|
||||||
|
for user, items := range p.allItems() {
|
||||||
|
<tr><th class="text-left" colspan="3">{ user }</th></tr>
|
||||||
|
for _, thing := range items {
|
||||||
|
@p.renderItem(user, thing)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
templ (p *CounterPlugin) renderItem(user string, item Item) {
|
||||||
|
<tr id={ fmt.Sprintf("item%d", item.ID) }>
|
||||||
|
<td>
|
||||||
|
{ item.Item }
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{ fmt.Sprintf("%d", item.Count) }
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<button
|
||||||
|
class="button tiny alert"
|
||||||
|
style="vertical-align: baseline"
|
||||||
|
hx-target={ "#"+fmt.Sprintf("item%d", item.ID) }
|
||||||
|
hx-include="[name='password']"
|
||||||
|
hx-swap="outerHTML"
|
||||||
|
hx-post={ urlFor(user, item.Item, "decrement") }
|
||||||
|
>-</button>
|
||||||
|
<button
|
||||||
|
class="button tiny success"
|
||||||
|
style="vertical-align: baseline"
|
||||||
|
hx-target={ "#"+fmt.Sprintf("item%d", item.ID) }
|
||||||
|
hx-include="[name='password']"
|
||||||
|
hx-swap="outerHTML"
|
||||||
|
hx-post={ urlFor(user, item.Item, "increment") }
|
||||||
|
>+</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
|
@ -0,0 +1,168 @@
|
||||||
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
|
// templ: version: v0.2.543
|
||||||
|
package counter
|
||||||
|
|
||||||
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|
||||||
|
import "github.com/a-h/templ"
|
||||||
|
import "context"
|
||||||
|
import "io"
|
||||||
|
import "bytes"
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
func urlFor(who, what, dir string) string {
|
||||||
|
return fmt.Sprintf("/counter/users/%s/items/%s/%s", who, what, dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *CounterPlugin) allItems() map[string][]Item {
|
||||||
|
items, err := GetAllItemsByUser(p.db)
|
||||||
|
if err != nil {
|
||||||
|
return map[string][]Item{"error": []Item{}}
|
||||||
|
}
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *CounterPlugin) index() templ.Component {
|
||||||
|
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
templ_7745c5c3_Buffer = templ.GetBuffer()
|
||||||
|
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var1 == nil {
|
||||||
|
templ_7745c5c3_Var1 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"grid-container\"><div class=\"grid-x\"><h2>Counter</h2></div><div class=\"grid-x\"><div class=\"input-group\"><span class=\"input-group-label\">Password</span> <input class=\"input-group-field\" type=\"text\" name=\"password\"></div></div><table>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
for user, items := range p.allItems() {
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<tr><th class=\"text-left\" colspan=\"3\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var2 string
|
||||||
|
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(user)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `plugins/counter/counter.templ`, Line: 29, Col: 60}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</th></tr>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
for _, thing := range items {
|
||||||
|
templ_7745c5c3_Err = p.renderItem(user, thing).Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</table></div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
|
||||||
|
}
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *CounterPlugin) renderItem(user string, item Item) templ.Component {
|
||||||
|
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
templ_7745c5c3_Buffer = templ.GetBuffer()
|
||||||
|
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var3 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var3 == nil {
|
||||||
|
templ_7745c5c3_Var3 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<tr id=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(fmt.Sprintf("item%d", item.ID)))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\"><td>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var4 string
|
||||||
|
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(item.Item)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `plugins/counter/counter.templ`, Line: 41, Col: 23}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</td><td>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var5 string
|
||||||
|
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", item.Count))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `plugins/counter/counter.templ`, Line: 44, Col: 43}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</td><td><button class=\"button tiny alert\" style=\"vertical-align: baseline\" hx-target=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString("#" + fmt.Sprintf("item%d", item.ID)))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\" hx-include=\"[name='password']\" hx-swap=\"outerHTML\" hx-post=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(urlFor(user, item.Item, "decrement")))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\">-</button> <button class=\"button tiny success\" style=\"vertical-align: baseline\" hx-target=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString("#" + fmt.Sprintf("item%d", item.ID)))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\" hx-include=\"[name='password']\" hx-swap=\"outerHTML\" hx-post=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(urlFor(user, item.Item, "increment")))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\">+</button></td></tr>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
|
||||||
|
}
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
})
|
||||||
|
}
|
|
@ -4,12 +4,12 @@ package counter
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/velour/catbase/config"
|
||||||
|
"github.com/velour/catbase/connectors/irc"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/velour/catbase/plugins/cli"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
"github.com/velour/catbase/bot"
|
"github.com/velour/catbase/bot"
|
||||||
|
@ -33,7 +33,7 @@ func makeMessage(payload string, r *regexp.Regexp) bot.Request {
|
||||||
}
|
}
|
||||||
values := bot.ParseValues(r, payload)
|
values := bot.ParseValues(r, payload)
|
||||||
return bot.Request{
|
return bot.Request{
|
||||||
Conn: &cli.CliPlugin{},
|
Conn: irc.New(&config.Config{}),
|
||||||
Msg: msg.Message{
|
Msg: msg.Message{
|
||||||
User: &user.User{Name: "tester", ID: "id"},
|
User: &user.User{Name: "tester", ID: "id"},
|
||||||
Body: payload,
|
Body: payload,
|
||||||
|
@ -280,6 +280,6 @@ func TestInspectMe(t *testing.T) {
|
||||||
func TestHelp(t *testing.T) {
|
func TestHelp(t *testing.T) {
|
||||||
mb, c := setup(t)
|
mb, c := setup(t)
|
||||||
assert.NotNil(t, c)
|
assert.NotNil(t, c)
|
||||||
c.help(&cli.CliPlugin{}, bot.Help, msg.Message{Channel: "channel"}, []string{})
|
c.help(nil, bot.Help, msg.Message{Channel: "channel"}, []string{})
|
||||||
assert.Greater(t, len(mb.Messages), 1)
|
assert.Greater(t, len(mb.Messages), 1)
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"github.com/bwmarrin/discordgo"
|
"github.com/bwmarrin/discordgo"
|
||||||
"github.com/velour/catbase/plugins/emojy"
|
"github.com/velour/catbase/plugins/emojy"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/velour/catbase/connectors/discord"
|
"github.com/velour/catbase/connectors/discord"
|
||||||
|
|
||||||
|
@ -72,26 +73,25 @@ func (p *Cowboy) register() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Cowboy) makeCowboy(r bot.Request) {
|
func (p *Cowboy) makeCowboy(r bot.Request) {
|
||||||
what := r.Values["what"]
|
|
||||||
// This'll add the image to the cowboy_cache before discord tries to access it over http
|
|
||||||
overlays := p.c.GetMap("cowboy.overlays", defaultOverlays)
|
overlays := p.c.GetMap("cowboy.overlays", defaultOverlays)
|
||||||
hat := overlays["hat"]
|
hat := overlays["hat"]
|
||||||
i, err := cowboy(p.emojyPath, p.baseEmojyURL, hat, what)
|
what := r.Values["what"]
|
||||||
|
_, err := p.mkEmojy(what, hat)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg(":cowboy_fail:")
|
p.b.Send(r.Conn, bot.Message, r.Msg.Channel, "Couldn't cowboify that, pardner.")
|
||||||
p.b.Send(r.Conn, bot.Ephemeral, r.Msg.Channel, r.Msg.User.ID, "Hey cowboy, that image wasn't there.")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.Debug().Msgf("makeCowboy: %s", r.Values["what"])
|
|
||||||
base := p.c.Get("baseURL", "http://127.0.0.1:1337")
|
|
||||||
u := base + "/cowboy/img/hat/" + r.Values["what"]
|
|
||||||
p.b.Send(r.Conn, bot.Delete, r.Msg.Channel, r.Msg.ID)
|
p.b.Send(r.Conn, bot.Delete, r.Msg.Channel, r.Msg.ID)
|
||||||
p.b.Send(r.Conn, bot.Message, r.Msg.Channel, "", bot.ImageAttachment{
|
e := ":cowboy_" + what + ":"
|
||||||
URL: u,
|
|
||||||
AltTxt: fmt.Sprintf("%s: %s", r.Msg.User.Name, r.Msg.Body),
|
switch c := r.Conn.(type) {
|
||||||
Width: i.Bounds().Max.X,
|
case *discord.Discord:
|
||||||
Height: i.Bounds().Max.Y,
|
list := emojy.InvertEmojyList(c.GetEmojiList(true))
|
||||||
})
|
e = strings.Trim(e, ":")
|
||||||
|
e = fmt.Sprintf("<:%s:%s>", e, list[e])
|
||||||
|
}
|
||||||
|
p.b.Send(r.Conn, bot.Message, r.Msg.Channel, r.Msg.User.Name+":")
|
||||||
|
p.b.Send(r.Conn, bot.Message, r.Msg.Channel, e)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Cowboy) registerCmds(d *discord.Discord) {
|
func (p *Cowboy) registerCmds(d *discord.Discord) {
|
||||||
|
@ -142,56 +142,69 @@ func (p *Cowboy) registerCmds(d *discord.Discord) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *Cowboy) mkEmojy(name, overlay string) (string, error) {
|
||||||
|
lastEmojy := p.c.Get("cowboy.lastEmojy", "rust")
|
||||||
|
emojyPlugin := emojy.NewAPI(p.b)
|
||||||
|
list := map[string]string{}
|
||||||
|
|
||||||
|
msg := fmt.Sprintf("You asked for %s overlaid by %s", name, overlay)
|
||||||
|
log.Debug().Msgf("got a cowboy command for %s overlaid by %s replacing %s",
|
||||||
|
name, overlay, lastEmojy)
|
||||||
|
prefix := overlay
|
||||||
|
|
||||||
|
newEmojy, err := cowboy(p.emojyPath, p.baseEmojyURL, overlay, name)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = emojyPlugin.RmEmojy(p.b.DefaultConnector(), lastEmojy)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if overlay == "hat" {
|
||||||
|
prefix = "cowboy"
|
||||||
|
}
|
||||||
|
name = emojy.SanitizeName(prefix + "_" + name)
|
||||||
|
err = emojyPlugin.UploadEmojyImage(p.b.DefaultConnector(), name, newEmojy)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
p.c.Set("cowboy.lastEmojy", name)
|
||||||
|
|
||||||
|
list = emojy.InvertEmojyList(p.b.DefaultConnector().GetEmojiList(true))
|
||||||
|
msg = fmt.Sprintf("You replaced %s with a new emojy %s <:%s:%s>, pardner!",
|
||||||
|
lastEmojy, name, name, list[name])
|
||||||
|
|
||||||
|
return msg, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (p *Cowboy) mkOverlayCB(overlay string) func(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
func (p *Cowboy) mkOverlayCB(overlay string) func(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
||||||
return func(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
return func(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
||||||
lastEmojy := p.c.Get("cowboy.lastEmojy", "rust")
|
|
||||||
emojyPlugin := emojy.NewAPI(p.b)
|
|
||||||
list := map[string]string{}
|
|
||||||
|
|
||||||
name := i.ApplicationCommandData().Options[0].StringValue()
|
name := i.ApplicationCommandData().Options[0].StringValue()
|
||||||
if overlay == "" {
|
if overlay == "" {
|
||||||
overlay = name
|
overlay = name
|
||||||
name = i.ApplicationCommandData().Options[1].StringValue()
|
name = i.ApplicationCommandData().Options[1].StringValue()
|
||||||
}
|
}
|
||||||
msg := fmt.Sprintf("You asked for %s overlaid by %s", name, overlay)
|
|
||||||
log.Debug().Msgf("got a cowboy command for %s overlaid by %s replacing %s",
|
|
||||||
name, overlay, lastEmojy)
|
|
||||||
prefix := overlay
|
|
||||||
|
|
||||||
newEmojy, err := cowboy(p.emojyPath, p.baseEmojyURL, overlay, name)
|
msg, err := p.mkEmojy(name, overlay)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
msg = err.Error()
|
s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
|
||||||
goto resp
|
Type: discordgo.InteractionResponseChannelMessageWithSource,
|
||||||
|
Data: &discordgo.InteractionResponseData{
|
||||||
|
Content: err.Error(),
|
||||||
|
Flags: discordgo.MessageFlagsEphemeral,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = emojyPlugin.RmEmojy(p.b.DefaultConnector(), lastEmojy)
|
|
||||||
if err != nil {
|
|
||||||
msg = err.Error()
|
|
||||||
goto resp
|
|
||||||
}
|
|
||||||
|
|
||||||
if overlay == "hat" {
|
|
||||||
prefix = "cowboy"
|
|
||||||
}
|
|
||||||
name = emojy.SanitizeName(prefix + "_" + name)
|
|
||||||
err = emojyPlugin.UploadEmojyImage(p.b.DefaultConnector(), name, newEmojy)
|
|
||||||
if err != nil {
|
|
||||||
msg = err.Error()
|
|
||||||
goto resp
|
|
||||||
}
|
|
||||||
|
|
||||||
p.c.Set("cowboy.lastEmojy", name)
|
|
||||||
|
|
||||||
list = emojy.InvertEmojyList(p.b.DefaultConnector().GetEmojiList(true))
|
|
||||||
msg = fmt.Sprintf("You replaced %s with a new emojy %s <:%s:%s>, pardner!",
|
|
||||||
lastEmojy, name, name, list[name])
|
|
||||||
|
|
||||||
resp:
|
|
||||||
s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
|
s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
|
||||||
Type: discordgo.InteractionResponseChannelMessageWithSource,
|
Type: discordgo.InteractionResponseChannelMessageWithSource,
|
||||||
Data: &discordgo.InteractionResponseData{
|
Data: &discordgo.InteractionResponseData{
|
||||||
Content: msg,
|
Content: msg,
|
||||||
Flags: uint64(discordgo.MessageFlagsEphemeral),
|
Flags: discordgo.MessageFlagsEphemeral,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,7 @@ import (
|
||||||
func (p *Cowboy) registerWeb() {
|
func (p *Cowboy) registerWeb() {
|
||||||
r := chi.NewRouter()
|
r := chi.NewRouter()
|
||||||
r.HandleFunc("/img/{overlay}/{what}", p.handleImage)
|
r.HandleFunc("/img/{overlay}/{what}", p.handleImage)
|
||||||
p.b.RegisterWeb(r, "/cowboy")
|
p.b.GetWeb().RegisterWeb(r, "/cowboy")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Cowboy) handleImage(w http.ResponseWriter, r *http.Request) {
|
func (p *Cowboy) handleImage(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
|
@ -6,8 +6,6 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/velour/catbase/plugins/cli"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/velour/catbase/bot"
|
"github.com/velour/catbase/bot"
|
||||||
"github.com/velour/catbase/bot/msg"
|
"github.com/velour/catbase/bot/msg"
|
||||||
|
@ -21,7 +19,6 @@ func makeMessage(payload string) bot.Request {
|
||||||
}
|
}
|
||||||
values := bot.ParseValues(rollRegex, payload)
|
values := bot.ParseValues(rollRegex, payload)
|
||||||
return bot.Request{
|
return bot.Request{
|
||||||
Conn: &cli.CliPlugin{},
|
|
||||||
Kind: bot.Message,
|
Kind: bot.Message,
|
||||||
Values: values,
|
Values: values,
|
||||||
Msg: msg.Message{
|
Msg: msg.Message{
|
||||||
|
@ -67,6 +64,6 @@ func TestHelp(t *testing.T) {
|
||||||
mb := bot.NewMockBot()
|
mb := bot.NewMockBot()
|
||||||
c := New(mb)
|
c := New(mb)
|
||||||
assert.NotNil(t, c)
|
assert.NotNil(t, c)
|
||||||
c.help(&cli.CliPlugin{}, bot.Help, msg.Message{Channel: "channel"}, []string{})
|
c.help(nil, bot.Help, msg.Message{Channel: "channel"}, []string{})
|
||||||
assert.Len(t, mb.Messages, 1)
|
assert.Len(t, mb.Messages, 1)
|
||||||
}
|
}
|
||||||
|
|
|
@ -61,7 +61,7 @@ resp:
|
||||||
Type: discordgo.InteractionResponseChannelMessageWithSource,
|
Type: discordgo.InteractionResponseChannelMessageWithSource,
|
||||||
Data: &discordgo.InteractionResponseData{
|
Data: &discordgo.InteractionResponseData{
|
||||||
Content: msg,
|
Content: msg,
|
||||||
Flags: uint64(discordgo.MessageFlagsEphemeral),
|
Flags: discordgo.MessageFlagsEphemeral,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,107 +0,0 @@
|
||||||
<!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="https://unpkg.com/axios/dist/axios.min.js"></script>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<title>Memes</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<div id="app">
|
|
||||||
<b-navbar>
|
|
||||||
<b-navbar-brand>Emojys</b-navbar-brand>
|
|
||||||
<b-navbar-nav>
|
|
||||||
<b-nav-item v-for="item in nav" :href="item.url" :active="item.name === 'Meme'" :key="item.key">{{ item.name
|
|
||||||
}}
|
|
||||||
</b-nav-item>
|
|
||||||
</b-navbar-nav>
|
|
||||||
</b-navbar>
|
|
||||||
|
|
||||||
<b-navbar>
|
|
||||||
<b-navbar-nav>
|
|
||||||
<b-nav-item href="/emojy/stats">Stats</b-nav-item>
|
|
||||||
<b-nav-item active href="/emojy/list">List</b-nav-item>
|
|
||||||
<b-nav-item href="/emojy/new">Upload</b-nav-item>
|
|
||||||
</b-navbar-nav>
|
|
||||||
</b-navbar>
|
|
||||||
|
|
||||||
<div
|
|
||||||
style="background-color:red;"
|
|
||||||
variant="error"
|
|
||||||
v-if="err != ''"
|
|
||||||
@click="err = ''">
|
|
||||||
{{ err }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row row-cols-5">
|
|
||||||
<div class="card text-center" v-for="name in fileKeys" key="name">
|
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
var app = new Vue({
|
|
||||||
el: '#app',
|
|
||||||
data: function () {
|
|
||||||
return {
|
|
||||||
err: '',
|
|
||||||
view: '',
|
|
||||||
nav: [],
|
|
||||||
results: [],
|
|
||||||
fileKeys: [],
|
|
||||||
fileList: {},
|
|
||||||
image: null,
|
|
||||||
password: ''
|
|
||||||
}
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
view(newView, oldView) {
|
|
||||||
this.err = '';
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
axios.get('/nav')
|
|
||||||
.then(resp => {
|
|
||||||
this.nav = resp.data;
|
|
||||||
})
|
|
||||||
.catch(err => console.log(err))
|
|
||||||
this.refresh();
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
refresh: function () {
|
|
||||||
axios.get('/emojy/all')
|
|
||||||
.then(resp => {
|
|
||||||
this.results = resp.data
|
|
||||||
this.err = ''
|
|
||||||
})
|
|
||||||
.catch(err => (this.err = err))
|
|
||||||
axios.get('/emojy/allFiles')
|
|
||||||
.then(resp => {
|
|
||||||
// stole this somewhere or other as a quick hack
|
|
||||||
this.fileKeys = Object.keys(resp.data).sort()
|
|
||||||
this.fileList = resp.data
|
|
||||||
this.err = ''
|
|
||||||
})
|
|
||||||
.catch(err => (this.err = err))
|
|
||||||
},
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
package emojy
|
||||||
|
|
||||||
|
templ (p *EmojyPlugin) listTempl(emojy emojyMap) {
|
||||||
|
<div class="grid-container">
|
||||||
|
<div class="grid-x">
|
||||||
|
<div class="cell">
|
||||||
|
<h2>Emojy</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid-x">
|
||||||
|
<div class="cell">
|
||||||
|
@p.emojyNav()
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid-x grid-margin-x small-up-3 medium-up-6 large-up-8">
|
||||||
|
for _, v := range emojy {
|
||||||
|
for _, c := range v {
|
||||||
|
<div class="cell">
|
||||||
|
<div class="card"
|
||||||
|
style="max-width: 100px">
|
||||||
|
<img src={ c.URL }
|
||||||
|
style="max-height: 100px"
|
||||||
|
style="max-width: 100px"
|
||||||
|
alt={ c.Emojy }
|
||||||
|
/>
|
||||||
|
<div class="card-divider">
|
||||||
|
{ c.Emojy }
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
|
@ -0,0 +1,84 @@
|
||||||
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
|
// templ: version: v0.2.543
|
||||||
|
package emojy
|
||||||
|
|
||||||
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|
||||||
|
import "github.com/a-h/templ"
|
||||||
|
import "context"
|
||||||
|
import "io"
|
||||||
|
import "bytes"
|
||||||
|
|
||||||
|
func (p *EmojyPlugin) listTempl(emojy emojyMap) templ.Component {
|
||||||
|
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
templ_7745c5c3_Buffer = templ.GetBuffer()
|
||||||
|
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var1 == nil {
|
||||||
|
templ_7745c5c3_Var1 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"grid-container\"><div class=\"grid-x\"><div class=\"cell\"><h2>Emojy</h2></div></div><div class=\"grid-x\"><div class=\"cell\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = p.emojyNav().Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div></div><div class=\"grid-x grid-margin-x small-up-3 medium-up-6 large-up-8\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
for _, v := range emojy {
|
||||||
|
for _, c := range v {
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"cell\"><div class=\"card\" style=\"max-width: 100px\"><img src=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(c.URL))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\" style=\"max-height: 100px\" style=\"max-width: 100px\" alt=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(c.Emojy))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\"><div class=\"card-divider\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var2 string
|
||||||
|
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(c.Emojy)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `plugins/emojy/list.templ`, Line: 26, Col: 33}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div></div></div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div></div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
|
||||||
|
}
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
})
|
||||||
|
}
|
|
@ -1,113 +0,0 @@
|
||||||
<!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="https://unpkg.com/axios/dist/axios.min.js"></script>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<title>Memes</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<div id="app">
|
|
||||||
<b-navbar>
|
|
||||||
<b-navbar-brand>Emojys</b-navbar-brand>
|
|
||||||
<b-navbar-nav>
|
|
||||||
<b-nav-item v-for="item in nav" :href="item.url" :active="item.name === 'Meme'" :key="item.key">{{ item.name
|
|
||||||
}}
|
|
||||||
</b-nav-item>
|
|
||||||
</b-navbar-nav>
|
|
||||||
</b-navbar>
|
|
||||||
|
|
||||||
<b-navbar>
|
|
||||||
<b-navbar-nav>
|
|
||||||
<b-nav-item active href="/emojy/stats">Stats</b-nav-item>
|
|
||||||
<b-nav-item href="/emojy/list">List</b-nav-item>
|
|
||||||
<b-nav-item href="/emojy/new">Upload</b-nav-item>
|
|
||||||
</b-navbar-nav>
|
|
||||||
</b-navbar>
|
|
||||||
|
|
||||||
<div
|
|
||||||
style="background-color:red;"
|
|
||||||
variant="error"
|
|
||||||
v-if="err != ''"
|
|
||||||
@click="err = ''">
|
|
||||||
{{ err }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ul style="list-style-type: none;">
|
|
||||||
<li v-for="category in results">
|
|
||||||
<ul v-if="category" style="list-style-type: none;">
|
|
||||||
<li v-for="emojy in category" key="emojy">
|
|
||||||
{{emojy.count}} -
|
|
||||||
<span v-if="name != 'emoji'">
|
|
||||||
<span v-if="emojy.onServer">✅</span>
|
|
||||||
<span v-else>❎</span>
|
|
||||||
-
|
|
||||||
</span>
|
|
||||||
<span v-if="emojy.url">
|
|
||||||
<img :src="emojy.url" :alt="emojy.name" class="img-thumbnail"
|
|
||||||
style="max-width: 64px; max-height: 64px"/> {{emojy.emojy}}
|
|
||||||
</span>
|
|
||||||
<span v-else>{{emojy.emojy}}</span>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
var app = new Vue({
|
|
||||||
el: '#app',
|
|
||||||
data: function () {
|
|
||||||
return {
|
|
||||||
err: '',
|
|
||||||
view: '',
|
|
||||||
nav: [],
|
|
||||||
results: [],
|
|
||||||
fileList: {},
|
|
||||||
image: null,
|
|
||||||
password: ''
|
|
||||||
}
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
view(newView, oldView) {
|
|
||||||
this.err = '';
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
axios.get('/nav')
|
|
||||||
.then(resp => {
|
|
||||||
this.nav = resp.data;
|
|
||||||
})
|
|
||||||
.catch(err => console.log(err))
|
|
||||||
this.refresh();
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
refresh: function () {
|
|
||||||
axios.get('/emojy/all')
|
|
||||||
.then(resp => {
|
|
||||||
this.results = [resp.data["emojy"], resp.data["unknown"], resp.data["emoji"]]
|
|
||||||
this.err = ''
|
|
||||||
})
|
|
||||||
.catch(err => (this.err = err))
|
|
||||||
axios.get('/emojy/allFiles')
|
|
||||||
.then(resp => {
|
|
||||||
this.fileList = resp.data
|
|
||||||
this.err = ''
|
|
||||||
})
|
|
||||||
.catch(err => (this.err = err))
|
|
||||||
},
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
|
@ -0,0 +1,53 @@
|
||||||
|
package emojy
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
templ (p *EmojyPlugin) emojyNav() {
|
||||||
|
<ul class="menu">
|
||||||
|
<li>
|
||||||
|
<a href="/emojy/stats">Stats</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="/emojy/list">List</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="/emojy/new">Upload</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
}
|
||||||
|
|
||||||
|
templ (p *EmojyPlugin) statsIndex(emojy emojyMap) {
|
||||||
|
<div class="grid-container">
|
||||||
|
<div class="grid-x">
|
||||||
|
<div class="cell">
|
||||||
|
<h2>Emojy</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid-x">
|
||||||
|
<div class="cell">
|
||||||
|
@p.emojyNav()
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="cell">
|
||||||
|
for categoryName, v := range emojy {
|
||||||
|
<ul class="no-bullet">
|
||||||
|
for _, c := range v {
|
||||||
|
<li class="">
|
||||||
|
{ fmt.Sprintf("%d", c.Count) } -
|
||||||
|
if categoryName != "emoji" && c.OnServer {
|
||||||
|
<span>✅</span>
|
||||||
|
} else if categoryName != "emoji" {
|
||||||
|
<span>✅</span>
|
||||||
|
}
|
||||||
|
if c.URL != "" {
|
||||||
|
<img src={ c.URL } alt={ c.Emojy } />
|
||||||
|
} else {
|
||||||
|
{ c.Emojy }
|
||||||
|
}
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
|
@ -0,0 +1,149 @@
|
||||||
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
|
// templ: version: v0.2.543
|
||||||
|
package emojy
|
||||||
|
|
||||||
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|
||||||
|
import "github.com/a-h/templ"
|
||||||
|
import "context"
|
||||||
|
import "io"
|
||||||
|
import "bytes"
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
func (p *EmojyPlugin) emojyNav() templ.Component {
|
||||||
|
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
templ_7745c5c3_Buffer = templ.GetBuffer()
|
||||||
|
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var1 == nil {
|
||||||
|
templ_7745c5c3_Var1 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<ul class=\"menu\"><li><a href=\"/emojy/stats\">Stats</a></li><li><a href=\"/emojy/list\">List</a></li><li><a href=\"/emojy/new\">Upload</a></li></ul>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
|
||||||
|
}
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *EmojyPlugin) statsIndex(emojy emojyMap) templ.Component {
|
||||||
|
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
templ_7745c5c3_Buffer = templ.GetBuffer()
|
||||||
|
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var2 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var2 == nil {
|
||||||
|
templ_7745c5c3_Var2 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"grid-container\"><div class=\"grid-x\"><div class=\"cell\"><h2>Emojy</h2></div></div><div class=\"grid-x\"><div class=\"cell\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = p.emojyNav().Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div></div><div class=\"cell\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
for categoryName, v := range emojy {
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<ul class=\"no-bullet\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
for _, c := range v {
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<li class=\"\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var3 string
|
||||||
|
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", c.Count))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `plugins/emojy/stats.templ`, Line: 35, Col: 52}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" - ")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if categoryName != "emoji" && c.OnServer {
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<span>✅</span>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
} else if categoryName != "emoji" {
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<span>✅</span>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if c.URL != "" {
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<img src=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(c.URL))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\" alt=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(c.Emojy))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
var templ_7745c5c3_Var4 string
|
||||||
|
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(c.Emojy)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `plugins/emojy/stats.templ`, Line: 44, Col: 37}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</li>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</ul>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div></div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
|
||||||
|
}
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
package emojy
|
||||||
|
|
||||||
|
templ (p *EmojyPlugin) uploadIndex() {
|
||||||
|
<div class="grid-container">
|
||||||
|
<div class="grid-x">
|
||||||
|
<div class="cell">
|
||||||
|
<h2>Emojy</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid-x">
|
||||||
|
<div class="cell">
|
||||||
|
@p.emojyNav()
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid-x">
|
||||||
|
<div class="cell">
|
||||||
|
<label>Passphrase</label>
|
||||||
|
<input type="text" name="password" placeholder="Password..."></input>
|
||||||
|
</div>
|
||||||
|
<div class="cell">
|
||||||
|
<label>File
|
||||||
|
<input type="file" />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="cell">
|
||||||
|
<button class="button" hx-post="/emojy/upload">Submit</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
|
@ -0,0 +1,43 @@
|
||||||
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
|
// templ: version: v0.2.543
|
||||||
|
package emojy
|
||||||
|
|
||||||
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|
||||||
|
import "github.com/a-h/templ"
|
||||||
|
import "context"
|
||||||
|
import "io"
|
||||||
|
import "bytes"
|
||||||
|
|
||||||
|
func (p *EmojyPlugin) uploadIndex() templ.Component {
|
||||||
|
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
templ_7745c5c3_Buffer = templ.GetBuffer()
|
||||||
|
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var1 == nil {
|
||||||
|
templ_7745c5c3_Var1 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"grid-container\"><div class=\"grid-x\"><div class=\"cell\"><h2>Emojy</h2></div></div><div class=\"grid-x\"><div class=\"cell\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = p.emojyNav().Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div></div><div class=\"grid-x\"><div class=\"cell\"><label>Passphrase</label> <input type=\"text\" name=\"password\" placeholder=\"Password...\"></div><div class=\"cell\"><label>File <input type=\"file\"></label></div><div class=\"cell\"><button class=\"button\" hx-post=\"/emojy/upload\">Submit</button></div></div></div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
|
||||||
|
}
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
})
|
||||||
|
}
|
|
@ -25,15 +25,40 @@ func (p *EmojyPlugin) registerWeb() {
|
||||||
r.HandleFunc("/allFiles", p.handleAllFiles)
|
r.HandleFunc("/allFiles", p.handleAllFiles)
|
||||||
r.HandleFunc("/upload", p.handleUpload)
|
r.HandleFunc("/upload", p.handleUpload)
|
||||||
r.HandleFunc("/file/{name}", p.handleEmojy)
|
r.HandleFunc("/file/{name}", p.handleEmojy)
|
||||||
r.HandleFunc("/stats", p.handlePage("stats.html"))
|
r.HandleFunc("/stats", p.handleStats)
|
||||||
r.HandleFunc("/list", p.handlePage("list.html"))
|
r.HandleFunc("/list", p.handleList)
|
||||||
r.HandleFunc("/new", p.handlePage("upload.html"))
|
r.HandleFunc("/new", p.handleUploadForm)
|
||||||
r.HandleFunc("/", p.handleIndex)
|
r.HandleFunc("/", p.handleStats)
|
||||||
p.b.RegisterWebName(r, "/emojy", "Emojys")
|
p.b.GetWeb().RegisterWebName(r, "/emojy", "Emojys")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *EmojyPlugin) handleIndex(w http.ResponseWriter, r *http.Request) {
|
type emojyMap map[string][]EmojyCount
|
||||||
http.Redirect(w, r, "/emojy/stats", http.StatusPermanentRedirect)
|
|
||||||
|
func (p *EmojyPlugin) handleUploadForm(w http.ResponseWriter, r *http.Request) {
|
||||||
|
p.b.GetWeb().Index("Emojy", p.uploadIndex()).Render(r.Context(), w)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *EmojyPlugin) handleList(w http.ResponseWriter, r *http.Request) {
|
||||||
|
threshold := p.c.GetInt("emojy.statthreshold", 1)
|
||||||
|
emojy, err := p.allCounts(threshold)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(w, "Error: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
p.b.GetWeb().Index("Emojy", p.listTempl(emojy)).Render(r.Context(), w)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *EmojyPlugin) handleStats(w http.ResponseWriter, r *http.Request) {
|
||||||
|
threshold := p.c.GetInt("emojy.statthreshold", 1)
|
||||||
|
emojy, err := p.allCounts(threshold)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(500)
|
||||||
|
log.Error().Err(err).Msgf("handleAll")
|
||||||
|
out, _ := json.Marshal(struct{ err error }{err})
|
||||||
|
w.Write(out)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
p.b.GetWeb().Index("Emojy", p.statsIndex(emojy)).Render(r.Context(), w)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *EmojyPlugin) handlePage(file string) func(w http.ResponseWriter, r *http.Request) {
|
func (p *EmojyPlugin) handlePage(file string) func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
@ -74,18 +99,16 @@ func (p *EmojyPlugin) handleAllFiles(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *EmojyPlugin) handleUpload(w http.ResponseWriter, r *http.Request) {
|
func (p *EmojyPlugin) handleUpload(w http.ResponseWriter, r *http.Request) {
|
||||||
enc := json.NewEncoder(w)
|
|
||||||
newFilePath, err := p.FileSave(r)
|
newFilePath, err := p.FileSave(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msgf("could not upload file")
|
log.Error().Err(err).Msgf("could not upload file")
|
||||||
w.WriteHeader(500)
|
w.WriteHeader(500)
|
||||||
enc.Encode(struct{ err error }{fmt.Errorf("file not saved: %s", err)})
|
fmt.Fprintf(w, "Error with file upload")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.Debug().Msgf("uploaded file to %s", newFilePath)
|
log.Debug().Msgf("uploaded file to %s", newFilePath)
|
||||||
w.WriteHeader(200)
|
w.WriteHeader(200)
|
||||||
enc.Encode(struct{ file string }{newFilePath})
|
fmt.Fprintf(w, "success")
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *EmojyPlugin) FileSave(r *http.Request) (string, error) {
|
func (p *EmojyPlugin) FileSave(r *http.Request) (string, error) {
|
||||||
|
@ -101,7 +124,7 @@ func (p *EmojyPlugin) FileSave(r *http.Request) (string, error) {
|
||||||
return "", fmt.Errorf("no files")
|
return "", fmt.Errorf("no files")
|
||||||
}
|
}
|
||||||
|
|
||||||
password := r.Form.Get("password")
|
password := r.FormValue("password")
|
||||||
if password != p.b.GetPassword() {
|
if password != p.b.GetPassword() {
|
||||||
return "", fmt.Errorf("incorrect password")
|
return "", fmt.Errorf("incorrect password")
|
||||||
}
|
}
|
||||||
|
|
|
@ -50,6 +50,9 @@ func findAlias(db *sqlx.DB, fact string) (bool, *Factoid) {
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
f, err := a.resolve(db)
|
f, err := a.resolve(db)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("findAlias")
|
||||||
|
}
|
||||||
return err == nil, f
|
return err == nil, f
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,58 @@
|
||||||
|
package fact
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
templ (p *FactoidPlugin) factIndex() {
|
||||||
|
<div class="grid-container">
|
||||||
|
<div class="grid-x">
|
||||||
|
<div class="cell">
|
||||||
|
<h2>Factoid</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<form
|
||||||
|
hx-post="/factoid/search"
|
||||||
|
hx-target="#results">
|
||||||
|
<div class="grid-x grid-margin-x">
|
||||||
|
<div class="cell auto">
|
||||||
|
<input type="text"
|
||||||
|
name="query"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="Query..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="cell small-1">
|
||||||
|
<button class="button">Search</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<div class="grid-x" id="results">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
templ (p *FactoidPlugin) searchResults(facts []*Factoid) {
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Fact</th>
|
||||||
|
<th>Tidbit</th>
|
||||||
|
<th>Owner</th>
|
||||||
|
<th>Count</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
for _, f := range facts {
|
||||||
|
@p.searchResult(f)
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
}
|
||||||
|
|
||||||
|
templ (p *FactoidPlugin) searchResult(fact *Factoid) {
|
||||||
|
<tr>
|
||||||
|
<td>{ fact.Fact }</td>
|
||||||
|
<td>{ fact.Tidbit }</td>
|
||||||
|
<td>{ fact.Owner }</td>
|
||||||
|
<td>{ fmt.Sprint(fact.Count) }</td>
|
||||||
|
</tr>
|
||||||
|
}
|
|
@ -0,0 +1,147 @@
|
||||||
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
|
// templ: version: v0.2.543
|
||||||
|
package fact
|
||||||
|
|
||||||
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|
||||||
|
import "github.com/a-h/templ"
|
||||||
|
import "context"
|
||||||
|
import "io"
|
||||||
|
import "bytes"
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
func (p *FactoidPlugin) factIndex() templ.Component {
|
||||||
|
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
templ_7745c5c3_Buffer = templ.GetBuffer()
|
||||||
|
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var1 == nil {
|
||||||
|
templ_7745c5c3_Var1 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"grid-container\"><div class=\"grid-x\"><div class=\"cell\"><h2>Factoid</h2></div></div><form hx-post=\"/factoid/search\" hx-target=\"#results\"><div class=\"grid-x grid-margin-x\"><div class=\"cell auto\"><input type=\"text\" name=\"query\" class=\"form-control\" placeholder=\"Query...\"></div><div class=\"cell small-1\"><button class=\"button\">Search</button></div></div></form><div class=\"grid-x\" id=\"results\"></div></div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
|
||||||
|
}
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *FactoidPlugin) searchResults(facts []*Factoid) templ.Component {
|
||||||
|
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
templ_7745c5c3_Buffer = templ.GetBuffer()
|
||||||
|
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var2 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var2 == nil {
|
||||||
|
templ_7745c5c3_Var2 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<table class=\"table\"><thead><tr><th>Fact</th><th>Tidbit</th><th>Owner</th><th>Count</th></tr></thead> <tbody>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
for _, f := range facts {
|
||||||
|
templ_7745c5c3_Err = p.searchResult(f).Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</tbody></table>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
|
||||||
|
}
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *FactoidPlugin) searchResult(fact *Factoid) templ.Component {
|
||||||
|
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
templ_7745c5c3_Buffer = templ.GetBuffer()
|
||||||
|
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var3 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var3 == nil {
|
||||||
|
templ_7745c5c3_Var3 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<tr><td>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var4 string
|
||||||
|
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(fact.Fact)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `plugins/fact/fact.templ`, Line: 52, Col: 23}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</td><td>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var5 string
|
||||||
|
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(fact.Tidbit)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `plugins/fact/fact.templ`, Line: 53, Col: 25}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</td><td>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var6 string
|
||||||
|
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(fact.Owner)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `plugins/fact/fact.templ`, Line: 54, Col: 24}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</td><td>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var7 string
|
||||||
|
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprint(fact.Count))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `plugins/fact/fact.templ`, Line: 55, Col: 36}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</td></tr>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
|
||||||
|
}
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
})
|
||||||
|
}
|
|
@ -90,21 +90,8 @@ func New(botInst bot.Bot) *FactoidPlugin {
|
||||||
|
|
||||||
// findAction simply regexes a string for the action verb
|
// findAction simply regexes a string for the action verb
|
||||||
func findAction(message string) string {
|
func findAction(message string) string {
|
||||||
r, err := regexp.Compile("<.+?>")
|
r := regexp.MustCompile("<.+?>")
|
||||||
if err != nil {
|
return r.FindString(message)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// learnFact assumes we have a learning situation and inserts a new fact
|
// learnFact assumes we have a learning situation and inserts a new fact
|
||||||
|
@ -153,7 +140,7 @@ func (p *FactoidPlugin) learnFact(message msg.Message, fact, verb, tidbit string
|
||||||
|
|
||||||
// findTrigger checks to see if a given string is a trigger or not
|
// findTrigger checks to see if a given string is a trigger or not
|
||||||
func (p *FactoidPlugin) findTrigger(fact string) (bool, *Factoid) {
|
func (p *FactoidPlugin) findTrigger(fact string) (bool, *Factoid) {
|
||||||
fact = strings.ToLower(fact) // TODO: make sure this needs to be lowered here
|
fact = strings.TrimSpace(strings.ToLower(fact))
|
||||||
|
|
||||||
f, err := GetSingleFact(p.db, fact)
|
f, err := GetSingleFact(p.db, fact)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -465,7 +452,6 @@ func (p *FactoidPlugin) register() {
|
||||||
|
|
||||||
log.Debug().Msgf("Message: %+v", r)
|
log.Debug().Msgf("Message: %+v", r)
|
||||||
|
|
||||||
// This plugin has no business with normal messages
|
|
||||||
if !message.Command {
|
if !message.Command {
|
||||||
// look for any triggers in the db matching this message
|
// look for any triggers in the db matching this message
|
||||||
return p.trigger(c, message)
|
return p.trigger(c, message)
|
||||||
|
@ -485,18 +471,7 @@ func (p *FactoidPlugin) register() {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
notFound := p.c.GetArray("fact.notfound", []string{
|
return false
|
||||||
"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
|
|
||||||
}},
|
}},
|
||||||
}
|
}
|
||||||
p.b.RegisterTable(p, p.handlers)
|
p.b.RegisterTable(p, p.handlers)
|
||||||
|
|
|
@ -2,7 +2,6 @@ package fact
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"embed"
|
"embed"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
"html/template"
|
"html/template"
|
||||||
|
@ -16,10 +15,10 @@ var embeddedFS embed.FS
|
||||||
// Register any web URLs desired
|
// Register any web URLs desired
|
||||||
func (p *FactoidPlugin) registerWeb() {
|
func (p *FactoidPlugin) registerWeb() {
|
||||||
r := chi.NewRouter()
|
r := chi.NewRouter()
|
||||||
r.HandleFunc("/api", p.serveAPI)
|
r.Post("/search", p.handleSearch)
|
||||||
r.HandleFunc("/req", p.serveQuery)
|
r.HandleFunc("/req", p.serveQuery)
|
||||||
r.HandleFunc("/", p.serveQuery)
|
r.HandleFunc("/", p.serveQuery)
|
||||||
p.b.RegisterWebName(r, "/factoid", "Factoid")
|
p.b.GetWeb().RegisterWebName(r, "/factoid", "Factoid")
|
||||||
}
|
}
|
||||||
|
|
||||||
func linkify(text string) template.HTML {
|
func linkify(text string) template.HTML {
|
||||||
|
@ -32,39 +31,19 @@ func linkify(text string) template.HTML {
|
||||||
return template.HTML(strings.Join(parts, " "))
|
return template.HTML(strings.Join(parts, " "))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *FactoidPlugin) serveAPI(w http.ResponseWriter, r *http.Request) {
|
func (p *FactoidPlugin) handleSearch(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != http.MethodPost {
|
query := r.FormValue("query")
|
||||||
fmt.Fprintf(w, "Incorrect HTTP method")
|
|
||||||
return
|
entries, err := getFacts(p.db, query, "")
|
||||||
}
|
|
||||||
info := struct {
|
|
||||||
Query string `json:"query"`
|
|
||||||
}{}
|
|
||||||
decoder := json.NewDecoder(r.Body)
|
|
||||||
err := decoder.Decode(&info)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
w.WriteHeader(500)
|
w.WriteHeader(500)
|
||||||
fmt.Fprint(w, err)
|
fmt.Fprint(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
entries, err := getFacts(p.db, info.Query, "")
|
p.searchResults(entries).Render(r.Context(), w)
|
||||||
if err != nil {
|
|
||||||
w.WriteHeader(500)
|
|
||||||
fmt.Fprint(w, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
data, err := json.Marshal(entries)
|
|
||||||
if err != nil {
|
|
||||||
w.WriteHeader(500)
|
|
||||||
fmt.Fprint(w, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.Write(data)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *FactoidPlugin) serveQuery(w http.ResponseWriter, r *http.Request) {
|
func (p *FactoidPlugin) serveQuery(w http.ResponseWriter, r *http.Request) {
|
||||||
index, _ := embeddedFS.ReadFile("index.html")
|
p.b.GetWeb().Index("Fact", p.factIndex()).Render(r.Context(), w)
|
||||||
w.Write(index)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -81,5 +81,5 @@ func (p *GitPlugin) registerWeb() {
|
||||||
r.HandleFunc("/gitea/event", p.giteaEvent)
|
r.HandleFunc("/gitea/event", p.giteaEvent)
|
||||||
r.HandleFunc("/github/event", p.githubEvent)
|
r.HandleFunc("/github/event", p.githubEvent)
|
||||||
r.HandleFunc("/gitlab/event", p.gitlabEvent)
|
r.HandleFunc("/gitlab/event", p.gitlabEvent)
|
||||||
p.b.RegisterWeb(r, "/git")
|
p.b.GetWeb().RegisterWeb(r, "/git")
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,50 @@
|
||||||
|
package gpt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
import "github.com/andrewstuart/openai"
|
||||||
|
|
||||||
|
var session openai.ChatSession
|
||||||
|
var client *openai.Client
|
||||||
|
|
||||||
|
func (p *GPTPlugin) getClient() (*openai.Client, error) {
|
||||||
|
token := p.c.Get("gpt.token", "")
|
||||||
|
if token == "" {
|
||||||
|
return nil, fmt.Errorf("no GPT token given")
|
||||||
|
}
|
||||||
|
return openai.NewClient(token)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *GPTPlugin) chatGPT(request string) (string, error) {
|
||||||
|
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) getDefaultPrompt() string {
|
||||||
|
return p.c.Get("gpt.prompt", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *GPTPlugin) setPrompt(prompt string) error {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,259 @@
|
||||||
|
package gpt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"math/rand"
|
||||||
|
"net/http"
|
||||||
|
"reflect"
|
||||||
|
"regexp"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"github.com/velour/catbase/bot"
|
||||||
|
"github.com/velour/catbase/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
const gpt3URL = "https://api.openai.com/v1/engines/%s/completions"
|
||||||
|
const gpt3ModURL = "https://api.openai.com/v1/moderations"
|
||||||
|
|
||||||
|
type GPTPlugin struct {
|
||||||
|
b bot.Bot
|
||||||
|
c *config.Config
|
||||||
|
h bot.HandlerTable
|
||||||
|
|
||||||
|
chatCount int
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(b bot.Bot) *GPTPlugin {
|
||||||
|
p := &GPTPlugin{
|
||||||
|
b: b,
|
||||||
|
c: b.Config(),
|
||||||
|
}
|
||||||
|
p.register()
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *GPTPlugin) register() {
|
||||||
|
p.h = bot.HandlerTable{
|
||||||
|
{
|
||||||
|
Kind: bot.Message, IsCmd: true,
|
||||||
|
Regex: regexp.MustCompile(`(?is)^gpt3 (?P<text>.*)`),
|
||||||
|
HelpText: "request text completion",
|
||||||
|
Handler: p.message,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Kind: bot.Message, IsCmd: true,
|
||||||
|
Regex: regexp.MustCompile(`(?is)^gpt (?P<text>.*)`),
|
||||||
|
HelpText: "chat completion",
|
||||||
|
Handler: p.chatMessageForce,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Kind: bot.Message, IsCmd: true,
|
||||||
|
Regex: regexp.MustCompile(`(?is)^gpt-prompt: (?P<text>.*)`),
|
||||||
|
HelpText: "set the ChatGPT prompt",
|
||||||
|
Handler: p.setPromptMessage,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Kind: bot.Message, IsCmd: true,
|
||||||
|
Regex: regexp.MustCompile(`(?P<text>.*)`),
|
||||||
|
Handler: p.chatMessage,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
p.b.RegisterTable(p, p.h)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *GPTPlugin) setPromptMessage(r bot.Request) bool {
|
||||||
|
prompt := r.Values["text"]
|
||||||
|
if err := p.setPrompt(prompt); err != nil {
|
||||||
|
resp := fmt.Sprintf("Error: %s", err)
|
||||||
|
p.b.Send(r.Conn, bot.Message, r.Msg.Channel, resp)
|
||||||
|
}
|
||||||
|
p.b.Send(r.Conn, bot.Message, r.Msg.Channel, fmt.Sprintf(`Okay. I set the prompt to: "%s"`, prompt))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *GPTPlugin) chatMessage(r bot.Request) bool {
|
||||||
|
if slices.Contains(p.c.GetArray("gpt.silence", []string{}), r.Msg.Channel) {
|
||||||
|
log.Debug().Msgf("%s silenced", r.Msg.Channel)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return p.chatMessageForce(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *GPTPlugin) chatMessageForce(r bot.Request) bool {
|
||||||
|
resp, err := p.chatGPT(r.Values["text"])
|
||||||
|
if err != nil {
|
||||||
|
resp = fmt.Sprintf("Error: %s", err)
|
||||||
|
}
|
||||||
|
p.b.Send(r.Conn, bot.Message, r.Msg.Channel, resp)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *GPTPlugin) message(r bot.Request) bool {
|
||||||
|
stem := r.Values["text"]
|
||||||
|
p.b.Send(r.Conn, bot.Message, r.Msg.Channel, p.gpt3(stem))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *GPTPlugin) gpt3(stem string) string {
|
||||||
|
log.Debug().Msgf("Got GPT3 request: %s", stem)
|
||||||
|
if err := p.checkStem(stem); err != nil {
|
||||||
|
return "GPT3 moderation " + err.Error()
|
||||||
|
}
|
||||||
|
postStruct := gpt3Request{
|
||||||
|
Prompt: stem,
|
||||||
|
MaxTokens: p.c.GetInt("gpt3.tokens", 16),
|
||||||
|
Temperature: p.c.GetFloat64("gpt3.temperature", 1),
|
||||||
|
TopP: p.c.GetFloat64("gpt3.top_p", 1),
|
||||||
|
N: p.c.GetInt("gpt3.n", 1),
|
||||||
|
Stop: p.c.GetArray("gpt3.stop", []string{"\n"}),
|
||||||
|
Echo: p.c.GetBool("gpt3.echo", false),
|
||||||
|
}
|
||||||
|
val, err := p.mkRequest(gpt3URL, postStruct)
|
||||||
|
if err != nil {
|
||||||
|
return err.Error()
|
||||||
|
}
|
||||||
|
choices := val.(gpt3Response).Choices
|
||||||
|
if len(choices) > 0 {
|
||||||
|
return choices[rand.Intn(len(choices))].Text
|
||||||
|
}
|
||||||
|
return "OpenAI is too shitty to respond to that."
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *GPTPlugin) mkRequest(endPoint string, postStruct interface{}) (interface{}, error) {
|
||||||
|
postBody, _ := json.Marshal(postStruct)
|
||||||
|
client := &http.Client{}
|
||||||
|
u := fmt.Sprintf(endPoint, p.c.Get("gpt3.engine", "ada"))
|
||||||
|
req, err := http.NewRequest("POST", u, bytes.NewBuffer(postBody))
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("could not make gpt3 request")
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
gpt3Key := p.c.Get("gpt3.bearer", "")
|
||||||
|
if gpt3Key == "" {
|
||||||
|
log.Error().Msgf("no GPT3 key given")
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req.Header.Add("Content-Type", "application/json")
|
||||||
|
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", gpt3Key))
|
||||||
|
res, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
resBody, _ := io.ReadAll(res.Body)
|
||||||
|
gpt3Resp := gpt3Response{}
|
||||||
|
err = json.Unmarshal(resBody, &gpt3Resp)
|
||||||
|
|
||||||
|
log.Debug().
|
||||||
|
Str("body", string(resBody)).
|
||||||
|
Interface("resp", gpt3Resp).
|
||||||
|
Msg("OpenAI Response")
|
||||||
|
|
||||||
|
return gpt3Resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *GPTPlugin) checkStem(stem string) error {
|
||||||
|
if !p.c.GetBool("gpt3.moderation", true) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
postBody, _ := json.Marshal(gpt3ModRequest{Input: stem})
|
||||||
|
client := &http.Client{}
|
||||||
|
req, err := http.NewRequest("POST", gpt3ModURL, bytes.NewBuffer(postBody))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
gpt3Key := p.c.Get("gpt3.bearer", "")
|
||||||
|
if gpt3Key == "" {
|
||||||
|
return fmt.Errorf("no GPT3 API key")
|
||||||
|
}
|
||||||
|
req.Header.Add("Content-Type", "application/json")
|
||||||
|
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", gpt3Key))
|
||||||
|
res, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
resBody, _ := io.ReadAll(res.Body)
|
||||||
|
log.Debug().Str("resBody", string(resBody)).Msg("res")
|
||||||
|
gpt3Resp := gpt3Moderation{}
|
||||||
|
err = json.Unmarshal(resBody, &gpt3Resp)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.Debug().Interface("GPT3 Moderation", gpt3Resp).Msg("Moderation result")
|
||||||
|
for _, res := range gpt3Resp.Results {
|
||||||
|
if res.Flagged {
|
||||||
|
list := ""
|
||||||
|
categories := reflect.ValueOf(res.Categories)
|
||||||
|
fields := reflect.VisibleFields(reflect.TypeOf(res.Categories))
|
||||||
|
for i := 0; i < categories.NumField(); i++ {
|
||||||
|
if categories.Field(i).Bool() {
|
||||||
|
list += fields[i].Name + ", "
|
||||||
|
}
|
||||||
|
}
|
||||||
|
list = strings.TrimSuffix(list, ", ")
|
||||||
|
return fmt.Errorf("flagged: %s", list)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type gpt3Request struct {
|
||||||
|
Prompt string `json:"prompt"`
|
||||||
|
MaxTokens int `json:"max_tokens"`
|
||||||
|
Temperature float64 `json:"temperature"`
|
||||||
|
TopP float64 `json:"top_p"`
|
||||||
|
N int `json:"n"`
|
||||||
|
Stream bool `json:"stream"`
|
||||||
|
Logprobs any `json:"logprobs"`
|
||||||
|
Stop []string `json:"stop"`
|
||||||
|
Echo bool `json:"echo"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type gpt3ModRequest struct {
|
||||||
|
Input string `json:"input"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type gpt3Response struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Object string `json:"object"`
|
||||||
|
Created int `json:"created"`
|
||||||
|
Model string `json:"model"`
|
||||||
|
Choices []struct {
|
||||||
|
Text string `json:"text"`
|
||||||
|
Index int `json:"index"`
|
||||||
|
Logprobs any `json:"logprobs"`
|
||||||
|
FinishReason string `json:"finish_reason"`
|
||||||
|
} `json:"choices"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type gpt3Moderation struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Model string `json:"model"`
|
||||||
|
Results []struct {
|
||||||
|
Categories struct {
|
||||||
|
Hate bool `json:"hate"`
|
||||||
|
HateThreatening bool `json:"hate/threatening"`
|
||||||
|
SelfHarm bool `json:"self-harm"`
|
||||||
|
Sexual bool `json:"sexual"`
|
||||||
|
SexualMinors bool `json:"sexual/minors"`
|
||||||
|
Violence bool `json:"violence"`
|
||||||
|
ViolenceGraphic bool `json:"violence/graphic"`
|
||||||
|
} `json:"categories"`
|
||||||
|
CategoryScores struct {
|
||||||
|
Hate float64 `json:"hate"`
|
||||||
|
HateThreatening float64 `json:"hate/threatening"`
|
||||||
|
SelfHarm float64 `json:"self-harm"`
|
||||||
|
Sexual float64 `json:"sexual"`
|
||||||
|
SexualMinors float64 `json:"sexual/minors"`
|
||||||
|
Violence float64 `json:"violence"`
|
||||||
|
ViolenceGraphic float64 `json:"violence/graphic"`
|
||||||
|
} `json:"category_scores"`
|
||||||
|
Flagged bool `json:"flagged"`
|
||||||
|
} `json:"results"`
|
||||||
|
}
|
|
@ -1,120 +0,0 @@
|
||||||
package gpt3
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"math/rand"
|
|
||||||
"net/http"
|
|
||||||
"regexp"
|
|
||||||
|
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
"github.com/velour/catbase/bot"
|
|
||||||
"github.com/velour/catbase/config"
|
|
||||||
)
|
|
||||||
|
|
||||||
const gpt3URL = "https://api.openai.com/v1/engines/%s/completions"
|
|
||||||
|
|
||||||
type GPT3Plugin struct {
|
|
||||||
b bot.Bot
|
|
||||||
c *config.Config
|
|
||||||
h bot.HandlerTable
|
|
||||||
}
|
|
||||||
|
|
||||||
func New(b bot.Bot) *GPT3Plugin {
|
|
||||||
p := &GPT3Plugin{
|
|
||||||
b: b,
|
|
||||||
c: b.Config(),
|
|
||||||
}
|
|
||||||
p.register()
|
|
||||||
return p
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *GPT3Plugin) register() {
|
|
||||||
p.h = bot.HandlerTable{
|
|
||||||
{
|
|
||||||
Kind: bot.Message, IsCmd: true,
|
|
||||||
Regex: regexp.MustCompile(`(?is)^gpt3 (?P<text>.*)`),
|
|
||||||
HelpText: "request text completion",
|
|
||||||
Handler: p.message,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
log.Debug().Msg("Registering GPT3 handlers")
|
|
||||||
p.b.RegisterTable(p, p.h)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *GPT3Plugin) message(r bot.Request) bool {
|
|
||||||
stem := r.Values["text"]
|
|
||||||
log.Debug().Msgf("Got GPT3 request: %s", stem)
|
|
||||||
postStruct := gpt3Request{
|
|
||||||
Prompt: stem,
|
|
||||||
MaxTokens: p.c.GetInt("gpt3.tokens", 16),
|
|
||||||
Temperature: p.c.GetFloat64("gpt3.temperature", 1),
|
|
||||||
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,
|
|
||||||
}
|
|
||||||
postBody, _ := json.Marshal(postStruct)
|
|
||||||
client := &http.Client{}
|
|
||||||
u := fmt.Sprintf(gpt3URL, p.c.Get("gpt3.engine", "ada"))
|
|
||||||
req, err := http.NewRequest("POST", u, bytes.NewBuffer(postBody))
|
|
||||||
if err != nil {
|
|
||||||
log.Error().Err(err).Msg("could not make gpt3 request")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
gpt3Key := p.c.Get("gpt3.bearer", "")
|
|
||||||
if gpt3Key == "" {
|
|
||||||
log.Error().Msgf("no GPT3 key given")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
req.Header.Add("Content-Type", "application/json")
|
|
||||||
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", gpt3Key))
|
|
||||||
res, err := client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
log.Error().Err(err).Msg("could not make gpt3 request")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
resBody, _ := ioutil.ReadAll(res.Body)
|
|
||||||
gpt3Resp := gpt3Response{}
|
|
||||||
err = json.Unmarshal(resBody, &gpt3Resp)
|
|
||||||
|
|
||||||
log.Debug().
|
|
||||||
Str("body", string(resBody)).
|
|
||||||
Interface("resp", gpt3Resp).
|
|
||||||
Msg("OpenAI Response")
|
|
||||||
|
|
||||||
msg := "OpenAI is too shitty to respond to that."
|
|
||||||
if len(gpt3Resp.Choices) > 0 {
|
|
||||||
msg = gpt3Resp.Choices[rand.Intn(len(gpt3Resp.Choices))].Text
|
|
||||||
}
|
|
||||||
p.b.Send(r.Conn, bot.Message, r.Msg.Channel, msg)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
type gpt3Request struct {
|
|
||||||
Prompt string `json:"prompt"`
|
|
||||||
MaxTokens int `json:"max_tokens"`
|
|
||||||
Temperature float64 `json:"temperature"`
|
|
||||||
TopP float64 `json:"top_p"`
|
|
||||||
N int `json:"n"`
|
|
||||||
Stream bool `json:"stream"`
|
|
||||||
Logprobs any `json:"logprobs"`
|
|
||||||
Stop []string `json:"stop"`
|
|
||||||
Echo bool `json:"echo"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type gpt3Response struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
Object string `json:"object"`
|
|
||||||
Created int `json:"created"`
|
|
||||||
Model string `json:"model"`
|
|
||||||
Choices []struct {
|
|
||||||
Text string `json:"text"`
|
|
||||||
Index int `json:"index"`
|
|
||||||
Logprobs any `json:"logprobs"`
|
|
||||||
FinishReason string `json:"finish_reason"`
|
|
||||||
} `json:"choices"`
|
|
||||||
}
|
|
|
@ -5,8 +5,6 @@ package leftpad
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/velour/catbase/plugins/cli"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/velour/catbase/bot"
|
"github.com/velour/catbase/bot"
|
||||||
"github.com/velour/catbase/bot/msg"
|
"github.com/velour/catbase/bot/msg"
|
||||||
|
@ -18,7 +16,6 @@ func makeMessage(payload string) bot.Request {
|
||||||
values := bot.ParseValues(leftpadRegex, payload)
|
values := bot.ParseValues(leftpadRegex, payload)
|
||||||
return bot.Request{
|
return bot.Request{
|
||||||
Kind: bot.Message,
|
Kind: bot.Message,
|
||||||
Conn: &cli.CliPlugin{},
|
|
||||||
Values: values,
|
Values: values,
|
||||||
Msg: msg.Message{
|
Msg: msg.Message{
|
||||||
User: &user.User{Name: "tester"},
|
User: &user.User{Name: "tester"},
|
||||||
|
|
|
@ -1,213 +0,0 @@
|
||||||
<!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="https://unpkg.com/vue-router@^2"></script>
|
|
||||||
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<title>Memes</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<div id="app">
|
|
||||||
<b-navbar>
|
|
||||||
<b-navbar-brand>Memes</b-navbar-brand>
|
|
||||||
<b-navbar-nav>
|
|
||||||
<b-nav-item v-for="item in nav" :href="item.url" :active="item.name === 'Meme'">{{ item.name }}</b-nav-item>
|
|
||||||
</b-navbar-nav>
|
|
||||||
</b-navbar>
|
|
||||||
<b-alert
|
|
||||||
dismissable
|
|
||||||
variant="error"
|
|
||||||
v-if="err"
|
|
||||||
@dismissed="err = ''">
|
|
||||||
{{ err }}
|
|
||||||
</b-alert>
|
|
||||||
<b-form @submit="saveConfig" v-if="editConfig">
|
|
||||||
<b-container>
|
|
||||||
<b-row>
|
|
||||||
<b-col cols="1">
|
|
||||||
Name:
|
|
||||||
</b-col>
|
|
||||||
<b-col>
|
|
||||||
{{ editConfig.name }}
|
|
||||||
</b-col>
|
|
||||||
</b-row>
|
|
||||||
<b-row>
|
|
||||||
<b-col cols="1">
|
|
||||||
Image:
|
|
||||||
</b-col>
|
|
||||||
<b-col>
|
|
||||||
<img :src="editConfig.url" :alt="editConfig.url" rounded block fluid />
|
|
||||||
</b-col>
|
|
||||||
</b-row>
|
|
||||||
<b-row>
|
|
||||||
<b-col cols="1">
|
|
||||||
URL:
|
|
||||||
</b-col>
|
|
||||||
<b-col>
|
|
||||||
<b-input placeholder="URL..." v-model="editConfig.url"></b-input>
|
|
||||||
</b-col>
|
|
||||||
</b-row>
|
|
||||||
<b-row>
|
|
||||||
<b-col cols="1">
|
|
||||||
Config:
|
|
||||||
</b-col>
|
|
||||||
<b-col>
|
|
||||||
<b-form-textarea v-model="editConfig.config" rows="10">
|
|
||||||
</b-form-textarea>
|
|
||||||
</b-col>
|
|
||||||
</b-row>
|
|
||||||
<b-row>
|
|
||||||
<b-button type="submit" variant="primary">Save</b-button>
|
|
||||||
|
|
||||||
<b-button @click="rm" variant="danger">Delete</b-button>
|
|
||||||
|
|
||||||
<b-button type="cancel" @click="editConfig = null" variant="secondary">Cancel</b-button>
|
|
||||||
</b-row>
|
|
||||||
</b-container>
|
|
||||||
</b-form>
|
|
||||||
<b-form @submit="addMeme" v-if="!editConfig">
|
|
||||||
<b-container>
|
|
||||||
<b-row>
|
|
||||||
<b-col cols="3">
|
|
||||||
<b-input placeholder="Name..." v-model="name"></b-input>
|
|
||||||
</b-col>
|
|
||||||
<b-col cols="3">
|
|
||||||
<b-input placeholder="URL..." v-model="url"></b-input>
|
|
||||||
</b-col>
|
|
||||||
<b-col cols="3">
|
|
||||||
<b-input placeholder="Config..." v-model="config"></b-input>
|
|
||||||
</b-col>
|
|
||||||
<b-col cols="3">
|
|
||||||
<b-button type="submit">Add Meme</b-button>
|
|
||||||
</b-col>
|
|
||||||
</b-row>
|
|
||||||
<b-row>
|
|
||||||
<b-col>
|
|
||||||
<b-table
|
|
||||||
fixed
|
|
||||||
:items="results"
|
|
||||||
:fields="fields">
|
|
||||||
<template v-slot:cell(config)="data">
|
|
||||||
<pre>{{data.item.config | pretty}}</pre>
|
|
||||||
<b-button @click="startEdit(data.item)">Edit</b-button>
|
|
||||||
</template>
|
|
||||||
<template v-slot:cell(image)="data">
|
|
||||||
<b-img :src="data.item.url" rounded block fluid />
|
|
||||||
</template>
|
|
||||||
</b-table>
|
|
||||||
</b-col>
|
|
||||||
</b-row>
|
|
||||||
</b-container>
|
|
||||||
</b-form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
var router = new VueRouter({
|
|
||||||
mode: 'history',
|
|
||||||
routes: []
|
|
||||||
});
|
|
||||||
var app = new Vue({
|
|
||||||
el: '#app',
|
|
||||||
router,
|
|
||||||
data: {
|
|
||||||
err: '',
|
|
||||||
nav: [],
|
|
||||||
name: '',
|
|
||||||
url: '',
|
|
||||||
config: '',
|
|
||||||
results: [],
|
|
||||||
editConfig: null,
|
|
||||||
fields: [
|
|
||||||
{ key: 'name', sortable: true },
|
|
||||||
{ key: 'config' },
|
|
||||||
{ key: 'image' }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
axios.get('/nav')
|
|
||||||
.then(resp => {
|
|
||||||
this.nav = resp.data;
|
|
||||||
})
|
|
||||||
.catch(err => console.log(err))
|
|
||||||
this.refresh();
|
|
||||||
},
|
|
||||||
filters: {
|
|
||||||
pretty: function(value) {
|
|
||||||
if (!value) {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return JSON.stringify(JSON.parse(value), null, 2);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
refresh: function() {
|
|
||||||
axios.get('/meme/all')
|
|
||||||
.catch(err => (this.err = err))
|
|
||||||
.then(resp => {
|
|
||||||
this.results = resp.data
|
|
||||||
})
|
|
||||||
},
|
|
||||||
addMeme: function(evt) {
|
|
||||||
if (evt) {
|
|
||||||
evt.preventDefault();
|
|
||||||
evt.stopPropagation()
|
|
||||||
}
|
|
||||||
if (this.name && this.url)
|
|
||||||
axios.post('/meme/add', {name: this.name, url: this.url, config: this.config})
|
|
||||||
.then(resp => {
|
|
||||||
this.results = resp.data;
|
|
||||||
this.name = "";
|
|
||||||
this.url = "";
|
|
||||||
this.config = "";
|
|
||||||
this.refresh();
|
|
||||||
})
|
|
||||||
.catch(err => (this.err = err));
|
|
||||||
},
|
|
||||||
startEdit: function(item) {
|
|
||||||
this.editConfig = item;
|
|
||||||
},
|
|
||||||
saveConfig: function(evt) {
|
|
||||||
if (evt) {
|
|
||||||
evt.preventDefault();
|
|
||||||
evt.stopPropagation();
|
|
||||||
}
|
|
||||||
if (this.editConfig && this.editConfig.name && this.editConfig.url) {
|
|
||||||
axios.post('/meme/add', this.editConfig)
|
|
||||||
.then(resp => {
|
|
||||||
this.results = resp.data;
|
|
||||||
this.editConfig = null;
|
|
||||||
this.refresh();
|
|
||||||
})
|
|
||||||
.catch(err => this.err = err);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
rm: function(evt) {
|
|
||||||
if (evt) {
|
|
||||||
evt.preventDefault();
|
|
||||||
evt.stopPropagation();
|
|
||||||
}
|
|
||||||
if (confirm("Are you sure you want to delete this meme?")) {
|
|
||||||
axios.delete('/meme/rm', { data: this.editConfig })
|
|
||||||
.then(resp => {
|
|
||||||
this.editConfig = null;
|
|
||||||
this.refresh();
|
|
||||||
})
|
|
||||||
.catch(err => this.err = err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/google/uuid"
|
||||||
"image"
|
"image"
|
||||||
"image/color"
|
"image/color"
|
||||||
"image/draw"
|
"image/draw"
|
||||||
|
@ -209,7 +210,7 @@ func (p *MemePlugin) sendMeme(c bot.Connector, channel, channelName, msgID strin
|
||||||
|
|
||||||
encodedSpec, _ := json.Marshal(spec)
|
encodedSpec, _ := json.Marshal(spec)
|
||||||
|
|
||||||
w, h, err := p.checkMeme(imgURL)
|
_, _, err = p.checkMeme(imgURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
msg := fmt.Sprintf("Hey %v, I couldn't download that image you asked for.", from.Name)
|
msg := fmt.Sprintf("Hey %v, I couldn't download that image you asked for.", from.Name)
|
||||||
p.bot.Send(c, bot.Ephemeral, channel, from.ID, msg)
|
p.bot.Send(c, bot.Ephemeral, channel, from.ID, msg)
|
||||||
|
@ -222,12 +223,18 @@ func (p *MemePlugin) sendMeme(c bot.Connector, channel, channelName, msgID strin
|
||||||
q.Add("spec", string(encodedSpec))
|
q.Add("spec", string(encodedSpec))
|
||||||
u.RawQuery = q.Encode()
|
u.RawQuery = q.Encode()
|
||||||
|
|
||||||
|
img, err := p.genMeme(spec)
|
||||||
|
if err != nil {
|
||||||
|
msg := fmt.Sprintf("Hey %v, I couldn't download that image you asked for.", from.Name)
|
||||||
|
p.bot.Send(c, bot.Ephemeral, channel, from.ID, msg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
log.Debug().Msgf("image is at %s", u.String())
|
log.Debug().Msgf("image is at %s", u.String())
|
||||||
_, err = p.bot.Send(c, bot.Message, channel, "", bot.ImageAttachment{
|
p.bot.Send(c, bot.Message, channel, fmt.Sprintf("%s sent a meme:", from.Name))
|
||||||
URL: u.String(),
|
_, err = p.bot.Send(c, bot.Message, channel, "", bot.File{
|
||||||
AltTxt: fmt.Sprintf("%s: %s", from.Name, message),
|
Description: uuid.NewString(),
|
||||||
Width: w,
|
Data: img,
|
||||||
Height: h,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
if err == nil && msgID != "" {
|
if err == nil && msgID != "" {
|
||||||
|
@ -306,7 +313,15 @@ var defaultFormats = map[string]string{
|
||||||
"raptor": "https://imgflip.com/s/meme/Philosoraptor.jpg",
|
"raptor": "https://imgflip.com/s/meme/Philosoraptor.jpg",
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *MemePlugin) findFontSize(config []memeText, fontLocation string, w, h int, sizes []float64) float64 {
|
func FindFontSizeConfigs(c *config.Config, configs []memeText, fontLocation string, w, h int, sizes []float64) float64 {
|
||||||
|
texts := []string{}
|
||||||
|
for _, c := range configs {
|
||||||
|
texts = append(texts, c.Text)
|
||||||
|
}
|
||||||
|
return FindFontSize(c, texts, fontLocation, w, h, sizes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func FindFontSize(c *config.Config, config []string, fontLocation string, w, h int, sizes []float64) float64 {
|
||||||
fontSize := 12.0
|
fontSize := 12.0
|
||||||
|
|
||||||
m := gg.NewContext(w, h)
|
m := gg.NewContext(w, h)
|
||||||
|
@ -314,21 +329,21 @@ func (p *MemePlugin) findFontSize(config []memeText, fontLocation string, w, h i
|
||||||
longestStr, longestW := "", 0.0
|
longestStr, longestW := "", 0.0
|
||||||
|
|
||||||
for _, s := range config {
|
for _, s := range config {
|
||||||
err := m.LoadFontFace(fontLocation, 12) // problem
|
err := m.LoadFontFace(GetFont(c, fontLocation), 12)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("could not load font")
|
log.Error().Err(err).Msg("could not load font")
|
||||||
return fontSize
|
return fontSize
|
||||||
}
|
}
|
||||||
|
|
||||||
w, _ := m.MeasureString(s.Text)
|
w, _ := m.MeasureString(s)
|
||||||
if w > longestW {
|
if w > longestW {
|
||||||
longestStr = s.Text
|
longestStr = s
|
||||||
longestW = w
|
longestW = w
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, sz := range sizes {
|
for _, sz := range sizes {
|
||||||
err := m.LoadFontFace(fontLocation, sz) // problem
|
err := m.LoadFontFace(GetFont(c, fontLocation), sz) // problem
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("could not load font")
|
log.Error().Err(err).Msg("could not load font")
|
||||||
return fontSize
|
return fontSize
|
||||||
|
@ -449,6 +464,7 @@ func (p *MemePlugin) genMeme(spec specification) ([]byte, error) {
|
||||||
// Apply black stroke
|
// Apply black stroke
|
||||||
m.SetHexColor("#000")
|
m.SetHexColor("#000")
|
||||||
strokeSize := 6
|
strokeSize := 6
|
||||||
|
fontSize := FindFontSizeConfigs(p.c, spec.Configs, defaultFont, w, h, fontSizes)
|
||||||
for dy := -strokeSize; dy <= strokeSize; dy++ {
|
for dy := -strokeSize; dy <= strokeSize; dy++ {
|
||||||
for dx := -strokeSize; dx <= strokeSize; dx++ {
|
for dx := -strokeSize; dx <= strokeSize; dx++ {
|
||||||
// give it rounded corners
|
// give it rounded corners
|
||||||
|
@ -460,8 +476,7 @@ func (p *MemePlugin) genMeme(spec specification) ([]byte, error) {
|
||||||
if fontLocation == "" {
|
if fontLocation == "" {
|
||||||
fontLocation = defaultFont
|
fontLocation = defaultFont
|
||||||
}
|
}
|
||||||
fontSize := p.findFontSize(spec.Configs, fontLocation, w, h, fontSizes)
|
m.LoadFontFace(GetFont(p.c, fontLocation), fontSize)
|
||||||
m.LoadFontFace(fontLocation, fontSize)
|
|
||||||
x := float64(w)*c.XPerc + float64(dx)
|
x := float64(w)*c.XPerc + float64(dx)
|
||||||
y := float64(h)*c.YPerc + float64(dy)
|
y := float64(h)*c.YPerc + float64(dy)
|
||||||
m.DrawStringAnchored(c.Text, x, y, 0.5, 0.5)
|
m.DrawStringAnchored(c.Text, x, y, 0.5, 0.5)
|
||||||
|
@ -476,8 +491,7 @@ func (p *MemePlugin) genMeme(spec specification) ([]byte, error) {
|
||||||
if fontLocation == "" {
|
if fontLocation == "" {
|
||||||
fontLocation = defaultFont
|
fontLocation = defaultFont
|
||||||
}
|
}
|
||||||
fontSize := p.findFontSize(spec.Configs, fontLocation, w, h, fontSizes)
|
m.LoadFontFace(GetFont(p.c, fontLocation), fontSize)
|
||||||
m.LoadFontFace(fontLocation, fontSize)
|
|
||||||
x := float64(w) * c.XPerc
|
x := float64(w) * c.XPerc
|
||||||
y := float64(h) * c.YPerc
|
y := float64(h) * c.YPerc
|
||||||
m.DrawStringAnchored(c.Text, x, y, 0.5, 0.5)
|
m.DrawStringAnchored(c.Text, x, y, 0.5, 0.5)
|
||||||
|
@ -492,6 +506,15 @@ func (p *MemePlugin) genMeme(spec specification) ([]byte, error) {
|
||||||
return p.images[jsonSpec].repr, nil
|
return p.images[jsonSpec].repr, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
name = file
|
||||||
|
}
|
||||||
|
return path.Join(location, name)
|
||||||
|
}
|
||||||
|
|
||||||
func (p *MemePlugin) applyStamp(img image.Image, bullyURL string) (image.Image, error) {
|
func (p *MemePlugin) applyStamp(img image.Image, bullyURL string) (image.Image, error) {
|
||||||
u, _ := url.Parse(bullyURL)
|
u, _ := url.Parse(bullyURL)
|
||||||
bullyImg, err := DownloadTemplate(u)
|
bullyImg, err := DownloadTemplate(u)
|
||||||
|
|
|
@ -0,0 +1,94 @@
|
||||||
|
package meme
|
||||||
|
|
||||||
|
templ (p *MemePlugin) index(all webResps) {
|
||||||
|
<div class="grid-container">
|
||||||
|
<h2>Meme</h2>
|
||||||
|
<form>
|
||||||
|
<div class="grid-x grid-margin-x">
|
||||||
|
<div class="cell auto">
|
||||||
|
<input type="text" name="name" placeholder="Name..." />
|
||||||
|
</div>
|
||||||
|
<div class="cell auto">
|
||||||
|
<input type="text" name="url" placeholder="URL..." />
|
||||||
|
</div>
|
||||||
|
<div class="cell auto">
|
||||||
|
<textarea name="config">
|
||||||
|
</textarea>
|
||||||
|
</div>
|
||||||
|
<div class="cell small-2">
|
||||||
|
<button class="button"
|
||||||
|
hx-post="/meme/add"
|
||||||
|
hx-target="#newMemes"
|
||||||
|
>Save</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<div id="newMemes">
|
||||||
|
</div>
|
||||||
|
for _, meme := range all {
|
||||||
|
@p.Show(meme)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
templ (p *MemePlugin) Show(meme webResp) {
|
||||||
|
<div class="grid-x grid-margin-x" id={ meme.Name }>
|
||||||
|
<div class="cell small-3">
|
||||||
|
<div class="card"
|
||||||
|
style="max-width: 200px">
|
||||||
|
<img
|
||||||
|
class="thumbnail"
|
||||||
|
style="max-height: 250px; max-width: 250px;"
|
||||||
|
alt={ meme.Name }
|
||||||
|
src={ meme.URL } />
|
||||||
|
<div class="card-divider">
|
||||||
|
<p>{ meme.Name }</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="cell small-7">
|
||||||
|
<pre>
|
||||||
|
{ meme.Config }
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
<div class="cell small-2">
|
||||||
|
<button class="button"
|
||||||
|
hx-get={ "/meme/edit/"+meme.Name }
|
||||||
|
hx-target={ "#"+meme.Name }
|
||||||
|
hx-swap="outerHTML"
|
||||||
|
>Edit</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
templ (p *MemePlugin) Edit(meme webResp) {
|
||||||
|
<form>
|
||||||
|
<div class="grid-x grid-margin-x" id={ meme.Name }>
|
||||||
|
<div class="cell-small-3">
|
||||||
|
<img
|
||||||
|
class="thumbnail"
|
||||||
|
style="max-height: 150px"
|
||||||
|
alt={ meme.Name }
|
||||||
|
src={ meme.URL } />
|
||||||
|
</div>
|
||||||
|
<div class="cell small-7">
|
||||||
|
<textarea name="config" rows="10">
|
||||||
|
{ meme.Config }
|
||||||
|
</textarea>
|
||||||
|
<input type="text" name="url" value={ meme.URL } />
|
||||||
|
</div>
|
||||||
|
<div class="cell small-2">
|
||||||
|
<button class="button"
|
||||||
|
hx-put={ "/meme/save/"+meme.Name }
|
||||||
|
hx-target={ "#"+meme.Name }
|
||||||
|
hx-swap="outerHTML"
|
||||||
|
>Save</button>
|
||||||
|
<button class="button alert"
|
||||||
|
hx-delete={ "/meme/rm/"+meme.Name }
|
||||||
|
hx-target={ "#"+meme.Name }
|
||||||
|
hx-swap="outerHTML"
|
||||||
|
>Delete</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
}
|
|
@ -0,0 +1,236 @@
|
||||||
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
|
// templ: version: v0.2.543
|
||||||
|
package meme
|
||||||
|
|
||||||
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|
||||||
|
import "github.com/a-h/templ"
|
||||||
|
import "context"
|
||||||
|
import "io"
|
||||||
|
import "bytes"
|
||||||
|
|
||||||
|
func (p *MemePlugin) index(all webResps) templ.Component {
|
||||||
|
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
templ_7745c5c3_Buffer = templ.GetBuffer()
|
||||||
|
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var1 == nil {
|
||||||
|
templ_7745c5c3_Var1 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"grid-container\"><h2>Meme</h2><form><div class=\"grid-x grid-margin-x\"><div class=\"cell auto\"><input type=\"text\" name=\"name\" placeholder=\"Name...\"></div><div class=\"cell auto\"><input type=\"text\" name=\"url\" placeholder=\"URL...\"></div><div class=\"cell auto\"><textarea name=\"config\"></textarea></div><div class=\"cell small-2\"><button class=\"button\" hx-post=\"/meme/add\" hx-target=\"#newMemes\">Save</button></div></div></form><div id=\"newMemes\"></div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
for _, meme := range all {
|
||||||
|
templ_7745c5c3_Err = p.Show(meme).Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
|
||||||
|
}
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *MemePlugin) Show(meme webResp) templ.Component {
|
||||||
|
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
templ_7745c5c3_Buffer = templ.GetBuffer()
|
||||||
|
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var2 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var2 == nil {
|
||||||
|
templ_7745c5c3_Var2 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"grid-x grid-margin-x\" id=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(meme.Name))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\"><div class=\"cell small-3\"><div class=\"card\" style=\"max-width: 200px\"><img class=\"thumbnail\" style=\"max-height: 250px; max-width: 250px;\" alt=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(meme.Name))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\" src=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(meme.URL))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\"><div class=\"card-divider\"><p>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var3 string
|
||||||
|
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(meme.Name)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `plugins/meme/meme.templ`, Line: 44, Col: 34}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</p></div></div></div><div class=\"cell small-7\"><pre>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var4 string
|
||||||
|
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(meme.Config)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `plugins/meme/meme.templ`, Line: 50, Col: 29}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</pre></div><div class=\"cell small-2\"><button class=\"button\" hx-get=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString("/meme/edit/" + meme.Name))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\" hx-target=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString("#" + meme.Name))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\" hx-swap=\"outerHTML\">Edit</button></div></div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
|
||||||
|
}
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *MemePlugin) Edit(meme webResp) templ.Component {
|
||||||
|
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
templ_7745c5c3_Buffer = templ.GetBuffer()
|
||||||
|
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var5 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var5 == nil {
|
||||||
|
templ_7745c5c3_Var5 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<form><div class=\"grid-x grid-margin-x\" id=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(meme.Name))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\"><div class=\"cell-small-3\"><img class=\"thumbnail\" style=\"max-height: 150px\" alt=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(meme.Name))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\" src=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(meme.URL))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\"></div><div class=\"cell small-7\"><textarea name=\"config\" rows=\"10\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var6 string
|
||||||
|
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(meme.Config)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `plugins/meme/meme.templ`, Line: 75, Col: 29}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</textarea> <input type=\"text\" name=\"url\" value=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(meme.URL))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\"></div><div class=\"cell small-2\"><button class=\"button\" hx-put=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString("/meme/save/" + meme.Name))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\" hx-target=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString("#" + meme.Name))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\" hx-swap=\"outerHTML\">Save</button> <button class=\"button alert\" hx-delete=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString("/meme/rm/" + meme.Name))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\" hx-target=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString("#" + meme.Name))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\" hx-swap=\"outerHTML\">Delete</button></div></div></form>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
|
||||||
|
}
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
})
|
||||||
|
}
|
|
@ -1,7 +1,6 @@
|
||||||
package meme
|
package meme
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"embed"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
@ -14,18 +13,16 @@ import (
|
||||||
"github.com/velour/catbase/bot"
|
"github.com/velour/catbase/bot"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed *.html
|
|
||||||
var embeddedFS embed.FS
|
|
||||||
|
|
||||||
func (p *MemePlugin) registerWeb(c bot.Connector) {
|
func (p *MemePlugin) registerWeb(c bot.Connector) {
|
||||||
r := chi.NewRouter()
|
r := chi.NewRouter()
|
||||||
r.HandleFunc("/slash", p.slashMeme(c))
|
r.HandleFunc("/slash", p.slashMeme(c))
|
||||||
r.HandleFunc("/img", p.img)
|
r.Get("/img", p.img)
|
||||||
r.HandleFunc("/all", p.all)
|
r.Put("/save/{name}", p.saveMeme)
|
||||||
r.HandleFunc("/add", p.addMeme)
|
r.Post("/add", p.saveMeme)
|
||||||
r.HandleFunc("/rm", p.rmMeme)
|
r.Delete("/rm/{name}", p.rmMeme)
|
||||||
r.HandleFunc("/", p.webRoot)
|
r.Get("/edit/{name}", p.editMeme)
|
||||||
p.bot.RegisterWebName(r, "/meme", "Memes")
|
r.Get("/", p.webRoot)
|
||||||
|
p.bot.GetWeb().RegisterWebName(r, "/meme", "Memes")
|
||||||
}
|
}
|
||||||
|
|
||||||
type webResp struct {
|
type webResp struct {
|
||||||
|
@ -43,7 +40,7 @@ type ByName struct{ webResps }
|
||||||
|
|
||||||
func (s ByName) Less(i, j int) bool { return s.webResps[i].Name < s.webResps[j].Name }
|
func (s ByName) Less(i, j int) bool { return s.webResps[i].Name < s.webResps[j].Name }
|
||||||
|
|
||||||
func (p *MemePlugin) all(w http.ResponseWriter, r *http.Request) {
|
func (p *MemePlugin) all() webResps {
|
||||||
memes := p.c.GetMap("meme.memes", defaultFormats)
|
memes := p.c.GetMap("meme.memes", defaultFormats)
|
||||||
configs := p.c.GetMap("meme.memeconfigs", map[string]string{})
|
configs := p.c.GetMap("meme.memeconfigs", map[string]string{})
|
||||||
|
|
||||||
|
@ -51,7 +48,7 @@ func (p *MemePlugin) all(w http.ResponseWriter, r *http.Request) {
|
||||||
for n, u := range memes {
|
for n, u := range memes {
|
||||||
config, ok := configs[n]
|
config, ok := configs[n]
|
||||||
if !ok {
|
if !ok {
|
||||||
b, _ := json.Marshal(p.defaultFormatConfig())
|
b, _ := json.MarshalIndent(p.defaultFormatConfig(), " ", " ")
|
||||||
config = string(b)
|
config = string(b)
|
||||||
}
|
}
|
||||||
realURL, err := url.Parse(u)
|
realURL, err := url.Parse(u)
|
||||||
|
@ -64,13 +61,7 @@ func (p *MemePlugin) all(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
sort.Sort(ByName{values})
|
sort.Sort(ByName{values})
|
||||||
|
|
||||||
out, err := json.Marshal(values)
|
return values
|
||||||
if err != nil {
|
|
||||||
w.WriteHeader(500)
|
|
||||||
log.Error().Err(err).Msgf("could not serve all memes route")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.Write(out)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func mkCheckError(w http.ResponseWriter) func(error) bool {
|
func mkCheckError(w http.ResponseWriter) func(error) bool {
|
||||||
|
@ -87,56 +78,61 @@ func mkCheckError(w http.ResponseWriter) func(error) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *MemePlugin) rmMeme(w http.ResponseWriter, r *http.Request) {
|
func (p *MemePlugin) rmMeme(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != http.MethodDelete {
|
name := chi.URLParam(r, "name")
|
||||||
w.WriteHeader(405)
|
|
||||||
fmt.Fprintf(w, "Incorrect HTTP method")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
checkError := mkCheckError(w)
|
|
||||||
decoder := json.NewDecoder(r.Body)
|
|
||||||
values := webResp{}
|
|
||||||
err := decoder.Decode(&values)
|
|
||||||
if checkError(err) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
formats := p.c.GetMap("meme.memes", defaultFormats)
|
formats := p.c.GetMap("meme.memes", defaultFormats)
|
||||||
delete(formats, values.Name)
|
delete(formats, name)
|
||||||
err = p.c.SetMap("meme.memes", formats)
|
err := p.c.SetMap("meme.memes", formats)
|
||||||
checkError(err)
|
mkCheckError(w)(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *MemePlugin) addMeme(w http.ResponseWriter, r *http.Request) {
|
func (p *MemePlugin) saveMeme(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != http.MethodPost {
|
name := chi.URLParam(r, "name")
|
||||||
w.WriteHeader(405)
|
if name == "" {
|
||||||
fmt.Fprintf(w, "Incorrect HTTP method")
|
name = r.FormValue("name")
|
||||||
return
|
|
||||||
}
|
}
|
||||||
checkError := mkCheckError(w)
|
checkError := mkCheckError(w)
|
||||||
decoder := json.NewDecoder(r.Body)
|
|
||||||
values := webResp{}
|
|
||||||
err := decoder.Decode(&values)
|
|
||||||
if checkError(err) {
|
|
||||||
log.Error().Err(err).Msgf("could not decode body")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
log.Debug().Msgf("POSTed values: %+v", values)
|
|
||||||
formats := p.c.GetMap("meme.memes", defaultFormats)
|
formats := p.c.GetMap("meme.memes", defaultFormats)
|
||||||
formats[values.Name] = values.URL
|
formats[name] = r.FormValue("url")
|
||||||
err = p.c.SetMap("meme.memes", formats)
|
err := p.c.SetMap("meme.memes", formats)
|
||||||
checkError(err)
|
checkError(err)
|
||||||
|
|
||||||
if values.Config == "" {
|
config := r.FormValue("config")
|
||||||
values.Config = p.defaultFormatConfigJSON()
|
if config == "" {
|
||||||
|
config = p.defaultFormatConfigJSON()
|
||||||
}
|
}
|
||||||
configs := p.c.GetMap("meme.memeconfigs", map[string]string{})
|
configs := p.c.GetMap("meme.memeconfigs", map[string]string{})
|
||||||
configs[values.Name] = values.Config
|
configs[name] = config
|
||||||
err = p.c.SetMap("meme.memeconfigs", configs)
|
err = p.c.SetMap("meme.memeconfigs", configs)
|
||||||
checkError(err)
|
checkError(err)
|
||||||
|
|
||||||
|
meme := webResp{
|
||||||
|
Name: name,
|
||||||
|
URL: formats[name],
|
||||||
|
Config: configs[name],
|
||||||
|
}
|
||||||
|
|
||||||
|
p.Show(meme).Render(r.Context(), w)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *MemePlugin) webRoot(w http.ResponseWriter, r *http.Request) {
|
func (p *MemePlugin) webRoot(w http.ResponseWriter, r *http.Request) {
|
||||||
index, _ := embeddedFS.ReadFile("index.html")
|
p.bot.GetWeb().Index("Meme", p.index(p.all())).Render(r.Context(), w)
|
||||||
w.Write(index)
|
}
|
||||||
|
|
||||||
|
func (p *MemePlugin) editMeme(w http.ResponseWriter, r *http.Request) {
|
||||||
|
name := chi.URLParam(r, "name")
|
||||||
|
memes := p.c.GetMap("meme.memes", defaultFormats)
|
||||||
|
configs := p.c.GetMap("meme.memeconfigs", map[string]string{})
|
||||||
|
meme, ok := memes[name]
|
||||||
|
if !ok {
|
||||||
|
fmt.Fprintf(w, "Didn't find that meme.")
|
||||||
|
}
|
||||||
|
resp := webResp{
|
||||||
|
Name: name,
|
||||||
|
URL: meme,
|
||||||
|
Config: configs[name],
|
||||||
|
}
|
||||||
|
p.Edit(resp).Render(r.Context(), w)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *MemePlugin) img(w http.ResponseWriter, r *http.Request) {
|
func (p *MemePlugin) img(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
|
@ -12,8 +12,6 @@ import (
|
||||||
|
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
|
|
||||||
"github.com/velour/catbase/plugins/cli"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
"github.com/velour/catbase/bot"
|
"github.com/velour/catbase/bot"
|
||||||
|
@ -45,7 +43,6 @@ func makeMessage(payload string) bot.Request {
|
||||||
payload = payload[1:]
|
payload = payload[1:]
|
||||||
}
|
}
|
||||||
return bot.Request{
|
return bot.Request{
|
||||||
Conn: &cli.CliPlugin{},
|
|
||||||
Kind: bot.Message,
|
Kind: bot.Message,
|
||||||
Msg: msg.Message{
|
Msg: msg.Message{
|
||||||
User: &user.User{Name: "tester"},
|
User: &user.User{Name: "tester"},
|
||||||
|
|
|
@ -44,7 +44,7 @@ func New(b bot.Bot) *NewsBid {
|
||||||
var balanceRegex = regexp.MustCompile(`(?i)^balance$`)
|
var balanceRegex = regexp.MustCompile(`(?i)^balance$`)
|
||||||
var bidsRegex = regexp.MustCompile(`(?i)^bids$`)
|
var bidsRegex = regexp.MustCompile(`(?i)^bids$`)
|
||||||
var scoresRegex = regexp.MustCompile(`(?i)^scores$`)
|
var scoresRegex = regexp.MustCompile(`(?i)^scores$`)
|
||||||
var bidRegex = regexp.MustCompile(`(?i)^bid (?P<amount>\S+) (?P<url>)\S+$`)
|
var bidRegex = regexp.MustCompile(`(?i)^bid (?P<amount>\S+) (?P<url>\S+)\s?(?P<comment>.+)?$`)
|
||||||
var checkRegex = regexp.MustCompile(`(?i)^check ngate$`)
|
var checkRegex = regexp.MustCompile(`(?i)^check ngate$`)
|
||||||
|
|
||||||
func (p *NewsBid) balanceCmd(r bot.Request) bool {
|
func (p *NewsBid) balanceCmd(r bot.Request) bool {
|
||||||
|
@ -112,8 +112,6 @@ func (p *NewsBid) bidCmd(r bot.Request) bool {
|
||||||
ch := r.Msg.Channel
|
ch := r.Msg.Channel
|
||||||
conn := r.Conn
|
conn := r.Conn
|
||||||
|
|
||||||
log.Debug().Interface("request", r).Msg("bid request")
|
|
||||||
|
|
||||||
parts := strings.Fields(body)
|
parts := strings.Fields(body)
|
||||||
if len(parts) != 3 {
|
if len(parts) != 3 {
|
||||||
p.bot.Send(conn, bot.Message, ch, "You must bid with an amount and a URL.")
|
p.bot.Send(conn, bot.Message, ch, "You must bid with an amount and a URL.")
|
||||||
|
@ -122,6 +120,13 @@ func (p *NewsBid) bidCmd(r bot.Request) bool {
|
||||||
|
|
||||||
amount, _ := strconv.Atoi(parts[1])
|
amount, _ := strconv.Atoi(parts[1])
|
||||||
url := parts[2]
|
url := parts[2]
|
||||||
|
log.Debug().Msgf("URL: %s", url)
|
||||||
|
if id, err := strconv.Atoi(url); err == nil {
|
||||||
|
url = fmt.Sprintf("https://news.ycombinator.com/item?id=%d", id)
|
||||||
|
log.Debug().Msgf("New URL: %s", url)
|
||||||
|
}
|
||||||
|
log.Debug().Msgf("URL: %s", url)
|
||||||
|
|
||||||
if bid, err := p.ws.Bid(r.Msg.User.Name, amount, parts[1], url); err != nil {
|
if bid, err := p.ws.Bid(r.Msg.User.Name, amount, parts[1], url); err != nil {
|
||||||
p.bot.Send(conn, bot.Message, ch, fmt.Sprintf("Error placing bid: %s", err))
|
p.bot.Send(conn, bot.Message, ch, fmt.Sprintf("Error placing bid: %s", err))
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -0,0 +1,134 @@
|
||||||
|
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) bot.Plugin {
|
||||||
|
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]
|
||||||
|
}
|
||||||
|
ua := p.c.Get("url.useragent", "catbase/1.0")
|
||||||
|
msg := handleURL(u, fullComment, r.Msg.User.Name, ua)
|
||||||
|
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()
|
||||||
|
ua := p.c.Get("url.useragent", "catbase/1.0")
|
||||||
|
msg := handleURL(u, cmt, "", ua)
|
||||||
|
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 handleURL(u, cmt, who, ua string) string {
|
||||||
|
if who != "" {
|
||||||
|
who = who + ": "
|
||||||
|
}
|
||||||
|
client := http.Client{}
|
||||||
|
req, err := http.NewRequest(http.MethodGet, u, nil)
|
||||||
|
if err != nil {
|
||||||
|
return "Couldn't parse that URL"
|
||||||
|
}
|
||||||
|
req.Header.Set("User-Agent", ua)
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil || resp.StatusCode > 299 {
|
||||||
|
log.Error().Err(err).Int("status", resp.StatusCode).Msgf("error with request")
|
||||||
|
return "Couldn't get that URL"
|
||||||
|
}
|
||||||
|
doc, err := goquery.NewDocumentFromReader(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return "Couldn't parse that URL"
|
||||||
|
}
|
||||||
|
wait := make(chan string, 1)
|
||||||
|
sel := doc.Find("title")
|
||||||
|
if sel.Length() == 0 {
|
||||||
|
return fmt.Sprintf("%s%s\n(<%s>)", who, cmt, u)
|
||||||
|
}
|
||||||
|
sel.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")
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,7 +12,6 @@ import (
|
||||||
"github.com/velour/catbase/bot"
|
"github.com/velour/catbase/bot"
|
||||||
"github.com/velour/catbase/bot/msg"
|
"github.com/velour/catbase/bot/msg"
|
||||||
"github.com/velour/catbase/bot/user"
|
"github.com/velour/catbase/bot/user"
|
||||||
"github.com/velour/catbase/plugins/cli"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func makeMessage(payload string) bot.Request {
|
func makeMessage(payload string) bot.Request {
|
||||||
|
@ -22,7 +21,6 @@ func makeMessage(payload string) bot.Request {
|
||||||
}
|
}
|
||||||
values := bot.ParseValues(pickRegex, payload)
|
values := bot.ParseValues(pickRegex, payload)
|
||||||
return bot.Request{
|
return bot.Request{
|
||||||
Conn: &cli.CliPlugin{},
|
|
||||||
Kind: bot.Message,
|
Kind: bot.Message,
|
||||||
Values: values,
|
Values: values,
|
||||||
Msg: msg.Message{
|
Msg: msg.Message{
|
||||||
|
|
|
@ -5,8 +5,6 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/velour/catbase/plugins/cli"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/velour/catbase/bot"
|
"github.com/velour/catbase/bot"
|
||||||
"github.com/velour/catbase/bot/msg"
|
"github.com/velour/catbase/bot/msg"
|
||||||
|
@ -20,7 +18,6 @@ func makeMessage(nick, payload string, r *regexp.Regexp) bot.Request {
|
||||||
payload = payload[1:]
|
payload = payload[1:]
|
||||||
}
|
}
|
||||||
return bot.Request{
|
return bot.Request{
|
||||||
Conn: &cli.CliPlugin{},
|
|
||||||
Kind: bot.Message,
|
Kind: bot.Message,
|
||||||
Values: bot.ParseValues(r, payload),
|
Values: bot.ParseValues(r, payload),
|
||||||
Msg: msg.Message{
|
Msg: msg.Message{
|
||||||
|
|
|
@ -120,7 +120,6 @@ func (p *ReminderPlugin) message(c bot.Connector, kind bot.Kind, message msg.Mes
|
||||||
channel := message.Channel
|
channel := message.Channel
|
||||||
from := message.User.Name
|
from := message.User.Name
|
||||||
|
|
||||||
message.Body = replaceDuration(p.when, message.Body)
|
|
||||||
parts := strings.Fields(message.Body)
|
parts := strings.Fields(message.Body)
|
||||||
|
|
||||||
if len(parts) >= 5 {
|
if len(parts) >= 5 {
|
||||||
|
|
|
@ -4,7 +4,6 @@ package reminder
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/velour/catbase/plugins/cli"
|
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
@ -24,7 +23,7 @@ func makeMessageBy(payload, by string) (bot.Connector, bot.Kind, msg.Message) {
|
||||||
if isCmd {
|
if isCmd {
|
||||||
payload = payload[1:]
|
payload = payload[1:]
|
||||||
}
|
}
|
||||||
return &cli.CliPlugin{}, bot.Message, msg.Message{
|
return nil, bot.Message, msg.Message{
|
||||||
User: &user.User{Name: by},
|
User: &user.User{Name: by},
|
||||||
Channel: "test",
|
Channel: "test",
|
||||||
Body: payload,
|
Body: payload,
|
||||||
|
@ -224,6 +223,6 @@ func TestLimitList(t *testing.T) {
|
||||||
func TestHelp(t *testing.T) {
|
func TestHelp(t *testing.T) {
|
||||||
c, mb := setup(t)
|
c, mb := setup(t)
|
||||||
assert.NotNil(t, c)
|
assert.NotNil(t, c)
|
||||||
c.help(&cli.CliPlugin{}, bot.Help, msg.Message{Channel: "channel"}, []string{})
|
c.help(nil, bot.Help, msg.Message{Channel: "channel"}, []string{})
|
||||||
assert.Len(t, mb.Messages, 1)
|
assert.Len(t, mb.Messages, 1)
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,7 +19,7 @@ type RolesPlugin struct {
|
||||||
h bot.HandlerTable
|
h bot.HandlerTable
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(b bot.Bot) *RolesPlugin {
|
func New(b bot.Bot) bot.Plugin {
|
||||||
p := &RolesPlugin{
|
p := &RolesPlugin{
|
||||||
b: b,
|
b: b,
|
||||||
c: b.Config(),
|
c: b.Config(),
|
||||||
|
|
|
@ -2,7 +2,6 @@ package rss
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/velour/catbase/plugins/cli"
|
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
@ -17,7 +16,7 @@ func makeMessage(payload string) (bot.Connector, bot.Kind, msg.Message) {
|
||||||
if isCmd {
|
if isCmd {
|
||||||
payload = payload[1:]
|
payload = payload[1:]
|
||||||
}
|
}
|
||||||
return &cli.CliPlugin{}, bot.Message, msg.Message{
|
return nil, bot.Message, msg.Message{
|
||||||
User: &user.User{Name: "tester"},
|
User: &user.User{Name: "tester"},
|
||||||
Channel: "test",
|
Channel: "test",
|
||||||
Body: payload,
|
Body: payload,
|
||||||
|
|
|
@ -1,120 +0,0 @@
|
||||||
<!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="https://unpkg.com/vue-router@^2"></script>
|
|
||||||
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<title>Memes</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<div id="app">
|
|
||||||
<b-navbar>
|
|
||||||
<b-navbar-brand>Memes</b-navbar-brand>
|
|
||||||
<b-navbar-nav>
|
|
||||||
<b-nav-item v-for="item in nav" :href="item.url" :active="item.name === 'Meme'" :key="item.key">{{ item.name }}</b-nav-item>
|
|
||||||
</b-navbar-nav>
|
|
||||||
</b-navbar>
|
|
||||||
<b-alert
|
|
||||||
dismissable
|
|
||||||
variant="error"
|
|
||||||
:show="err != ''"
|
|
||||||
@dismissed="err = ''">
|
|
||||||
{{ err }}
|
|
||||||
</b-alert>
|
|
||||||
<b-form @submit="add">
|
|
||||||
<b-container>
|
|
||||||
<b-row>
|
|
||||||
<b-col cols="3">
|
|
||||||
<b-input placeholder="Key..." v-model="secret.key"></b-input>
|
|
||||||
</b-col>
|
|
||||||
<b-col cols="3">
|
|
||||||
<b-input placeholder="Value..." v-model="secret.value"></b-input>
|
|
||||||
</b-col>
|
|
||||||
<b-col cols="3">
|
|
||||||
<b-button type="submit">Add Secret</b-button>
|
|
||||||
</b-col>
|
|
||||||
</b-row>
|
|
||||||
<b-row style="padding-top: 2em;">
|
|
||||||
<b-col>
|
|
||||||
<ul>
|
|
||||||
<li v-for="key in results" key="key"><a @click="rm(key)" href="#">X</a> {{key}}</li>
|
|
||||||
</ul>
|
|
||||||
</b-col>
|
|
||||||
</b-row>
|
|
||||||
</b-container>
|
|
||||||
</b-form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
var router = new VueRouter({
|
|
||||||
mode: 'history',
|
|
||||||
routes: []
|
|
||||||
});
|
|
||||||
var app = new Vue({
|
|
||||||
el: '#app',
|
|
||||||
router,
|
|
||||||
data: {
|
|
||||||
err: '',
|
|
||||||
nav: [],
|
|
||||||
secret: {key: '', value: ''},
|
|
||||||
results: [],
|
|
||||||
fields: [
|
|
||||||
{key: 'key', sortable: true},
|
|
||||||
]
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
axios.get('/nav')
|
|
||||||
.then(resp => {
|
|
||||||
this.nav = resp.data;
|
|
||||||
})
|
|
||||||
.catch(err => console.log(err))
|
|
||||||
this.refresh();
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
refresh: function () {
|
|
||||||
axios.get('/secrets/all')
|
|
||||||
.then(resp => {
|
|
||||||
this.results = resp.data
|
|
||||||
this.err = ''
|
|
||||||
})
|
|
||||||
.catch(err => (this.err = err))
|
|
||||||
},
|
|
||||||
add: function (evt) {
|
|
||||||
if (evt) {
|
|
||||||
evt.preventDefault();
|
|
||||||
evt.stopPropagation();
|
|
||||||
}
|
|
||||||
axios.post('/secrets/add', this.secret)
|
|
||||||
.then(resp => {
|
|
||||||
this.results = resp.data;
|
|
||||||
this.secret.key = '';
|
|
||||||
this.secret.value = '';
|
|
||||||
this.refresh();
|
|
||||||
})
|
|
||||||
.catch(err => this.err = err)
|
|
||||||
},
|
|
||||||
rm: function (key) {
|
|
||||||
if (confirm("Are you sure you want to delete this meme?")) {
|
|
||||||
axios.delete('/secrets/remove', {data: {key: key}})
|
|
||||||
.then(resp => {
|
|
||||||
this.refresh();
|
|
||||||
})
|
|
||||||
.catch(err => this.err = err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
|
@ -1,28 +1,25 @@
|
||||||
package secrets
|
package secrets
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"embed"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
"github.com/jmoiron/sqlx"
|
"github.com/jmoiron/sqlx"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"github.com/velour/catbase/bot"
|
"github.com/velour/catbase/bot"
|
||||||
"github.com/velour/catbase/config"
|
"github.com/velour/catbase/config"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed *.html
|
|
||||||
var embeddedFS embed.FS
|
|
||||||
|
|
||||||
type SecretsPlugin struct {
|
type SecretsPlugin struct {
|
||||||
b bot.Bot
|
b bot.Bot
|
||||||
c *config.Config
|
c *config.Config
|
||||||
db *sqlx.DB
|
db *sqlx.DB
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(b bot.Bot) *SecretsPlugin {
|
func New(b bot.Bot) bot.Plugin {
|
||||||
p := &SecretsPlugin{
|
p := &SecretsPlugin{
|
||||||
b: b,
|
b: b,
|
||||||
c: b.Config(),
|
c: b.Config(),
|
||||||
|
@ -36,14 +33,8 @@ func (p *SecretsPlugin) registerWeb() {
|
||||||
r := chi.NewRouter()
|
r := chi.NewRouter()
|
||||||
r.HandleFunc("/add", p.handleRegister)
|
r.HandleFunc("/add", p.handleRegister)
|
||||||
r.HandleFunc("/remove", p.handleRemove)
|
r.HandleFunc("/remove", p.handleRemove)
|
||||||
r.HandleFunc("/all", p.handleAll)
|
|
||||||
r.HandleFunc("/test", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
value := r.URL.Query().Get("test")
|
|
||||||
j, _ := json.Marshal(map[string]string{"value": value})
|
|
||||||
w.Write(j)
|
|
||||||
})
|
|
||||||
r.HandleFunc("/", p.handleIndex)
|
r.HandleFunc("/", p.handleIndex)
|
||||||
p.b.RegisterWebName(r, "/secrets", "Secrets")
|
p.b.GetWeb().RegisterWebName(r, "/secrets", "Secrets")
|
||||||
}
|
}
|
||||||
|
|
||||||
func mkCheckError(w http.ResponseWriter) func(error) bool {
|
func mkCheckError(w http.ResponseWriter) func(error) bool {
|
||||||
|
@ -68,20 +59,12 @@ func checkMethod(method string, w http.ResponseWriter, r *http.Request) bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *SecretsPlugin) keys() []string {
|
||||||
|
return p.c.SecretKeys()
|
||||||
|
}
|
||||||
|
|
||||||
func (p *SecretsPlugin) sendKeys(w http.ResponseWriter, r *http.Request) {
|
func (p *SecretsPlugin) sendKeys(w http.ResponseWriter, r *http.Request) {
|
||||||
checkError := mkCheckError(w)
|
p.keysList().Render(r.Context(), w)
|
||||||
log.Debug().Msgf("Keys before refresh: %v", p.c.SecretKeys())
|
|
||||||
err := p.c.RefreshSecrets()
|
|
||||||
log.Debug().Msgf("Keys after refresh: %v", p.c.SecretKeys())
|
|
||||||
if checkError(err) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
keys, err := json.Marshal(p.c.SecretKeys())
|
|
||||||
if checkError(err) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.WriteHeader(200)
|
|
||||||
w.Write(keys)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *SecretsPlugin) handleAll(w http.ResponseWriter, r *http.Request) {
|
func (p *SecretsPlugin) handleAll(w http.ResponseWriter, r *http.Request) {
|
||||||
|
@ -89,21 +72,13 @@ func (p *SecretsPlugin) handleAll(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *SecretsPlugin) handleRegister(w http.ResponseWriter, r *http.Request) {
|
func (p *SecretsPlugin) handleRegister(w http.ResponseWriter, r *http.Request) {
|
||||||
log.Debug().Msgf("handleRegister")
|
|
||||||
if checkMethod(http.MethodPost, w, r) {
|
if checkMethod(http.MethodPost, w, r) {
|
||||||
log.Debug().Msgf("failed post %s", r.Method)
|
log.Debug().Msgf("failed post %s", r.Method)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
checkError := mkCheckError(w)
|
checkError := mkCheckError(w)
|
||||||
decoder := json.NewDecoder(r.Body)
|
key, value := r.FormValue("key"), r.FormValue("value")
|
||||||
secret := config.Secret{}
|
err := p.c.RegisterSecret(key, value)
|
||||||
err := decoder.Decode(&secret)
|
|
||||||
log.Debug().Msgf("decoding: %s", err)
|
|
||||||
if checkError(err) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
log.Debug().Msgf("Secret: %s", secret)
|
|
||||||
err = p.c.RegisterSecret(secret.Key, secret.Value)
|
|
||||||
if checkError(err) {
|
if checkError(err) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -115,13 +90,16 @@ func (p *SecretsPlugin) handleRemove(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
checkError := mkCheckError(w)
|
checkError := mkCheckError(w)
|
||||||
decoder := json.NewDecoder(r.Body)
|
b, err := io.ReadAll(r.Body)
|
||||||
secret := config.Secret{}
|
|
||||||
err := decoder.Decode(&secret)
|
|
||||||
if checkError(err) {
|
if checkError(err) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err = p.c.RemoveSecret(secret.Key)
|
q, err := url.ParseQuery(string(b))
|
||||||
|
if checkError(err) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
secret := q.Get("key")
|
||||||
|
err = p.c.RemoveSecret(secret)
|
||||||
if checkError(err) {
|
if checkError(err) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -129,6 +107,5 @@ func (p *SecretsPlugin) handleRemove(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *SecretsPlugin) handleIndex(w http.ResponseWriter, r *http.Request) {
|
func (p *SecretsPlugin) handleIndex(w http.ResponseWriter, r *http.Request) {
|
||||||
index, _ := embeddedFS.ReadFile("index.html")
|
p.b.GetWeb().Index("Secrets", p.index()).Render(r.Context(), w)
|
||||||
w.Write(index)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,52 @@
|
||||||
|
package secrets
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
templ (s *SecretsPlugin) index() {
|
||||||
|
<div class="grid-container">
|
||||||
|
<form hx-post="/secrets/add" hx-target="#data">
|
||||||
|
<div class="grid-x">
|
||||||
|
<h2>Secrets</h2>
|
||||||
|
</div>
|
||||||
|
<div class="grid-x">
|
||||||
|
<div class="cell auto">
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-label">Key</span>
|
||||||
|
<input class="input-group-field" placeholder="Key..." name="key" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="cell auto">
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-label">Value</span>
|
||||||
|
<input class="input-group-field" placeholder="Value..." name="value" />
|
||||||
|
<div class="input-group-button">
|
||||||
|
<button class="button primary" type="submit">Add Secret</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<div class="grid-x grid-margin-x">
|
||||||
|
<div id="data">
|
||||||
|
@s.keysList()
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
templ (s *SecretsPlugin) keysList() {
|
||||||
|
<ul class="no-bullet">
|
||||||
|
for _, key := range s.keys() {
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
class="button tiny alert middle"
|
||||||
|
style="vertical-align: baseline"
|
||||||
|
hx-delete="/secrets/remove"
|
||||||
|
hx-confirm={ fmt.Sprintf("Are you sure you want to delete %s?", key) }
|
||||||
|
hx-target="#data"
|
||||||
|
hx-include="this"
|
||||||
|
name="key" value={ key }>X</button>
|
||||||
|
{ key }</li>
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
}
|
|
@ -0,0 +1,108 @@
|
||||||
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
|
// templ: version: v0.2.543
|
||||||
|
package secrets
|
||||||
|
|
||||||
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|
||||||
|
import "github.com/a-h/templ"
|
||||||
|
import "context"
|
||||||
|
import "io"
|
||||||
|
import "bytes"
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
func (s *SecretsPlugin) index() templ.Component {
|
||||||
|
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
templ_7745c5c3_Buffer = templ.GetBuffer()
|
||||||
|
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var1 == nil {
|
||||||
|
templ_7745c5c3_Var1 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"grid-container\"><form hx-post=\"/secrets/add\" hx-target=\"#data\"><div class=\"grid-x\"><h2>Secrets</h2></div><div class=\"grid-x\"><div class=\"cell auto\"><div class=\"input-group\"><span class=\"input-group-label\">Key</span> <input class=\"input-group-field\" placeholder=\"Key...\" name=\"key\"></div></div><div class=\"cell auto\"><div class=\"input-group\"><span class=\"input-group-label\">Value</span> <input class=\"input-group-field\" placeholder=\"Value...\" name=\"value\"><div class=\"input-group-button\"><button class=\"button primary\" type=\"submit\">Add Secret</button></div></div></div></div></form><div class=\"grid-x grid-margin-x\"><div id=\"data\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = s.keysList().Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div></div></div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
|
||||||
|
}
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SecretsPlugin) keysList() templ.Component {
|
||||||
|
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
templ_7745c5c3_Buffer = templ.GetBuffer()
|
||||||
|
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var2 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var2 == nil {
|
||||||
|
templ_7745c5c3_Var2 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<ul class=\"no-bullet\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
for _, key := range s.keys() {
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<li><button class=\"button tiny alert middle\" style=\"vertical-align: baseline\" hx-delete=\"/secrets/remove\" hx-confirm=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(fmt.Sprintf("Are you sure you want to delete %s?", key)))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\" hx-target=\"#data\" hx-include=\"this\" name=\"key\" value=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(key))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\">X</button> ")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var3 string
|
||||||
|
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(key)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `plugins/secrets/secrets.templ`, Line: 48, Col: 17}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</li>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</ul>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
|
||||||
|
}
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
})
|
||||||
|
}
|
|
@ -121,7 +121,7 @@ func (p *SMSPlugin) help(c bot.Connector, kind bot.Kind, message msg.Message, ar
|
||||||
func (p *SMSPlugin) registerWeb() {
|
func (p *SMSPlugin) registerWeb() {
|
||||||
r := chi.NewRouter()
|
r := chi.NewRouter()
|
||||||
r.HandleFunc("/new", p.receive)
|
r.HandleFunc("/new", p.receive)
|
||||||
p.b.RegisterWeb(r, "/sms")
|
p.b.GetWeb().RegisterWeb(r, "/sms")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *SMSPlugin) receive(w http.ResponseWriter, r *http.Request) {
|
func (p *SMSPlugin) receive(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package stock
|
package stock
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/velour/catbase/plugins/cli"
|
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
@ -16,7 +15,7 @@ func makeMessage(payload string) (bot.Connector, bot.Kind, msg.Message) {
|
||||||
if isCmd {
|
if isCmd {
|
||||||
payload = payload[1:]
|
payload = payload[1:]
|
||||||
}
|
}
|
||||||
return &cli.CliPlugin{}, bot.Message, msg.Message{
|
return nil, bot.Message, msg.Message{
|
||||||
User: &user.User{Name: "tester"},
|
User: &user.User{Name: "tester"},
|
||||||
Channel: "test",
|
Channel: "test",
|
||||||
Body: payload,
|
Body: payload,
|
||||||
|
|
|
@ -104,7 +104,7 @@ func (p *TalkerPlugin) message(c bot.Connector, kind bot.Kind, message msg.Messa
|
||||||
line = strings.Replace(line, "{nick}", nick, 1)
|
line = strings.Replace(line, "{nick}", nick, 1)
|
||||||
output += line + "\n"
|
output += line + "\n"
|
||||||
}
|
}
|
||||||
p.bot.Send(c, bot.Message, channel, output)
|
p.bot.Send(c, bot.Spoiler, channel, output)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -186,5 +186,5 @@ func (p *TalkerPlugin) registerWeb(c bot.Connector) {
|
||||||
p.bot.Send(c, bot.Message, channel, msg)
|
p.bot.Send(c, bot.Message, channel, msg)
|
||||||
w.WriteHeader(200)
|
w.WriteHeader(200)
|
||||||
})
|
})
|
||||||
p.bot.RegisterWeb(r, "/cowsay")
|
p.bot.GetWeb().RegisterWeb(r, "/cowsay")
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
package talker
|
package talker
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/velour/catbase/plugins/cli"
|
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
@ -18,7 +17,7 @@ func makeMessage(payload string) (bot.Connector, bot.Kind, msg.Message) {
|
||||||
if isCmd {
|
if isCmd {
|
||||||
payload = payload[1:]
|
payload = payload[1:]
|
||||||
}
|
}
|
||||||
return &cli.CliPlugin{}, bot.Message, msg.Message{
|
return nil, bot.Message, msg.Message{
|
||||||
User: &user.User{Name: "tester"},
|
User: &user.User{Name: "tester"},
|
||||||
Channel: "test",
|
Channel: "test",
|
||||||
Body: payload,
|
Body: payload,
|
||||||
|
@ -79,6 +78,6 @@ func TestHelp(t *testing.T) {
|
||||||
mb := bot.NewMockBot()
|
mb := bot.NewMockBot()
|
||||||
c := New(mb)
|
c := New(mb)
|
||||||
assert.NotNil(t, c)
|
assert.NotNil(t, c)
|
||||||
c.help(&cli.CliPlugin{}, bot.Help, msg.Message{Channel: "channel"}, []string{})
|
c.help(nil, bot.Help, msg.Message{Channel: "channel"}, []string{})
|
||||||
assert.Len(t, mb.Messages, 1)
|
assert.Len(t, mb.Messages, 1)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,149 @@
|
||||||
|
package tappd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"github.com/disintegration/imageorient"
|
||||||
|
"github.com/fogleman/gg"
|
||||||
|
"github.com/gabriel-vasile/mimetype"
|
||||||
|
"github.com/nfnt/resize"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"github.com/velour/catbase/plugins/meme"
|
||||||
|
"image"
|
||||||
|
"image/png"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"path"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (p *Tappd) getImage(url string) (image.Image, error) {
|
||||||
|
resp, err := http.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
img, _, err := imageorient.Decode(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
r := img.Bounds()
|
||||||
|
w := r.Dx()
|
||||||
|
h := r.Dy()
|
||||||
|
|
||||||
|
maxSz := p.c.GetFloat64("maxImgSz", 750.0)
|
||||||
|
|
||||||
|
if w > h {
|
||||||
|
scale := maxSz / float64(w)
|
||||||
|
w = int(float64(w) * scale)
|
||||||
|
h = int(float64(h) * scale)
|
||||||
|
} else {
|
||||||
|
scale := maxSz / float64(h)
|
||||||
|
w = int(float64(w) * scale)
|
||||||
|
h = int(float64(h) * scale)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug().Msgf("trynig to resize to %v, %v", w, h)
|
||||||
|
img = resize.Resize(uint(w), uint(h), img, resize.Lanczos3)
|
||||||
|
r = img.Bounds()
|
||||||
|
w = r.Dx()
|
||||||
|
h = r.Dy()
|
||||||
|
log.Debug().Msgf("resized to %v, %v", w, h)
|
||||||
|
|
||||||
|
return img, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type textSpec struct {
|
||||||
|
text string
|
||||||
|
// percentage location of text center
|
||||||
|
x float64
|
||||||
|
y float64
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultSpec() textSpec {
|
||||||
|
return textSpec{
|
||||||
|
x: 0.5,
|
||||||
|
y: 0.9,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Tappd) overlay(img image.Image, texts []textSpec) ([]byte, error) {
|
||||||
|
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()
|
||||||
|
h := r.Dy()
|
||||||
|
|
||||||
|
txts := []string{}
|
||||||
|
for _, t := range texts {
|
||||||
|
txts = append(txts, t.text)
|
||||||
|
}
|
||||||
|
|
||||||
|
fontSize := meme.FindFontSize(p.c, txts, font, w, h, fontSizes)
|
||||||
|
|
||||||
|
m := gg.NewContext(w, h)
|
||||||
|
m.DrawImage(img, 0, 0)
|
||||||
|
for _, spec := range texts {
|
||||||
|
// write some stuff on the image here
|
||||||
|
if err := m.LoadFontFace(font, fontSize); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply black stroke
|
||||||
|
m.SetHexColor("#000")
|
||||||
|
strokeSize := 6
|
||||||
|
for dy := -strokeSize; dy <= strokeSize; dy++ {
|
||||||
|
for dx := -strokeSize; dx <= strokeSize; dx++ {
|
||||||
|
// give it rounded corners
|
||||||
|
if dx*dx+dy*dy >= strokeSize*strokeSize {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
x := float64(w)*spec.x + float64(dx)
|
||||||
|
y := float64(h)*spec.y + float64(dy)
|
||||||
|
m.DrawStringAnchored(spec.text, x, y, 0.5, 0.5)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m.SetHexColor("#FFF")
|
||||||
|
x := float64(w) * spec.x
|
||||||
|
y := float64(h) * spec.y
|
||||||
|
m.DrawStringAnchored(spec.text, x, y, 0.5, 0.5)
|
||||||
|
}
|
||||||
|
i := bytes.Buffer{}
|
||||||
|
if err := png.Encode(&i, m.Image()); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return i.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Tappd) getAndOverlay(id, srcURL string, texts []textSpec) (imageInfo, error) {
|
||||||
|
baseURL := p.c.Get("BaseURL", ``)
|
||||||
|
u, _ := url.Parse(baseURL)
|
||||||
|
u.Path = path.Join(u.Path, "tappd", id)
|
||||||
|
img, err := p.getImage(srcURL)
|
||||||
|
if err != nil {
|
||||||
|
return imageInfo{}, err
|
||||||
|
}
|
||||||
|
data, err := p.overlay(img, texts)
|
||||||
|
if err != nil {
|
||||||
|
return imageInfo{}, err
|
||||||
|
}
|
||||||
|
bounds := img.Bounds()
|
||||||
|
mime := mimetype.Detect(data)
|
||||||
|
info := imageInfo{
|
||||||
|
ID: id,
|
||||||
|
SrcURL: srcURL,
|
||||||
|
BotURL: u.String(),
|
||||||
|
Img: img,
|
||||||
|
Repr: data,
|
||||||
|
W: bounds.Dx(),
|
||||||
|
H: bounds.Dy(),
|
||||||
|
MimeType: mime.String(),
|
||||||
|
FileName: id + mime.Extension(),
|
||||||
|
}
|
||||||
|
p.imageMap[id] = info
|
||||||
|
log.Debug().
|
||||||
|
Interface("BotURL", info.BotURL).
|
||||||
|
Str("ID", id).
|
||||||
|
Msgf("here's some info")
|
||||||
|
return info, nil
|
||||||
|
}
|
|
@ -0,0 +1,207 @@
|
||||||
|
package tappd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"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"
|
||||||
|
"image"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"regexp"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Tappd struct {
|
||||||
|
b bot.Bot
|
||||||
|
c *config.Config
|
||||||
|
imageMap map[string]imageInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
type imageInfo struct {
|
||||||
|
ID string
|
||||||
|
SrcURL string
|
||||||
|
BotURL string
|
||||||
|
Img image.Image
|
||||||
|
Repr []byte
|
||||||
|
W int
|
||||||
|
H int
|
||||||
|
MimeType string
|
||||||
|
FileName string
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(b bot.Bot) *Tappd {
|
||||||
|
t := &Tappd{
|
||||||
|
b: b,
|
||||||
|
c: b.Config(),
|
||||||
|
imageMap: make(map[string]imageInfo),
|
||||||
|
}
|
||||||
|
t.register()
|
||||||
|
t.registerWeb()
|
||||||
|
t.mkDB()
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Tappd) mkDB() error {
|
||||||
|
db := p.b.DB()
|
||||||
|
tx, err := db.Beginx()
|
||||||
|
if err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = tx.Exec(`create table if not exists tappd (
|
||||||
|
id string primary key,
|
||||||
|
who string,
|
||||||
|
channel string,
|
||||||
|
message string,
|
||||||
|
ts datetime
|
||||||
|
);`)
|
||||||
|
if err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = tx.Commit()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Tappd) log(id, who, channel, message string) error {
|
||||||
|
db := p.b.DB()
|
||||||
|
tx, err := db.Beginx()
|
||||||
|
if err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = tx.Exec(`insert into tappd (id, who, channel, message, ts) values (?, ?, ?, ? ,?)`,
|
||||||
|
id, who, channel, message, time.Now())
|
||||||
|
if err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = tx.Commit()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Tappd) registerDiscord(d *discord.Discord) {
|
||||||
|
cmd := discordgo.ApplicationCommand{
|
||||||
|
Name: "tap",
|
||||||
|
Description: "tappd a beer in",
|
||||||
|
Options: []*discordgo.ApplicationCommandOption{
|
||||||
|
{
|
||||||
|
Type: discordgo.ApplicationCommandOptionAttachment,
|
||||||
|
Name: "image",
|
||||||
|
Description: "Picture that beer, but on Discord",
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: discordgo.ApplicationCommandOptionString,
|
||||||
|
Name: "comment",
|
||||||
|
Description: "Comment on that beer",
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if err := d.RegisterSlashCmd(cmd, p.tap); err != nil {
|
||||||
|
log.Error().Err(err).Msgf("could not register")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Tappd) tap(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
||||||
|
who := i.Interaction.Member.Nick
|
||||||
|
channel := i.Interaction.ChannelID
|
||||||
|
shortMsg := i.ApplicationCommandData().Options[1].StringValue()
|
||||||
|
longMsg := fmt.Sprintf("%s checked in: %s",
|
||||||
|
i.Interaction.Member.Nick, shortMsg)
|
||||||
|
attachID := i.ApplicationCommandData().Options[0].Value.(string)
|
||||||
|
attach := i.ApplicationCommandData().Resolved.Attachments[attachID]
|
||||||
|
spec := defaultSpec()
|
||||||
|
spec.text = shortMsg
|
||||||
|
info, err := p.getAndOverlay(attachID, attach.URL, []textSpec{spec})
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msgf("error with interaction")
|
||||||
|
err = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
|
||||||
|
Type: discordgo.InteractionResponseChannelMessageWithSource,
|
||||||
|
Data: &discordgo.InteractionResponseData{
|
||||||
|
Content: "Error getting the image",
|
||||||
|
Flags: discordgo.MessageFlagsEphemeral,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msgf("error with interaction")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
embeds := []*discordgo.MessageEmbed{{
|
||||||
|
Description: longMsg,
|
||||||
|
Image: &discordgo.MessageEmbedImage{
|
||||||
|
URL: info.BotURL,
|
||||||
|
Width: info.W,
|
||||||
|
Height: info.H,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
files := []*discordgo.File{{
|
||||||
|
Name: info.FileName,
|
||||||
|
ContentType: info.MimeType,
|
||||||
|
Reader: bytes.NewBuffer(info.Repr),
|
||||||
|
}}
|
||||||
|
content := info.BotURL
|
||||||
|
// Yes, the configs are all stringly typed. Get over it.
|
||||||
|
useEmbed := p.c.GetBool("tappd.embed", false)
|
||||||
|
if useEmbed {
|
||||||
|
files = nil
|
||||||
|
} else {
|
||||||
|
embeds = nil
|
||||||
|
content = ""
|
||||||
|
}
|
||||||
|
err = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
|
||||||
|
Type: discordgo.InteractionResponseChannelMessageWithSource,
|
||||||
|
Data: &discordgo.InteractionResponseData{
|
||||||
|
Embeds: embeds,
|
||||||
|
Files: files,
|
||||||
|
Content: content,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msgf("error with interaction")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = p.log(info.ID, who, channel, shortMsg)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msgf("error recording tap")
|
||||||
|
}
|
||||||
|
imgPath := p.c.Get("tappd.imagepath", "tappdimg")
|
||||||
|
err = os.MkdirAll(imgPath, 0775)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msgf("error creating directory")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = os.WriteFile(path.Join(imgPath, info.FileName), info.Repr, 0664)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msgf("error writing file")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Tappd) register() {
|
||||||
|
ht := 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.registerDiscord(conn)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
p.b.RegisterTable(p, ht)
|
||||||
|
}
|
|
@ -0,0 +1,51 @@
|
||||||
|
package tappd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (p *Tappd) registerWeb() {
|
||||||
|
r := chi.NewRouter()
|
||||||
|
r.HandleFunc("/", p.serveImage)
|
||||||
|
p.b.GetWeb().RegisterWeb(r, "/tappd/{id}")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Tappd) getImg(id string) ([]byte, error) {
|
||||||
|
imgData, ok := p.imageMap[id]
|
||||||
|
if ok {
|
||||||
|
return imgData.Repr, nil
|
||||||
|
}
|
||||||
|
imgPath := p.c.Get("tappd.imagepath", "tappdimg")
|
||||||
|
entries, err := os.ReadDir(imgPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msgf("can't read image path")
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, e := range entries {
|
||||||
|
if strings.HasPrefix(e.Name(), id) {
|
||||||
|
return os.ReadFile(path.Join(imgPath, e.Name()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.Error().Msgf("didn't find image")
|
||||||
|
return nil, fmt.Errorf("file not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Tappd) serveImage(w http.ResponseWriter, r *http.Request) {
|
||||||
|
id := chi.URLParam(r, "id")
|
||||||
|
data, err := p.getImg(id)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msgf("error getting image")
|
||||||
|
w.WriteHeader(404)
|
||||||
|
out, _ := json.Marshal(struct{ err string }{"could not find ID: " + err.Error()})
|
||||||
|
w.Write(out)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Write(data)
|
||||||
|
}
|
|
@ -1,8 +1,14 @@
|
||||||
package tldr
|
package tldr
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/andrewstuart/openai"
|
||||||
|
"github.com/velour/catbase/config"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
"text/template"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/velour/catbase/bot"
|
"github.com/velour/catbase/bot"
|
||||||
|
@ -13,9 +19,14 @@ import (
|
||||||
"github.com/james-bowman/nlp"
|
"github.com/james-bowman/nlp"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const templateKey = "tldr.prompttemplate"
|
||||||
|
|
||||||
|
var defaultTemplate = "Summarize the following conversation:\n"
|
||||||
|
|
||||||
type TLDRPlugin struct {
|
type TLDRPlugin struct {
|
||||||
bot bot.Bot
|
b bot.Bot
|
||||||
history []history
|
c *config.Config
|
||||||
|
history map[string][]history
|
||||||
index int
|
index int
|
||||||
lastRequest time.Time
|
lastRequest time.Time
|
||||||
}
|
}
|
||||||
|
@ -28,120 +39,165 @@ type history struct {
|
||||||
|
|
||||||
func New(b bot.Bot) *TLDRPlugin {
|
func New(b bot.Bot) *TLDRPlugin {
|
||||||
plugin := &TLDRPlugin{
|
plugin := &TLDRPlugin{
|
||||||
bot: b,
|
b: b,
|
||||||
history: []history{},
|
c: b.Config(),
|
||||||
|
history: map[string][]history{},
|
||||||
index: 0,
|
index: 0,
|
||||||
lastRequest: time.Now().Add(-24 * time.Hour),
|
lastRequest: time.Now().Add(-24 * time.Hour),
|
||||||
}
|
}
|
||||||
b.Register(plugin, bot.Message, plugin.message)
|
plugin.register()
|
||||||
b.Register(plugin, bot.Help, plugin.help)
|
|
||||||
return plugin
|
return plugin
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *TLDRPlugin) message(c bot.Connector, kind bot.Kind, message msg.Message, args ...any) bool {
|
func (p *TLDRPlugin) register() {
|
||||||
timeLimit := time.Duration(p.bot.Config().GetInt("TLDR.HourLimit", 1))
|
p.b.RegisterTable(p, bot.HandlerTable{
|
||||||
lowercaseMessage := strings.ToLower(message.Body)
|
{
|
||||||
if lowercaseMessage == "tl;dr" && p.lastRequest.After(time.Now().Add(-timeLimit*time.Hour)) {
|
Kind: bot.Message, IsCmd: true,
|
||||||
p.bot.Send(c, bot.Message, message.Channel, "Slow down, cowboy. Read that tiny backlog.")
|
Regex: regexp.MustCompile(`old tl;dr`),
|
||||||
return true
|
HelpText: "Get a rather inaccurate summary of the channel",
|
||||||
} else if lowercaseMessage == "tl;dr" {
|
Handler: p.tldrCmd,
|
||||||
p.lastRequest = time.Now()
|
},
|
||||||
nTopics := p.bot.Config().GetInt("TLDR.Topics", 5)
|
{
|
||||||
|
Kind: bot.Message, IsCmd: true,
|
||||||
stopWordSlice := p.bot.Config().GetArray("TLDR.StopWords", []string{})
|
Regex: regexp.MustCompile(`tl;?dr-prompt$`),
|
||||||
if len(stopWordSlice) == 0 {
|
HelpText: "Get the tl;dr prompt",
|
||||||
stopWordSlice = THESE_ARE_NOT_THE_WORDS_YOU_ARE_LOOKING_FOR
|
Handler: p.squawkTLDR,
|
||||||
p.bot.Config().SetArray("TLDR.StopWords", stopWordSlice)
|
},
|
||||||
}
|
{
|
||||||
|
Kind: bot.Message, IsCmd: true,
|
||||||
vectoriser := nlp.NewCountVectoriser(stopWordSlice...)
|
Regex: regexp.MustCompile(`tl;?dr-prompt reset`),
|
||||||
lda := nlp.NewLatentDirichletAllocation(nTopics)
|
HelpText: "Reset the tl;dr prompt",
|
||||||
pipeline := nlp.NewPipeline(vectoriser, lda)
|
Handler: p.resetTLDR,
|
||||||
docsOverTopics, err := pipeline.FitTransform(p.getTopics()...)
|
},
|
||||||
|
{
|
||||||
if err != nil {
|
Kind: bot.Message, IsCmd: true,
|
||||||
log.Error().Err(err)
|
Regex: regexp.MustCompile(`tl;?dr-prompt (?P<prompt>.*)`),
|
||||||
return false
|
HelpText: "Set the tl;dr prompt",
|
||||||
}
|
Handler: p.setTLDR,
|
||||||
|
},
|
||||||
bestScores := make([][]float64, nTopics)
|
{
|
||||||
bestDocs := make([][]history, nTopics)
|
Kind: bot.Message, IsCmd: true,
|
||||||
|
Regex: regexp.MustCompile(`tl;?dr`),
|
||||||
supportingDocs := p.bot.Config().GetInt("TLDR.Support", 3)
|
HelpText: "Get a summary of the channel",
|
||||||
for i := 0; i < nTopics; i++ {
|
Handler: p.betterTLDR,
|
||||||
bestScores[i] = make([]float64, supportingDocs)
|
},
|
||||||
bestDocs[i] = make([]history, supportingDocs)
|
{
|
||||||
}
|
Kind: bot.Message, IsCmd: false,
|
||||||
|
Regex: regexp.MustCompile(`.*`),
|
||||||
dr, dc := docsOverTopics.Dims()
|
Handler: p.record,
|
||||||
for topic := 0; topic < dr; topic++ {
|
},
|
||||||
minScore, minIndex := min(bestScores[topic])
|
})
|
||||||
|
p.b.Register(p, bot.Help, p.help)
|
||||||
for doc := 0; doc < dc; doc++ {
|
}
|
||||||
score := docsOverTopics.At(topic, doc)
|
|
||||||
if score > minScore {
|
|
||||||
bestScores[topic][minIndex] = score
|
|
||||||
bestDocs[topic][minIndex] = p.history[doc]
|
|
||||||
minScore, minIndex = min(bestScores[topic])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
topicsOverWords := lda.Components()
|
|
||||||
tr, tc := topicsOverWords.Dims()
|
|
||||||
|
|
||||||
vocab := make([]string, len(vectoriser.Vocabulary))
|
|
||||||
for k, v := range vectoriser.Vocabulary {
|
|
||||||
vocab[v] = k
|
|
||||||
}
|
|
||||||
|
|
||||||
response := "Here you go captain 'too good to read backlog':\n"
|
|
||||||
|
|
||||||
for topic := 0; topic < tr; topic++ {
|
|
||||||
bestScore := -1.
|
|
||||||
bestTopic := ""
|
|
||||||
for word := 0; word < tc; word++ {
|
|
||||||
score := topicsOverWords.At(topic, word)
|
|
||||||
if score > bestScore {
|
|
||||||
bestScore = score
|
|
||||||
bestTopic = vocab[word]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
response += fmt.Sprintf("\n*Topic #%d: %s*\n", topic, bestTopic)
|
|
||||||
for i := range bestDocs[topic] {
|
|
||||||
response += fmt.Sprintf("<%s>%s\n", bestDocs[topic][i].user, bestDocs[topic][i].body)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
p.bot.Send(c, bot.Message, message.Channel, response)
|
|
||||||
|
|
||||||
|
func (p *TLDRPlugin) tldrCmd(r bot.Request) bool {
|
||||||
|
timeLimit := time.Duration(p.b.Config().GetInt("TLDR.HourLimit", 1))
|
||||||
|
if p.lastRequest.After(time.Now().Add(-timeLimit * time.Hour)) {
|
||||||
|
p.b.Send(r.Conn, bot.Message, r.Msg.Channel, "Slow down, cowboy. Read that tiny backlog.")
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *TLDRPlugin) record(r bot.Request) bool {
|
||||||
hist := history{
|
hist := history{
|
||||||
body: lowercaseMessage,
|
body: strings.ToLower(r.Msg.Body),
|
||||||
user: message.User.Name,
|
user: r.Msg.User.Name,
|
||||||
timestamp: time.Now(),
|
timestamp: time.Now(),
|
||||||
}
|
}
|
||||||
p.addHistory(hist)
|
p.addHistory(r.Msg.Channel, hist)
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *TLDRPlugin) addHistory(hist history) {
|
func (p *TLDRPlugin) oldTLDR(r bot.Request) bool {
|
||||||
p.history = append(p.history, hist)
|
p.lastRequest = time.Now()
|
||||||
sz := len(p.history)
|
nTopics := p.b.Config().GetInt("TLDR.Topics", 5)
|
||||||
max := p.bot.Config().GetInt("TLDR.HistorySize", 1000)
|
|
||||||
keepHrs := time.Duration(p.bot.Config().GetInt("TLDR.KeepHours", 24))
|
stopWordSlice := p.b.Config().GetArray("TLDR.StopWords", []string{})
|
||||||
|
if len(stopWordSlice) == 0 {
|
||||||
|
stopWordSlice = THESE_ARE_NOT_THE_WORDS_YOU_ARE_LOOKING_FOR
|
||||||
|
p.b.Config().SetArray("TLDR.StopWords", stopWordSlice)
|
||||||
|
}
|
||||||
|
|
||||||
|
vectoriser := nlp.NewCountVectoriser(stopWordSlice...)
|
||||||
|
lda := nlp.NewLatentDirichletAllocation(nTopics)
|
||||||
|
pipeline := nlp.NewPipeline(vectoriser, lda)
|
||||||
|
docsOverTopics, err := pipeline.FitTransform(p.getTopics()...)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
bestScores := make([][]float64, nTopics)
|
||||||
|
bestDocs := make([][]history, nTopics)
|
||||||
|
|
||||||
|
supportingDocs := p.b.Config().GetInt("TLDR.Support", 3)
|
||||||
|
for i := 0; i < nTopics; i++ {
|
||||||
|
bestScores[i] = make([]float64, supportingDocs)
|
||||||
|
bestDocs[i] = make([]history, supportingDocs)
|
||||||
|
}
|
||||||
|
|
||||||
|
dr, dc := docsOverTopics.Dims()
|
||||||
|
for topic := 0; topic < dr; topic++ {
|
||||||
|
minScore, minIndex := min(bestScores[topic])
|
||||||
|
|
||||||
|
for doc := 0; doc < dc; doc++ {
|
||||||
|
score := docsOverTopics.At(topic, doc)
|
||||||
|
if score > minScore {
|
||||||
|
bestScores[topic][minIndex] = score
|
||||||
|
bestDocs[topic][minIndex] = p.history[r.Msg.Channel][doc]
|
||||||
|
minScore, minIndex = min(bestScores[topic])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
topicsOverWords := lda.Components()
|
||||||
|
tr, tc := topicsOverWords.Dims()
|
||||||
|
|
||||||
|
vocab := make([]string, len(vectoriser.Vocabulary))
|
||||||
|
for k, v := range vectoriser.Vocabulary {
|
||||||
|
vocab[v] = k
|
||||||
|
}
|
||||||
|
|
||||||
|
response := "Here you go captain 'too good to read backlog':\n"
|
||||||
|
|
||||||
|
for topic := 0; topic < tr; topic++ {
|
||||||
|
bestScore := -1.
|
||||||
|
bestTopic := ""
|
||||||
|
for word := 0; word < tc; word++ {
|
||||||
|
score := topicsOverWords.At(topic, word)
|
||||||
|
if score > bestScore {
|
||||||
|
bestScore = score
|
||||||
|
bestTopic = vocab[word]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
response += fmt.Sprintf("\n*Topic #%d: %s*\n", topic, bestTopic)
|
||||||
|
for i := range bestDocs[topic] {
|
||||||
|
response += fmt.Sprintf("<%s>%s\n", bestDocs[topic][i].user, bestDocs[topic][i].body)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
p.b.Send(r.Conn, bot.Message, r.Msg.Channel, response)
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *TLDRPlugin) addHistory(ch string, hist history) {
|
||||||
|
p.history[ch] = append(p.history[ch], hist)
|
||||||
|
sz := len(p.history[ch])
|
||||||
|
max := p.b.Config().GetInt("TLDR.HistorySize", 1000)
|
||||||
|
keepHrs := time.Duration(p.b.Config().GetInt("TLDR.KeepHours", 24))
|
||||||
// Clamp the size of the history
|
// Clamp the size of the history
|
||||||
if sz > max {
|
if sz > max {
|
||||||
p.history = p.history[len(p.history)-max:]
|
p.history[ch] = p.history[ch][len(p.history)-max:]
|
||||||
}
|
}
|
||||||
// Remove old entries
|
// Remove old entries
|
||||||
yesterday := time.Now().Add(-keepHrs * time.Hour)
|
yesterday := time.Now().Add(-keepHrs * time.Hour)
|
||||||
begin := 0
|
begin := 0
|
||||||
for i, m := range p.history {
|
for i, m := range p.history[ch] {
|
||||||
if !m.timestamp.Before(yesterday) {
|
if !m.timestamp.Before(yesterday) {
|
||||||
begin = i - 1 // should keep this message
|
begin = i - 1 // should keep this message
|
||||||
if begin < 0 {
|
if begin < 0 {
|
||||||
|
@ -150,20 +206,22 @@ func (p *TLDRPlugin) addHistory(hist history) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
p.history = p.history[begin:]
|
p.history[ch] = p.history[ch][begin:]
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *TLDRPlugin) getTopics() []string {
|
func (p *TLDRPlugin) getTopics() []string {
|
||||||
hist := []string{}
|
hist := []string{}
|
||||||
for _, h := range p.history {
|
for _, ch := range p.history {
|
||||||
hist = append(hist, h.body)
|
for _, h := range ch {
|
||||||
|
hist = append(hist, h.body)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return hist
|
return hist
|
||||||
}
|
}
|
||||||
|
|
||||||
// Help responds to help requests. Every plugin must implement a help function.
|
// Help responds to help requests. Every plugin must implement a help function.
|
||||||
func (p *TLDRPlugin) help(c bot.Connector, kind bot.Kind, message msg.Message, args ...any) bool {
|
func (p *TLDRPlugin) help(c bot.Connector, kind bot.Kind, message msg.Message, args ...any) bool {
|
||||||
p.bot.Send(c, bot.Message, message.Channel, "tl;dr")
|
p.b.Send(c, bot.Message, message.Channel, "tl;dr")
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -178,3 +236,69 @@ func min(slice []float64) (float64, int) {
|
||||||
}
|
}
|
||||||
return minVal, minIndex
|
return minVal, minIndex
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *TLDRPlugin) betterTLDR(r bot.Request) bool {
|
||||||
|
ch := r.Msg.Channel
|
||||||
|
c, err := p.getClient()
|
||||||
|
if err != nil {
|
||||||
|
p.b.Send(r.Conn, bot.Message, r.Msg.Channel, "Couldn't fetch an OpenAI client")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
promptConfig := p.c.Get(templateKey, defaultTemplate)
|
||||||
|
promptTpl := template.Must(template.New("gptprompt").Parse(promptConfig))
|
||||||
|
prompt := bytes.Buffer{}
|
||||||
|
data := p.c.GetMap("tldr.promptdata", map[string]string{})
|
||||||
|
promptTpl.Execute(&prompt, data)
|
||||||
|
backlog := ""
|
||||||
|
maxLen := p.c.GetInt("tldr.maxgpt", 4096)
|
||||||
|
for i := len(p.history[ch]) - 1; i >= 0; i-- {
|
||||||
|
h := p.history[ch][i]
|
||||||
|
str := fmt.Sprintf("%s: %s\n", h.user, h.body)
|
||||||
|
if len(backlog) > maxLen {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
backlog = str + backlog
|
||||||
|
}
|
||||||
|
sess := c.NewChatSession(prompt.String())
|
||||||
|
completion, err := sess.Complete(context.TODO(), backlog)
|
||||||
|
if err != nil {
|
||||||
|
p.b.Send(r.Conn, bot.Message, r.Msg.Channel, "Couldn't run the OpenAI request")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
log.Debug().
|
||||||
|
Str("prompt", prompt.String()).
|
||||||
|
Str("backlog", backlog).
|
||||||
|
Str("completion", completion).
|
||||||
|
Msgf("tl;dr")
|
||||||
|
p.b.Send(r.Conn, bot.Message, r.Msg.Channel, completion)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *TLDRPlugin) squawkTLDR(r bot.Request) bool {
|
||||||
|
prompt := p.c.Get(templateKey, defaultTemplate)
|
||||||
|
p.b.Send(r.Conn, bot.Message, r.Msg.Channel, fmt.Sprintf(`Current prompt is: "%s"`,
|
||||||
|
strings.TrimSpace(prompt)))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *TLDRPlugin) resetTLDR(r bot.Request) bool {
|
||||||
|
p.c.Set(templateKey, defaultTemplate)
|
||||||
|
p.b.Send(r.Conn, bot.Message, r.Msg.Channel, fmt.Sprintf(`Set prompt to: "%s"`,
|
||||||
|
strings.TrimSpace(defaultTemplate)))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *TLDRPlugin) setTLDR(r bot.Request) bool {
|
||||||
|
prompt := r.Values["prompt"] + "\n"
|
||||||
|
p.c.Set(templateKey, prompt)
|
||||||
|
p.b.Send(r.Conn, bot.Message, r.Msg.Channel, fmt.Sprintf(`Set prompt to: "%s"`, strings.TrimSpace(prompt)))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *TLDRPlugin) getClient() (*openai.Client, error) {
|
||||||
|
token := p.c.Get("gpt.token", "")
|
||||||
|
if token == "" {
|
||||||
|
return nil, fmt.Errorf("no GPT token given")
|
||||||
|
}
|
||||||
|
return openai.NewClient(token)
|
||||||
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package tldr
|
package tldr
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/velour/catbase/plugins/cli"
|
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -20,20 +19,26 @@ func init() {
|
||||||
log.Logger = log.Logger.Output(zerolog.ConsoleWriter{Out: os.Stderr})
|
log.Logger = log.Logger.Output(zerolog.ConsoleWriter{Out: os.Stderr})
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeMessageBy(payload, by string) (bot.Connector, bot.Kind, msg.Message) {
|
var ch = "test"
|
||||||
|
|
||||||
|
func makeMessageBy(payload, by string) bot.Request {
|
||||||
isCmd := strings.HasPrefix(payload, "!")
|
isCmd := strings.HasPrefix(payload, "!")
|
||||||
if isCmd {
|
if isCmd {
|
||||||
payload = payload[1:]
|
payload = payload[1:]
|
||||||
}
|
}
|
||||||
return &cli.CliPlugin{}, bot.Message, msg.Message{
|
|
||||||
User: &user.User{Name: by},
|
return bot.Request{
|
||||||
Channel: "test",
|
Kind: bot.Message,
|
||||||
Body: payload,
|
Msg: msg.Message{
|
||||||
Command: isCmd,
|
User: &user.User{Name: by},
|
||||||
|
Channel: ch,
|
||||||
|
Body: payload,
|
||||||
|
Command: isCmd,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeMessage(payload string) (bot.Connector, bot.Kind, msg.Message) {
|
func makeMessage(payload string) bot.Request {
|
||||||
return makeMessageBy(payload, "tester")
|
return makeMessageBy(payload, "tester")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -43,51 +48,12 @@ func setup(t *testing.T) (*TLDRPlugin, *bot.MockBot) {
|
||||||
return r, mb
|
return r, mb
|
||||||
}
|
}
|
||||||
|
|
||||||
func Test(t *testing.T) {
|
|
||||||
c, mb := setup(t)
|
|
||||||
res := c.message(makeMessage("The quick brown fox jumped over the lazy dog"))
|
|
||||||
res = c.message(makeMessage("The cow jumped over the moon"))
|
|
||||||
res = c.message(makeMessage("The little dog laughed to see such fun"))
|
|
||||||
res = c.message(makeMessage("tl;dr"))
|
|
||||||
assert.True(t, res)
|
|
||||||
assert.Len(t, mb.Messages, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDoubleUp(t *testing.T) {
|
|
||||||
c, mb := setup(t)
|
|
||||||
res := c.message(makeMessage("The quick brown fox jumped over the lazy dog"))
|
|
||||||
res = c.message(makeMessage("The cow jumped over the moon"))
|
|
||||||
res = c.message(makeMessage("The little dog laughed to see such fun"))
|
|
||||||
res = c.message(makeMessage("tl;dr"))
|
|
||||||
res = c.message(makeMessage("tl;dr"))
|
|
||||||
assert.True(t, res)
|
|
||||||
assert.Len(t, mb.Messages, 2)
|
|
||||||
assert.Contains(t, mb.Messages[1], "Slow down, cowboy.")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAddHistoryLimitsMessages(t *testing.T) {
|
|
||||||
c, _ := setup(t)
|
|
||||||
max := 1000
|
|
||||||
c.bot.Config().Set("TLDR.HistorySize", strconv.Itoa(max))
|
|
||||||
c.bot.Config().Set("TLDR.KeepHours", "24")
|
|
||||||
t0 := time.Now().Add(-24 * time.Hour)
|
|
||||||
for i := 0; i < max*2; i++ {
|
|
||||||
hist := history{
|
|
||||||
body: "test",
|
|
||||||
user: "tester",
|
|
||||||
timestamp: t0.Add(time.Duration(i) * time.Second),
|
|
||||||
}
|
|
||||||
c.addHistory(hist)
|
|
||||||
}
|
|
||||||
assert.Len(t, c.history, max)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAddHistoryLimitsDays(t *testing.T) {
|
func TestAddHistoryLimitsDays(t *testing.T) {
|
||||||
c, _ := setup(t)
|
c, _ := setup(t)
|
||||||
hrs := 24
|
hrs := 24
|
||||||
expected := 24
|
expected := 24
|
||||||
c.bot.Config().Set("TLDR.HistorySize", "100")
|
c.b.Config().Set("TLDR.HistorySize", "100")
|
||||||
c.bot.Config().Set("TLDR.KeepHours", strconv.Itoa(hrs))
|
c.b.Config().Set("TLDR.KeepHours", strconv.Itoa(hrs))
|
||||||
t0 := time.Now().Add(-time.Duration(hrs*2) * time.Hour)
|
t0 := time.Now().Add(-time.Duration(hrs*2) * time.Hour)
|
||||||
for i := 0; i < 48; i++ {
|
for i := 0; i < 48; i++ {
|
||||||
hist := history{
|
hist := history{
|
||||||
|
@ -95,7 +61,7 @@ func TestAddHistoryLimitsDays(t *testing.T) {
|
||||||
user: "tester",
|
user: "tester",
|
||||||
timestamp: t0.Add(time.Duration(i) * time.Hour),
|
timestamp: t0.Add(time.Duration(i) * time.Hour),
|
||||||
}
|
}
|
||||||
c.addHistory(hist)
|
c.addHistory(ch, hist)
|
||||||
}
|
}
|
||||||
assert.Len(t, c.history, expected, "%d != %d", len(c.history), expected)
|
assert.Len(t, c.history[ch], expected, "%d != %d", len(c.history), expected)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,142 @@
|
||||||
|
package twitch
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/cenkalti/backoff/v4"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"github.com/velour/catbase/bot"
|
||||||
|
"github.com/velour/catbase/connectors/discord"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (t *Twitch) mkBridge(r bot.Request) bool {
|
||||||
|
ircCh := "#" + r.Values["twitchChannel"]
|
||||||
|
if err := t.startConn(); err != nil {
|
||||||
|
t.b.Send(r.Conn, bot.Message, r.Msg.Channel, fmt.Sprintf("Could not connect to IRC: %s", err))
|
||||||
|
}
|
||||||
|
t.irc.Join(ircCh)
|
||||||
|
t.bridgeMap[r.Msg.Channel] = ircCh
|
||||||
|
t.b.Send(r.Conn, bot.Message, r.Msg.Channel, fmt.Sprintf("This post is tracking %s", ircCh))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Twitch) rmBridge(r bot.Request) bool {
|
||||||
|
ch, ok := t.bridgeMap[r.Msg.Channel]
|
||||||
|
if ok {
|
||||||
|
delete(t.bridgeMap, r.Msg.Channel)
|
||||||
|
t.b.Send(r.Conn, bot.Message, r.Msg.Channel, fmt.Sprintf("No longer tracking %s.", ch))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
t.b.Send(r.Conn, bot.Message, r.Msg.Channel, "This is not a connected bridge channel.")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Twitch) bridgeMsg(r bot.Request) bool {
|
||||||
|
if ircCh := t.bridgeMap[r.Msg.Channel]; ircCh != "" {
|
||||||
|
if t.irc == nil {
|
||||||
|
if err := t.startConn(); err != nil {
|
||||||
|
t.b.Send(r.Conn, bot.Message, r.Msg.Channel, fmt.Sprintf("Could not connect to IRC: %s", err))
|
||||||
|
}
|
||||||
|
t.irc.Join(ircCh)
|
||||||
|
}
|
||||||
|
replaceSet := t.c.Get("twitch.replaceset", "\"'-")
|
||||||
|
who := r.Msg.User.Name
|
||||||
|
for _, c := range replaceSet {
|
||||||
|
who = strings.ReplaceAll(who, string(c), "")
|
||||||
|
}
|
||||||
|
who = strings.Split(who, " ")[0][:9]
|
||||||
|
msg := fmt.Sprintf("%s: %s", who, r.Msg.Body)
|
||||||
|
t.irc.sendMessage(ircCh, msg)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Twitch) ircMsg(channel, who, body string) {
|
||||||
|
for thread, ircCh := range t.bridgeMap {
|
||||||
|
if ircCh == channel {
|
||||||
|
t.b.Send(t.b.DefaultConnector(), bot.Message, thread, fmt.Sprintf("%s: %s", who, body))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Twitch) startBridgeMsg(threadName, twitchChannel, msg string) error {
|
||||||
|
if !strings.HasPrefix(twitchChannel, "#") {
|
||||||
|
twitchChannel = "#" + twitchChannel
|
||||||
|
}
|
||||||
|
if err := t.startConn(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
chID, err := t.mkForumPost(threadName, msg)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.Debug().Msgf("Opened thread %s", chID)
|
||||||
|
|
||||||
|
err = t.irc.Join(twitchChannel)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
t.bridgeMap[chID] = twitchChannel
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Twitch) startBridge(threadName, twitchChannel string) error {
|
||||||
|
if !strings.HasPrefix(twitchChannel, "#") {
|
||||||
|
twitchChannel = "#" + twitchChannel
|
||||||
|
}
|
||||||
|
msg := fmt.Sprintf("This post is tracking %s", twitchChannel)
|
||||||
|
return t.startBridgeMsg(threadName, twitchChannel, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Twitch) startConn() error {
|
||||||
|
if t.irc == nil {
|
||||||
|
err := backoff.Retry(func() error {
|
||||||
|
err := t.connect()
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("could not connect to IRC")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}, backoff.WithMaxRetries(backoff.NewExponentialBackOff(), 5))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Twitch) connect() error {
|
||||||
|
t.ircLock.Lock()
|
||||||
|
defer t.ircLock.Unlock()
|
||||||
|
twitchServer := t.c.Get("twitch.ircserver", "irc.chat.twitch.tv:6697")
|
||||||
|
twitchNick := t.c.Get("twitch.nick", "")
|
||||||
|
twitchPass := t.c.Get("twitch.pass", "")
|
||||||
|
twitchIRC, err := t.ConnectIRC(twitchServer, twitchNick, twitchPass, t.ircMsg, func() {
|
||||||
|
t.ircLock.Lock()
|
||||||
|
defer t.ircLock.Unlock()
|
||||||
|
t.irc = nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
t.irc = twitchIRC
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Twitch) mkForumPost(name, msg string) (string, error) {
|
||||||
|
forum := t.c.Get("twitch.forum", "")
|
||||||
|
if forum == "" {
|
||||||
|
return "", fmt.Errorf("no forum available")
|
||||||
|
}
|
||||||
|
switch c := t.b.DefaultConnector().(type) {
|
||||||
|
case *discord.Discord:
|
||||||
|
chID, err := c.CreateRoom(name, msg, forum, t.c.GetInt("twitch.threadduration", 60))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return chID, nil
|
||||||
|
default:
|
||||||
|
return "", fmt.Errorf("non-Discord connectors not supported")
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -0,0 +1,177 @@
|
||||||
|
package twitch
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"github.com/velour/catbase/config"
|
||||||
|
"github.com/velour/velour/irc"
|
||||||
|
"io"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var throttle <-chan time.Time
|
||||||
|
|
||||||
|
type eventFunc func(channel, who, body string)
|
||||||
|
|
||||||
|
type IRC struct {
|
||||||
|
t *Twitch
|
||||||
|
c *config.Config
|
||||||
|
client *irc.Client
|
||||||
|
event eventFunc
|
||||||
|
quit chan bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Twitch) ConnectIRC(server, user, pass string, handler eventFunc, disconnect func()) (*IRC, error) {
|
||||||
|
log.Debug().Msgf("Connecting to %s, %s:%s", server, user, pass)
|
||||||
|
i := &IRC{
|
||||||
|
t: t,
|
||||||
|
c: t.c,
|
||||||
|
event: handler}
|
||||||
|
wait := make(chan bool)
|
||||||
|
go i.serve(server, user, pass, wait, disconnect)
|
||||||
|
<-wait
|
||||||
|
return i, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *IRC) Join(channel string) error {
|
||||||
|
i.client.Out <- irc.Msg{Cmd: irc.JOIN, Args: []string{channel}}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *IRC) Say(channel, body string) error {
|
||||||
|
if _, err := i.sendMessage(channel, body); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *IRC) serve(server, user, pass string, wait chan bool, disconnect func()) {
|
||||||
|
if i.event == nil {
|
||||||
|
log.Error().Msgf("Missing event handler")
|
||||||
|
wait <- true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
i.client, err = irc.DialSSL(server, user, user, pass, true)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().
|
||||||
|
Err(err).
|
||||||
|
Strs("args", []string{server, user, pass}).
|
||||||
|
Msgf("Could not connect")
|
||||||
|
wait <- true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
i.client.Out <- irc.Msg{Cmd: "CAP REQ", Args: []string{":twitch.tv/membership"}}
|
||||||
|
|
||||||
|
i.quit = make(chan bool)
|
||||||
|
go i.handleConnection()
|
||||||
|
wait <- true
|
||||||
|
<-i.quit
|
||||||
|
disconnect()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *IRC) handleMsg(msg irc.Msg) {
|
||||||
|
switch msg.Cmd {
|
||||||
|
case irc.ERROR:
|
||||||
|
log.Info().Msgf("Received error: " + msg.Raw)
|
||||||
|
|
||||||
|
case irc.PING:
|
||||||
|
i.client.Out <- irc.Msg{Cmd: irc.PONG}
|
||||||
|
|
||||||
|
case irc.PONG:
|
||||||
|
// OK, ignore
|
||||||
|
|
||||||
|
case irc.KICK:
|
||||||
|
fallthrough
|
||||||
|
|
||||||
|
case irc.TOPIC:
|
||||||
|
fallthrough
|
||||||
|
|
||||||
|
case irc.NOTICE:
|
||||||
|
fallthrough
|
||||||
|
|
||||||
|
case irc.PRIVMSG:
|
||||||
|
if len(msg.Args) < 2 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
i.event(msg.Args[0], msg.Origin, msg.Args[1])
|
||||||
|
|
||||||
|
case irc.QUIT:
|
||||||
|
log.Debug().
|
||||||
|
Interface("msg", msg).
|
||||||
|
Msgf("QUIT")
|
||||||
|
i.quit <- true
|
||||||
|
|
||||||
|
default:
|
||||||
|
log.Debug().
|
||||||
|
Interface("msg", msg).
|
||||||
|
Msgf("IRC EVENT")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *IRC) sendMessage(channel, message string, args ...any) (string, error) {
|
||||||
|
for len(message) > 0 {
|
||||||
|
m := irc.Msg{
|
||||||
|
Cmd: "PRIVMSG",
|
||||||
|
Args: []string{channel, message},
|
||||||
|
}
|
||||||
|
_, err := m.RawString()
|
||||||
|
if err != nil {
|
||||||
|
mtl := err.(irc.MsgTooLong)
|
||||||
|
m.Args[1] = message[:mtl.NTrunc]
|
||||||
|
message = message[mtl.NTrunc:]
|
||||||
|
} else {
|
||||||
|
message = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if throttle == nil {
|
||||||
|
ratePerSec := i.c.GetInt("RatePerSec", 5)
|
||||||
|
throttle = time.Tick(time.Second / time.Duration(ratePerSec))
|
||||||
|
}
|
||||||
|
|
||||||
|
<-throttle
|
||||||
|
|
||||||
|
i.client.Out <- m
|
||||||
|
}
|
||||||
|
return "NO_IRC_IDENTIFIERS", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *IRC) handleConnection() {
|
||||||
|
pingTime := time.Duration(i.c.GetInt("twitch.pingtime", 60)) * time.Second
|
||||||
|
t := time.NewTimer(pingTime)
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
t.Stop()
|
||||||
|
close(i.client.Out)
|
||||||
|
for err := range i.client.Errors {
|
||||||
|
if err != io.EOF {
|
||||||
|
log.Error().Err(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case msg, ok := <-i.client.In:
|
||||||
|
if !ok { // disconnect
|
||||||
|
i.quit <- true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
t.Stop()
|
||||||
|
t = time.NewTimer(pingTime)
|
||||||
|
i.handleMsg(msg)
|
||||||
|
|
||||||
|
case <-t.C:
|
||||||
|
i.client.Out <- irc.Msg{Cmd: irc.PING, Args: []string{i.client.Server}}
|
||||||
|
t = time.NewTimer(pingTime)
|
||||||
|
|
||||||
|
case err, ok := <-i.client.Errors:
|
||||||
|
if ok && err != io.EOF {
|
||||||
|
log.Error().Err(err)
|
||||||
|
i.quit <- true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,12 +3,15 @@ package twitch
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"github.com/nicklaw5/helix"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"text/template"
|
"text/template"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -26,21 +29,33 @@ const (
|
||||||
stoppedStreamingTplFallback = "{{.Name}} just stopped streaming"
|
stoppedStreamingTplFallback = "{{.Name}} just stopped streaming"
|
||||||
)
|
)
|
||||||
|
|
||||||
type TwitchPlugin struct {
|
type Twitch struct {
|
||||||
b bot.Bot
|
b bot.Bot
|
||||||
c *config.Config
|
c *config.Config
|
||||||
twitchList map[string]*Twitcher
|
twitchList map[string]*Twitcher
|
||||||
tbl bot.HandlerTable
|
tbl bot.HandlerTable
|
||||||
|
ircConnected bool
|
||||||
|
irc *IRC
|
||||||
|
ircLock sync.Mutex
|
||||||
|
bridgeMap map[string]string
|
||||||
|
|
||||||
|
helix *helix.Client
|
||||||
|
subs map[string]bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type Twitcher struct {
|
type Twitcher struct {
|
||||||
name string
|
id string
|
||||||
gameID string
|
gameID string
|
||||||
|
online bool
|
||||||
|
|
||||||
|
Name string
|
||||||
|
Game string
|
||||||
|
URL string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t Twitcher) URL() string {
|
func (t Twitcher) url() string {
|
||||||
u, _ := url.Parse("https://twitch.tv/")
|
u, _ := url.Parse("https://twitch.tv/")
|
||||||
u2, _ := url.Parse(t.name)
|
u2, _ := url.Parse(t.Name)
|
||||||
return u.ResolveReference(u2).String()
|
return u.ResolveReference(u2).String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -62,71 +77,35 @@ type stream struct {
|
||||||
} `json:"pagination"`
|
} `json:"pagination"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(b bot.Bot) *TwitchPlugin {
|
func New(b bot.Bot) *Twitch {
|
||||||
p := &TwitchPlugin{
|
p := &Twitch{
|
||||||
b: b,
|
b: b,
|
||||||
c: b.Config(),
|
c: b.Config(),
|
||||||
twitchList: map[string]*Twitcher{},
|
twitchList: map[string]*Twitcher{},
|
||||||
|
bridgeMap: map[string]string{},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, ch := range p.c.GetArray("Twitch.Channels", []string{}) {
|
for _, twitcherName := range p.c.GetArray("Twitch.Users", []string{}) {
|
||||||
for _, twitcherName := range p.c.GetArray("Twitch."+ch+".Users", []string{}) {
|
twitcherName = strings.ToLower(twitcherName)
|
||||||
twitcherName = strings.ToLower(twitcherName)
|
p.twitchList[twitcherName] = &Twitcher{
|
||||||
if _, ok := p.twitchList[twitcherName]; !ok {
|
Name: twitcherName,
|
||||||
p.twitchList[twitcherName] = &Twitcher{
|
|
||||||
name: twitcherName,
|
|
||||||
gameID: "",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
go p.twitchChannelLoop(b.DefaultConnector(), ch)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
go p.twitchAuthLoop(b.DefaultConnector())
|
|
||||||
|
|
||||||
p.register()
|
p.register()
|
||||||
p.registerWeb()
|
p.registerWeb()
|
||||||
|
|
||||||
return p
|
return p
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *TwitchPlugin) registerWeb() {
|
func (p *Twitch) registerWeb() {
|
||||||
r := chi.NewRouter()
|
r := chi.NewRouter()
|
||||||
r.HandleFunc("/{user}", p.serveStreaming)
|
r.HandleFunc("/online", p.onlineCB)
|
||||||
p.b.RegisterWeb(r, "/isstreaming")
|
r.HandleFunc("/offline", p.offlineCB)
|
||||||
|
p.b.GetWeb().RegisterWeb(r, "/twitch")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *TwitchPlugin) serveStreaming(w http.ResponseWriter, r *http.Request) {
|
func (p *Twitch) register() {
|
||||||
userName := strings.ToLower(chi.URLParam(r, "user"))
|
|
||||||
if userName == "" {
|
|
||||||
fmt.Fprint(w, "User not found.")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
twitcher := p.twitchList[userName]
|
|
||||||
if twitcher == nil {
|
|
||||||
fmt.Fprint(w, "User not found.")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
status := "NO."
|
|
||||||
if twitcher.gameID != "" {
|
|
||||||
status = "YES."
|
|
||||||
}
|
|
||||||
context := map[string]any{"Name": twitcher.name, "Status": status}
|
|
||||||
|
|
||||||
t, err := template.New("streaming").Parse(page)
|
|
||||||
if err != nil {
|
|
||||||
log.Error().Err(err).Msg("Could not parse template!")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
err = t.Execute(w, context)
|
|
||||||
if err != nil {
|
|
||||||
log.Error().Err(err).Msg("Could not execute template!")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *TwitchPlugin) register() {
|
|
||||||
p.tbl = bot.HandlerTable{
|
p.tbl = bot.HandlerTable{
|
||||||
{
|
{
|
||||||
Kind: bot.Message, IsCmd: true,
|
Kind: bot.Message, IsCmd: true,
|
||||||
|
@ -146,21 +125,144 @@ func (p *TwitchPlugin) register() {
|
||||||
HelpText: "Reset the twitch templates",
|
HelpText: "Reset the twitch templates",
|
||||||
Handler: p.resetTwitch,
|
Handler: p.resetTwitch,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Kind: bot.Message, IsCmd: true,
|
||||||
|
Regex: regexp.MustCompile(`(?i)^track (?P<twitchChannel>.+)$`),
|
||||||
|
HelpText: "Bridge to this channel",
|
||||||
|
Handler: p.mkBridge,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Kind: bot.Message, IsCmd: true,
|
||||||
|
Regex: regexp.MustCompile(`(?i)^untrack$`),
|
||||||
|
HelpText: "Disconnect a bridge (only in bridged channel)",
|
||||||
|
Handler: p.rmBridge,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Kind: bot.Message, IsCmd: false,
|
||||||
|
Regex: regexp.MustCompile(`.*`),
|
||||||
|
Handler: p.bridgeMsg,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Kind: bot.Startup, IsCmd: false,
|
||||||
|
Regex: regexp.MustCompile(`.*`),
|
||||||
|
Handler: p.startup,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Kind: bot.Shutdown, IsCmd: false,
|
||||||
|
Regex: regexp.MustCompile(`.*`),
|
||||||
|
Handler: p.shutdown,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
p.b.Register(p, bot.Help, p.help)
|
p.b.Register(p, bot.Help, p.help)
|
||||||
p.b.RegisterTable(p, p.tbl)
|
p.b.RegisterTable(p, p.tbl)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *TwitchPlugin) twitchStatus(r bot.Request) bool {
|
func (p *Twitch) shutdown(r bot.Request) bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Twitch) startup(r bot.Request) bool {
|
||||||
|
var err error
|
||||||
|
clientID := p.c.Get("twitch.clientid", "")
|
||||||
|
clientSecret := p.c.Get("twitch.secret", "")
|
||||||
|
|
||||||
|
if clientID == "" || clientSecret == "" {
|
||||||
|
log.Info().Msg("No clientID/secret, twitch disabled")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
p.helix, err = helix.NewClient(&helix.Options{
|
||||||
|
ClientID: clientID,
|
||||||
|
ClientSecret: clientSecret,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Login error: %v", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
access, err := p.helix.RequestAppAccessToken([]string{"user:read:email"})
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Login error: %v", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("%+v\n", access)
|
||||||
|
|
||||||
|
// Set the access token on the client
|
||||||
|
p.helix.SetAppAccessToken(access.Data.AccessToken)
|
||||||
|
|
||||||
|
p.subs = map[string]bool{}
|
||||||
|
|
||||||
|
for _, t := range p.twitchList {
|
||||||
|
if err := p.follow(t); err != nil {
|
||||||
|
log.Error().Err(err).Msg("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Twitch) follow(twitcher *Twitcher) error {
|
||||||
|
if twitcher.id == "" {
|
||||||
|
users, err := p.helix.GetUsers(&helix.UsersParams{
|
||||||
|
Logins: []string{twitcher.Name},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if users.Error != "" {
|
||||||
|
return errors.New(users.Error)
|
||||||
|
}
|
||||||
|
twitcher.id = users.Data.Users[0].ID
|
||||||
|
}
|
||||||
|
|
||||||
|
base := p.c.Get("baseurl", "") + "/twitch"
|
||||||
|
|
||||||
|
_, err := p.helix.CreateEventSubSubscription(&helix.EventSubSubscription{
|
||||||
|
Type: helix.EventSubTypeStreamOnline,
|
||||||
|
Version: "1",
|
||||||
|
Condition: helix.EventSubCondition{
|
||||||
|
BroadcasterUserID: twitcher.id,
|
||||||
|
},
|
||||||
|
Transport: helix.EventSubTransport{
|
||||||
|
Method: "webhook",
|
||||||
|
Callback: base + "/online",
|
||||||
|
Secret: "s3cre7w0rd",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = p.helix.CreateEventSubSubscription(&helix.EventSubSubscription{
|
||||||
|
Type: helix.EventSubTypeStreamOffline,
|
||||||
|
Version: "1",
|
||||||
|
Condition: helix.EventSubCondition{
|
||||||
|
BroadcasterUserID: twitcher.id,
|
||||||
|
},
|
||||||
|
Transport: helix.EventSubTransport{
|
||||||
|
Method: "webhook",
|
||||||
|
Callback: base + "/offline",
|
||||||
|
Secret: "s3cre7w0rd",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Twitch) twitchStatus(r bot.Request) bool {
|
||||||
channel := r.Msg.Channel
|
channel := r.Msg.Channel
|
||||||
if users := p.c.GetArray("Twitch."+channel+".Users", []string{}); len(users) > 0 {
|
if users := p.c.GetArray("Twitch."+channel+".Users", []string{}); len(users) > 0 {
|
||||||
for _, twitcherName := range users {
|
for _, twitcherName := range users {
|
||||||
twitcherName = strings.ToLower(twitcherName)
|
twitcherName = strings.ToLower(twitcherName)
|
||||||
// we could re-add them here instead of needing to restart the bot.
|
// we could re-add them here instead of needing to restart the bot.
|
||||||
if t, ok := p.twitchList[twitcherName]; ok {
|
if t, ok := p.twitchList[twitcherName]; ok {
|
||||||
err := p.checkTwitch(r.Conn, channel, t, true)
|
if t.online {
|
||||||
if err != nil {
|
p.streaming(r.Conn, r.Msg.Channel, t)
|
||||||
log.Error().Err(err).Msgf("error in checking twitch")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -168,13 +270,11 @@ func (p *TwitchPlugin) twitchStatus(r bot.Request) bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *TwitchPlugin) twitchUserStatus(r bot.Request) bool {
|
func (p *Twitch) twitchUserStatus(r bot.Request) bool {
|
||||||
who := strings.ToLower(r.Values["who"])
|
who := strings.ToLower(r.Values["who"])
|
||||||
if t, ok := p.twitchList[who]; ok {
|
if t, ok := p.twitchList[who]; ok {
|
||||||
err := p.checkTwitch(r.Conn, r.Msg.Channel, t, true)
|
if t.online {
|
||||||
if err != nil {
|
p.streaming(r.Conn, r.Msg.Channel, t)
|
||||||
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 {
|
} else {
|
||||||
p.b.Send(r.Conn, bot.Message, r.Msg.Channel, "I don't know who that is.")
|
p.b.Send(r.Conn, bot.Message, r.Msg.Channel, "I don't know who that is.")
|
||||||
|
@ -182,7 +282,7 @@ func (p *TwitchPlugin) twitchUserStatus(r bot.Request) bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *TwitchPlugin) resetTwitch(r bot.Request) bool {
|
func (p *Twitch) resetTwitch(r bot.Request) bool {
|
||||||
p.c.Set("twitch.istpl", isStreamingTplFallback)
|
p.c.Set("twitch.istpl", isStreamingTplFallback)
|
||||||
p.c.Set("twitch.nottpl", notStreamingTplFallback)
|
p.c.Set("twitch.nottpl", notStreamingTplFallback)
|
||||||
p.c.Set("twitch.stoppedtpl", stoppedStreamingTplFallback)
|
p.c.Set("twitch.stoppedtpl", stoppedStreamingTplFallback)
|
||||||
|
@ -190,7 +290,7 @@ func (p *TwitchPlugin) resetTwitch(r bot.Request) bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *TwitchPlugin) help(c bot.Connector, kind bot.Kind, message msg.Message, args ...any) bool {
|
func (p *Twitch) help(c bot.Connector, kind bot.Kind, message msg.Message, args ...any) bool {
|
||||||
msg := "You can set the templates for streams with\n"
|
msg := "You can set the templates for streams with\n"
|
||||||
msg += fmt.Sprintf("twitch.istpl (default: %s)\n", isStreamingTplFallback)
|
msg += fmt.Sprintf("twitch.istpl (default: %s)\n", isStreamingTplFallback)
|
||||||
msg += fmt.Sprintf("twitch.nottpl (default: %s)\n", notStreamingTplFallback)
|
msg += fmt.Sprintf("twitch.nottpl (default: %s)\n", notStreamingTplFallback)
|
||||||
|
@ -201,224 +301,191 @@ func (p *TwitchPlugin) help(c bot.Connector, kind bot.Kind, message msg.Message,
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *TwitchPlugin) twitchAuthLoop(c bot.Connector) {
|
func (p *Twitch) connectBridge(c bot.Connector, ch string, info *Twitcher) {
|
||||||
frequency := p.c.GetInt("Twitch.AuthFreq", 60*60)
|
msg := fmt.Sprintf("This post is tracking #%s\n<%s>", info.Name, info.url())
|
||||||
cid := p.c.Get("twitch.clientid", "")
|
err := p.startBridgeMsg(
|
||||||
secret := p.c.Get("twitch.secret", "")
|
fmt.Sprintf("%s-%s-%s", info.Name, info.Game, time.Now().Format("2006-01-02-15:04")),
|
||||||
if cid == "" || secret == "" {
|
info.Name,
|
||||||
log.Info().Msgf("Disabling twitch autoauth.")
|
msg,
|
||||||
return
|
)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msgf("unable to start bridge")
|
||||||
|
p.b.Send(c, bot.Message, ch, fmt.Sprintf("Unable to start bridge: %s", err))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
log.Info().Msgf("Checking auth every %d seconds", frequency)
|
func (p *Twitch) disconnectBridge(c bot.Connector, twitcher *Twitcher) {
|
||||||
|
log.Debug().Msgf("Disconnecting bridge: %s -> %+v", twitcher.Name, p.bridgeMap)
|
||||||
if err := p.validateCredentials(); err != nil {
|
for threadID, ircCh := range p.bridgeMap {
|
||||||
log.Error().Err(err).Msgf("error checking twitch validity")
|
if strings.HasSuffix(ircCh, twitcher.Name) {
|
||||||
}
|
delete(p.bridgeMap, threadID)
|
||||||
|
p.b.Send(c, bot.Message, threadID, "Stopped tracking #"+twitcher.Name)
|
||||||
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) {
|
func (p *Twitch) stopped(c bot.Connector, ch string, info *Twitcher) {
|
||||||
frequency := p.c.GetInt("Twitch.Freq", 60)
|
notStreamingTpl := p.c.Get("Twitch.StoppedTpl", stoppedStreamingTplFallback)
|
||||||
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
|
|
||||||
client := &http.Client{}
|
|
||||||
req, err := http.NewRequest("GET", url, nil)
|
|
||||||
if err != nil {
|
|
||||||
goto errCase
|
|
||||||
}
|
|
||||||
|
|
||||||
req.Header.Add("Client-ID", clientID)
|
|
||||||
req.Header.Add("Authorization", bearer)
|
|
||||||
|
|
||||||
resp, err = client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
goto errCase
|
|
||||||
}
|
|
||||||
|
|
||||||
body, err = ioutil.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
goto errCase
|
|
||||||
}
|
|
||||||
return body, resp.StatusCode, true
|
|
||||||
|
|
||||||
errCase:
|
|
||||||
log.Error().Err(err)
|
|
||||||
return []byte{}, resp.StatusCode, false
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
|
||||||
err := fmt.Errorf("error parsing twitch stream URL")
|
|
||||||
log.Error().Msg(err.Error())
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
query := baseURL.Query()
|
|
||||||
query.Add("user_login", twitcher.name)
|
|
||||||
|
|
||||||
baseURL.RawQuery = query.Encode()
|
|
||||||
|
|
||||||
cid := p.c.Get("twitch.clientid", "")
|
|
||||||
token := p.c.Get("twitch.token", "")
|
|
||||||
if cid == token && cid == "" {
|
|
||||||
log.Info().Msgf("Twitch plugin not enabled.")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
body, status, ok := getRequest(baseURL.String(), cid, token)
|
|
||||||
if !ok {
|
|
||||||
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).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.c.Get("Twitch.NotTpl", notStreamingTplFallback)
|
|
||||||
isStreamingTpl := p.c.Get("Twitch.IsTpl", isStreamingTplFallback)
|
|
||||||
stoppedStreamingTpl := p.c.Get("Twitch.StoppedTpl", stoppedStreamingTplFallback)
|
|
||||||
buf := bytes.Buffer{}
|
buf := bytes.Buffer{}
|
||||||
|
t, err := template.New("notStreaming").Parse(notStreamingTpl)
|
||||||
info := struct {
|
if err != nil {
|
||||||
Name string
|
log.Error().Err(err)
|
||||||
Game string
|
p.b.Send(c, bot.Message, ch, err)
|
||||||
URL string
|
t = template.Must(template.New("notStreaming").Parse(stoppedStreamingTplFallback))
|
||||||
}{
|
|
||||||
twitcher.name,
|
|
||||||
title,
|
|
||||||
twitcher.URL(),
|
|
||||||
}
|
}
|
||||||
|
t.Execute(&buf, info)
|
||||||
|
p.b.Send(c, bot.Message, ch, buf.String())
|
||||||
|
}
|
||||||
|
|
||||||
if alwaysPrintStatus {
|
func (p *Twitch) streaming(c bot.Connector, channel string, info *Twitcher) {
|
||||||
if gameID == "" {
|
isStreamingTpl := p.c.Get("Twitch.IsTpl", isStreamingTplFallback)
|
||||||
t, err := template.New("notStreaming").Parse(notStreamingTpl)
|
buf := bytes.Buffer{}
|
||||||
if err != nil {
|
t, err := template.New("isStreaming").Parse(isStreamingTpl)
|
||||||
log.Error().Err(err)
|
if err != nil {
|
||||||
p.b.Send(c, bot.Message, channel, err)
|
log.Error().Err(err)
|
||||||
t = template.Must(template.New("notStreaming").Parse(notStreamingTplFallback))
|
p.b.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())
|
t.Execute(&buf, info)
|
||||||
} else {
|
p.b.Send(c, bot.Message, channel, buf.String())
|
||||||
t, err := template.New("isStreaming").Parse(isStreamingTpl)
|
}
|
||||||
if err != nil {
|
|
||||||
log.Error().Err(err)
|
func (p *Twitch) notStreaming(c bot.Connector, ch string, info *Twitcher) {
|
||||||
p.b.Send(c, bot.Message, channel, err)
|
notStreamingTpl := p.c.Get("Twitch.NotTpl", notStreamingTplFallback)
|
||||||
t = template.Must(template.New("isStreaming").Parse(isStreamingTplFallback))
|
buf := bytes.Buffer{}
|
||||||
}
|
t, err := template.New("notStreaming").Parse(notStreamingTpl)
|
||||||
t.Execute(&buf, info)
|
if err != nil {
|
||||||
p.b.Send(c, bot.Message, channel, buf.String())
|
log.Error().Err(err)
|
||||||
}
|
p.b.Send(c, bot.Message, ch, err)
|
||||||
} else if gameID == "" {
|
t = template.Must(template.New("notStreaming").Parse(notStreamingTplFallback))
|
||||||
if twitcher.gameID != "" {
|
}
|
||||||
t, err := template.New("stoppedStreaming").Parse(stoppedStreamingTpl)
|
t.Execute(&buf, info)
|
||||||
if err != nil {
|
p.b.Send(c, bot.Message, ch, buf.String())
|
||||||
log.Error().Err(err)
|
}
|
||||||
p.b.Send(c, bot.Message, channel, err)
|
|
||||||
t = template.Must(template.New("stoppedStreaming").Parse(stoppedStreamingTplFallback))
|
type twitchCB struct {
|
||||||
}
|
Challenge string `json:"challenge"`
|
||||||
t.Execute(&buf, info)
|
Subscription struct {
|
||||||
p.b.Send(c, bot.Message, channel, buf.String())
|
ID string `json:"id"`
|
||||||
}
|
Type string `json:"type"`
|
||||||
twitcher.gameID = ""
|
Version string `json:"version"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Cost int `json:"cost"`
|
||||||
|
Condition struct {
|
||||||
|
BroadcasterUserID string `json:"broadcaster_user_id"`
|
||||||
|
} `json:"condition"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
Transport struct {
|
||||||
|
Method string `json:"method"`
|
||||||
|
Callback string `json:"callback"`
|
||||||
|
} `json:"transport"`
|
||||||
|
} `json:"subscription"`
|
||||||
|
Event struct {
|
||||||
|
BroadcasterUserID string `json:"broadcaster_user_id"`
|
||||||
|
BroadcasterUserLogin string `json:"broadcaster_user_login"`
|
||||||
|
BroadcasterUserName string `json:"broadcaster_user_name"`
|
||||||
|
} `json:"event"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Twitch) offlineCB(w http.ResponseWriter, r *http.Request) {
|
||||||
|
body, err := io.ReadAll(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer r.Body.Close()
|
||||||
|
// verify that the notification came from twitch using the secret.
|
||||||
|
if !helix.VerifyEventSubNotification("s3cre7w0rd", r.Header, string(body)) {
|
||||||
|
log.Error().Msg("no valid signature on subscription")
|
||||||
|
return
|
||||||
} else {
|
} else {
|
||||||
if twitcher.gameID != gameID {
|
log.Info().Msg("verified signature for subscription")
|
||||||
t, err := template.New("isStreaming").Parse(isStreamingTpl)
|
}
|
||||||
if err != nil {
|
var vals twitchCB
|
||||||
log.Error().Err(err)
|
if err = json.Unmarshal(body, &vals); err != nil {
|
||||||
p.b.Send(c, bot.Message, channel, err)
|
log.Error().Err(err).Msg("")
|
||||||
t = template.Must(template.New("isStreaming").Parse(isStreamingTplFallback))
|
return
|
||||||
}
|
}
|
||||||
t.Execute(&buf, info)
|
|
||||||
p.b.Send(c, bot.Message, channel, buf.String())
|
if vals.Challenge != "" {
|
||||||
|
w.Write([]byte(vals.Challenge))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("got offline webhook: %v\n", vals)
|
||||||
|
w.WriteHeader(200)
|
||||||
|
w.Write([]byte("ok"))
|
||||||
|
|
||||||
|
twitcher := p.twitchList[vals.Event.BroadcasterUserLogin]
|
||||||
|
if !twitcher.online {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
twitcher.online = false
|
||||||
|
twitcher.URL = twitcher.url()
|
||||||
|
if ch := p.c.Get("twitch.channel", ""); ch != "" {
|
||||||
|
p.stopped(p.b.DefaultConnector(), ch, twitcher)
|
||||||
|
if p.c.GetBool("twitch.irc", false) {
|
||||||
|
p.disconnectBridge(p.b.DefaultConnector(), twitcher)
|
||||||
}
|
}
|
||||||
twitcher.gameID = gameID
|
|
||||||
}
|
}
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *TwitchPlugin) validateCredentials() error {
|
func (p *Twitch) onlineCB(w http.ResponseWriter, r *http.Request) {
|
||||||
cid := p.c.Get("twitch.clientid", "")
|
body, err := io.ReadAll(r.Body)
|
||||||
token := p.c.Get("twitch.token", "")
|
if err != nil {
|
||||||
if token == "" {
|
log.Error().Err(err).Msg("")
|
||||||
return p.reAuthenticate()
|
return
|
||||||
}
|
}
|
||||||
_, status, ok := getRequest("https://id.twitch.tv/oauth2/validate", cid, token)
|
defer r.Body.Close()
|
||||||
if !ok || status == http.StatusUnauthorized {
|
// verify that the notification came from twitch using the secret.
|
||||||
return p.reAuthenticate()
|
if !helix.VerifyEventSubNotification("s3cre7w0rd", r.Header, string(body)) {
|
||||||
|
log.Error().Msg("no valid signature on subscription")
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
log.Info().Msg("verified signature for subscription")
|
||||||
|
}
|
||||||
|
var vals twitchCB
|
||||||
|
if err = json.Unmarshal(body, &vals); err != nil {
|
||||||
|
log.Error().Err(err).Msg("")
|
||||||
|
return
|
||||||
}
|
}
|
||||||
log.Debug().Msgf("checked credentials and they were valid")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *TwitchPlugin) reAuthenticate() error {
|
if vals.Challenge != "" {
|
||||||
cid := p.c.Get("twitch.clientid", "")
|
w.Write([]byte(vals.Challenge))
|
||||||
secret := p.c.Get("twitch.secret", "")
|
return
|
||||||
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},
|
log.Printf("got online webhook: %v\n", vals)
|
||||||
"client_secret": {secret},
|
w.WriteHeader(200)
|
||||||
"grant_type": {"client_credentials"},
|
w.Write([]byte("ok"))
|
||||||
|
|
||||||
|
streams, err := p.helix.GetStreams(&helix.StreamsParams{
|
||||||
|
UserIDs: []string{vals.Event.BroadcasterUserID},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
log.Error().Err(err).Msg("")
|
||||||
|
return
|
||||||
}
|
}
|
||||||
body, err := ioutil.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
twitcher := p.twitchList[vals.Event.BroadcasterUserLogin]
|
||||||
return err
|
|
||||||
|
if twitcher.online {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
twitcher.online = true
|
||||||
|
twitcher.URL = twitcher.url()
|
||||||
|
if len(streams.Data.Streams) > 0 {
|
||||||
|
twitcher.gameID = streams.Data.Streams[0].GameID
|
||||||
|
twitcher.Game = streams.Data.Streams[0].GameName
|
||||||
|
} else {
|
||||||
|
twitcher.gameID = "-1"
|
||||||
|
twitcher.Game = p.c.Get("twitch.unknowngame", "IDK, Twitch didn't tell me")
|
||||||
|
}
|
||||||
|
if ch := p.c.Get("twitch.channel", ""); ch != "" {
|
||||||
|
p.streaming(p.b.DefaultConnector(), ch, twitcher)
|
||||||
|
if p.c.GetBool("twitch.irc", false) {
|
||||||
|
p.connectBridge(p.b.DefaultConnector(), ch, twitcher)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
package twitch
|
package twitch
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/velour/catbase/plugins/cli"
|
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
@ -29,7 +28,7 @@ func makeMessage(payload string) (bot.Connector, bot.Kind, msg.Message) {
|
||||||
if isCmd {
|
if isCmd {
|
||||||
payload = payload[1:]
|
payload = payload[1:]
|
||||||
}
|
}
|
||||||
return &cli.CliPlugin{}, bot.Message, msg.Message{
|
return nil, bot.Message, msg.Message{
|
||||||
User: &user.User{Name: "tester"},
|
User: &user.User{Name: "tester"},
|
||||||
Channel: "test",
|
Channel: "test",
|
||||||
Body: payload,
|
Body: payload,
|
||||||
|
@ -37,7 +36,7 @@ func makeMessage(payload string) (bot.Connector, bot.Kind, msg.Message) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeTwitchPlugin(t *testing.T) (*TwitchPlugin, *bot.MockBot) {
|
func makeTwitchPlugin(t *testing.T) (*Twitch, *bot.MockBot) {
|
||||||
mb := bot.NewMockBot()
|
mb := bot.NewMockBot()
|
||||||
c := New(mb)
|
c := New(mb)
|
||||||
mb.Config().Set("twitch.clientid", "fake")
|
mb.Config().Set("twitch.clientid", "fake")
|
||||||
|
@ -47,15 +46,9 @@ func makeTwitchPlugin(t *testing.T) (*TwitchPlugin, *bot.MockBot) {
|
||||||
assert.NotNil(t, c)
|
assert.NotNil(t, c)
|
||||||
|
|
||||||
c.twitchList["drseabass"] = &Twitcher{
|
c.twitchList["drseabass"] = &Twitcher{
|
||||||
name: "drseabass",
|
Name: "drseabass",
|
||||||
gameID: "",
|
gameID: "",
|
||||||
}
|
}
|
||||||
|
|
||||||
return c, mb
|
return c, mb
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTwitch(t *testing.T) {
|
|
||||||
b, mb := makeTwitchPlugin(t)
|
|
||||||
b.twitchStatus(makeRequest("!twitch status"))
|
|
||||||
assert.NotEmpty(t, mb.Messages)
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,20 +0,0 @@
|
||||||
package twitch
|
|
||||||
|
|
||||||
var page = `
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<title>Is {{.Name}} streaming?</title>
|
|
||||||
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body style="text-align: center; padding-top: 200px;">
|
|
||||||
|
|
||||||
<a style="font-weight: bold; font-size: 120pt;
|
|
||||||
font-family: Arial, sans-serif; text-decoration: none; color: black;"
|
|
||||||
title="{{.Status}}">{{.Status}}</a>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
`
|
|
|
@ -6,8 +6,6 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/velour/catbase/plugins/cli"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/velour/catbase/bot"
|
"github.com/velour/catbase/bot"
|
||||||
"github.com/velour/catbase/bot/msg"
|
"github.com/velour/catbase/bot/msg"
|
||||||
|
@ -20,7 +18,6 @@ func makeMessage(payload string) bot.Request {
|
||||||
payload = payload[1:]
|
payload = payload[1:]
|
||||||
}
|
}
|
||||||
return bot.Request{
|
return bot.Request{
|
||||||
Conn: &cli.CliPlugin{},
|
|
||||||
Kind: bot.Message,
|
Kind: bot.Message,
|
||||||
Msg: msg.Message{
|
Msg: msg.Message{
|
||||||
User: &user.User{Name: "tester"},
|
User: &user.User{Name: "tester"},
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
package stats
|
Loading…
Reference in New Issue