Skip to content

Instantly share code, notes, and snippets.

@seratch
Last active March 26, 2023 14:17
Show Gist options
  • Save seratch/46c404c6141d34060a1c607822dd25d0 to your computer and use it in GitHub Desktop.
Save seratch/46c404c6141d34060a1c607822dd25d0 to your computer and use it in GitHub Desktop.
Slack app built with Play Framework (Scala)
name := """bolt-play-scala"""
organization := "com.example"
version := "1.0-SNAPSHOT"
lazy val root = (project in file(".")).enablePlugins(PlayScala)
scalaVersion := "2.13.2"
libraryDependencies += guice
libraryDependencies += "com.slack.api" % "bolt-servlet" % "1.0.8"
package controllers
import javax.inject._
import play.api.mvc._
@Singleton
class HomeController @Inject()(val controllerComponents: ControllerComponents) extends BaseController {
def index() = Action { implicit request: Request[AnyContent] =>
Ok("Hello World!")
}
}
addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.8.2")
addSbtPlugin("org.foundweekends.giter8" % "sbt-giter8-scaffold" % "0.11.0")
# Routes
# This file defines all application routes (Higher priority routes first)
# https://www.playframework.com/documentation/latest/ScalaRouting
# ~~~~
# An example controller showing a sample home page
GET / controllers.HomeController.index
POST /slack/events controllers.SlackAppController.index
# Map static resources from the /public folder to the /assets URL path
GET /assets/*file controllers.Assets.versioned(path="/public", file: Asset)
package controllers
import javax.inject._
import play.api.mvc._
import services.SlackAppService
@Singleton
class SlackAppController @Inject()(
val controllerComponents: ControllerComponents,
service: SlackAppService) extends BaseController {
def index() = Action(parse.raw)(service.run(_))
}
package services
import akka.util.ByteString
import com.slack.api.bolt.handler.builtin.SlashCommandHandler
import com.slack.api.bolt.request.{RequestHeaders, Request => SlackRequest}
import com.slack.api.bolt.response.{Response => SlackResponse}
import com.slack.api.bolt.util.SlackRequestParser
import com.slack.api.bolt.{App => SlackApp}
import javax.inject.Singleton
import play.api.http.HttpEntity
import play.api.mvc._
import scala.jdk.CollectionConverters._
@Singleton
class SlackAppService {
private[this] lazy val app = new SlackApp()
private[this] lazy val parser = new SlackRequestParser(app.config)
val makeRequestHandler: SlashCommandHandler = (req, ctx) => {
ctx.ack(s"Thanks <@${req.getPayload.getUserId}>!")
}
app.command("/make-request", makeRequestHandler)
def run(implicit request: Request[RawBuffer]): Result = {
toPlayResponse(app.run(parseRequest(request)))
}
def parseRequest(implicit request: Request[RawBuffer]): SlackRequest[_] = {
parser.parse(SlackRequestParser.HttpRequest.builder()
.requestUri(request.uri)
.queryString(request.queryString.map { case (k, vs) => k -> vs.asJava }.asJava)
.headers(new RequestHeaders(request.headers.toMap.map { case (k, vs) => k -> vs.asJava }.asJava))
.requestBody(request.body.asBytes().map(_.utf8String).getOrElse(""))
.remoteAddress(request.remoteAddress)
.build())
}
def toPlayResponse(response: SlackResponse): Result = {
val headers = response.getHeaders.asScala
.filter { case (_, v) => v != null && v.size() > 0 }
.map { case (k, v) => k -> v.get(0) }
.toMap
Result(
header = ResponseHeader(response.getStatusCode, headers),
body = HttpEntity.Strict(ByteString(response.getBody), Some(response.getContentType))
)
}
}
@cdmckay
Copy link

cdmckay commented Nov 25, 2020

I just tried this with the latest version of Bolt (1.3) and it works fine except it appears that response.getBody can sometimes return null, causing a NullPointerException.

It can be easily fixed with this extra line:

def toPlayResponse(response: SlackResponse): Result = {
  val headers = response.getHeaders.asScala
    .filter { case (_, v) => v != null && v.size() > 0 }
    .map { case (k, v) => k -> v.get(0) }
    .toMap
  // Make sure to handle null case for getBody
  val byteString = Option(response.getBody).map(ByteString(_)).getOrElse(ByteString.empty)
  Result(
      header = ResponseHeader(response.getStatusCode, headers),
      body = HttpEntity.Strict(byteString, Some(response.getContentType))
  )
}

@teetreejp
Copy link

How to set SLACK_BOT_TOKEN and SLACK_SIGNING_SECRET?

@cdmckay
Copy link

cdmckay commented May 19, 2022

Try this:

private val signingSecret = conf.get[String]("com.slack.signingSecret")

private lazy val app = {
  val appConfig = new AppConfig()
  // These need to be set to null, otherwise Slack auto-detects that we're an OAuth app which causes problems
  // This means is that you can't use ctx.client() in the handlers
  appConfig.setClientId(null)
  appConfig.setClientSecret(null)
  appConfig.setSigningSecret(signingSecret)
  new SlackApp(appConfig)
}

The bot token is per workspace, so you want to set that dynamically when you're interacting with Slack.

@teetreejp
Copy link

Can we set Bot User OAuth Token? I am not sure it is setSingleTeamBotToken or not.

@teetreejp
Copy link

Do you know how to create unit test with bolt?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment