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 {
|
||||
Port int
|
||||
AB string
|
||||
Port int
|
||||
Debug bool
|
||||
InitializeSlack bool
|
||||
SlackToken string
|
||||
SlackChannels string
|
||||
}
|
||||
|
||||
func newConfig() (Config, error) {
|
||||
@@ -55,6 +58,12 @@ func newConfigFromEnv(getEnv func(string) string) (Config, error) {
|
||||
return Config{}, err
|
||||
}
|
||||
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 {
|
||||
case "PORT":
|
||||
return "1"
|
||||
case "A_B":
|
||||
return "2"
|
||||
case "INITIALIZE_SLACK":
|
||||
return "true"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
@@ -17,7 +17,7 @@ func TestNewConfig(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
} else if got.Port != 1 {
|
||||
t.Error(got)
|
||||
} else if got.AB != "2" {
|
||||
} else if !got.InitializeSlack {
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"os/signal"
|
||||
"slices"
|
||||
"strings"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
@@ -53,8 +57,65 @@ func listenAndServe(ctx context.Context, cfg Config) chan error {
|
||||
}
|
||||
|
||||
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) {
|
||||
b, _ := io.ReadAll(r.Body)
|
||||
log.Printf("%s %s | %s", r.Method, r.URL, b)
|
||||
if cfg.Debug {
|
||||
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