diff --git a/adapter/adapter.go b/adapter/adapter.go index e403955..b6dab3b 100644 --- a/adapter/adapter.go +++ b/adapter/adapter.go @@ -86,6 +86,10 @@ var ( // Adapter represents a connection to a chat provider. type Adapter interface { + // BookmarkLink bookmarks a link in the specified chanel, if the adapter + // supports this. + BookmarkLink(ctx context.Context, channelID, title, link string) error + // GetChannelInfo provides info on a specific provider channel accessible // to the adapter. GetChannelInfo(channelID string) (*io.ChannelInfo, error) @@ -276,7 +280,7 @@ func SendEnvelope(ctx context.Context, a Adapter, channelID string, envelope dat ctx, sp := tr.Start(ctx, "adapter.SendEnvelope") defer sp.End() - handleAdvancedOutput(ctx, envelope.Response.Advanced) // TODO: handle error + handleAdvancedOutput(ctx, a, envelope.Response.Advanced) // TODO: handle error e := adapterLogEntry(ctx, log.WithContext(ctx), a).WithField("message.type", tt) @@ -327,38 +331,60 @@ func SendEnvelope(ctx context.Context, a Adapter, channelID string, envelope dat // handleAdvancedOutput executes a slice of adapter actions specified as // io.AdvancedInput. -func handleAdvancedOutput(ctx context.Context, output []io.AdvancedOutput) error { +func handleAdvancedOutput(ctx context.Context, adapter Adapter, output []io.AdvancedOutput) error { e := adapterLogEntry(ctx, log.WithContext(ctx)) for _, o := range output { + var a = adapter e1 := e.WithField("output.action", o.Action).WithField("output.messageref", o.MessageRef) - var msgRef MessageRef - err := json.NewDecoder(strings.NewReader(o.MessageRef)).Decode(&msgRef) - if err != nil { - e1.WithError(err).Errorf("Badly formatted MessageRef") - } - // An alternate way to get the adapter will be necessary for actions - // that don't involve existing messages. - a, err := GetAdapter(msgRef.Adapter) + var msgRef = new(MessageRef) + err := json.NewDecoder(strings.NewReader(o.MessageRef)).Decode(msgRef) if err != nil { - e.WithError(err).Error("Couldn't find specified adapter") - return nil + msgRef = nil + if o.Adapter != "" { + a1, err := GetAdapter(o.Adapter) + if err != nil { + a = a1 + } + } } else { - e1 = e1.WithField("adapter.name", a.GetName()) + a, err = GetAdapter(msgRef.Adapter) + if err != nil { + e.WithError(err).Error("Couldn't find specified adapter from MessageRef") + return nil + } } + + e1 = e1.WithField("adapter.name", a.GetName()) + switch o.Action { case io.ActionReply: - err = a.Reply(ctx, msgRef, o.Content) - if err != nil { - e1.WithError(err).Errorf("Failed to create reply") + if msgRef != nil { + err = a.Reply(ctx, *msgRef, o.Content) + if err != nil { + e1.WithError(err).Errorf("Failed to create reply") + } else { + e1.Debug("Replied!") + } } else { - e1.Debug("Replied!") + e1.WithError(err).Errorf("Badly formatted MessageRef") } case io.ActionReact: - err = a.React(ctx, msgRef, emoji.From(o.Content)) + if msgRef != nil { + err = a.React(ctx, *msgRef, emoji.From(o.Content)) + if err != nil { + e1.WithError(err).Error("Failed to react") + } else { + e1.Debug("Reacted!") + } + } else { + e1.WithError(err).Errorf("Badly formatted MessageRef") + } + case io.ActionBookmark: + err = a.BookmarkLink(ctx, o.ChannelID, o.Title, o.Content) if err != nil { - e1.WithError(err).Error("Failed to react") + e1.WithError(err).Error("Failed to bookmark") } else { - e1.Debug("Reacted!") + e1.Debug("Bookmarked!") } default: e1.Error("Unknown action") diff --git a/adapter/adapter_test.go b/adapter/adapter_test.go index be7d5d4..038230a 100644 --- a/adapter/adapter_test.go +++ b/adapter/adapter_test.go @@ -257,6 +257,11 @@ var _ Adapter = &testAdapter{} type testAdapter struct{} +func (t *testAdapter) BookmarkLink(ctx context.Context, channelID, title, link string) error { + //TODO implement me + panic("implement me") +} + func (t *testAdapter) React(ctx context.Context, message MessageRef, emoji emoji.Emoji) error { //TODO implement me panic("implement me") diff --git a/adapter/discord/adapter.go b/adapter/discord/adapter.go index b7c42be..e1e1486 100644 --- a/adapter/discord/adapter.go +++ b/adapter/discord/adapter.go @@ -60,6 +60,10 @@ type Adapter struct { events chan *adapter.ProviderEvent } +func (s *Adapter) BookmarkLink(ctx context.Context, channelID, title, link string) error { + return fmt.Errorf("discord does not curently support bookmarking links") +} + func (s *Adapter) React(ctx context.Context, message adapter.MessageRef, emoji emoji.Emoji) error { return s.session.MessageReactionAdd(message.ChannelID, message.ID, string(emoji.Unicode())) } diff --git a/adapter/slack/classic_adapter.go b/adapter/slack/classic_adapter.go index eccfbc0..d8f8194 100644 --- a/adapter/slack/classic_adapter.go +++ b/adapter/slack/classic_adapter.go @@ -44,6 +44,16 @@ type ClassicAdapter struct { rtm *slack.RTM } +func (s *ClassicAdapter) BookmarkLink(ctx context.Context, channelID, title, link string) error { + _, err := s.client.AddBookmarkContext(ctx, channelID, slack.AddBookmarkParameters{ + Title: title, + Type: "link", + Link: link, + }) + //TODO this could support emoji + return err +} + func (s *ClassicAdapter) React(ctx context.Context, message adapter.MessageRef, emoji emoji.Emoji) error { return s.client.AddReactionContext(ctx, emoji.Shortname(), slack.ItemRef{ Channel: message.ChannelID, diff --git a/adapter/slack/socketmode_adapter.go b/adapter/slack/socketmode_adapter.go index e9171d9..03d8cdc 100644 --- a/adapter/slack/socketmode_adapter.go +++ b/adapter/slack/socketmode_adapter.go @@ -45,6 +45,16 @@ type SocketModeAdapter struct { provider data.SlackProvider } +func (s *SocketModeAdapter) BookmarkLink(ctx context.Context, channelID, title, link string) error { + _, err := s.client.AddBookmarkContext(ctx, channelID, slack.AddBookmarkParameters{ + Title: title, + Type: "link", + Link: link, + }) + //TODO this could support emoji + return err +} + func (s *SocketModeAdapter) React(ctx context.Context, message adapter.MessageRef, emoji emoji.Emoji) error { return s.client.AddReactionContext(ctx, emoji.Shortname(), slack.ItemRef{ Channel: message.ChannelID, diff --git a/data/io/advancedio.go b/data/io/advancedio.go index 955eac1..64231e5 100644 --- a/data/io/advancedio.go +++ b/data/io/advancedio.go @@ -27,6 +27,11 @@ import ( const ( ActionReply = "reply" ActionReact = "react" + + // ActionBookmark is used to bookmark a link in a given chanel. This is + // currently only supported on slack. It requires a link in Content, a + // Title, a ChannelID, and optionally an Adapter. + ActionBookmark = "bookmark" ) // CommandInfo represents a command typed in by a user. Unlike @@ -123,4 +128,14 @@ type AdvancedOutput struct { // Content is the content associated with an action, for example text for a // reply or the emoji for a reaction. Content string + + // Title is the title of something to be created, for example a slack + // bookmark. + Title string + + // Adapter is the name of a specific adapter in which to perform the action. + // This is usually optional, and will default to the adapter from which the + // command that generated this output was triggered. Actions that take + // MessageRef may use the adapter of that message. + Adapter string }