Skip to content

Commit

Permalink
Merge pull request #32 from theothertomelliott/theothertomelliott/iss…
Browse files Browse the repository at this point in the history
…ue31

Add errors to actions to allow for custom handling
  • Loading branch information
theothertomelliott authored Jan 22, 2024
2 parents 202a7b0 + dab722f commit 09aa68e
Show file tree
Hide file tree
Showing 18 changed files with 266 additions and 21 deletions.
2 changes: 2 additions & 0 deletions action.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package spanner
import "context"

type Action interface {
HasError

Type() string
Data() interface{}
}
Expand Down
9 changes: 9 additions & 0 deletions api.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ type SlashCommand interface {
// It can be used to create blocks and handle submission or closing of the modal.
type Modal interface {
BlockUI

SubmitButton(title string) ModalSubmission
CloseButton(title string) bool
}
Expand All @@ -75,6 +76,14 @@ type EphemeralSender interface {
// Messages are constructed using BlockUI commands.
type Message interface {
BlockUI
HasError

Channel(channelID string)
}

type NonInteractiveMessage interface {
NonInteractiveBlockUI
HasError

Channel(channelID string)
}
8 changes: 8 additions & 0 deletions blocks.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,17 @@ package spanner

// BlockUI allows the creation of Slack blocks in a message or modal.
type BlockUI interface {
NonInteractiveBlockUI
InteractiveBlockUI
}

type NonInteractiveBlockUI interface {
Header(message string)
PlainText(text string)
Markdown(text string)
}

type InteractiveBlockUI interface {
TextInput(label string, hint string, placeholder string) string
MultilineTextInput(label string, hint string, placeholder string) string
Divider()
Expand Down
18 changes: 18 additions & 0 deletions error.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package spanner

import "context"

type HasError interface {
ErrorFunc(ErrorFunc)
}

type ErrorFunc func(ctx context.Context, ev ErrorEvent)

type ErrorEvent interface {
SendMessage(channelID string) ErrorMessage
ReceiveError() error
}

type ErrorMessage interface {
NonInteractiveMessage
}
11 changes: 11 additions & 0 deletions examples/error/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package main
import (
"context"
"encoding/json"
"fmt"
"log"
"os"

Expand Down Expand Up @@ -63,12 +64,22 @@ func main() {

replyGood := ev.SendMessage(msg.Channel().ID())
replyGood.PlainText("This message should succeed")
replyGood.ErrorFunc(func(ctx context.Context, ev spanner.ErrorEvent) {
panic("did not expect this message to fail")
})

replyBad := ev.SendMessage("invalid_channel")
replyBad.PlainText("This message will always fail to post")
replyBad.ErrorFunc(func(ctx context.Context, ev spanner.ErrorEvent) {
errorNotice := ev.SendMessage(msg.Channel().ID())
errorNotice.PlainText(fmt.Sprintf("There was an error sending a message: %v", ev.ReceiveError()))
})

replySkipped := ev.SendMessage(msg.Channel().ID())
replySkipped.PlainText("This message should be skipped because of the previous error")
replySkipped.ErrorFunc(func(ctx context.Context, ev spanner.ErrorEvent) {
panic("did not expect this message to fail")
})
}
return nil
})
Expand Down
2 changes: 2 additions & 0 deletions slack/action.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ type action interface {

// exec performs and action and returns a payload to acknowledge the request as appropriate
exec(ctx context.Context, req request) (interface{}, error)

getErrorFunc() spanner.ErrorFunc
}

type actionQueue struct {
Expand Down
2 changes: 1 addition & 1 deletion slack/app_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (
)

func TestHandlerIsCalledForEachEvent(t *testing.T) {
client := newTestClient()
client := newTestClient([]string{"ABC123"})
testApp := client.CreateApp()

results := make(chan struct{}, 2)
Expand Down
9 changes: 9 additions & 0 deletions slack/channel.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,15 @@ var _ action = &joinChannelAction{}

type joinChannelAction struct {
channelID string
errFunc spanner.ErrorFunc
}

func (j *joinChannelAction) ErrorFunc(ef spanner.ErrorFunc) {
j.errFunc = ef
}

func (j *joinChannelAction) getErrorFunc() spanner.ErrorFunc {
return j.errFunc
}

// Data implements action.
Expand Down
10 changes: 10 additions & 0 deletions slack/ephemeral.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,16 @@ var _ action = &sendEphemeralMessageAction{}

type sendEphemeralMessageAction struct {
text string

errFunc spanner.ErrorFunc
}

func (e *sendEphemeralMessageAction) ErrorFunc(ef spanner.ErrorFunc) {
e.errFunc = ef
}

func (e *sendEphemeralMessageAction) getErrorFunc() spanner.ErrorFunc {
return e.errFunc
}

// Data implements action.
Expand Down
73 changes: 73 additions & 0 deletions slack/error_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package slack

import (
"context"
"encoding/json"
"fmt"
"strings"
"testing"

"github.com/slack-go/slack/slackevents"
"github.com/theothertomelliott/spanner"
)

// TestErrorHandling verifies that error handlers are called appropriately
func TestErrorHandling(t *testing.T) {
client := newTestClient([]string{"ABC123"})
testApp := client.CreateApp()

go func() {
err := testApp.Run(handlerTestErrors)
if err != nil {
t.Errorf("error running app: %v", err)
}
}()

// Send hello message
client.SendEventToApp(messageEvent(
slackevents.MessageEvent{
Text: "hello",
Channel: "ABC123",
User: "DEF456",
},
))

// Expect a single message and clear the message list
if len(client.messagesSent) != 2 {
t.Errorf("expected two messages to be sent, got %d", len(client.messagesSent))
}

firstBlocks, _ := json.MarshalIndent(client.messagesSent[0].blocks, "", " ")
if !strings.Contains(string(firstBlocks), `This message should succeed`) {
t.Errorf("first message content was not as expected, got: %v", string(firstBlocks))
}

secondBlocks, _ := json.MarshalIndent(client.messagesSent[1].blocks, "", " ")
if !strings.Contains(string(secondBlocks), `There was an error sending a message`) {
t.Errorf("first message content was not as expected, got: %v", string(secondBlocks))
}
}

func handlerTestErrors(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")
replyGood.ErrorFunc(func(ctx context.Context, ev spanner.ErrorEvent) {
panic("did not expect this message to fail")
})

replyBad := ev.SendMessage("invalid_channel")
replyBad.PlainText("This message will always fail to post")
replyBad.ErrorFunc(func(ctx context.Context, ev spanner.ErrorEvent) {
errorNotice := ev.SendMessage(msg.Channel().ID())
errorNotice.PlainText(fmt.Sprintf("There was an error sending a message: %v", ev.ReceiveError()))
})

replySkipped := ev.SendMessage(msg.Channel().ID())
replySkipped.PlainText("This message should be skipped because of the previous error")
replySkipped.ErrorFunc(func(ctx context.Context, ev spanner.ErrorEvent) {
panic("did not expect this message to fail")
})
}
return nil
}
30 changes: 30 additions & 0 deletions slack/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"

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

func renderSlackError(err error) error {
Expand All @@ -16,3 +17,32 @@ func renderSlackError(err error) error {
}
return err
}

var _ spanner.ErrorEvent = &errorEvent{}

func newErrorEvent(err error) *errorEvent {
q := &actionQueue{}
return &errorEvent{
actionQueue: q,
sender: &MessageSender{
actionQueue: q,
},
err: err,
}
}

type errorEvent struct {
actionQueue *actionQueue
sender *MessageSender

err error
}

func (e *errorEvent) SendMessage(channelID string) spanner.ErrorMessage {
return e.sender.SendMessage(channelID)
}

// ReceiveError implements spanner.ErrorEvent.
func (e *errorEvent) ReceiveError() error {
return e.err
}
57 changes: 44 additions & 13 deletions slack/event.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,19 @@ type eventPopulator interface {

var _ spanner.Event = &event{}

func newEvent() *event {
q := &actionQueue{}
return &event{
eventType: "unknown",
state: eventState{
actionQueue: q,
MessageSender: &MessageSender{
actionQueue: q,
},
},
}
}

type event struct {
hash string
eventType string
Expand Down Expand Up @@ -92,9 +105,19 @@ func (e *event) finishEvent(
ctx context.Context,
actionInterceptor spanner.ActionInterceptor,
req request,
) error {
return finishEvent(ctx, actionInterceptor, req, e.state.actionQueue, true)
}

func finishEvent(
ctx context.Context,
actionInterceptor spanner.ActionInterceptor,
req request,
actionQueue *actionQueue,
shouldAck bool,
) error {
var payload interface{}
for _, a := range e.state.actionQueue.actions {
for _, a := range actionQueue.actions {
var (
newPayload interface{}
execFunc = func(ctx context.Context) error {
Expand All @@ -105,9 +128,23 @@ func (e *event) finishEvent(
)

err := actionInterceptor(ctx, a, execFunc)

if err != nil {
if ef := a.getErrorFunc(); ef != nil {
// Set up and run handler for error
errorEvent := newErrorEvent(err)
ef(ctx, errorEvent)

// Process actions from error event
err := finishEvent(ctx, actionInterceptor, req, errorEvent.actionQueue, false)
if err != nil {
return fmt.Errorf("executing error event: %w", err)
}
}

return fmt.Errorf("executing action: %w", err)
}

if newPayload != nil {
if payload != nil {
// TODO: Make this log configurable
Expand All @@ -117,11 +154,14 @@ func (e *event) finishEvent(
}
}

// Acknowledge the event
if payload == nil {
payload = map[string]interface{}{}
}
req.client.Ack(req.req, payload)

if shouldAck {
// Acknowledge the event
req.client.Ack(req.req, payload)
}

return nil
}
Expand All @@ -136,16 +176,7 @@ type eventPopulation struct {
}

func parseCombinedEvent(ctx context.Context, client socketClient, ce combinedEvent) *event {
q := &actionQueue{}
out := &event{
eventType: "unknown",
state: eventState{
actionQueue: q,
MessageSender: &MessageSender{
actionQueue: q,
},
},
}
out := newEvent()

defer func() {
// Set clients in metadata
Expand Down
2 changes: 1 addition & 1 deletion slack/examples_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import (
// TestGettingStarted verifies that the code in examples/gettingstarted
// interacts with Slack in the expected way
func TestGettingStarted(t *testing.T) {
client := newTestClient()
client := newTestClient([]string{"ABC123"})
testApp := client.CreateApp()

go func() {
Expand Down
10 changes: 10 additions & 0 deletions slack/message.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,16 @@ type message struct {
currentEventDepth int
actionMessageTS string
unsent bool

errFunc spanner.ErrorFunc
}

func (m *message) ErrorFunc(ef spanner.ErrorFunc) {
m.errFunc = ef
}

func (m *message) getErrorFunc() spanner.ErrorFunc {
return m.errFunc
}

func (m *message) Type() string {
Expand Down
Loading

0 comments on commit 09aa68e

Please sign in to comment.