Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Only possible with Elixir - ubots Case Study

Only possible with Elixir - ubots Case Study

How to build four successful products with a tiny team of two working part-time after-hours? The answer is: Elixir.

- What are Slack Bots and why you should care
- How we decided on our architecture and what were the consequences
- How we've built a new framework using old ideas
- How we've built an end-to-end testing framework for complex interactions

Tymon Tobolski

May 07, 2021

More Decks by Tymon Tobolski

Other Decks in Programming


  1. My name is Tymon Tobolski. I'm the one behind tesla

    HTTP library. Co-founder of UBOTS - Useful bots for Slack @teamon @ GitHub @iteamon @ Twitter
  2. Why we succeeded? We didn't quit our jobs We didn't

    move to Silicon Valley We didn't take VC money
  3. !! != One Git repository One OTP application One Erlang

    release One Docker image + Two Developers ------------------------------ One Majestic Monolith
  4. Top-Level Modules lib !"" core/ !"" core.ex !"" core_web/ !""

    core_web.ex Core CoreWeb lib !"" bdgt/ !"" bdgt.ex !"" lunch/ !"" lunch.ex Bdgt Lunch lib !"" queue/ !"" queue.ex !"" td/ #"" td.ex Queue Td
  5. This must be slow lib/ 21.000 LOC $ time mix

    compile Compiling 277 files (.ex) real 0m34.552s test/ 10.000 LOC $ mix test Finished in 17.1 seconds 18 doctests, 483 tests
  6. HTTP POST /events content-type: application/json { "payload": { "user_id": "U123",

    "team_id": "T456" "command": "/help" } } --- POST /events content-type: text/form-urlencoded user_id=U123&team_id=T456&command=/help Core.Bot %Context{ user_id: "U123", team_id: "T456", responses: [...] ... } # Actions %Command{ command: "/queue", text: "help" } %Button{value: "join"} %Shortcut{...} ...
  7. Ubots Slack "Framework" Web App Slack App Plug.Conn Context Struct

    params Action Structs Plug Middleware Phoenix.Router (Pattern Matching) Phoenix.Controller Controller Phoenix.View View
  8. Pattern Matching in Actions ( ) def build(%{ "command" =>

    command, "text" => text }) do %Command{command: command, text: text} end def build(%{ "type" => "shortcut", "callback_id" => callback_id }) do %Shortcut{callback_id: callback_id} end def build(%{ "type" => "view_submission", "view" => %{ "callback_id" => callback_id, "external_id" => external_id, "state" => %{"values" => values} } }) do %Submit{ callback_id: callback_id, external_id: decode_external_id(external_id), values: decode_values(values) } end
  9. Pattern Matching in Controllers defmodule Queue.Controllers.Main do use Core.Bot.Controller def

    call(ctx, %Button{action_id: "button_join"}), do: # ... def call(ctx, %Command{text: "join"}), do: # ... def call(ctx, %Button{action_id: "button_leave"}), do: # ... def call(ctx, %Command{text: "leave"}), do: # ...
  10. Macros for Controller Pipelines defmodule Queue do use Core.Bot.Controller plug

    Core.Bot.Middleware.Logger plug Core.Bot.Middleware.Ignorables plug Core.Bot.Middleware.Duplicates plug Core.Bot.Middleware.Support plug Queue.Controllers.Main plug Queue.Controllers.Home plug Queue.Controllers.Workflows # ...
  11. Macros for DRY defmodule Core.Bot.Actions do use Core.Bot.ActionsBuilder defaction(Button, action_id:

    binary, value: binary ) defaction(Datepicker, action_id: binary, action_ts: binary, block_id: binary, initial_date: binary | nil, selected_date: binary )
  12. Unleashing the full potential setup do [ alice: Slack.user(), dev:

    Slack.channel() ] end test "/queue", %{alice: alice, dev: dev} do alice |> go_to_channel(dev) |> command("/queue") |> click_button("Join") assert visible?(dev, "People in queue") end
  13. Lax.Server (Single GenSever) defmodule Lax.Server do use GenServer # ...

    def handle_call({:get_channel, channel_id}, from, state) do handle_reply(Lax.State.get_channel(state, channel_id), from) end def handle_call({:access, action}, from, state) do handle_reply(Lax.Access.on(action, state), from) end def handle_call({:ui, action}, from, state) do handle_reply(Lax.UI.on(action, state), from) end def handle_call({:outgoing, ...}) do handle_reply(Lax.API.on(method, body, state), from) end # ... end ... The chery on top of all elixir features is the amazing community
  14. 10 out of 10 boxes say: YES! ✅ Tooling ✅

    Stability ✅ Readability ✅ Productivity ✅ Developer Experience ✅ Rich ecosystem ✅ Performance ✅ Scalability ✅ Community ✅ Great choice for business
  15. ! Q & A ! Visit ubots.xyz for Useful Slack

    Bots ! Sign up at moonhome.io for the Future of Remote Development ! Follow @iteamon on Twitter to stay up-to-date