Skip to content

Commit

Permalink
Merge pull request #28 from theothertomelliott:theothertomelliott/iss…
Browse files Browse the repository at this point in the history
…ue27

Allow retry behavior to be disabled
  • Loading branch information
theothertomelliott authored Jan 11, 2024
2 parents cccc364 + 79d5dfc commit 7ce80ee
Show file tree
Hide file tree
Showing 11 changed files with 192 additions and 43 deletions.
12 changes: 12 additions & 0 deletions action.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
package spanner

import "context"

type Action interface {
Type() string
Data() interface{}
}

// ActionInterceptor intercepts a single action to allow for instrumentation.
// The next function must be called to perform the action itself.
// The configuration for the action cannot be changed.
type ActionInterceptor func(ctx context.Context, action Action, next func(context.Context) error) error

// FinishInterceptor intercepts the finishing step of handling an event to allow for instrumentation.
// The finish function must be called to perform the required actions.
// The set of actions cannot be changed.
type FinishInterceptor func(ctx context.Context, actions []Action, finish func(context.Context) error) error
5 changes: 0 additions & 5 deletions api.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,13 @@ import "context"
type App interface {
Run(EventHandlerFunc) error
SendCustom(context.Context, CustomEvent) error
SetPostEventFunc(PostEventFunc)
}

// EventHandlerFunc represents a function that processes chat events from Spanner.
// This function will be called multiple times and is responsible both for creating
// UI elements and responding to the input received.
type EventHandlerFunc func(context.Context, Event) error

// PostEventFunc represents a function that is called after an event is procesed by a
// Spanner app.
type PostEventFunc func(context.Context)

// Event represents an event received from the Slack platform.
// It provides functions representing each type of event that can be received.
// For example, ReceivedMessage will return a message that may have been received in this event.
Expand Down
70 changes: 70 additions & 0 deletions examples/error/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package main

import (
"context"
"encoding/json"
"log"
"os"

"github.com/theothertomelliott/spanner"
"github.com/theothertomelliott/spanner/slack"
)

func main() {
botToken := os.Getenv("SLACK_BOT_TOKEN")
appToken := os.Getenv("SLACK_APP_TOKEN")

app, err := slack.NewApp(
slack.AppConfig{
BotToken: botToken,
AppToken: appToken,
AckOnError: true,
FinishInterceptor: func(ctx context.Context, actions []spanner.Action, finish func(context.Context) error) error {
if len(actions) > 0 {
var data []interface{}
for _, action := range actions {
data = append(data, action.Data())
}
dataJson, err := json.MarshalIndent(data, "", " ")
if err != nil {
log.Println("marshalling action data:", err)
}
log.Println("Will attempt actions: ", string(dataJson))
}
return finish(ctx)
},
ActionInterceptor: func(ctx context.Context, action spanner.Action, exec func(context.Context) error) error {
err := exec(ctx)
if err != nil {
dataJson, jsonErr := json.MarshalIndent(action.Data(), "", " ")
if jsonErr != nil {
log.Println("marshalling action data:", err)
}
log.Printf("error: %q, when executing action: %v", err, string(dataJson))
}
return err
},
},
)
if err != nil {
log.Fatal(err)
}

err = app.Run(func(ctx context.Context, ev spanner.Event) error {
if msg := ev.ReceiveMessage(); msg != nil && msg.Text() == "hello" {

replyGood := ev.SendMessage(msg.Channel().ID())
replyGood.PlainText("This message should succeed")

replyBad := ev.SendMessage("invalid_channel")
replyBad.PlainText("This message will always fail to post")

replySkipped := ev.SendMessage(msg.Channel().ID())
replySkipped.PlainText("This message should be skipped because of the previous error")
}
return nil
})
if err != nil {
log.Fatal(err)
}
}
8 changes: 8 additions & 0 deletions slack/action.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@ type actionQueue struct {
actions []action
}

func (a *actionQueue) Actions() []spanner.Action {
var out []spanner.Action
for _, action := range a.actions {
out = append(out, action)
}
return out
}

func (a *actionQueue) enqueue(ac action) {
a.actions = append(a.actions, ac)
}
72 changes: 50 additions & 22 deletions slack/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@ type AppConfig struct {
BotToken string
AppToken string
Debug bool

// AckOnError acknowledges messages when there is an error performing actions to prevent
// Slack from sending a retry. This will avoid actions being duplicated.
AckOnError bool

ActionInterceptor spanner.ActionInterceptor
FinishInterceptor spanner.FinishInterceptor
}

// NewApp creates a new slack app.
Expand Down Expand Up @@ -52,11 +59,23 @@ func NewApp(config AppConfig) (spanner.App, error) {

return newAppWithClient(&wrappedClient{
Client: client,
}, events), nil
}, config, events), nil
}

func newAppWithClient(client socketClient, slackEvents chan socketmode.Event) spanner.App {
func newAppWithClient(client socketClient, config AppConfig, slackEvents chan socketmode.Event) spanner.App {
if config.ActionInterceptor == nil {
config.ActionInterceptor = func(ctx context.Context, action spanner.Action, next func(ctx context.Context) error) error {
return next(ctx)
}
}
if config.FinishInterceptor == nil {
config.FinishInterceptor = func(ctx context.Context, actions []spanner.Action, finish func(ctx context.Context) error) error {
return finish(ctx)
}
}

return &app{
config: config,
client: client,
slackEvents: slackEvents,
combinedEvent: make(chan combinedEvent, 2),
Expand All @@ -79,11 +98,11 @@ func (w *wrappedClient) UpdateMessageWithMetadata(ctx context.Context, channelID
type app struct {
client socketClient

config AppConfig

slackEvents chan socketmode.Event
customEvents chan *customEvent
combinedEvent chan combinedEvent

postEventFunc spanner.PostEventFunc
}

type combinedEvent struct {
Expand Down Expand Up @@ -130,36 +149,45 @@ func (s *app) Run(handler spanner.EventHandlerFunc) error {
}
}

func (s *app) SetPostEventFunc(f spanner.PostEventFunc) {
s.postEventFunc = f
}

func (s *app) handleEvent(ctx context.Context, handler spanner.EventHandlerFunc, ce combinedEvent) {
var (
req socketmode.Request
hasReq bool
)
if evt := ce.ev; evt != nil && evt.Request != nil {
req = *evt.Request
hasReq = true
}

es := parseCombinedEvent(ctx, s.client, ce)
err := handler(ctx, es)
if err != nil {
return // Move on without acknowledging, will force a repeat
log.Printf("handling event: %v", err)
if s.config.AckOnError && hasReq {
log.Printf("Acknowledging failed event to prevent retries")
s.client.Ack(req, map[string]interface{}{})
}
return
}
var req socketmode.Request

if evt := ce.ev; evt != nil && evt.Request != nil {
req = *evt.Request
var finishFunc = func(ctx context.Context) error {
return es.finishEvent(ctx, s.config.ActionInterceptor, request{
req: req,
es: es,
hash: es.hash,
client: s.client,
})
}

err = es.finishEvent(ctx, request{
req: req,
es: es,
hash: es.hash,
client: s.client,
})
err = s.config.FinishInterceptor(ctx, es.state.actionQueue.Actions(), finishFunc)
if err != nil {
log.Printf("handling request: %v", renderSlackError(err))
if s.config.AckOnError && hasReq {
log.Printf("Acknowledging failed event to prevent retries")
s.client.Ack(req, map[string]interface{}{})
}
return // Move on without acknowledging, will force a repeat
}

if s.postEventFunc != nil {
s.postEventFunc(ctx)
}
}

func (s *app) SendCustom(ctx context.Context, c spanner.CustomEvent) error {
Expand Down
7 changes: 5 additions & 2 deletions slack/channel.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,11 @@ type joinChannelAction struct {
}

// Data implements action.
func (*joinChannelAction) Data() interface{} {
panic("unimplemented")
func (j *joinChannelAction) Data() interface{} {
// TODO: This should be more well-defined
return map[string]interface{}{
"channel_id": j.channelID,
}
}

// Type implements action.
Expand Down
7 changes: 5 additions & 2 deletions slack/ephemeral.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,11 @@ type sendEphemeralMessageAction struct {
}

// Data implements action.
func (*sendEphemeralMessageAction) Data() interface{} {
panic("unimplemented")
func (e *sendEphemeralMessageAction) Data() interface{} {
// TODO: This should be more well-defined
return map[string]interface{}{
"text": e.text,
}
}

// Type implements action.
Expand Down
19 changes: 16 additions & 3 deletions slack/event.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,17 +87,30 @@ func (e *event) SendMessage(channelID string) spanner.Message {
return e.state.SendMessage(channelID)
}

func (e *event) finishEvent(ctx context.Context, req request) error {
func (e *event) finishEvent(
ctx context.Context,
actionInterceptor spanner.ActionInterceptor,
req request,
) error {
var payload interface{}
for _, a := range e.state.actionQueue.actions {
newPayload, err := a.exec(ctx, req)
var (
newPayload interface{}
execFunc = func(ctx context.Context) error {
var out error
newPayload, out = a.exec(ctx, req)
return out
}
)

err := actionInterceptor(ctx, a, execFunc)
if err != nil {
return fmt.Errorf("executing action: %w", err)
}
if newPayload != nil {
if payload != nil {
// TODO: Make this log configurable
log.Print("received multiple payloads, will use the last one one")
log.Print("received multiple payloads, will use the last one generated")
}
payload = newPayload
}
Expand Down
6 changes: 5 additions & 1 deletion slack/message.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,11 @@ func (m *message) Type() string {
}

func (m *message) Data() interface{} {
panic("unimplemented")
// TODO: This should be more well-defined
return map[string]interface{}{
"channel_id": m.ChannelID,
"blocks": m.blocks,
}
}

func (m *message) Channel(channelID string) {
Expand Down
18 changes: 14 additions & 4 deletions slack/modal.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,8 +155,15 @@ func (*modal) Type() string {
return "modal"
}

func (*modal) Data() interface{} {
panic("unimplemented")
func (m *modal) Data() interface{} {
// TODO: This should be more well-defined
return map[string]interface{}{
"title": m.Title,
"blocks": m.blocks,
"channel_id": m.ChannelID,
"view_id": m.ViewID,
"view_id_external": m.ViewExternalID,
}
}

var _ spanner.ModalSubmission = &modalSubmission{}
Expand Down Expand Up @@ -203,6 +210,9 @@ func (*modalSubmission) Type() string {
return "modal-submission"
}

func (*modalSubmission) Data() interface{} {
panic("unimplemented")
func (ms *modalSubmission) Data() interface{} {
// TODO: This should be more well defined
return map[string]interface{}{
"next_modal": ms.NextModal,
}
}
11 changes: 7 additions & 4 deletions slack/slackclient_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,18 +49,21 @@ type updatedMessage struct {
func (r *testClient) CreateApp() spanner.App {
testApp := newAppWithClient(
r,
AppConfig{
FinishInterceptor: r.FinishInterceptor,
},
r.Events,
)

testApp.SetPostEventFunc(r.PostEventFunc)

return testApp
}

// PostEventFunc provides a spanner.PostEventFunc to use with a test app
// FinishInterceptor provides a spanner.FinishInterceptor to use with a test app
// This is automatically applied by the CreateApp function
func (r *testClient) PostEventFunc(ctx context.Context) {
func (r *testClient) FinishInterceptor(ctx context.Context, _ []spanner.Action, finish func(context.Context) error) error {
err := finish(ctx)
r.postEvent <- struct{}{}
return err
}

// SendEventToApp sends an event to the connected app (created with CreateApp)
Expand Down

0 comments on commit 7ce80ee

Please sign in to comment.