Practical Pedestal/Swagger: Slack integration – Meme generation



Practical Pedestal/Swagger: Slack integration – Meme generation

0 0


slacky-pres

Practical Pedestal/Swagger: Slack integration

On Github oliyh / slacky-pres

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

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