Compare commits

...

10 Commits

Author SHA1 Message Date
Bel LaPointe
8ee774240e some type stubs 2024-04-12 07:53:29 -06:00
Bel LaPointe
63d7454fb6 testdata for slack events 2024-04-11 17:38:04 -06:00
Bel LaPointe
fb5e7b580e log not impl'd slack event 2024-04-11 17:21:44 -06:00
Bel LaPointe
bdc40e91b4 only write METHOD PATH | BODY if $DEBUG=true 2024-04-11 17:21:07 -06:00
Bel LaPointe
64af8f5085 only handle slack events in $SLACK_CHANNELS csv 2024-04-11 17:19:32 -06:00
Bel LaPointe
6a6524692a POST /api/v1/events/slack asserts .token==$SLACK_TOKEN 2024-04-11 17:11:43 -06:00
Bel LaPointe
2372fa8bb9 route /api/v1/events/slack differently when INITIALIZE_SLACK=true 2024-04-11 17:02:03 -06:00
Bel LaPointe
052a093ad7 config accepts INITIALIZE_SLACK 2024-04-11 17:01:45 -06:00
Bel LaPointe
967d33b03b log 2024-04-11 16:51:23 -06:00
Bel LaPointe
84e4600737 http.ServeMux to route POST /api/v1/events/x$ 2024-04-11 16:49:17 -06:00
11 changed files with 330 additions and 7 deletions

View File

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

View File

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

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

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

3
writer.go Normal file
View File

@@ -0,0 +1,3 @@
package main
type Writer struct{}