Compare commits
10 Commits
bf7e067628
...
8ee774240e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8ee774240e | ||
|
|
63d7454fb6 | ||
|
|
fb5e7b580e | ||
|
|
bdc40e91b4 | ||
|
|
64af8f5085 | ||
|
|
6a6524692a | ||
|
|
2372fa8bb9 | ||
|
|
052a093ad7 | ||
|
|
967d33b03b | ||
|
|
84e4600737 |
13
config.go
13
config.go
@@ -11,8 +11,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Port int
|
Port int
|
||||||
AB string
|
Debug bool
|
||||||
|
InitializeSlack bool
|
||||||
|
SlackToken string
|
||||||
|
SlackChannels string
|
||||||
}
|
}
|
||||||
|
|
||||||
func newConfig() (Config, error) {
|
func newConfig() (Config, error) {
|
||||||
@@ -55,6 +58,12 @@ func newConfigFromEnv(getEnv func(string) string) (Config, error) {
|
|||||||
return Config{}, err
|
return Config{}, err
|
||||||
}
|
}
|
||||||
m[k] = n
|
m[k] = n
|
||||||
|
case bool:
|
||||||
|
got, err := strconv.ParseBool(s)
|
||||||
|
if err != nil {
|
||||||
|
return Config{}, err
|
||||||
|
}
|
||||||
|
m[k] = got
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ func TestNewConfig(t *testing.T) {
|
|||||||
switch k {
|
switch k {
|
||||||
case "PORT":
|
case "PORT":
|
||||||
return "1"
|
return "1"
|
||||||
case "A_B":
|
case "INITIALIZE_SLACK":
|
||||||
return "2"
|
return "true"
|
||||||
default:
|
default:
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
@@ -17,7 +17,7 @@ func TestNewConfig(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
} else if got.Port != 1 {
|
} else if got.Port != 1 {
|
||||||
t.Error(got)
|
t.Error(got)
|
||||||
} else if got.AB != "2" {
|
} else if !got.InitializeSlack {
|
||||||
t.Error(got)
|
t.Error(got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
94
driver.go
Normal file
94
driver.go
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"go.etcd.io/bbolt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Driver interface {
|
||||||
|
Close() error
|
||||||
|
ForEach(context.Context, string, func(string, []byte) error) error
|
||||||
|
Get(context.Context, string, string) ([]byte, error)
|
||||||
|
Set(context.Context, string, string, []byte) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type BBolt struct {
|
||||||
|
db *bbolt.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTestDB() BBolt {
|
||||||
|
d, err := ioutil.TempDir(os.TempDir(), "test-db-*")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
db, err := NewDB(d)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return db
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDB(p string) (BBolt, error) {
|
||||||
|
db, err := bbolt.Open(p, 0600, &bbolt.Options{
|
||||||
|
Timeout: time.Second,
|
||||||
|
})
|
||||||
|
return BBolt{db: db}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bb BBolt) Close() error {
|
||||||
|
return bb.db.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bb BBolt) ForEach(ctx context.Context, db string, cb func(string, []byte) error) error {
|
||||||
|
return bb.db.View(func(tx *bbolt.Tx) error {
|
||||||
|
bkt := tx.Bucket([]byte(db))
|
||||||
|
if bkt == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
c := bkt.Cursor()
|
||||||
|
for k, v := c.First(); k != nil && ctx.Err() == nil; k, v = c.Next() {
|
||||||
|
if err := cb(string(k), v); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.Err()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bb BBolt) Get(_ context.Context, db, id string) ([]byte, error) {
|
||||||
|
var b []byte
|
||||||
|
err := bb.db.View(func(tx *bbolt.Tx) error {
|
||||||
|
bkt := tx.Bucket([]byte(db))
|
||||||
|
if bkt == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
b = bkt.Get([]byte(id))
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
return b, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bb BBolt) Set(_ context.Context, db, id string, value []byte) error {
|
||||||
|
return bb.db.Update(func(tx *bbolt.Tx) error {
|
||||||
|
bkt := tx.Bucket([]byte(db))
|
||||||
|
if bkt == nil {
|
||||||
|
var err error
|
||||||
|
bkt, err = tx.CreateBucket([]byte(db))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if value == nil {
|
||||||
|
return bkt.Delete([]byte(id))
|
||||||
|
}
|
||||||
|
return bkt.Put([]byte(id), value)
|
||||||
|
})
|
||||||
|
}
|
||||||
7
go.mod
7
go.mod
@@ -1,3 +1,10 @@
|
|||||||
module github.com/breel-render/spoc-bot-vr
|
module github.com/breel-render/spoc-bot-vr
|
||||||
|
|
||||||
go 1.22.1
|
go 1.22.1
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/go-errors/errors v1.5.1
|
||||||
|
go.etcd.io/bbolt v1.3.9
|
||||||
|
)
|
||||||
|
|
||||||
|
require golang.org/x/sys v0.4.0 // indirect
|
||||||
|
|||||||
6
go.sum
Normal file
6
go.sum
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk=
|
||||||
|
github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
|
||||||
|
go.etcd.io/bbolt v1.3.9 h1:8x7aARPEXiXbHmtUwAIv7eV2fQFHrLLavdiJ3uzJXoI=
|
||||||
|
go.etcd.io/bbolt v1.3.9/go.mod h1:zaO32+Ti0PK1ivdPtgMESzuzL2VPoIG1PCQNvOdo/dE=
|
||||||
|
golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18=
|
||||||
|
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
65
main.go
65
main.go
@@ -1,13 +1,17 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -53,8 +57,65 @@ func listenAndServe(ctx context.Context, cfg Config) chan error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func newHandler(cfg Config) http.HandlerFunc {
|
func newHandler(cfg Config) http.HandlerFunc {
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
|
||||||
|
mux.Handle("POST /api/v1/events/slack", http.HandlerFunc(newHandlerPostAPIV1EventsSlack(cfg)))
|
||||||
|
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
b, _ := io.ReadAll(r.Body)
|
if cfg.Debug {
|
||||||
log.Printf("%s %s | %s", r.Method, r.URL, b)
|
b, _ := io.ReadAll(r.Body)
|
||||||
|
r.Body = io.NopCloser(bytes.NewReader(b))
|
||||||
|
log.Printf("%s %s | %s", r.Method, r.URL, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
mux.ServeHTTP(w, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newHandlerPostAPIV1EventsSlack(cfg Config) http.HandlerFunc {
|
||||||
|
if cfg.InitializeSlack {
|
||||||
|
return handlerPostAPIV1EventsSlackInitialize
|
||||||
|
}
|
||||||
|
return _newHandlerPostAPIV1EventsSlack(cfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handlerPostAPIV1EventsSlackInitialize(w http.ResponseWriter, r *http.Request) {
|
||||||
|
b, _ := io.ReadAll(r.Body)
|
||||||
|
var challenge struct {
|
||||||
|
Token string
|
||||||
|
Challenge string
|
||||||
|
Type string
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(b, &challenge); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
json.NewEncoder(w).Encode(map[string]any{"challenge": challenge.Challenge})
|
||||||
|
}
|
||||||
|
|
||||||
|
func _newHandlerPostAPIV1EventsSlack(cfg Config) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
b, _ := io.ReadAll(r.Body)
|
||||||
|
r.Body = io.NopCloser(bytes.NewReader(b))
|
||||||
|
|
||||||
|
var allowList struct {
|
||||||
|
Token string
|
||||||
|
Event struct {
|
||||||
|
Channel string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(b, &allowList); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
} else if allowList.Token != cfg.SlackToken {
|
||||||
|
http.Error(w, "invalid .token", http.StatusForbidden)
|
||||||
|
return
|
||||||
|
} else if !slices.Contains(strings.Split(cfg.SlackChannels, ","), allowList.Event.Channel) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("slack event: %s", b)
|
||||||
|
http.Error(w, "not impl", http.StatusNotImplemented)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
41
message.go
Normal file
41
message.go
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import "encoding/json"
|
||||||
|
|
||||||
|
type Message struct {
|
||||||
|
ID string
|
||||||
|
TS uint64
|
||||||
|
Plain string
|
||||||
|
Source string
|
||||||
|
Channel string
|
||||||
|
Thread string
|
||||||
|
EventName string
|
||||||
|
EventID string
|
||||||
|
AssetID string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Message) Empty() bool {
|
||||||
|
return m == (Message{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Message) Serialize() []byte {
|
||||||
|
b, err := json.Marshal(m)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
func MustDeserialize(b []byte) Message {
|
||||||
|
m, err := Deserialize(b)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
func Deserialize(b []byte) (Message, error) {
|
||||||
|
var m Message
|
||||||
|
err := json.Unmarshal(b, &m)
|
||||||
|
return m, err
|
||||||
|
}
|
||||||
62
queue.go
Normal file
62
queue.go
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-errors/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Queue struct {
|
||||||
|
driver Driver
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTestQueue() Queue {
|
||||||
|
return Queue{driver: NewTestDB()}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewQueue(driver Driver) Queue {
|
||||||
|
return Queue{driver: driver}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q Queue) Push(ctx context.Context, m Message) error {
|
||||||
|
return q.driver.Set(ctx, "q", m.ID, m.Serialize())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q Queue) PeekFirst(ctx context.Context) (Message, error) {
|
||||||
|
for {
|
||||||
|
m, err := q.peekFirst(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return m, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !m.Empty() {
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return Message{}, ctx.Err()
|
||||||
|
case <-time.After(time.Second):
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q Queue) Ack(ctx context.Context, id string) error {
|
||||||
|
return q.driver.Set(ctx, "q", id, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q Queue) peekFirst(ctx context.Context) (Message, error) {
|
||||||
|
var m Message
|
||||||
|
subctx, subcan := context.WithCancel(ctx)
|
||||||
|
defer subcan()
|
||||||
|
err := q.driver.ForEach(subctx, "q", func(_ string, value []byte) error {
|
||||||
|
m = MustDeserialize(value)
|
||||||
|
subcan()
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if errors.Is(err, subctx.Err()) {
|
||||||
|
err = nil
|
||||||
|
}
|
||||||
|
return m, err
|
||||||
|
}
|
||||||
37
storage.go
Normal file
37
storage.go
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrNotFound = errors.New("not found")
|
||||||
|
)
|
||||||
|
|
||||||
|
type Storage struct {
|
||||||
|
driver Driver
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTestStorage() Storage {
|
||||||
|
return Storage{driver: NewTestDB()}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewStorage(driver Driver) Storage {
|
||||||
|
return Storage{driver: driver}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Storage) Upsert(ctx context.Context, m Message) error {
|
||||||
|
return s.driver.Set(ctx, "storage", m.ID, m.Serialize())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Storage) Get(ctx context.Context, id string) (Message, error) {
|
||||||
|
b, err := s.driver.Get(ctx, "storage", id)
|
||||||
|
if err != nil {
|
||||||
|
return Message{}, err
|
||||||
|
}
|
||||||
|
if b == nil {
|
||||||
|
return Message{}, ErrNotFound
|
||||||
|
}
|
||||||
|
return MustDeserialize(b), nil
|
||||||
|
}
|
||||||
3
testdata/slack_events.json
vendored
Normal file
3
testdata/slack_events.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{"token":"redacted","team_id":"T9RQLQ0KV","context_team_id":"T9RQLQ0KV","context_enterprise_id":null,"api_app_id":"A06TYH7CALB","event":{"user":"U06868T6ADV","type":"message","ts":"1712878479.415559","client_msg_id":"fce57841-0446-4720-aa1d-557e162c7667","text":"BOTH OF YOU GET IN HERE HEEEHEEEHEEE","team":"T9RQLQ0KV","blocks":[{"type":"rich_text","block_id":"6G5BZ","elements":[{"type":"rich_text_section","elements":[{"type":"text","text":"BOTH OF YOU GET IN HERE HEEEHEEEHEEE"}]}]}],"channel":"C06U1DDBBU4","event_ts":"1712878479.415559","channel_type":"channel"},"type":"event_callback","event_id":"Ev06U1F01XMJ","event_time":1712878479,"authorizations":[{"enterprise_id":null,"team_id":"T9RQLQ0KV","user_id":"U06TS9M7ABG","is_bot":true,"is_enterprise_install":false}],"is_ext_shared_channel":false,"event_context":"4-eyJldCI6Im1lc3NhZ2UiLCJ0aWQiOiJUOVJRTFEwS1YiLCJhaWQiOiJBMDZUWUg3Q0FMQiIsImNpZCI6IkMwNlUxRERCQlU0In0"}
|
||||||
|
{"token":"redacted","team_id":"T9RQLQ0KV","context_team_id":"T9RQLQ0KV","context_enterprise_id":null,"api_app_id":"A06TYH7CALB","event":{"user":"U06868T6ADV","type":"message","ts":"1712878566.650149","client_msg_id":"5d9d586b-eee0-40f8-b15e-66245c073a4c","text":"in a thread","team":"T9RQLQ0KV","thread_ts":"1712877772.926539","parent_user_id":"U06868T6ADV","blocks":[{"type":"rich_text","block_id":"KHSCu","elements":[{"type":"rich_text_section","elements":[{"type":"text","text":"in a thread"}]}]}],"channel":"C06U1DDBBU4","event_ts":"1712878566.650149","channel_type":"channel"},"type":"event_callback","event_id":"Ev06TW3WE58V","event_time":1712878566,"authorizations":[{"enterprise_id":null,"team_id":"T9RQLQ0KV","user_id":"U06TS9M7ABG","is_bot":true,"is_enterprise_install":false}],"is_ext_shared_channel":false,"event_context":"4-eyJldCI6Im1lc3NhZ2UiLCJ0aWQiOiJUOVJRTFEwS1YiLCJhaWQiOiJBMDZUWUg3Q0FMQiIsImNpZCI6IkMwNlUxRERCQlU0In0"}
|
||||||
|
|
||||||
Reference in New Issue
Block a user