Slack
A developer-friendly chat service
- Introduce Slack
- Developer-friendly chat service
- Can do everything with a keyboard
- Inline code, images, videos, link previews
- API for extensions
- Many extensions already exist for Twitter, Github etc but one missing...
Memes!
"an idea, behavior, style, or usage that spreads from person to person within a culture"
- Previously in Hipchat had a bot which allowed inline meme generation
- Moved to Slack for other reasons but found we missed the memes
- Previous slide was top secret JUXT conversation about logging
- Frankie wanted to remind us of Rich Hickey's catchphrase "Simple made complex"
- So what did he do?
The Slack "Slash" command
POST
token=gIkuvaNzQIHg97ATvDxqgjtO
team_id=T0001
team_domain=juxt
channel_id=C2147483705
channel_name=general
user_id=U2147483697
user_name=frankie
command=/meme
text=rich hickey | simple | made complex
api.slack.com/slash-commands
- One type of extension is called a Slash command - like IRC
- Captures command and sends to HTTP endpoint
- Documentation tells you what it will look like
- Want to implement this doc now - not all the bits in between (deserialisation, validation etc)
pedestal-swagger
Deserialisation, coercion and related error handling
(s/defschema SlackRequest
{(req :token) s/Str
(req :team_id) s/Str
(req :team_domain) s/Str
(req :channel_id) s/Str
(req :channel_name) s/Str
(req :user_id) s/Str
(req :user_name) s/Str
(req :command) s/Str
(req :text) s/Str})
(swagger/defhandler slack-meme
{:summary "Responds asynchonously to
Slash commands from Slack"
:parameters {:formData SlackRequest}
:responses {200 {:schema MemeUrl}}}
[{:keys [form-params] :as request}]
(let [text (:text form-params)]
{:status 200
:body (meme/generate-meme text)))
frankiesardo/pedestal-swagger
- pedestal-swagger lets me do just that
- Define schema of request and attach to a handler
- pedestal-swagger does everything in between to ensure the Swagger contract is met
- Handler only knows about happy path
- Handler not complicated by error handling
- Nicely documented in code
- Saves 75% of code (made that up)
Routing and interceptors
(swagger/defroutes api-routes
{:info {:title "Slacky"
:description "Memes and more for Slack"
:externalDocs {:description "Find out more"
:url "https://github.com/oliyh/slacky"}
:version "2.0"}
:tags [{:name "meme"
:description "All the memes!"}]}
[[["/api" ^:interceptors [(swagger/body-params)
bootstrap/json-body
(swagger/coerce-request)
(swagger/validate-response)]
["/slack"
["/meme" ^:interceptors [(annotate {:tags ["meme"]})]
{:post slack-meme}]]
["/swagger.json" {:get [(swagger/swagger-json)]}]
["/*resource" {:get [(swagger/swagger-ui)]}]]]])
- Routing table
- Batteries included interceptors that deal with unpleasant bits
- What is all that metadata for?
Swagger UI
- Swagger UI
- Driven by JSON schema
- Brilliant for documenting APIs and trying them out
- JSON schema also useful for offshore teams / 3rd parties to code against
- Provided for free by pedestal-swagger
Meme generation
- Quickly cover what we do with a meme request
- Parse using regex
- Send search term to google to find template image
Meme generation
- Send image url to MemeCaptain which returns us a template id
Meme generation
- Ask MemeCaptain to generate meme using template id, upper and lower text
- Returns a url to meme
- Complicated enough!
Responding to Slack
api.slack.com/incoming-webhooks
- Need a webhook url to send a message to a particular channel/team in Slack
- Can map the unique token in Slash command post to a webhook url for response
- We'll need something to deal with matching the token to the right webhook url
Authentication interceptor
(swagger/defbefore authenticate-slack-call
{:description "Ensures caller has registered an incoming webhook"
:parameters {:formData {:token s/Str}}
:responses {403 {}}}
[{:keys [request response] :as context}]
(let [db (:db-connection request)
token (get-in request [:form-params :token])
account (accounts/lookup-slack-account db token)]
(if-let [webhook-url (:key account)]
(update context :request merge {::slack-webhook-url webhook-url
::account-id (:id account)})
(-> (terminate context)
(assoc :response
{:status 403
:headers {}
:body (str "You are not permitted to use this service.\n"
"Please register your token '" token
"'at https://slacky-server.herokuapp.com")})))))
- I need to either return a 403 unauthorised or provide the webhook url to my handler
- I don't want to pollute my handler with this logic
- This interceptor handles that flow completely and keeps my handler agnostic of databases, tokens etc
- It all gets merged into the Swagger definition too so clients know a 403 might be returned
Self-migrating database
Ensure database is migrated before use
(defn- migrate-db [url]
(joplin/migrate-db
{:db {:type :jdbc :url url}
:migrator "migrators/jdbc"}))
(defn create-db-connection [url]
(migrate-db url)
(condp re-find url
#":postgresql:" (pool "org.postgresql.Driver" url))
#":h2:" (pool "org.h2.Driver" url))
juxt/joplin
- So now I need a database, with at least some tables with names and columns that match my code
- Sometimes for a new feature the tables and columns have to change, how do I keep my database and the code in sync?
- Database migrations, run idempotently and handled by Joplin
- Run them every time we connect to the database
- No defensive code needed, no command line tools, release scripts etc
- Code and database always in sync
- Note code here deals with both H2 and postgres, one click clone and test
- Joplin a JUXT library, used in production, handles many kinds of store inc ES, JDBC and more
Extending to other services
(defroutes routes
["/api" ^:interceptors [rate-limiter]
["/slack" ^:interceptors [slack-auth] ...]
["/hipchat" ^:interceptors [hipchat-auth] ...]])
- Consider extending Slacky to another chat service which authenticates a different way (hipchat-auth interceptor)
- Consider also wanting a rate limiter which applies to all users of the API
- The rate-limiter interceptor needs an account-id to track how much each account uses the service
- Pedestal will run this interceptor before the authentication interceptors
- It complects scope with order of execution
Angel interceptor
Decomplect interceptor scope from ordering
(require '[angel.interceptor :as angel])
(defroutes routes
["/api" ^:interceptors [(angel/requires rate-limiter :account)]
["/slack" ^:interceptors
[(angel/provides slack-auth :account)] ...]
["/hipchat" ^:interceptors
[(angel/provides hipchat-auth :account)] ...]])
(def service
(angel/satisfy
{:io.pedestal.http/routes routes}))
oliyh/angel-interceptor
- Declare relationships between interceptors
- angel/satisfy will reorder them without affecting their scope
- Decomplects scope from order of execution
- New library available on my Github
Slacky
slacky-server.herokuapp.com
- What do we have now?
- Simple service, self-contained functions
- Easily extendable without having to rewrite interceptors
- One click dev environment
- One click deploy - Heroku deployed directly from Github
- Existing functionality - other templates, and register custom templates
- Show them demo from Slack (show help function / custom templates - introduce David via meme?)
- Show them website?
Epic memes
slacky-server.herokuapp.com
oliyh/slacky
- What's next?
- Custom template rollout
- Browser plugins on the way - should be easily incorporated
- Available as a service - register your Slack at this link
- Available on GitHub
- TV advertising - endorsements from Jean-Claude and Enya
Practical Pedestal/Swagger: Slack integration
slacky-server.herokuapp.com
oliyh/slacky
Created by Oliver Hine / @oliyh
Oliver Hine
JUXTer
Background in banks, OnTheMarket
Patterns for keeping things simple in a small REST service