Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add errors to actions to allow for custom handling #32

Merged
merged 1 commit into from
Jan 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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