Skip to content

Instantly share code, notes, and snippets.

@seratch
Last active June 7, 2020 02:55
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save seratch/6ea67ae66c05417c87670917e978378d to your computer and use it in GitHub Desktop.
Save seratch/6ea67ae66c05417c87670917e978378d to your computer and use it in GitHub Desktop.
Slack app built with HTTP4K
brew install gradle
export SLACK_BOT_TOKEN=xoxb-111-222-xxx
export SLACK_SIGNING_SECRET=xxx
gradle run
plugins {
id("org.jetbrains.kotlin.jvm") version "1.3.72"
id("application")
}
repositories {
mavenCentral()
}
dependencies {
implementation(platform("org.jetbrains.kotlin:kotlin-bom"))
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
implementation("com.slack.api:bolt:1.0.8")
implementation("org.http4k:http4k-server-jetty:3.248.0")
implementation("ch.qos.logback:logback-classic:1.2.3")
}
application {
mainClassName = "MyAppKt" // add "Kt" suffix for main function source file
}
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="trace">
<appender-ref ref="STDOUT"/>
</root>
<logger name="org.eclipse.jetty" level="INFO"/>
<logger name="io.netty" level="INFO"/>
</configuration>
import com.slack.api.bolt.App
import com.slack.api.bolt.request.RequestHeaders
import com.slack.api.bolt.util.SlackRequestParser
import com.slack.api.model.block.Blocks.*
import com.slack.api.model.block.composition.BlockCompositions.*
import com.slack.api.model.block.element.BlockElements.plainTextInput
import com.slack.api.model.block.element.BlockElements.staticSelect
import com.slack.api.model.view.View
import com.slack.api.model.view.Views.*
import org.http4k.core.*
import org.http4k.server.Jetty
import org.http4k.server.asServer
fun main() {
val app = App()
app.globalShortcut("meeting-arrangement") { req, ctx ->
val res = ctx.client().viewsOpen { it.triggerId(req.payload.triggerId).view(buildView()) }
ctx.logger.info("views.open: {}", res)
ctx.ack()
}
app.blockAction("category-selection-action") { _, ctx ->
ctx.ack()
}
// when a user clicks "Submit"
app.viewSubmission("meeting-arrangement") { req, ctx ->
val stateValues = req.payload.view.state.values
val agenda = stateValues["agenda-block"]!!["agenda-action"]!!.value
val errors = mutableMapOf<String, String>()
if (agenda.length <= 10) {
errors["agenda-block"] = "Agenda needs to be longer than 10 characters."
}
if (errors.isNotEmpty()) {
ctx.ack { it.responseAction("errors").errors(errors) }
} else {
// TODO: may store the stateValues and privateMetadata
// Responding with an empty body means closing the modal now.
// If your app has next steps, respond with other response_action and a modal view.
ctx.ack()
}
}
val parser = SlackRequestParser(app.config())
val httpApp = { request: Request ->
val slackRequest = parser.parse(SlackRequestParser.HttpRequest.builder()
.requestUri(request.uri.path)
.queryString(request.uri.query.toParameters().toParametersMap())
.requestBody(request.bodyString())
.headers(RequestHeaders(request.headers.toParametersMap()))
.remoteAddress(null) // https://github.com/http4k/http4k/issues/120
.build())
val slackResponse = app.run(slackRequest)
val responseStatus = Status(slackResponse.statusCode, "")
slackResponse.headers.putIfAbsent("Content-Type", listOf(slackResponse.contentType))
val responseHeaders: Parameters = slackResponse.headers
.filter { it.value != null && it.value.size > 0 }
.mapValues { it.value.first() }
.toList()
val responseBody = if (slackResponse.body != null) slackResponse.body else ""
Response.invoke(responseStatus).headers(responseHeaders).body(responseBody)
}
val server = httpApp.asServer(Jetty(3000))
server.start()
}
fun buildView(): View {
return view {
it.callbackId("meeting-arrangement")
.type("modal")
.notifyOnClose(true)
.title(viewTitle { title -> title.type("plain_text").text("Meeting Arrangement").emoji(true) })
.submit(viewSubmit { submit -> submit.type("plain_text").text("Submit").emoji(true) })
.close(viewClose { close -> close.type("plain_text").text("Cancel").emoji(true) })
.blocks(asBlocks(
section { section ->
section
.blockId("category-block")
.text(markdownText("Select a category of the meeting!"))
.accessory(staticSelect { staticSelect ->
staticSelect
.actionId("category-selection-action")
.placeholder(plainText("Select a category"))
.options(asOptions(
option(plainText("Customer"), "customer"),
option(plainText("Partner"), "partner"),
option(plainText("Internal"), "internal")
))
})
},
input { input ->
input
.blockId("agenda-block")
.element(plainTextInput { pti -> pti.actionId("agenda-action").multiline(true) })
.label(plainText { pt -> pt.text("Detailed Agenda").emoji(true) })
}
))
}
}
@seratch
Copy link
Author

seratch commented Jun 7, 2020

Screen Shot 2020-06-07 at 11 47 49

Screen Shot 2020-06-07 at 11 48 14

Screen Shot 2020-06-07 at 11 49 49

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