slacker

package module
v1.4.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 29, 2023 License: MIT Imports: 11 Imported by: 318

README

slacker Build Status Go Report Card GoDoc Mentioned in Awesome Go

Built on top of the Slack API github.com/slack-go/slack, Slacker is a low-friction framework for creating Slack Bots.

Features

  • Supports Slack Apps using Socket Mode
  • Easy definitions of commands and their input
  • Simple parsing of String, Integer, Float and Boolean parameters
  • Built-in help command
  • Slash Command and Block Interactions supported
  • Available bot initialization, errors and default handlers
  • Contains support for context.Context
  • Replies can be new messages or in threads
  • Supports authorization
  • Supports Cron Jobs using https://github.com/robfig/cron
  • Bot responds to mentions and direct messages
  • Handlers run concurrently via goroutines
  • Produces events for executed commands
  • Full access to the Slack API github.com/slack-go/slack

Install

go get github.com/shomali11/slacker

Preparing your Slack App

To use Slacker you'll need to create a Slack App, either manually or with an app manifest. The app manifest feature is easier, but is a beta feature from Slack and thus may break/change without much notice.

Manual Steps

Slacker works by communicating with the Slack Events API using the Socket Mode connection protocol.

To get started, you must have or create a Slack App and enable Socket Mode, which will generate your app token (SLACK_APP_TOKEN in the examples) that will be needed to authenticate.

Additionally, you need to subscribe to events for your bot to respond to under the Event Subscriptions section. Common event subscriptions for bots include app_mention or message.im.

After setting up your subscriptions, add scopes necessary to your bot in the OAuth & Permissions. The following scopes are recommended for getting started, though you may need to add/remove scopes depending on your bots purpose:

  • app_mentions:read
  • channels:history
  • chat:write
  • groups:history
  • im:history
  • mpim:history

Once you've selected your scopes install your app to the workspace and navigate back to the OAuth & Permissions section. Here you can retrieve yor bot's OAuth token (SLACK_BOT_TOKEN in the examples) from the top of the page.

With both tokens in hand, you can now proceed with the examples below.

App Manifest

Slack App Manifests make it easy to share a app configurations. We provide a simple manifest that should work with all the examples provided below.

The manifest provided will send all messages in channels your bot is in to the bot (including DMs) and not just ones that actually mention them in the message.

If you wish to only have your bot respond to messages they are directly messaged in, you will need to add the app_mentions:read scope, and remove:

  • im:history # single-person dm
  • mpim:history # multi-person dm
  • channels:history # public channels
  • groups:history # private channels

You'll also need to adjust the event subscriptions, adding app_mention and removing:

  • message.channels
  • message.groups
  • message.im
  • message.mpim

Examples

Example 1

Defining a command using slacker

package main

import (
	"context"
	"log"
	"os"

	"github.com/shomali11/slacker"
)

func main() {
	bot := slacker.NewClient(os.Getenv("SLACK_BOT_TOKEN"), os.Getenv("SLACK_APP_TOKEN"))

	definition := &slacker.CommandDefinition{
		Handler: func(botCtx slacker.BotContext, request slacker.Request, response slacker.ResponseWriter) {
			response.Reply("pong")
		},
	}

	bot.Command("ping", definition)

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	err := bot.Listen(ctx)
	if err != nil {
		log.Fatal(err)
	}
}

Example 2

Defining a command with an optional description and example. The handler replies to a thread.

package main

import (
	"context"
	"log"
	"os"

	"github.com/shomali11/slacker"
)

func main() {
	bot := slacker.NewClient(os.Getenv("SLACK_BOT_TOKEN"), os.Getenv("SLACK_APP_TOKEN"))

	definition := &slacker.CommandDefinition{
		Description: "Ping!",
		Examples:    []string{"ping"},
		Handler: func(botCtx slacker.BotContext, request slacker.Request, response slacker.ResponseWriter) {
			response.Reply("pong", slacker.WithThreadReply(true))
		},
	}

	bot.Command("ping", definition)

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	err := bot.Listen(ctx)
	if err != nil {
		log.Fatal(err)
	}
}

Example 3

Defining a command with a parameter. Parameters surrounded with {} will be satisfied with a word. Parameters surrounded with <> are "greedy" and will take as much input as fed.

package main

import (
	"context"
	"log"
	"os"

	"github.com/shomali11/slacker"
)

func main() {
	bot := slacker.NewClient(os.Getenv("SLACK_BOT_TOKEN"), os.Getenv("SLACK_APP_TOKEN"))

	bot.Command("echo {word}", &slacker.CommandDefinition{
		Description: "Echo a word!",
		Examples:    []string{"echo hello"},
		Handler: func(botCtx slacker.BotContext, request slacker.Request, response slacker.ResponseWriter) {
			word := request.Param("word")
			response.Reply(word)
		},
	})

	bot.Command("say <sentence>", &slacker.CommandDefinition{
		Description: "Say a sentence!",
		Examples:    []string{"say hello there everyone!"},
		Handler: func(botCtx slacker.BotContext, request slacker.Request, response slacker.ResponseWriter) {
			sentence := request.Param("sentence")
			response.Reply(sentence)
		},
	})

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	err := bot.Listen(ctx)
	if err != nil {
		log.Fatal(err)
	}
}

Example 4

Defining a command with two parameters. Parsing one as a string and the other as an integer. (The second parameter is the default value in case no parameter was passed or could not parse the value)

package main

import (
	"context"
	"log"
	"os"

	"github.com/shomali11/slacker"
)

func main() {
	bot := slacker.NewClient(os.Getenv("SLACK_BOT_TOKEN"), os.Getenv("SLACK_APP_TOKEN"))

	definition := &slacker.CommandDefinition{
		Description: "Repeat a word a number of times!",
		Examples:    []string{"repeat hello 10"},
		Handler: func(botCtx slacker.BotContext, request slacker.Request, response slacker.ResponseWriter) {
			word := request.StringParam("word", "Hello!")
			number := request.IntegerParam("number", 1)
			for i := 0; i < number; i++ {
				response.Reply(word)
			}
		},
	}

	bot.Command("repeat {word} {number}", definition)

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	err := bot.Listen(ctx)
	if err != nil {
		log.Fatal(err)
	}
}

Example 5

Defines two commands that display sending errors to the Slack channel. One that replies as a new message. The other replies to the thread.

package main

import (
	"context"
	"errors"
	"log"
	"os"

	"github.com/shomali11/slacker"
)

func main() {
	bot := slacker.NewClient(os.Getenv("SLACK_BOT_TOKEN"), os.Getenv("SLACK_APP_TOKEN"))

	messageReplyDefinition := &slacker.CommandDefinition{
		Description: "Tests errors in new messages",
		Handler: func(botCtx slacker.BotContext, request slacker.Request, response slacker.ResponseWriter) {
			response.ReportError(errors.New("oops, an error occurred"))
		},
	}

	threadReplyDefinition := &slacker.CommandDefinition{
		Description: "Tests errors in threads",
		Handler: func(botCtx slacker.BotContext, request slacker.Request, response slacker.ResponseWriter) {
			response.ReportError(errors.New("oops, an error occurred"), slacker.WithThreadReplyError(true))
		},
	}

	bot.Command("message", messageReplyDefinition)
	bot.Command("thread", threadReplyDefinition)

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	err := bot.Listen(ctx)
	if err != nil {
		log.Fatal(err)
	}
}

Example 6

Showcasing the ability to access the github.com/slack-go/slack API and upload a file

package main

import (
	"context"
	"fmt"
	"log"
	"os"

	"github.com/shomali11/slacker"
	"github.com/slack-go/slack"
)

func main() {
	bot := slacker.NewClient(os.Getenv("SLACK_BOT_TOKEN"), os.Getenv("SLACK_APP_TOKEN"))

	definition := &slacker.CommandDefinition{
		Description: "Upload a sentence!",
		Handler: func(botCtx slacker.BotContext, request slacker.Request, response slacker.ResponseWriter) {
			sentence := request.Param("sentence")
			apiClient := botCtx.ApiClient()
			event := botCtx.Event()

			if event.ChannelID != "" {
				apiClient.PostMessage(event.ChannelID, slack.MsgOptionText("Uploading file ...", false))
				_, err := apiClient.UploadFile(slack.FileUploadParameters{Content: sentence, Channels: []string{event.ChannelID}})
				if err != nil {
					fmt.Printf("Error encountered when uploading file: %+v\n", err)
				}
			}
		},
	}

	bot.Command("upload <sentence>", definition)

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	err := bot.Listen(ctx)
	if err != nil {
		log.Fatal(err)
	}
}

Example 7

Showcasing the ability to leverage context.Context to add a timeout

package main

import (
	"context"
	"errors"
	"log"
	"math/rand"
	"os"
	"time"

	"github.com/shomali11/slacker"
)

func main() {
	bot := slacker.NewClient(os.Getenv("SLACK_BOT_TOKEN"), os.Getenv("SLACK_APP_TOKEN"))

	definition := &slacker.CommandDefinition{
		Description: "Process!",
		Handler: func(botCtx slacker.BotContext, request slacker.Request, response slacker.ResponseWriter) {
			timedContext, cancel := context.WithTimeout(botCtx.Context(), 5*time.Second)
			defer cancel()

			duration := time.Duration(rand.Int()%10+1) * time.Second

			select {
			case <-timedContext.Done():
				response.ReportError(errors.New("timed out"))
			case <-time.After(duration):
				response.Reply("Processing done!")
			}
		},
	}

	bot.Command("process", definition)

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	err := bot.Listen(ctx)
	if err != nil {
		log.Fatal(err)
	}
}

Example 8

Showcasing the ability to add attachments to a Reply

package main

import (
	"context"
	"log"
	"os"

	"github.com/shomali11/slacker"
	"github.com/slack-go/slack"
)

func main() {
	bot := slacker.NewClient(os.Getenv("SLACK_BOT_TOKEN"), os.Getenv("SLACK_APP_TOKEN"))

	definition := &slacker.CommandDefinition{
		Description: "Echo a word!",
		Handler: func(botCtx slacker.BotContext, request slacker.Request, response slacker.ResponseWriter) {
			word := request.Param("word")

			attachments := []slack.Attachment{}
			attachments = append(attachments, slack.Attachment{
				Color:      "red",
				AuthorName: "Raed Shomali",
				Title:      "Attachment Title",
				Text:       "Attachment Text",
			})

			response.Reply(word, slacker.WithAttachments(attachments))
		},
	}

	bot.Command("echo {word}", definition)

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	err := bot.Listen(ctx)
	if err != nil {
		log.Fatal(err)
	}
}

Example 9

Showcasing the ability to add blocks to a Reply

package main

import (
	"context"
	"log"
	"os"

	"github.com/shomali11/slacker"
	"github.com/slack-go/slack"
)

func main() {
	bot := slacker.NewClient(os.Getenv("SLACK_BOT_TOKEN"), os.Getenv("SLACK_APP_TOKEN"))

	definition := &slacker.CommandDefinition{
		Description: "Echo a word!",
		Handler: func(botCtx slacker.BotContext, request slacker.Request, response slacker.ResponseWriter) {
			word := request.Param("word")

			attachments := []slack.Block{}
			attachments = append(attachments, slack.NewContextBlock("1",
				slack.NewTextBlockObject("mrkdwn", word, false, false)),
			)

			// When using blocks the message argument will be thrown away and can be left blank.
			response.Reply("", slacker.WithBlocks(attachments))
		},
	}

	bot.Command("echo {word}", definition)

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	err := bot.Listen(ctx)
	if err != nil {
		log.Fatal(err)
	}
}

Example 10

Showcasing the ability to create custom responses via CustomResponse

package main

import (
	"context"
	"errors"
	"fmt"
	"log"
	"os"

	"github.com/shomali11/slacker"
	"github.com/slack-go/slack"
)

const (
	errorFormat = "> Custom Error: _%s_"
)

func main() {
	bot := slacker.NewClient(os.Getenv("SLACK_BOT_TOKEN"), os.Getenv("SLACK_APP_TOKEN"))

	bot.CustomResponse(NewCustomResponseWriter)

	definition := &slacker.CommandDefinition{
		Description: "Custom!",
		Handler: func(botCtx slacker.BotContext, request slacker.Request, response slacker.ResponseWriter) {
			response.Reply("custom")
			response.ReportError(errors.New("oops, an error occurred"))
		},
	}

	bot.Command("custom", definition)

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	err := bot.Listen(ctx)
	if err != nil {
		log.Fatal(err)
	}
}

// NewCustomResponseWriter creates a new ResponseWriter structure
func NewCustomResponseWriter(botCtx slacker.BotContext) slacker.ResponseWriter {
	return &MyCustomResponseWriter{botCtx: botCtx}
}

// MyCustomResponseWriter a custom response writer
type MyCustomResponseWriter struct {
	botCtx slacker.BotContext
}

// ReportError sends back a formatted error message to the channel where we received the event from
func (r *MyCustomResponseWriter) ReportError(err error, options ...slacker.ReportErrorOption) {
	defaults := slacker.NewReportErrorDefaults(options...)

	apiClient := r.botCtx.ApiClient()
	event := r.botCtx.Event()

	opts := []slack.MsgOption{
		slack.MsgOptionText(fmt.Sprintf(errorFormat, err.Error()), false),
	}
	if defaults.ThreadResponse {
		opts = append(opts, slack.MsgOptionTS(event.TimeStamp))
	}

	_, _, err = apiClient.PostMessage(event.ChannelID, opts...)
	if err != nil {
		fmt.Printf("failed to report error: %v\n", err)
	}
}

// Reply send a message to the current channel
func (r *MyCustomResponseWriter) Reply(message string, options ...slacker.ReplyOption) error {
	ev := r.botCtx.Event()
	if ev == nil {
		return fmt.Errorf("unable to get message event details")
	}
	return r.Post(ev.ChannelID, message, options...)
}

// Post send a message to a channel
func (r *MyCustomResponseWriter) Post(channel string, message string, options ...slacker.ReplyOption) error {
	defaults := slacker.NewReplyDefaults(options...)

	apiClient := r.botCtx.ApiClient()
	ev := r.botCtx.Event()
	if ev == nil {
		return fmt.Errorf("unable to get message event details")
	}

	opts := []slack.MsgOption{
		slack.MsgOptionText(message, false),
		slack.MsgOptionAttachments(defaults.Attachments...),
		slack.MsgOptionBlocks(defaults.Blocks...),
	}

	if defaults.ThreadResponse {
		opts = append(opts, slack.MsgOptionTS(ev.TimeStamp))
	}

	_, _, err := apiClient.PostMessage(
		channel,
		opts...,
	)
	return err
}

Example 11

Showcasing the ability to toggle the slack Debug option via WithDebug

package main

import (
	"context"
	"log"
	"os"

	"github.com/shomali11/slacker"
)

func main() {
	bot := slacker.NewClient(os.Getenv("SLACK_BOT_TOKEN"), os.Getenv("SLACK_APP_TOKEN"), slacker.WithDebug(true))

	definition := &slacker.CommandDefinition{
		Description: "Ping!",
		Handler: func(botCtx slacker.BotContext, request slacker.Request, response slacker.ResponseWriter) {
			response.Reply("pong")
		},
	}

	bot.Command("ping", definition)

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	err := bot.Listen(ctx)
	if err != nil {
		log.Fatal(err)
	}
}

Example 12

Defining a command that can only be executed by authorized users

package main

import (
	"context"
	"log"
	"os"

	"github.com/shomali11/slacker"
)

func main() {
	bot := slacker.NewClient(os.Getenv("SLACK_BOT_TOKEN"), os.Getenv("SLACK_APP_TOKEN"))

	authorizedUserIds := []string{"<User ID>"}
	authorizedUserNames := []string{"<User Name>"}

	authorizedDefinitionById := &slacker.CommandDefinition{
		Description: "Very secret stuff",
		AuthorizationFunc: func(botCtx slacker.BotContext, request slacker.Request) bool {
			return contains(authorizedUserIds, botCtx.Event().UserID)
		},
		Handler: func(botCtx slacker.BotContext, request slacker.Request, response slacker.ResponseWriter) {
			response.Reply("You are authorized!")
		},
	}

	authorizedDefinitionByName := &slacker.CommandDefinition{
		Description: "Very secret stuff",
		AuthorizationFunc: func(botCtx slacker.BotContext, request slacker.Request) bool {
			return contains(authorizedUserNames, botCtx.Event().UserProfile.DisplayName)
		},
		Handler: func(botCtx slacker.BotContext, request slacker.Request, response slacker.ResponseWriter) {
			response.Reply("You are authorized!")
		},
	}

	bot.Command("secret-id", authorizedDefinitionById)
	bot.Command("secret-name", authorizedDefinitionByName)

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	err := bot.Listen(ctx)
	if err != nil {
		log.Fatal(err)
	}
}

func contains(list []string, element string) bool {
	for _, value := range list {
		if value == element {
			return true
		}
	}
	return false
}

Example 13

Adding handlers to when the bot is connected, encounters an error and a default for when none of the commands match, adding default inner event handler when event type isn't message or app_mention

package main

import (
	"log"
	"os"

	"context"
	"fmt"

	"github.com/shomali11/slacker"
	"github.com/slack-go/slack/socketmode"
)

func main() {
	bot := slacker.NewClient(os.Getenv("SLACK_BOT_TOKEN"), os.Getenv("SLACK_APP_TOKEN"))

	bot.Init(func() {
		log.Println("Connected!")
	})

	bot.Err(func(err string) {
		log.Println(err)
	})

	bot.DefaultCommand(func(botCtx slacker.BotContext, request slacker.Request, response slacker.ResponseWriter) {
		response.Reply("Say what?")
	})

	bot.DefaultEvent(func(event interface{}) {
		fmt.Println(event)
	})

	bot.DefaultInnerEvent(func(ctx context.Context, evt interface{}, request *socketmode.Request) {
		fmt.Printf("Handling inner event: %s", evt)
	})

	definition := &slacker.CommandDefinition{
		Description: "help!",
		Handler: func(botCtx slacker.BotContext, request slacker.Request, response slacker.ResponseWriter) {
			response.Reply("Your own help function...")
		},
	}

	bot.Help(definition)

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	err := bot.Listen(ctx)
	if err != nil {
		log.Fatal(err)
	}
}

Example 14

Listening to the Commands Events being produced

package main

import (
	"fmt"
	"log"
	"os"

	"context"

	"github.com/shomali11/slacker"
)

func printCommandEvents(analyticsChannel <-chan *slacker.CommandEvent) {
	for event := range analyticsChannel {
		fmt.Println("Command Events")
		fmt.Println(event.Timestamp)
		fmt.Println(event.Command)
		fmt.Println(event.Parameters)
		fmt.Println(event.Event)
		fmt.Println()
	}
}

func main() {
	bot := slacker.NewClient(os.Getenv("SLACK_BOT_TOKEN"), os.Getenv("SLACK_APP_TOKEN"))

	go printCommandEvents(bot.CommandEvents())

	bot.Command("ping", &slacker.CommandDefinition{
		Handler: func(botCtx slacker.BotContext, request slacker.Request, response slacker.ResponseWriter) {
			response.Reply("pong")
		},
	})

	bot.Command("echo {word}", &slacker.CommandDefinition{
		Description: "Echo a word!",
		Examples:    []string{"echo hello"},
		Handler: func(botCtx slacker.BotContext, request slacker.Request, response slacker.ResponseWriter) {
			word := request.Param("word")
			response.Reply(word)
		},
	})

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	err := bot.Listen(ctx)
	if err != nil {
		log.Fatal(err)
	}
}

Example 15

Slack interaction example

package main

import (
	"context"
	"log"
	"os"

	"github.com/shomali11/slacker"
	"github.com/slack-go/slack"
)

func main() {
	bot := slacker.NewClient(os.Getenv("SLACK_BOT_TOKEN"), os.Getenv("SLACK_APP_TOKEN"))

	bot.Interactive(func(botCtx slacker.InteractiveBotContext, callback *slack.InteractionCallback) {
		if callback.Type != slack.InteractionTypeBlockActions {
			return
		}

		if len(callback.ActionCallback.BlockActions) != 1 {
			return
		}

		action := callback.ActionCallback.BlockActions[0]
		if action.BlockID != "mood-block" {
			return
		}

		var text string
		switch action.ActionID {
		case "happy":
			text = "I'm happy to hear you are happy!"
		case "sad":
			text = "I'm sorry to hear you are sad."
		default:
			text = "I don't understand your mood..."
		}

		_, _, _ = botCtx.ApiClient().PostMessage(callback.Channel.ID, slack.MsgOptionText(text, false),
			slack.MsgOptionReplaceOriginal(callback.ResponseURL))

		botCtx.SocketModeClient().Ack(*botCtx.Event().Request)
	})

	definition := &slacker.CommandDefinition{
		Handler: func(botCtx slacker.BotContext, request slacker.Request, response slacker.ResponseWriter) {
			happyBtn := slack.NewButtonBlockElement("happy", "true", slack.NewTextBlockObject("plain_text", "Happy 🙂", true, false))
			happyBtn.Style = "primary"
			sadBtn := slack.NewButtonBlockElement("sad", "false", slack.NewTextBlockObject("plain_text", "Sad ☹️", true, false))
			sadBtn.Style = "danger"

			err := response.Reply("", slacker.WithBlocks([]slack.Block{
				slack.NewSectionBlock(slack.NewTextBlockObject(slack.PlainTextType, "What is your mood today?", true, false), nil, nil),
				slack.NewActionBlock("mood-block", happyBtn, sadBtn),
			}))

			if err != nil {
				response.ReportError(err)
			}
		},
	}

	bot.Command("mood", definition)

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	err := bot.Listen(ctx)
	if err != nil {
		log.Fatal(err)
	}
}

Example 16

Configure bot to process other bot events

package main

import (
	"context"
	"log"
	"os"

	"github.com/shomali11/slacker"
)

func main() {
	bot := slacker.NewClient(
		os.Getenv("SLACK_BOT_TOKEN"),
		os.Getenv("SLACK_APP_TOKEN"),
		slacker.WithBotInteractionMode(slacker.BotInteractionModeIgnoreApp),
	)

	bot.Command("hello", &slacker.CommandDefinition{
		Handler: func(botCtx slacker.BotContext, request slacker.Request, response slacker.ResponseWriter) {
			response.Reply("hai!")
		},
	})

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	err := bot.Listen(ctx)
	if err != nil {
		log.Fatal(err)
	}
}

Example 17

Override the default event input cleaning function (to sanitize the messages received by Slacker)

package main

import (
	"context"
	"fmt"
	"log"
	"os"
	"strings"

	"github.com/shomali11/slacker"
)

func main() {
	bot := slacker.NewClient(os.Getenv("SLACK_BOT_TOKEN"), os.Getenv("SLACK_APP_TOKEN"))
	bot.SanitizeEventText(func(text string) string {
		fmt.Println("My slack bot does not like backticks!")
		return strings.ReplaceAll(text, "`", "")
	})

	bot.Command("my-command", &slacker.CommandDefinition{
		Handler: func(botCtx slacker.BotContext, request slacker.Request, response slacker.ResponseWriter) {
			response.Reply("it works!")
		},
	})

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	err := bot.Listen(ctx)
	if err != nil {
		log.Fatal(err)
	}
}

Example 18

Showcase the ability to define Cron Jobs

package main

import (
	"context"
	"log"
	"os"

	"github.com/shomali11/slacker"
	"github.com/slack-go/slack"
)

func main() {
	bot := slacker.NewClient(os.Getenv("SLACK_BOT_TOKEN"), os.Getenv("SLACK_APP_TOKEN"))
	bot.Command("ping", &slacker.CommandDefinition{
		Handler: func(botCtx slacker.BotContext, request slacker.Request, response slacker.ResponseWriter) {
			response.Reply("pong")
		},
	})

	// Run every minute
	bot.Job("0 * * * * *", &slacker.JobDefinition{
		Description: "A cron job that runs every minute",
		Handler: func(jobCtx slacker.JobContext) {
			jobCtx.ApiClient().PostMessage("#test", slack.MsgOptionText("Hello!", false))
		},
	})

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	err := bot.Listen(ctx)
	if err != nil {
		log.Fatal(err)
	}
}

Example 19

Override the default command constructor to add a prefix to all commands and print log message before command execution

package main

import (
	"context"
	"fmt"
	"log"
	"os"

	"github.com/shomali11/commander"
	"github.com/shomali11/proper"
	"github.com/shomali11/slacker"
	"github.com/slack-go/slack"
	"github.com/slack-go/slack/socketmode"
)

func main() {
	bot := slacker.NewClient(os.Getenv("SLACK_BOT_TOKEN"), os.Getenv("SLACK_APP_TOKEN"), slacker.WithDebug(true))
	bot.CustomCommand(func(usage string, definition *slacker.CommandDefinition) slacker.Command {
		return &cmd{
			usage:      usage,
			definition: definition,
			command:    commander.NewCommand(fmt.Sprintf("custom-prefix %s", usage)),
		}
	})

	// Invoked by `custom-prefix ping`
	bot.Command("ping", &slacker.CommandDefinition{
		Handler: func(botCtx slacker.BotContext, request slacker.Request, response slacker.ResponseWriter) {
			_ = response.Reply("it works!")
		},
	})

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	err := bot.Listen(ctx)
	if err != nil {
		log.Fatal(err)
	}
}

type cmd struct {
	usage      string
	definition *slacker.CommandDefinition
	command    *commander.Command
}

func (c *cmd) Usage() string {
	return c.usage
}

func (c *cmd) Definition() *slacker.CommandDefinition {
	return c.definition
}

func (c *cmd) Match(text string) (*proper.Properties, bool) {
	return c.command.Match(text)
}

func (c *cmd) Tokenize() []*commander.Token {
	return c.command.Tokenize()
}

func (c *cmd) Execute(botCtx slacker.BotContext, request slacker.Request, response slacker.ResponseWriter) {
	log.Printf("Executing command [%s] invoked by %s", c.usage, botCtx.Event().UserID)
	c.definition.Handler(botCtx, request, response)
}

func (c *cmd) Interactive(slacker.InteractiveBotContext, *socketmode.Request, *slack.InteractionCallback) {
}

Contributing / Submitting an Issue

Please review our Contribution Guidelines if you have found an issue with Slacker or wish to contribute to the project.

Troubleshooting

My bot is not responding to events

There are a few common issues that can cause this:

  • The OAuth (bot) Token may be incorrect. In this case authentication does not fail like it does if the App Token is incorrect, and the bot will simply have no scopes and be unable to respond.
  • Required scopes are missing from the OAuth (bot) Token. Similar to the incorrect OAuth Token, without the necessary scopes, the bot cannot respond.
  • The bot does not have the correct event subscriptions setup, and is not receiving events to respond to.

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type BotContext

type BotContext interface {
	Context() context.Context
	Event() *MessageEvent
	APIClient() *slack.Client
	SocketModeClient() *socketmode.Client
}

BotContext interface is for bot command contexts

func NewBotContext

func NewBotContext(ctx context.Context, apiClient *slack.Client, socketModeClient *socketmode.Client, event *MessageEvent) BotContext

NewBotContext creates a new bot context

type BotInteractionMode

type BotInteractionMode int

BotInteractionMode instruct the bot on how to handle incoming events that originated from a bot.

const (
	// BotInteractionModeIgnoreAll instructs our bot to ignore any activity coming
	// from other bots, including our self.
	BotInteractionModeIgnoreAll BotInteractionMode = iota

	// BotInteractionModeIgnoreApp will ignore any events that originate from a
	// bot that is associated with the same App (ie. share the same App ID) as
	// this bot. OAuth scope `user:read` is required for this mode.
	BotInteractionModeIgnoreApp

	// BotInteractionModeIgnoreNone will not ignore any bots, including our self.
	// This can lead to bots "talking" to each other so care must be taken when
	// selecting this option.
	BotInteractionModeIgnoreNone
)

type ClientDefaults

type ClientDefaults struct {
	Debug   bool
	BotMode BotInteractionMode
}

ClientDefaults configuration

type ClientOption

type ClientOption func(*ClientDefaults)

ClientOption an option for client values

func WithBotInteractionMode

func WithBotInteractionMode(mode BotInteractionMode) ClientOption

WithBotInteractionMode instructs Slacker on how to handle message events coming from a bot.

func WithDebug

func WithDebug(debug bool) ClientOption

WithDebug sets debug toggle

type Command added in v1.4.0

type Command interface {
	Usage() string
	Definition() *CommandDefinition

	Match(string) (*proper.Properties, bool)
	Tokenize() []*commander.Token
	Execute(BotContext, Request, ResponseWriter)
	Interactive(InteractiveBotContext, *socketmode.Request, *slack.InteractionCallback)
}

Command interface

func NewCommand added in v1.4.0

func NewCommand(usage string, definition *CommandDefinition) Command

NewCommand creates a new bot command object

type CommandDefinition

type CommandDefinition struct {
	Description       string
	Examples          []string
	BlockID           string
	AuthorizationFunc func(BotContext, Request) bool
	Handler           func(BotContext, Request, ResponseWriter)
	Interactive       func(InteractiveBotContext, *socketmode.Request, *slack.InteractionCallback)

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

CommandDefinition structure contains definition of the bot command

type CommandEvent

type CommandEvent struct {
	Timestamp  time.Time
	Command    string
	Parameters *proper.Properties
	Event      *MessageEvent
}

CommandEvent is an event to capture executed commands

func NewCommandEvent

func NewCommandEvent(command string, parameters *proper.Properties, event *MessageEvent) *CommandEvent

NewCommandEvent creates a new command event

type InteractiveBotContext added in v1.4.0

type InteractiveBotContext interface {
	Context() context.Context
	Event() *socketmode.Event
	APIClient() *slack.Client
	SocketModeClient() *socketmode.Client
}

InteractiveBotContext interface is interactive bot command contexts

func NewInteractiveBotContext added in v1.4.0

func NewInteractiveBotContext(ctx context.Context, apiClient *slack.Client, socketModeClient *socketmode.Client, event *socketmode.Event) InteractiveBotContext

NewInteractiveBotContext creates a new interactive bot context

type Job added in v1.4.0

type Job interface {
	Spec() string
	Definition() *JobDefinition
	Callback(JobContext) func()
}

Job interface

func NewJob added in v1.4.0

func NewJob(spec string, definition *JobDefinition) Job

NewJob creates a new job object

type JobContext added in v1.4.0

type JobContext interface {
	Context() context.Context
	APIClient() *slack.Client
	SocketModeClient() *socketmode.Client
}

JobContext interface is for job command contexts

func NewJobContext added in v1.4.0

func NewJobContext(ctx context.Context, apiClient *slack.Client, socketModeClient *socketmode.Client) JobContext

NewJobContext creates a new bot context

type JobDefinition added in v1.4.0

type JobDefinition struct {
	Description string
	Handler     func(JobContext)

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

JobDefinition structure contains definition of the job

type MessageEvent

type MessageEvent 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
	Text string

	// 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 interface{}

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

	// BotID of the bot that sent this message. If a bot did not send this
	// message, this will be an empty string.
	BotID string
}

MessageEvent 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.

func NewMessageEvent added in v1.4.0

func NewMessageEvent(slacker *Slacker, event interface{}, req *socketmode.Request) *MessageEvent

NewMessageEvent creates a new message event structure

func (*MessageEvent) IsBot

func (e *MessageEvent) IsBot() bool

IsBot indicates if the message was sent by a bot

func (*MessageEvent) IsThread

func (e *MessageEvent) IsThread() bool

IsThread indicates if a message event took place in a thread.

type ReplyDefaults

type ReplyDefaults struct {
	Attachments    []slack.Attachment
	Blocks         []slack.Block
	ThreadResponse bool
}

ReplyDefaults configuration

func NewReplyDefaults

func NewReplyDefaults(options ...ReplyOption) *ReplyDefaults

NewReplyDefaults builds our ReplyDefaults from zero or more ReplyOption.

type ReplyOption

type ReplyOption func(*ReplyDefaults)

ReplyOption an option for reply values

func WithAttachments

func WithAttachments(attachments []slack.Attachment) ReplyOption

WithAttachments sets message attachments

func WithBlocks

func WithBlocks(blocks []slack.Block) ReplyOption

WithBlocks sets message blocks

func WithThreadReply

func WithThreadReply(useThread bool) ReplyOption

WithThreadReply specifies the reply to be inside a thread of the original message

type ReportErrorDefaults

type ReportErrorDefaults struct {
	ThreadResponse bool
}

ReportErrorDefaults configuration

func NewReportErrorDefaults

func NewReportErrorDefaults(options ...ReportErrorOption) *ReportErrorDefaults

NewReportErrorDefaults builds our ReportErrorDefaults from zero or more ReportErrorOption.

type ReportErrorOption

type ReportErrorOption func(*ReportErrorDefaults)

ReportErrorOption an option for report error values

func WithThreadReplyError added in v1.4.0

func WithThreadReplyError(useThread bool) ReportErrorOption

WithThreadReplyError specifies the reply to be inside a thread of the original message

type Request

type Request interface {
	Param(key string) string
	StringParam(key string, defaultValue string) string
	BooleanParam(key string, defaultValue bool) bool
	IntegerParam(key string, defaultValue int) int
	FloatParam(key string, defaultValue float64) float64
	Properties() *proper.Properties
}

Request interface that contains the Event received and parameters

func NewRequest

func NewRequest(botCtx BotContext, properties *proper.Properties) Request

NewRequest creates a new Request structure

type ResponseWriter

type ResponseWriter interface {
	Post(channel string, message string, options ...ReplyOption) error
	Reply(text string, options ...ReplyOption) error
	ReportError(err error, options ...ReportErrorOption)
}

A ResponseWriter interface is used to respond to an event

func NewResponse

func NewResponse(botCtx BotContext) ResponseWriter

NewResponse creates a new response structure

type Slacker

type Slacker struct {
	// contains filtered or unexported fields
}

Slacker contains the Slack API, botCommands, and handlers

func NewClient

func NewClient(botToken, appToken string, options ...ClientOption) *Slacker

NewClient creates a new client using the Slack API

func (*Slacker) APIClient added in v1.4.1

func (s *Slacker) APIClient() *slack.Client

APIClient returns the internal slack.Client of Slacker struct

func (*Slacker) BotCommands

func (s *Slacker) BotCommands() []Command

BotCommands returns Bot Commands

func (*Slacker) Command

func (s *Slacker) Command(usage string, definition *CommandDefinition)

Command define a new command and append it to the list of existing bot commands

func (*Slacker) CommandEvents

func (s *Slacker) CommandEvents() <-chan *CommandEvent

CommandEvents returns read only command events channel

func (*Slacker) CustomBotContext added in v0.1.0

func (s *Slacker) CustomBotContext(botContextConstructor func(context.Context, *slack.Client, *socketmode.Client, *MessageEvent) BotContext)

CustomBotContext creates a new bot context

func (*Slacker) CustomCommand added in v0.1.0

func (s *Slacker) CustomCommand(commandConstructor func(usage string, definition *CommandDefinition) Command)

CustomCommand creates a new BotCommand

func (*Slacker) CustomInteractiveBotContext added in v1.4.0

func (s *Slacker) CustomInteractiveBotContext(interactiveBotContextConstructor func(context.Context, *slack.Client, *socketmode.Client, *socketmode.Event) InteractiveBotContext)

CustomInteractiveBotContext creates a new interactive bot context

func (*Slacker) CustomJobContext added in v1.4.0

func (s *Slacker) CustomJobContext(jobContextConstructor func(context.Context, *slack.Client, *socketmode.Client) JobContext)

CustomJobContext creates a new job context

func (*Slacker) CustomRequest

func (s *Slacker) CustomRequest(requestConstructor func(botCtx BotContext, properties *proper.Properties) Request)

CustomRequest creates a new request

func (*Slacker) CustomResponse

func (s *Slacker) CustomResponse(responseConstructor func(botCtx BotContext) ResponseWriter)

CustomResponse creates a new response writer

func (*Slacker) DefaultCommand

func (s *Slacker) DefaultCommand(defaultMessageHandler func(botCtx BotContext, request Request, response ResponseWriter))

DefaultCommand handle messages when none of the commands are matched

func (*Slacker) DefaultEvent

func (s *Slacker) DefaultEvent(defaultEventHandler func(interface{}))

DefaultEvent handle events when an unknown event is seen

func (*Slacker) DefaultInnerEvent added in v1.3.0

func (s *Slacker) DefaultInnerEvent(defaultInnerEventHandler func(ctx context.Context, evt interface{}, request *socketmode.Request))

DefaultInnerEvent handle events when an unknown inner event is seen

func (*Slacker) Err

func (s *Slacker) Err(errorHandler func(err string))

Err handle when errors are encountered

func (*Slacker) Help

func (s *Slacker) Help(definition *CommandDefinition)

Help handle the help message, it will use the default if not set

func (*Slacker) Init

func (s *Slacker) Init(initHandler func())

Init handle the event when the bot is first connected

func (*Slacker) Interactive

func (s *Slacker) Interactive(interactiveEventHandler func(InteractiveBotContext, *slack.InteractionCallback))

Interactive assigns an interactive event handler

func (*Slacker) Job added in v1.4.0

func (s *Slacker) Job(spec string, definition *JobDefinition)

Job define a new cron job and append it to the list of existing jobs

func (*Slacker) Listen

func (s *Slacker) Listen(ctx context.Context) error

Listen receives events from Slack and each is handled as needed

func (*Slacker) SanitizeEventText added in v1.4.0

func (s *Slacker) SanitizeEventText(sanitizeEventText func(in string) string)

SanitizeEventText allows the api consumer to override the default event text sanitization

func (*Slacker) SocketModeClient added in v1.4.0

func (s *Slacker) SocketModeClient() *socketmode.Client

SocketModeClient returns the internal socketmode.Client of Slacker struct

func (*Slacker) UnAuthorizedError

func (s *Slacker) UnAuthorizedError(errUnauthorized error)

UnAuthorizedError error message

Directories

Path Synopsis
examples
1
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL