Skip to content
Open
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
59 changes: 59 additions & 0 deletions context.go
Original file line number Diff line number Diff line change
Expand Up @@ -222,3 +222,62 @@ func (r *JobContext) WithContext(ctx context.Context) *JobContext {
r.ctx = ctx
return r
}

// newLinkContext creates a new bot context
func newLinkContext(ctx context.Context, logger Logger, slackClient *slack.Client, definition *LinkDefinition) *LinkContext {
writer := newWriter(ctx, logger, slackClient)
response := newWriterResponse(writer)
return &LinkContext{
ctx: ctx,
definition: definition,
slackClient: slackClient,
response: response,
logger: logger,
}
}

// LinkContext contains information relevant to the executed job
type LinkContext struct {
ctx context.Context
definition *LinkDefinition
slackClient *slack.Client
response *ResponseWriter
logger Logger
}

// Context returns the context
func (r *LinkContext) Context() context.Context {
return r.ctx
}

// Definition returns the job definition
func (r *LinkContext) Definition() *LinkDefinition {
return r.definition
}

// Response returns the response writer
func (r *LinkContext) Response() *ResponseWriter {
return r.response
}

// SlackClient returns the slack API client
func (r *LinkContext) SlackClient() *slack.Client {
return r.slackClient
}

// Logger returns the logger
func (r *LinkContext) Logger() Logger {
return r.logger
}

// WithContext returns a shallow copy of r with its context changed
// to ctx. The provided ctx must be non-nil.
func (r *LinkContext) WithContext(ctx context.Context) *LinkContext {
if ctx == nil {
panic("nil context")
}
r2 := new(LinkContext)
*r2 = *r
r.ctx = ctx
return r
}
14 changes: 14 additions & 0 deletions executors.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,17 @@ func executeJob(ctx *JobContext, handler JobHandler, middlewares ...JobMiddlewar
handler(ctx)
}
}

func executeLink(ctx *LinkContext, handler LinkHandler, middlewares ...LinkMiddlewareHandler) func() {
if handler == nil {
return func() {}
}

for i := len(middlewares) - 1; i >= 0; i-- {
handler = middlewares[i](handler)
}

return func() {
handler(ctx)
}
}
6 changes: 6 additions & 0 deletions handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,9 @@ type JobMiddlewareHandler func(JobHandler) JobHandler

// JobHandler represents the job handler function
type JobHandler func(*JobContext)

// LinkMiddlewareHandler represents the link middleware handler function
type LinkMiddlewareHandler func(LinkHandler) LinkHandler

// LinkHandler represents the link handler function
type LinkHandler func(*LinkContext)
30 changes: 30 additions & 0 deletions link.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package slacker

// LinkDefinition structure contains definition of the job
type LinkDefinition struct {
Domain string
Name string
Description string
Middlewares []LinkMiddlewareHandler
Handler LinkHandler

// HideHelp will hide this job definition from appearing in the `help` results.
HideHelp bool
}

// newJob creates a new job object
func newLink(definition *LinkDefinition) *Link {
return &Link{
definition: definition,
}
}

// Link structure contains the job's spec and handler
type Link struct {
definition *LinkDefinition
}

// Definition returns the job's definition
func (c *Link) Definition() *LinkDefinition {
return c.definition
}
84 changes: 84 additions & 0 deletions link_shared_event.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package slacker

import (
"net/url"

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

// LinkSharedEvent contains details common to message based events, including the
// raw event as returned from Slack along with the corresponding event type.
// The struct should be kept minimal and only include data that is commonly
// used to prevent frequent type assertions when evaluating the event.
type LinkSharedEvent struct {
// Channel ID where the message was sent
ChannelID string

// Channel contains information about the channel
Channel *slack.Channel

// User ID of the sender
UserID string

// UserProfile contains all the information details of a given user
UserProfile *slack.UserProfile

// Text is the unalterted text of the message, as returned by Slack
Links []url.URL

// TimeStamp is the message timestamp. For events that do not support
// threading (eg. slash commands) this will be unset.
// will be left unset.
TimeStamp string

// ThreadTimeStamp is the message thread timestamp. For events that do not
// support threading (eg. slash commands) this will be unset.
ThreadTimeStamp string

// Data is the raw event data returned from slack. Using Type, you can assert
// this into a slackevents *Event struct.
Data any

// Type is the type of the event, as returned by Slack. For instance,
// `app_mention` or `message`
Type string
}

// InThread indicates if a message event took place in a thread.
func (e *LinkSharedEvent) InThread() bool {
return isMessageInThread(e.ThreadTimeStamp, e.TimeStamp)
}

// newMessageEvent creates a new message event structure
func newLinkSharedEvent(logger Logger, slackClient *slack.Client, event any) *LinkSharedEvent {
var messageEvent *LinkSharedEvent

switch ev := event.(type) {
case *slackevents.LinkSharedEvent:
links := []url.URL{}
for _, link := range ev.Links {
if val, err := url.Parse(link.URL); err == nil {
// ignore malformed links
} else {
links = append(links, *val)
}
}

messageEvent = &LinkSharedEvent{
ChannelID: ev.Channel,
Channel: getChannel(logger, slackClient, ev.Channel),
UserID: ev.User,
UserProfile: getUserProfile(logger, slackClient, ev.User),
Links: links,
Data: event,
Type: ev.Type,
TimeStamp: ev.TimeStamp,
ThreadTimeStamp: ev.ThreadTimeStamp,
}
default:
return nil
}

return messageEvent
}
67 changes: 55 additions & 12 deletions slacker.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ type Slacker struct {
interactions map[slack.InteractionType][]*Interaction
jobMiddlewares []JobMiddlewareHandler
jobs []*Job
linkMiddlewares []LinkMiddlewareHandler
links map[string][]*Link
onHello func(socketmode.Event)
onConnected func(socketmode.Event)
onConnecting func(socketmode.Event)
Expand Down Expand Up @@ -215,6 +217,20 @@ func (s *Slacker) AddJobMiddleware(middleware JobMiddlewareHandler) {
s.jobMiddlewares = append(s.jobMiddlewares, middleware)
}

// AddLink define a new link and append it to the list of links
func (s *Slacker) AddLink(definition *LinkDefinition) {
if definition.Domain == "" {
s.logger.Error("missing `Domain`")
return
}
s.links[definition.Domain] = append(s.links[definition.Domain], newLink(definition))
}

// AddLinkMiddleware appends a new link middleware to the list of root level link middlewares
func (s *Slacker) AddLinkMiddleware(middleware LinkMiddlewareHandler) {
s.linkMiddlewares = append(s.linkMiddlewares, middleware)
}

// Listen receives events from Slack and each is handled as needed
func (s *Slacker) Listen(ctx context.Context) error {
s.prependHelpHandle()
Expand Down Expand Up @@ -281,25 +297,27 @@ func (s *Slacker) Listen(ctx context.Context) error {
// Acknowledge receiving the request
s.socketModeClient.Ack(*socketEvent.Request)

if event.Type != slackevents.CallbackEvent {
if s.unsupportedEventHandler != nil {
s.unsupportedEventHandler(socketEvent)
} else {
s.logger.Debug("unsupported event", "event", socketEvent)
switch event.Type {
case slackevents.CallbackEvent:
switch event.InnerEvent.Type {
case "message", "app_mention": // message-based events
go s.handleMessageEvent(ctx, event.InnerEvent.Data)
case "link_shared":
go s.handleLinkSharedEvent(ctx, event.InnerEvent.Data)
default:
if s.unsupportedEventHandler != nil {
s.unsupportedEventHandler(socketEvent)
} else {
s.logger.Debug("unsupported event", "event", socketEvent)
}
}
continue
}

switch event.InnerEvent.Type {
case "message", "app_mention": // message-based events
go s.handleMessageEvent(ctx, event.InnerEvent.Data)

default:
if s.unsupportedEventHandler != nil {
s.unsupportedEventHandler(socketEvent)
} else {
s.logger.Debug("unsupported event", "event", socketEvent)
}
continue
}

case socketmode.EventTypeSlashCommand:
Expand Down Expand Up @@ -541,6 +559,31 @@ func (s *Slacker) handleMessageEvent(ctx context.Context, event any) {
}
}

func (s *Slacker) handleLinkSharedEvent(ctx context.Context, event any) {
linkSharedEvent := newLinkSharedEvent(s.logger, s.slackClient, event)
if linkSharedEvent == nil {
// event doesn't appear to be a valid message type
return
}

middlewares := make([]LinkMiddlewareHandler, 0)
middlewares = append(middlewares, s.linkMiddlewares...)

for _, group := range s.links {
for _, link := range group {
definition := link.Definition()
if definition.Domain != linkSharedEvent.Links[0].Host {
continue
}

ctx := newLinkContext(ctx, s.logger, s.slackClient, definition)
middlewares = append(middlewares, definition.Middlewares...)
executeLink(ctx, definition.Handler, middlewares...)
return
}
}
}

func (s *Slacker) ignoreBotMessage(messageEvent *MessageEvent) bool {
switch s.botInteractionMode {
case BotModeIgnoreApp:
Expand Down