├── .formatter.exs ├── .github └── workflows │ ├── ci.yml │ └── publish.yml ├── .gitignore ├── .tool-versions ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── lib ├── knock.ex └── knock │ ├── api.ex │ ├── client.ex │ ├── errors │ └── api_key_missing.ex │ ├── resources │ ├── bulk_operations.ex │ ├── channels.ex │ ├── messages.ex │ ├── objects.ex │ ├── preferences.ex │ ├── resource_helpers.ex │ ├── tenants.ex │ ├── users.ex │ └── workflows.ex │ └── response.ex ├── mix.exs ├── mix.lock └── test ├── knock_test.exs └── test_helper.exs /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | 6 | name: Test 7 | 8 | on: 9 | push: 10 | branches: ["main"] 11 | pull_request: 12 | 13 | permissions: 14 | contents: read 15 | 16 | jobs: 17 | build: 18 | name: Build and test 19 | runs-on: ubuntu-latest 20 | 21 | steps: 22 | - name: Checkout 23 | uses: actions/checkout@v4 24 | 25 | - name: Set up Elixir 26 | uses: erlef/setup-beam@v1 27 | with: 28 | elixir-version: "1.18.3" 29 | otp-version: "27" 30 | - name: Restore dependencies cache 31 | uses: actions/cache@v3 32 | with: 33 | path: deps 34 | key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }} 35 | restore-keys: ${{ runner.os }}-mix- 36 | - name: Install dependencies 37 | run: mix deps.get 38 | - name: Run tests 39 | run: mix test 40 | - name: Format 41 | run: mix format --check-formatted 42 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish to Hex.pm 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | Publish: 9 | runs-on: ubuntu-latest 10 | env: 11 | HEX_API_KEY: ${{ secrets.KNOCK_HEX_API_KEY }} 12 | steps: 13 | - uses: actions/checkout@v4 14 | - uses: erlef/setup-beam@v1 15 | with: 16 | elixir-version: "1.16.1" 17 | otp-version: "26.2.1" 18 | - run: mix deps.get 19 | - run: mix compile --docs 20 | - run: mix hex.publish --yes 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where third-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | knock-*.tar 24 | 25 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | elixir 1.18.3-otp-27 2 | erlang 27.2.4 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## v0.4.7 2 | 3 | * Add support for `idempotency_key` on workflow triggers -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Getting Started 4 | 5 | 1. Install [asdf](https://asdf-vm.com) 6 | 2. Install the asdf Elixir and Erlang plugins 7 | 8 | ```bash 9 | # Read the docs for these plugins 10 | # To ensure you have the correct dependencies installed first 11 | asdf plugin add elixir # https://github.com/asdf-vm/asdf-elixir 12 | asdf plugin add erlang # https://github.com/asdf-vm/asdf-erlang 13 | ``` 14 | 15 | 3. Run `asdf install` to install the versions of Elixir and Erlang specified in the [.tool-versions](.tool-versions) file 16 | 17 | ## Running tests 18 | 19 | `mix test` 20 | 21 | ## Running the linter 22 | 23 | `mix format` 24 | 25 | ## Running the formatter 26 | 27 | `mix format` 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Knock Labs, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Knock 2 | 3 | Knock API access for applications written in Elixir. 4 | 5 | ## Documentation 6 | 7 | See the [package documentation](https://hexdocs.pm/knock) as well as [API documentation](https://docs.knock.app) for usage examples. 8 | 9 | ## Installation 10 | 11 | Add the package to your `mix.exs` file as follows: 12 | 13 | ```elixir 14 | def deps do 15 | [ 16 | {:knock, "~> 0.4"} 17 | ] 18 | end 19 | ``` 20 | 21 | ## Configuration 22 | 23 | Start by defining an Elixir module for your Knock instance: 24 | 25 | ```elixir 26 | defmodule MyApp.Knock do 27 | use Knock, otp_app: :my_app 28 | end 29 | ``` 30 | 31 | To use the library you must provide a secret API key, provided in the Knock dashboard. 32 | 33 | You can set it as an environment variable: 34 | 35 | ```bash 36 | KNOCK_API_KEY="sk_12345" 37 | ``` 38 | 39 | Or you can specify it manually in your configuration: 40 | 41 | ```elixir 42 | config :my_app, MyApp.Knock, 43 | api_key: "sk_12345" 44 | ``` 45 | 46 | Or you can pass it through when creating a client instance: 47 | 48 | ```elixir 49 | knock_client = MyApp.Knock.client(api_key: "sk_12345") 50 | ``` 51 | 52 | ## Usage 53 | 54 | ### Identifying users 55 | 56 | ```elixir 57 | MyApp.Knock.client() 58 | |> Knock.Users.identify("jhammond", %{ 59 | "name" => "John Hammond", 60 | "email" => "jhammond@ingen.net", 61 | }) 62 | ``` 63 | 64 | ### Retrieving users 65 | 66 | ```elixir 67 | MyApp.Knock.client() 68 | |> Knock.Users.get_user("jhammond") 69 | ``` 70 | 71 | ### Sending notifies 72 | 73 | ```elixir 74 | MyApp.Knock.client() 75 | |> Knock.Workflows.trigger("dinosaurs-loose", %{ 76 | # user id of who performed the action 77 | "actor" => "dnedry", 78 | # list of user ids for who should receive the notif 79 | "recipients" => ["jhammond", "agrant", "imalcolm", "esattler"], 80 | # an optional cancellation key 81 | "cancellation_key" => alert.id, 82 | # an optional tenant 83 | "tenant" => "jurassic-park", 84 | # data payload to send through 85 | "data" => %{ 86 | "type" => "trex", 87 | "priority" => 1, 88 | }, 89 | }) 90 | ``` 91 | 92 | ### User preferences 93 | 94 | ```elixir 95 | client = MyApp.Knock.client() 96 | 97 | # Set preference set for user 98 | Knock.Users.set_preferences(client, "jhammond", %{channel_types: %{email: true}}) 99 | 100 | # Set granular channel type preferences 101 | Knock.Users.set_channel_type_preferences(client, "jhammond", :email, true) 102 | 103 | # Set granular workflow preferences 104 | Knock.Users.set_workflow_preferences(client, "jhammond", "dinosaurs-loose", %{ 105 | channel_types: %{email: true} 106 | }) 107 | 108 | # Retrieve preferences 109 | Knock.Users.get_preferences(client, "jhammond") 110 | ``` 111 | 112 | ### Getting and setting channel data 113 | 114 | ```elixir 115 | client = MyApp.Knock.client() 116 | 117 | # Set channel data for an APNS 118 | Knock.Users.set_channel_data(client, "jhammond", KNOCK_APNS_CHANNEL_ID, %{ 119 | tokens: [apns_token], 120 | }) 121 | 122 | # Get channel data for the APNS channel 123 | Knock.Users.get_channel_data(client, "jhammond", KNOCK_APNS_CHANNEL_ID) 124 | ``` 125 | 126 | ### Canceling notifies 127 | 128 | ```elixir 129 | MyApp.Knock.client() 130 | |> Knock.Workflows.cancel("dinosaurs-loose", alert.id, %{ 131 | # optional list of user ids for who should have their notify canceled 132 | "recipients" => ["jhammond", "agrant", "imalcolm", "esattler"], 133 | }) 134 | ``` 135 | 136 | ### Signing JWTs 137 | 138 | You can use the excellent `joken` package to [sign JWTs easily](https://hexdocs.pm/joken/assymetric_cryptography_signers.html#using-asymmetric-algorithms). 139 | You will need to generate an environment specific signing key, which you can find in the Knock dashboard. 140 | 141 | If you're using a signing token you will need to pass this to your client to perform authentication. 142 | You can read more about [clientside authentication here](https://docs.knock.app/client-integration/authenticating-users). 143 | 144 | ```elixir 145 | priv = System.get_env("KNOCK_SIGNING_KEY") 146 | now = DateTime.utc_now() 147 | 148 | claims = %{ 149 | # The user id to sign this key for 150 | "sub" => user_id, 151 | # When the token was issued 152 | "iat" => DateTime.to_unix(now), 153 | # When the token expires (1 hour) 154 | "exp" => DateTime.add(now, 3600, :second) |> DateTime.to_unix() 155 | } 156 | 157 | 158 | signer = Joken.Signer.create("RS256", %{"pem" => priv}) 159 | {:ok, token, _} = Joken.generate_and_sign(%{}, claims, signer) 160 | ``` 161 | -------------------------------------------------------------------------------- /lib/knock.ex: -------------------------------------------------------------------------------- 1 | defmodule Knock do 2 | @moduledoc """ 3 | Official SDK for interacting with Knock. 4 | 5 | ## Example usage 6 | 7 | ### As a module 8 | 9 | The recommended way to configure Knock is as a module in your application. Doing so will 10 | allow you to customize the options via configuration in your app. 11 | 12 | ```elixir 13 | # lib/my_app/knock.ex 14 | 15 | defmodule MyApp.Knock do 16 | use Knock, otp_app: :my_app 17 | end 18 | 19 | # config/runtime.exs 20 | 21 | config :my_app, MyApp.KnockClient, 22 | api_key: System.get_env("KNOCK_API_KEY") 23 | ``` 24 | 25 | In your application you can now execute commands on your configured Knock instance. 26 | 27 | ```elixir 28 | client = MyApp.Knock.client() 29 | {:ok, user} = Knock.Users.get_user(client, "user_1") 30 | ``` 31 | 32 | ### Invoking directly 33 | 34 | Optionally you can forgo implementing your own Knock module and create client instances 35 | manually: 36 | 37 | ```elixir 38 | client = Knock.Client.new(api_key: "sk_test_12345") 39 | ``` 40 | 41 | ### Customizing options 42 | 43 | Out of the box the client will specify Tesla and Jason as the HTTP adapter and JSON client, 44 | respectively. However, you can customize this at will: 45 | 46 | ```elixir 47 | config :my_app, Knock, 48 | adapter: Tesla.Adapter.Finch, 49 | json_client: JSX 50 | ``` 51 | 52 | You can read more about the availble adapters in the [Tesla documentation](https://hexdocs.pm/tesla/readme.html#adapters) 53 | """ 54 | 55 | defmacro __using__(opts) do 56 | quote do 57 | @app_name Keyword.fetch!(unquote(opts), :otp_app) 58 | @api_key_env_var "KNOCK_API_KEY" 59 | 60 | alias Knock.Client 61 | 62 | @doc """ 63 | Creates a new client, reading the configuration set for this 64 | applicaton and module in the process 65 | """ 66 | def client(overrides \\ []) do 67 | overrides 68 | |> fetch_options() 69 | |> Client.new() 70 | end 71 | 72 | defp fetch_options(overrides) do 73 | Application.get_env(@app_name, __MODULE__, []) 74 | |> maybe_resolve_api_key() 75 | |> Keyword.merge(overrides) 76 | end 77 | 78 | defp maybe_resolve_api_key(opts) do 79 | case Keyword.get(opts, :api_key) do 80 | api_key when is_binary(api_key) -> opts 81 | {:system, var_name} -> Keyword.put(opts, :api_key, System.get_env(var_name)) 82 | _ -> Keyword.put(opts, :api_key, System.get_env(@api_key_env_var)) 83 | end 84 | end 85 | end 86 | end 87 | 88 | @doc """ 89 | Issues a notify call, triggering a workflow with the given key. 90 | """ 91 | defdelegate notify(client, key, properties, options \\ []), to: Knock.Workflows, as: :trigger 92 | end 93 | -------------------------------------------------------------------------------- /lib/knock/api.ex: -------------------------------------------------------------------------------- 1 | defmodule Knock.Api do 2 | @moduledoc """ 3 | Api client for interacting with Knock 4 | """ 5 | 6 | alias Knock.Client 7 | 8 | @lib_version Mix.Project.config()[:version] 9 | 10 | @typedoc """ 11 | Describes a response from calling an API function 12 | """ 13 | @type response :: {:ok, Knock.Response.t()} | {:error, Knock.Response.t()} | {:error, any()} 14 | 15 | @typedoc """ 16 | Defines available options to pass to an API function 17 | """ 18 | @type options :: [Tesla.option() | {:idempotency_key, binary()}] | [] 19 | 20 | @doc """ 21 | Executes a get request against the Knock api. 22 | """ 23 | @spec get(Client.t(), String.t(), options()) :: response() 24 | def get(client, path, opts \\ []) do 25 | client 26 | |> http_client() 27 | |> Tesla.get(path, opts) 28 | |> handle_response() 29 | end 30 | 31 | @doc """ 32 | Executes a put request against the Knock api 33 | """ 34 | @spec put(Client.t(), String.t(), map(), options()) :: response() 35 | def put(client, path, body, opts \\ []) do 36 | client 37 | |> http_client(opts) 38 | |> Tesla.put(path, body, opts) 39 | |> handle_response() 40 | end 41 | 42 | @doc """ 43 | Executes a post request against the Knock api. 44 | """ 45 | @spec post(Client.t(), String.t(), map(), options()) :: response() 46 | def post(client, path, body, opts \\ []) do 47 | client 48 | |> http_client(opts) 49 | |> Tesla.post(path, body, opts) 50 | |> handle_response() 51 | end 52 | 53 | @doc """ 54 | Executes a delete request against the Knock api. 55 | """ 56 | @spec delete(Client.t(), String.t(), options()) :: response() 57 | def delete(client, path, opts \\ []) do 58 | client 59 | |> http_client() 60 | |> Tesla.delete(path, opts) 61 | |> handle_response() 62 | end 63 | 64 | defp handle_response({:ok, %Tesla.Env{status: status} = env}) 65 | when status >= 200 and status < 300 do 66 | {:ok, %Knock.Response{status: status, headers: env.headers, body: env.body, url: env.url}} 67 | end 68 | 69 | defp handle_response({:ok, %Tesla.Env{status: status} = env}) do 70 | {:error, %Knock.Response{status: status, headers: env.headers, body: env.body, url: env.url}} 71 | end 72 | 73 | defp handle_response(result), do: result 74 | 75 | @doc """ 76 | Returns the current version for the library 77 | """ 78 | def library_version, do: @lib_version 79 | 80 | defp http_client(config, opts \\ []) do 81 | middleware = [ 82 | {Tesla.Middleware.BaseUrl, config.host <> "/v1"}, 83 | {Tesla.Middleware.JSON, engine: config.json_client}, 84 | {Tesla.Middleware.Headers, 85 | [ 86 | {"Authorization", "Bearer " <> config.api_key}, 87 | {"User-Agent", "knocklabs/knock-elixir@#{library_version()}"} 88 | ] ++ maybe_idempotency_key_header(Map.new(opts))} 89 | ] 90 | 91 | Tesla.client(middleware, config.adapter) 92 | end 93 | 94 | defp maybe_idempotency_key_header(%{idempotency_key: key}) when not is_nil(key), 95 | do: [{"Idempotency-Key", to_string(key)}] 96 | 97 | defp maybe_idempotency_key_header(_), do: [] 98 | end 99 | -------------------------------------------------------------------------------- /lib/knock/client.ex: -------------------------------------------------------------------------------- 1 | defmodule Knock.Client do 2 | @moduledoc """ 3 | Sets up a configurable client that can interface with the Knock API. Expects at least 4 | an API key to be provided. 5 | 6 | ### Example usage 7 | ```elixir 8 | # Setup a client instance directly 9 | client = Knock.Client.new(api_key: "sk_test_12345") 10 | ``` 11 | """ 12 | 13 | @enforce_keys [:api_key] 14 | defstruct host: "https://api.knock.app", 15 | api_key: nil, 16 | adapter: Tesla.Adapter.Hackney, 17 | json_client: Jason 18 | 19 | @typedoc """ 20 | Describes a Knock client 21 | """ 22 | @type t :: %__MODULE__{ 23 | host: String.t(), 24 | api_key: String.t(), 25 | adapter: atom(), 26 | json_client: atom() 27 | } 28 | 29 | @doc """ 30 | Creates a new client struct with the provided options. The options provided must at least 31 | contain an API secret key, which can be obtained in the Knock dashboard. 32 | """ 33 | @spec new(Keyword.t()) :: t() 34 | def new(opts) do 35 | unless Keyword.get(opts, :api_key) do 36 | raise Knock.ApiKeyMissingError 37 | end 38 | 39 | opts = Keyword.take(opts, [:host, :api_key, :adapter, :json_client]) 40 | struct!(__MODULE__, opts) 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/knock/errors/api_key_missing.ex: -------------------------------------------------------------------------------- 1 | defmodule Knock.ApiKeyMissingError do 2 | @moduledoc """ 3 | Exception for when a request is made without an API key. 4 | """ 5 | 6 | defexception message: """ 7 | The api_key setting is required to make requests to Knock. 8 | Please configure :api_key in config.exs, set the KNOCK_API_KEY 9 | environment variable, or pass into a new client instance. 10 | """ 11 | end 12 | -------------------------------------------------------------------------------- /lib/knock/resources/bulk_operations.ex: -------------------------------------------------------------------------------- 1 | defmodule Knock.BulkOperations do 2 | @moduledoc """ 3 | Knock resources for accessing Bulk Operations 4 | """ 5 | alias Knock.Api 6 | 7 | @doc """ 8 | Retrieves the current status of the bulk operation 9 | """ 10 | @spec get(Client.t(), String.t()) :: Api.response() 11 | def get(client, bulk_op_id) do 12 | Api.get(client, "/bulk_operations/#{bulk_op_id}") 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/knock/resources/channels.ex: -------------------------------------------------------------------------------- 1 | defmodule Knock.Channels do 2 | @moduledoc """ 3 | Knock resources for accessing channels 4 | """ 5 | 6 | alias Knock.Api 7 | alias Knock.Client 8 | 9 | @doc """ 10 | Bulk updates channel's messages with provided action. 11 | Supports filtering messages to be updated with the following options: 12 | 13 | - tenants: Scope messages to the list of tenant ids 14 | - has_tenant: Scope to where either do or do not have a tenant present 15 | - recipient_ids: Scope messages to the list of recipient ids 16 | - engagement_status: Scope messages by engagements status: read, unread, seen, 17 | unseen, archived, unarchived, interacted, link_clicked 18 | - archived: scopes to a particular type of archival status, one of 19 | exclude, include, only 20 | - delivery_status: scope to only messages by delivery status, these can be the following: 21 | queued, sent, undelivered, delivery_attempted, delivered 22 | - older_than: scope to only messages that were created before provided date 23 | - newer_than: scope to only messages that were created after provided date 24 | """ 25 | @spec bulk_set_messages_status(Client.t(), String.t(), String.t(), map()) :: Api.response() 26 | def bulk_set_messages_status(client, channel_id, action, filtering_options \\ %{}) do 27 | Api.post(client, "/channels/#{channel_id}/messages/bulk/#{action}", filtering_options) 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/knock/resources/messages.ex: -------------------------------------------------------------------------------- 1 | defmodule Knock.Messages do 2 | @moduledoc """ 3 | Knock resources for accessing messages 4 | """ 5 | import Knock.ResourceHelpers, only: [maybe_json_encode_param: 2] 6 | 7 | alias Knock.Api 8 | alias Knock.Client 9 | 10 | @doc """ 11 | Returns paginated messages for the provided environment 12 | 13 | # Available optional parameters: 14 | # 15 | # - page_size: specify size of the page to be returned by the api. (max limit: 50) 16 | # - after: after cursor for pagination 17 | # - before: before cursor for pagination 18 | # - status: list of statuses to filter messages with 19 | # - tenant: tenant_id to filter messages with 20 | # - channel_id: channel_id to filter messages with 21 | # - source: workflow key to filter messages with 22 | # - trigger_data: trigger payload to filter messages with 23 | 24 | """ 25 | @spec list(Client.t(), Keyword.t()) :: Api.response() 26 | def list(client, options \\ []) do 27 | options = maybe_json_encode_param(options, :trigger_data) 28 | 29 | Api.get(client, "/messages", query: options) 30 | end 31 | 32 | @doc """ 33 | Returns information about the message. 34 | """ 35 | @spec get(Client.t(), String.t()) :: Api.response() 36 | def get(client, message_id) do 37 | Api.get(client, "/messages/#{message_id}") 38 | end 39 | 40 | @doc """ 41 | Sets status of message: seen, read, archived 42 | """ 43 | @spec set_status(Client.t(), String.t(), String.t()) :: Api.response() 44 | def set_status(client, message_id, status) do 45 | Api.put(client, "/messages/#{message_id}/#{status}", %{}) 46 | end 47 | 48 | @doc """ 49 | Unsets status of message: unseen, unread, unarchived 50 | """ 51 | @spec unset_status(Client.t(), String.t(), String.t()) :: Api.response() 52 | def unset_status(client, message_id, status) do 53 | Api.delete(client, "/messages/#{message_id}/#{status}") 54 | end 55 | 56 | @doc """ 57 | Batch update messages statuses: seen, read, interacted, archived, unseen, unread, 58 | unarchived 59 | """ 60 | @spec batch_set_status(Client.t(), [String.t()], String.t()) :: Api.response() 61 | def batch_set_status(client, message_ids, status) do 62 | Api.post(client, "/messages/batch/#{status}", %{message_ids: message_ids}) 63 | end 64 | 65 | @doc """ 66 | Returns information about the message content. 67 | """ 68 | @spec get_content(Client.t(), String.t()) :: Api.response() 69 | def get_content(client, message_id) do 70 | Api.get(client, "/messages/#{message_id}/content") 71 | end 72 | 73 | @doc """ 74 | Returns information about the message content in batches. 75 | """ 76 | @spec batch_get_content(Client.t(), [String.t()]) :: Api.response() 77 | def batch_get_content(client, message_ids) do 78 | Api.get(client, "/messages/batch/content", query: [message_ids: message_ids]) 79 | end 80 | 81 | @doc """ 82 | Returns a paginated response with message's activities. 83 | 84 | # Available optional parameters: 85 | # 86 | # - page_size: specify size of the page to be returned by the api. (max limit: 50) 87 | # - after: after cursor for pagination 88 | # - before: before cursor for pagination 89 | # - trigger_data: filter activities by trigger payload data 90 | """ 91 | @spec get_activities(Client.t(), String.t(), Keyword.t()) :: Api.response() 92 | def get_activities(client, message_id, options \\ []) do 93 | options = maybe_json_encode_param(options, :trigger_data) 94 | 95 | Api.get(client, "/messages/#{message_id}/activities", query: options) 96 | end 97 | 98 | @doc """ 99 | Returns a paginated response with message's events. 100 | 101 | # Available optional parameters: 102 | # 103 | # - page_size: specify size of the page to be returned by the api. (max limit: 50) 104 | # - after: after cursor for pagination 105 | # - before: before cursor for pagination 106 | """ 107 | @spec get_events(Client.t(), String.t(), Keyword.t()) :: Api.response() 108 | def get_events(client, message_id, options \\ []) do 109 | Api.get(client, "/messages/#{message_id}/events", query: options) 110 | end 111 | end 112 | -------------------------------------------------------------------------------- /lib/knock/resources/objects.ex: -------------------------------------------------------------------------------- 1 | defmodule Knock.Objects do 2 | @moduledoc """ 3 | Knock resources for accessing Objects 4 | """ 5 | import Knock.ResourceHelpers, only: [maybe_json_encode_param: 2] 6 | 7 | alias Knock.Api 8 | alias Knock.Client 9 | 10 | @typedoc """ 11 | An object reference is how we refer to a particular object in a collection 12 | """ 13 | @type ref :: %{id: :string, collection: :string} 14 | 15 | @doc """ 16 | Returns paginated list of objects for a collection 17 | 18 | # Available optional parameters: 19 | # 20 | # - page_size: specify size of the page to be returned by the api. (max limit: 50) 21 | # - after: after cursor for pagination 22 | # - before: before cursor for pagination 23 | """ 24 | @spec list(Client.t(), String.t(), Keyword.t()) :: Api.response() 25 | def list(client, collection, options \\ []) do 26 | Api.get(client, "/objects/#{collection}", query: options) 27 | end 28 | 29 | @doc """ 30 | Builds an object reference, which can be used in workflow trigger calls. 31 | """ 32 | @spec build_ref(String.t(), String.t()) :: ref() 33 | def build_ref(collection, id), do: %{id: id, collection: collection} 34 | 35 | @doc """ 36 | Upserts the given object in the collection with the attrs provided. 37 | """ 38 | @spec set(Client.t(), String.t(), String.t(), map()) :: Api.response() 39 | def set(client, collection, id, attrs) do 40 | Api.put(client, "/objects/#{collection}/#{id}", attrs) 41 | end 42 | 43 | @doc """ 44 | Gets the given object. 45 | """ 46 | @spec get(Client.t(), String.t(), String.t()) :: Api.response() 47 | def get(client, collection, id) do 48 | Api.get(client, "/objects/#{collection}/#{id}") 49 | end 50 | 51 | @doc """ 52 | Deletes the given object. 53 | """ 54 | @spec delete(Client.t(), String.t(), String.t()) :: Api.response() 55 | def delete(client, collection, id) do 56 | Api.delete(client, "/objects/#{collection}/#{id}") 57 | end 58 | 59 | ## 60 | # Bulk functions 61 | ## 62 | 63 | @doc """ 64 | Bulk upserts one or more objects in a collection. 65 | """ 66 | @spec bulk_set(Client.t(), String.t(), [map()]) :: Api.response() 67 | def bulk_set(client, collection, objects) do 68 | Api.post(client, "/objects/#{collection}/bulk/set", %{objects: objects}) 69 | end 70 | 71 | @doc """ 72 | Bulk deletes one or more objects in a collection. 73 | """ 74 | @spec bulk_delete(Client.t(), String.t(), [String.t()]) :: Api.response() 75 | def bulk_delete(client, collection, object_ids) do 76 | Api.post(client, "/objects/#{collection}/bulk/delete", %{object_ids: object_ids}) 77 | end 78 | 79 | @doc """ 80 | Creates a bulk operation to create subscriptions for a set of recipients to a 81 | set of objects within the given collection. 82 | 83 | Each entry in the provided subscriptions list should have the properties: 84 | 85 | - id: the id of an object for subscribing 86 | - recipients: a list of recipients to subscribe to the object 87 | - properties (optional): a map of properties to apply to each recipient subscription 88 | """ 89 | @spec bulk_add_subscriptions(Client.t(), String.t(), [map()]) :: Api.response() 90 | def bulk_add_subscriptions(client, collection, subscriptions) do 91 | Api.post(client, "/objects/#{collection}/bulk/subscriptions/add", %{ 92 | subscriptions: subscriptions 93 | }) 94 | end 95 | 96 | ## 97 | # Channel data 98 | ## 99 | 100 | @doc """ 101 | Returns channel data for the given channel id. 102 | """ 103 | @spec get_channel_data(Client.t(), String.t(), String.t(), String.t()) :: Api.response() 104 | def get_channel_data(client, collection, id, channel_id) do 105 | Api.get(client, "/objects/#{collection}/#{id}/channel_data/#{channel_id}") 106 | end 107 | 108 | @doc """ 109 | Upserts channel data for the given channel id. 110 | """ 111 | @spec set_channel_data(Client.t(), String.t(), String.t(), String.t(), map()) :: Api.response() 112 | def set_channel_data(client, collection, id, channel_id, channel_data) do 113 | Api.put(client, "/objects/#{collection}/#{id}/channel_data/#{channel_id}", %{ 114 | data: channel_data 115 | }) 116 | end 117 | 118 | @doc """ 119 | Unsets the channel data for the given channel id. 120 | """ 121 | @spec unset_channel_data(Client.t(), String.t(), String.t(), String.t()) :: 122 | Api.response() 123 | def unset_channel_data(client, collection, id, channel_id) do 124 | Api.delete(client, "/objects/#{collection}/#{id}/channel_data/#{channel_id}") 125 | end 126 | 127 | ## 128 | # Messages 129 | ## 130 | 131 | @doc """ 132 | Returns paginated messages for the given object 133 | 134 | # Available optional parameters: 135 | # 136 | # - page_size: specify size of the page to be returned by the api. (max limit: 50) 137 | # - after: after cursor for pagination 138 | # - before: before cursor for pagination 139 | # - status: list of statuses to filter messages with 140 | # - tenant: tenant_id to filter messages with 141 | # - channel_id: channel_id to filter messages with 142 | # - source: workflow key to filter messages with 143 | # - trigger_data: trigger payload to filter messages with 144 | """ 145 | @spec get_messages(Client.t(), String.t(), String.t(), Keyword.t()) :: Api.response() 146 | def get_messages(client, collection, id, options \\ []) do 147 | options = maybe_json_encode_param(options, :trigger_data) 148 | 149 | Api.get(client, "/objects/#{collection}/#{id}/messages", query: options) 150 | end 151 | 152 | ## 153 | # Schedules 154 | ## 155 | 156 | @doc """ 157 | Returns paginated schedules for the given object 158 | 159 | # Available optional parameters: 160 | # 161 | # - page_size: specify size of the page to be returned by the api. (max limit: 50) 162 | # - after: after cursor for pagination 163 | # - before: before cursor for pagination 164 | # - tenant: tenant_id to filter messages with 165 | # - workflow: workflow key to filter messages with 166 | """ 167 | @spec get_schedules(Client.t(), String.t(), String.t(), Keyword.t()) :: Api.response() 168 | def get_schedules(client, collection, id, options \\ []) do 169 | Api.get(client, "/objects/#{collection}/#{id}/schedules", query: options) 170 | end 171 | 172 | ## 173 | # Subscriptions 174 | ## 175 | 176 | @doc """ 177 | Returns paginated subscriptions for the given object 178 | 179 | # Available optional parameters: 180 | # 181 | # - page_size: specify size of the page to be returned by the api. (max limit: 50) 182 | # - after: after cursor for pagination 183 | # - before: before cursor for pagination 184 | # - recipients: list of recipient identifiers to filter subscribers of the object 185 | """ 186 | @spec list_subscriptions(Client.t(), String.t(), String.t(), Keyword.t()) :: Api.response() 187 | def list_subscriptions(client, collection, id, options \\ []) do 188 | Api.get(client, "/objects/#{collection}/#{id}/subscriptions", query: options) 189 | end 190 | 191 | @doc """ 192 | Returns paginated subscriptions for the given object as recipient 193 | 194 | # Available optional parameters: 195 | # 196 | # - page_size: specify size of the page to be returned by the api. (max limit: 50) 197 | # - after: after cursor for pagination 198 | # - before: before cursor for pagination 199 | """ 200 | @spec get_subscriptions(Client.t(), String.t(), String.t(), Keyword.t()) :: Api.response() 201 | def get_subscriptions(client, collection, id, options \\ []) do 202 | options = Keyword.put(options, :mode, "recipient") 203 | Api.get(client, "/objects/#{collection}/#{id}/subscriptions", query: options) 204 | end 205 | 206 | @doc """ 207 | Adds subscriptions for all recipients passed as arguments 208 | 209 | Expected properties: 210 | - recipients: list of recipients to create subscriptions for 211 | - properties: data to be stored at the subscription level for each recipient 212 | """ 213 | @spec add_subscriptions(Client.t(), String.t(), String.t(), map()) :: Api.response() 214 | def add_subscriptions(client, collection, id, params) do 215 | Api.post(client, "/objects/#{collection}/#{id}/subscriptions", params) 216 | end 217 | 218 | @doc """ 219 | Delete subscriptions for recipients passed as arguments 220 | 221 | Expected properties: 222 | - recipients: list of recipients to create subscriptions for 223 | """ 224 | @spec delete_subscriptions( 225 | Client.t(), 226 | String.t(), 227 | String.t(), 228 | %{recipients: [String.t() | map()]} 229 | ) :: Api.response() 230 | def delete_subscriptions(client, collection, id, params) do 231 | recipients = Map.get(params, :recipients) 232 | 233 | Api.delete(client, "/objects/#{collection}/#{id}/subscriptions", 234 | body: %{recipients: recipients} 235 | ) 236 | end 237 | 238 | ## 239 | # Preferences 240 | ## 241 | 242 | @default_preference_set_id "default" 243 | 244 | @doc """ 245 | Returns all of the users preference sets 246 | """ 247 | @spec get_all_preferences(Client.t(), String.t(), String.t()) :: Api.response() 248 | def get_all_preferences(client, collection, id) do 249 | Api.get(client, "/objects/#{collection}/#{id}/preferences") 250 | end 251 | 252 | @doc """ 253 | Returns the preference set for the user. 254 | """ 255 | @spec get_preferences(Client.t(), String.t(), String.t(), Keyword.t()) :: Api.response() 256 | def get_preferences(client, collection, id, options \\ []) do 257 | preference_set_id = Keyword.get(options, :preference_set, @default_preference_set_id) 258 | 259 | Api.get(client, "/objects/#{collection}/#{id}/preferences/#{preference_set_id}") 260 | end 261 | 262 | @doc """ 263 | Sets an entire preference set for the user. Will overwrite any existing data. 264 | """ 265 | @spec set_preferences(Client.t(), String.t(), String.t(), map(), Keyword.t()) :: Api.response() 266 | def set_preferences(client, collection, id, preferences, options \\ []) do 267 | preference_set_id = Keyword.get(options, :preference_set, @default_preference_set_id) 268 | 269 | Api.put(client, "/objects/#{collection}/#{id}/preferences/#{preference_set_id}", preferences) 270 | end 271 | 272 | @doc """ 273 | Sets the channel type preferences for the user. 274 | """ 275 | @spec set_channel_types_preferences(Client.t(), String.t(), String.t(), map(), Keyword.t()) :: 276 | Api.response() 277 | def set_channel_types_preferences(client, collection, id, channel_types, options \\ []) do 278 | preference_set_id = Keyword.get(options, :preference_set, @default_preference_set_id) 279 | 280 | Api.put( 281 | client, 282 | "/objects/#{collection}/#{id}/preferences/#{preference_set_id}/channel_types", 283 | channel_types 284 | ) 285 | end 286 | 287 | @doc """ 288 | Sets the channel type preference for the user. 289 | """ 290 | @spec set_channel_type_preferences( 291 | Client.t(), 292 | String.t(), 293 | String.t(), 294 | String.t(), 295 | boolean(), 296 | Keyword.t() 297 | ) :: 298 | Api.response() 299 | def set_channel_type_preferences(client, collection, id, channel_type, setting, options \\ []) do 300 | preference_set_id = Keyword.get(options, :preference_set, @default_preference_set_id) 301 | 302 | Api.put( 303 | client, 304 | "/objects/#{collection}/#{id}/preferences/#{preference_set_id}/channel_types/#{channel_type}", 305 | %{subscribed: setting} 306 | ) 307 | end 308 | 309 | @doc """ 310 | Sets the workflow preferences for the user. 311 | """ 312 | @spec set_workflows_preferences(Client.t(), String.t(), String.t(), map(), Keyword.t()) :: 313 | Api.response() 314 | def set_workflows_preferences(client, collection, id, workflows, options \\ []) do 315 | preference_set_id = Keyword.get(options, :preference_set, @default_preference_set_id) 316 | 317 | Api.put( 318 | client, 319 | "/objects/#{collection}/#{id}/preferences/#{preference_set_id}/workflows", 320 | workflows 321 | ) 322 | end 323 | 324 | @doc """ 325 | Sets the workflow preference for the user. 326 | """ 327 | @spec set_workflow_preferences( 328 | Client.t(), 329 | String.t(), 330 | String.t(), 331 | String.t(), 332 | map() | boolean(), 333 | Keyword.t() 334 | ) :: Api.response() 335 | def set_workflow_preferences(client, collection, id, workflow_key, setting, options \\ []) do 336 | preference_set_id = Keyword.get(options, :preference_set, @default_preference_set_id) 337 | 338 | Api.put( 339 | client, 340 | "/objects/#{collection}/#{id}/preferences/#{preference_set_id}/workflows/#{workflow_key}", 341 | build_setting_param(setting) 342 | ) 343 | end 344 | 345 | @doc """ 346 | Sets the category preferences for the user. 347 | """ 348 | @spec set_categories_preferences(Client.t(), String.t(), String.t(), map(), Keyword.t()) :: 349 | Api.response() 350 | def set_categories_preferences(client, collection, id, categories, options \\ []) do 351 | preference_set_id = Keyword.get(options, :preference_set, @default_preference_set_id) 352 | 353 | Api.put( 354 | client, 355 | "/objects/#{collection}/#{id}/preferences/#{preference_set_id}/categories", 356 | categories 357 | ) 358 | end 359 | 360 | @doc """ 361 | Sets the category preference for the user. 362 | """ 363 | @spec set_category_preferences( 364 | Client.t(), 365 | String.t(), 366 | String.t(), 367 | String.t(), 368 | map() | boolean(), 369 | Keyword.t() 370 | ) :: Api.response() 371 | def set_category_preferences(client, collection, id, category_key, setting, options \\ []) do 372 | preference_set_id = Keyword.get(options, :preference_set, @default_preference_set_id) 373 | 374 | Api.put( 375 | client, 376 | "/objects/#{collection}/#{id}/preferences/#{preference_set_id}/categories/#{category_key}", 377 | build_setting_param(setting) 378 | ) 379 | end 380 | 381 | defp build_setting_param(setting) when is_map(setting), do: setting 382 | defp build_setting_param(setting), do: %{subscribed: setting} 383 | end 384 | -------------------------------------------------------------------------------- /lib/knock/resources/preferences.ex: -------------------------------------------------------------------------------- 1 | defmodule Knock.Preferences do 2 | @moduledoc """ 3 | Knock resources for accessing preferences. Note: this module and all of the functions here are 4 | deprecated and will be removed in the next version. 5 | """ 6 | alias Knock.Users 7 | 8 | @doc """ 9 | Returns all of the users preference sets 10 | """ 11 | @deprecated "Use Users.get_all_preferences/2 instead" 12 | def get_all(client, user_id) do 13 | Users.get_all_preferences(client, user_id) 14 | end 15 | 16 | @doc """ 17 | Returns the preference set for the user 18 | """ 19 | @deprecated "Use Users.get_preferences/3 instead" 20 | def get(client, user_id, options \\ []) do 21 | Users.get_preferences(client, user_id, options) 22 | end 23 | 24 | @doc """ 25 | Sets the entire preference set for the user 26 | """ 27 | @deprecated "Use Users.set_preferences/4 instead" 28 | def set(client, user_id, preferences, options \\ []) do 29 | Users.set_preferences(client, user_id, preferences, options) 30 | end 31 | 32 | @doc """ 33 | Sets the channel type preferences for the user 34 | """ 35 | @deprecated "Use Users.set_channel_types_preferences/4 instead" 36 | def set_channel_types(client, user_id, channel_types, options \\ []) do 37 | Users.set_channel_types_preferences(client, user_id, channel_types, options) 38 | end 39 | 40 | @doc """ 41 | Sets the channel type preferences for the user 42 | """ 43 | @deprecated "Use Users.set_channel_type_preferences/5 instead" 44 | def set_channel_type(client, user_id, channel_type, setting, options \\ []) do 45 | Users.set_channel_type_preferences(client, user_id, channel_type, setting, options) 46 | end 47 | 48 | @doc """ 49 | Sets the workflow preferences for the user 50 | """ 51 | @deprecated "Use Users.set_workflows_preferences/4 instead" 52 | def set_workflows(client, user_id, workflows, options \\ []) do 53 | Users.set_workflows_preferences(client, user_id, workflows, options) 54 | end 55 | 56 | @doc """ 57 | Sets the workflow preference for the user 58 | """ 59 | @deprecated "Use Users.set_workflow_preferences/5 instead" 60 | def set_workflow(client, user_id, workflow_key, setting, options \\ []) do 61 | Users.set_workflow_preferences(client, user_id, workflow_key, setting, options) 62 | end 63 | 64 | @doc """ 65 | Sets the workflow preferences for the user 66 | """ 67 | @deprecated "Use Users.set_categories_preferences/4 instead" 68 | def set_categories(client, user_id, categories, options \\ []) do 69 | Users.set_categories_preferences(client, user_id, categories, options) 70 | end 71 | 72 | @doc """ 73 | Sets the workflow preference for the user 74 | """ 75 | @deprecated "Use Users.set_category_preferences/5 instead" 76 | def set_category(client, user_id, category_key, setting, options \\ []) do 77 | Users.set_category_preferences(client, user_id, category_key, setting, options) 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /lib/knock/resources/resource_helpers.ex: -------------------------------------------------------------------------------- 1 | defmodule Knock.ResourceHelpers do 2 | @doc """ 3 | Helpers for building resources API requests 4 | """ 5 | 6 | @spec maybe_json_encode_param(Keyword.t(), atom()) :: Keyword.t() 7 | def maybe_json_encode_param(options, param_key) do 8 | case options[param_key] do 9 | param when is_map(param) -> 10 | encoded_param = Jason.encode!(param) 11 | Keyword.put(options, param_key, encoded_param) 12 | 13 | param when is_nil(param) -> 14 | options 15 | 16 | _ -> 17 | raise "Incorrect #{param_key} type, expected map" 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/knock/resources/tenants.ex: -------------------------------------------------------------------------------- 1 | defmodule Knock.Tenants do 2 | @moduledoc """ 3 | Knock resources for accessing Tenants 4 | """ 5 | alias Knock.Api 6 | alias Knock.Client 7 | 8 | @doc """ 9 | Upserts the given tenant with the tenant data provided. 10 | """ 11 | @spec set(Client.t(), String.t(), map()) :: Api.response() 12 | def set(client, id, tenant_data) do 13 | Api.put(client, "/tenants/#{id}", tenant_data) 14 | end 15 | 16 | @doc """ 17 | Gets the given tenant. 18 | """ 19 | @spec get(Client.t(), String.t()) :: Api.response() 20 | def get(client, id) do 21 | Api.get(client, "/tenants/#{id}") 22 | end 23 | 24 | @doc """ 25 | Deletes the given tenant. 26 | """ 27 | @spec delete(Client.t(), String.t()) :: Api.response() 28 | def delete(client, id) do 29 | Api.delete(client, "/tenants/#{id}") 30 | end 31 | 32 | @doc """ 33 | Returns paginated tenants for environment 34 | 35 | # Available optional parameters: 36 | # 37 | # - page_size: specify size of the page to be returned by the api. (max limit: 50) 38 | # - after: after cursor for pagination 39 | # - before: before cursor for pagination 40 | # - tenant_id: id of the tenant to filter for 41 | # - name: name of the tenant to filter for 42 | """ 43 | 44 | @spec list(Client.t(), Keyword.t()) :: Api.response() 45 | def list(client, options \\ []) do 46 | Api.get(client, "/tenants", query: options) 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/knock/resources/users.ex: -------------------------------------------------------------------------------- 1 | defmodule Knock.Users do 2 | @moduledoc """ 3 | Knock resources for accessing users 4 | """ 5 | import Knock.ResourceHelpers, only: [maybe_json_encode_param: 2] 6 | 7 | alias Knock.Api 8 | alias Knock.Client 9 | 10 | @default_preference_set_id "default" 11 | 12 | @doc """ 13 | Returns paginated list of users 14 | 15 | # Available optional parameters: 16 | # 17 | # - page_size: specify size of the page to be returned by the api. (max limit: 50) 18 | # - after: after cursor for pagination 19 | # - before: before cursor for pagination 20 | """ 21 | @spec list(Client.t(), Keyword.t()) :: Api.response() 22 | def list(client, options \\ []) do 23 | Api.get(client, "/users", query: options) 24 | end 25 | 26 | @doc """ 27 | Returns information about the user from the `user_id` given. 28 | """ 29 | @spec get_user(Client.t(), String.t()) :: Api.response() 30 | @deprecated "Use get/2 instead" 31 | def get_user(client, user_id) do 32 | get(client, user_id) 33 | end 34 | 35 | @doc """ 36 | Returns information about the user. 37 | """ 38 | @spec get(Client.t(), String.t()) :: Api.response() 39 | def get(client, user_id) do 40 | Api.get(client, "/users/#{user_id}") 41 | end 42 | 43 | @doc """ 44 | Upserts the user specified via the `user_id` with the given properties. 45 | """ 46 | @spec identify(Client.t(), String.t(), map()) :: Api.response() 47 | def identify(client, user_id, properties) do 48 | Api.put(client, "/users/#{user_id}", properties) 49 | end 50 | 51 | @doc """ 52 | Issues a delete request against the user specified 53 | """ 54 | @spec delete(Client.t(), String.t()) :: Api.response() 55 | def delete(client, user_id) do 56 | Api.delete(client, "/users/#{user_id}") 57 | end 58 | 59 | @doc """ 60 | Returns a feed for the user with the given channel_id. Optionally supports all of the options 61 | for fetching the feed. 62 | 63 | # Available optional parameters: 64 | # 65 | # - page_size: specify size of the page to be returned by the api. (max limit: 50) 66 | # - after: after cursor for pagination 67 | # - before: before cursor for pagination 68 | # - status: list of statuses to filter feed items with 69 | # - tenant: tenant_id to filter messages with 70 | # - has_tenant: optionally scope items by a tenant id or no tenant 71 | # - archived: scope items by a given archived status (defaults to "exclude") 72 | # - trigger_data: trigger payload to filter feed items with 73 | """ 74 | @spec get_feed(Client.t(), String.t(), String.t(), Keyword.t()) :: Api.response() 75 | def get_feed(client, user_id, channel_id, options \\ []) do 76 | options = maybe_json_encode_param(options, :trigger_data) 77 | 78 | Api.get(client, "/users/#{user_id}/feeds/#{channel_id}", query: options) 79 | end 80 | 81 | @doc """ 82 | Merges the user specified with `from_user_id` into the user specified with `user_id`. 83 | """ 84 | @spec merge(Client.t(), String.t(), String.t()) :: Api.response() 85 | def merge(client, user_id, from_user_id) do 86 | Api.post(client, "/users/#{user_id}/merge", %{from_user_id: from_user_id}) 87 | end 88 | 89 | ## 90 | # Bulk actions 91 | ## 92 | 93 | @doc """ 94 | Bulk identifies the list of users given. Can accept a maximum of 100 users at a time. 95 | """ 96 | @spec bulk_identify(Client.t(), [map()]) :: Api.response() 97 | def bulk_identify(client, users) do 98 | Api.post(client, "/users/bulk/identify", %{users: users}) 99 | end 100 | 101 | @doc """ 102 | Bulk deletes the list of users given. Can accept a maximum of 100 users at a time. 103 | """ 104 | @spec bulk_delete(Client.t(), [String.t()]) :: Api.response() 105 | def bulk_delete(client, user_ids) do 106 | Api.post(client, "/users/bulk/delete", %{user_ids: user_ids}) 107 | end 108 | 109 | ## 110 | # Channel data 111 | ## 112 | 113 | @doc """ 114 | Returns user's channel data for the given channel id. 115 | """ 116 | @spec get_channel_data(Client.t(), String.t(), String.t()) :: Api.response() 117 | def get_channel_data(client, user_id, channel_id) do 118 | Api.get(client, "/users/#{user_id}/channel_data/#{channel_id}") 119 | end 120 | 121 | @doc """ 122 | Upserts user's channel data for the given channel id. 123 | """ 124 | @spec set_channel_data(Client.t(), String.t(), String.t(), map()) :: Api.response() 125 | def set_channel_data(client, user_id, channel_id, channel_data) do 126 | Api.put(client, "/users/#{user_id}/channel_data/#{channel_id}", %{data: channel_data}) 127 | end 128 | 129 | @doc """ 130 | Unsets the user's channel data for the given channel id. 131 | """ 132 | @spec unset_channel_data(Client.t(), String.t(), String.t()) :: Api.response() 133 | def unset_channel_data(client, user_id, channel_id) do 134 | Api.delete(client, "/users/#{user_id}/channel_data/#{channel_id}") 135 | end 136 | 137 | ## 138 | # Preferences 139 | ## 140 | 141 | @doc """ 142 | Returns all of the users preference sets 143 | """ 144 | @spec get_all_preferences(Client.t(), String.t()) :: Api.response() 145 | def get_all_preferences(client, user_id) do 146 | Api.get(client, "/users/#{user_id}/preferences") 147 | end 148 | 149 | @doc """ 150 | Returns the preference set for the user. 151 | """ 152 | @spec get_preferences(Client.t(), String.t(), Keyword.t()) :: Api.response() 153 | def get_preferences(client, user_id, options \\ []) do 154 | preference_set_id = Keyword.get(options, :preference_set, @default_preference_set_id) 155 | 156 | Api.get(client, "/users/#{user_id}/preferences/#{preference_set_id}") 157 | end 158 | 159 | @doc """ 160 | Sets an entire preference set for the user. Will overwrite any existing data. 161 | """ 162 | @spec set_preferences(Client.t(), String.t(), map(), Keyword.t()) :: Api.response() 163 | def set_preferences(client, user_id, preferences, options \\ []) do 164 | preference_set_id = Keyword.get(options, :preference_set, @default_preference_set_id) 165 | 166 | Api.put(client, "/users/#{user_id}/preferences/#{preference_set_id}", preferences) 167 | end 168 | 169 | @doc """ 170 | Bulk sets the preferences given for the list of user ids. Will overwrite the any existing 171 | preferences for these users. 172 | """ 173 | @spec bulk_set_preferences(Client.t(), [String.t()], map(), Keyword.t()) :: Api.response() 174 | def bulk_set_preferences(client, user_ids, preferences, options \\ []) do 175 | preference_set_id = Keyword.get(options, :preference_set, @default_preference_set_id) 176 | preferences = Map.put_new(preferences, "id", preference_set_id) 177 | 178 | Api.post(client, "/users/bulk/preferences", %{ 179 | user_ids: user_ids, 180 | preferences: preferences 181 | }) 182 | end 183 | 184 | @doc """ 185 | Sets the channel type preferences for the user. 186 | """ 187 | @spec set_channel_types_preferences(Client.t(), String.t(), map(), Keyword.t()) :: 188 | Api.response() 189 | def set_channel_types_preferences(client, user_id, channel_types, options \\ []) do 190 | preference_set_id = Keyword.get(options, :preference_set, @default_preference_set_id) 191 | 192 | Api.put( 193 | client, 194 | "/users/#{user_id}/preferences/#{preference_set_id}/channel_types", 195 | channel_types 196 | ) 197 | end 198 | 199 | @doc """ 200 | Sets the channel type preference for the user. 201 | """ 202 | @spec set_channel_type_preferences(Client.t(), String.t(), String.t(), boolean(), Keyword.t()) :: 203 | Api.response() 204 | def set_channel_type_preferences(client, user_id, channel_type, setting, options \\ []) do 205 | preference_set_id = Keyword.get(options, :preference_set, @default_preference_set_id) 206 | 207 | Api.put( 208 | client, 209 | "/users/#{user_id}/preferences/#{preference_set_id}/channel_types/#{channel_type}", 210 | %{subscribed: setting} 211 | ) 212 | end 213 | 214 | @doc """ 215 | Sets the workflow preferences for the user. 216 | """ 217 | @spec set_workflows_preferences(Client.t(), String.t(), map(), Keyword.t()) :: Api.response() 218 | def set_workflows_preferences(client, user_id, workflows, options \\ []) do 219 | preference_set_id = Keyword.get(options, :preference_set, @default_preference_set_id) 220 | 221 | Api.put( 222 | client, 223 | "/users/#{user_id}/preferences/#{preference_set_id}/workflows", 224 | workflows 225 | ) 226 | end 227 | 228 | @doc """ 229 | Sets the workflow preference for the user. 230 | """ 231 | @spec set_workflow_preferences( 232 | Client.t(), 233 | String.t(), 234 | String.t(), 235 | map() | boolean(), 236 | Keyword.t() 237 | ) :: Api.response() 238 | def set_workflow_preferences(client, user_id, workflow_key, setting, options \\ []) do 239 | preference_set_id = Keyword.get(options, :preference_set, @default_preference_set_id) 240 | 241 | Api.put( 242 | client, 243 | "/users/#{user_id}/preferences/#{preference_set_id}/workflows/#{workflow_key}", 244 | build_setting_param(setting) 245 | ) 246 | end 247 | 248 | @doc """ 249 | Sets the category preferences for the user. 250 | """ 251 | @spec set_categories_preferences(Client.t(), String.t(), map(), Keyword.t()) :: Api.response() 252 | def set_categories_preferences(client, user_id, categories, options \\ []) do 253 | preference_set_id = Keyword.get(options, :preference_set, @default_preference_set_id) 254 | 255 | Api.put( 256 | client, 257 | "/users/#{user_id}/preferences/#{preference_set_id}/categories", 258 | categories 259 | ) 260 | end 261 | 262 | @doc """ 263 | Sets the category preference for the user. 264 | """ 265 | @spec set_category_preferences( 266 | Client.t(), 267 | String.t(), 268 | String.t(), 269 | map() | boolean(), 270 | Keyword.t() 271 | ) :: Api.response() 272 | def set_category_preferences(client, user_id, category_key, setting, options \\ []) do 273 | preference_set_id = Keyword.get(options, :preference_set, @default_preference_set_id) 274 | 275 | Api.put( 276 | client, 277 | "/users/#{user_id}/preferences/#{preference_set_id}/categories/#{category_key}", 278 | build_setting_param(setting) 279 | ) 280 | end 281 | 282 | ## 283 | # Messages 284 | ## 285 | 286 | @doc """ 287 | Returns paginated messages for the given user 288 | 289 | # Available optional parameters: 290 | # 291 | # - page_size: specify size of the page to be returned by the api. (max limit: 50) 292 | # - after: after cursor for pagination 293 | # - before: before cursor for pagination 294 | # - status: list of statuses to filter messages with 295 | # - tenant: tenant_id to filter messages with 296 | # - channel_id: channel_id to filter messages with 297 | # - source: workflow key to filter messages with 298 | # - trigger_data: trigger payload to filter messages with 299 | """ 300 | @spec get_messages(Client.t(), String.t(), Keyword.t()) :: Api.response() 301 | def get_messages(client, id, options \\ []) do 302 | options = maybe_json_encode_param(options, :trigger_data) 303 | 304 | Api.get(client, "/users/#{id}/messages", query: options) 305 | end 306 | 307 | ## 308 | # Schedules 309 | ## 310 | 311 | @doc """ 312 | Returns paginated schedules for the given user 313 | 314 | # Available optional parameters: 315 | # 316 | # - page_size: specify size of the page to be returned by the api. (max limit: 50) 317 | # - after: after cursor for pagination 318 | # - before: before cursor for pagination 319 | # - tenant: tenant_id to filter messages with 320 | # - workflow: workflow key to filter messages with 321 | """ 322 | @spec get_schedules(Client.t(), String.t(), Keyword.t()) :: Api.response() 323 | def get_schedules(client, id, options \\ []) do 324 | Api.get(client, "/users/#{id}/schedules", query: options) 325 | end 326 | 327 | ## 328 | # Subscriptions 329 | ## 330 | 331 | @doc """ 332 | Returns paginated subscriptions for the given user 333 | 334 | # Available optional parameters: 335 | # 336 | # - page_size: specify size of the page to be returned by the api. (max limit: 50) 337 | # - after: after cursor for pagination 338 | # - before: before cursor for pagination 339 | """ 340 | @spec get_subscriptions(Client.t(), String.t(), Keyword.t()) :: Api.response() 341 | def get_subscriptions(client, id, options \\ []) do 342 | Api.get(client, "/users/#{id}/subscriptions", query: options) 343 | end 344 | 345 | defp build_setting_param(setting) when is_map(setting), do: setting 346 | defp build_setting_param(setting), do: %{subscribed: setting} 347 | end 348 | -------------------------------------------------------------------------------- /lib/knock/resources/workflows.ex: -------------------------------------------------------------------------------- 1 | defmodule Knock.Workflows do 2 | @moduledoc """ 3 | Functions for interacting with Knock notify resources. 4 | """ 5 | alias Knock.Api 6 | 7 | @doc """ 8 | Executes a notify call for the workflow with the given key. 9 | 10 | Note: properties must contain at least `recipents` for the call to be valid. 11 | 12 | Options can include: 13 | * `idempotency_key`: A unique key to prevent duplicate requests 14 | """ 15 | @spec trigger(Knock.Client.t(), String.t(), map(), keyword()) :: Api.response() 16 | def trigger(client, key, properties, options \\ []) do 17 | Api.post(client, "/workflows/#{key}/trigger", properties, options) 18 | end 19 | 20 | @doc """ 21 | Cancels the workflow with the given cancellation key. 22 | 23 | Can optionally be provided with: 24 | 25 | - `recipients`: A list of recipients to cancel the notify for 26 | """ 27 | @spec cancel(Knock.Client.t(), String.t(), String.t(), map()) :: Api.response() 28 | def cancel(client, key, cancellation_key, properties \\ %{}) do 29 | attrs = Map.put(properties, "cancellation_key", cancellation_key) 30 | Api.post(client, "/workflows/#{key}/cancel", attrs) 31 | end 32 | 33 | @doc """ 34 | Creates schedule instances for the specified recipients on the properties map. 35 | 36 | Expected properties: 37 | - recipients: list of recipients for schedules to be created for 38 | - actor: actor to be used when trigger the target workflow 39 | - repeats: repeat rules to specify when the workflow must be triggered 40 | - data: data to be used as variables when the workflow runs 41 | - tenant: tenant to be used for when the workflow runs 42 | """ 43 | @spec create_schedules(Knock.Client.t(), String.t(), map()) :: Api.response() 44 | def create_schedules(client, key, properties \\ %{}) do 45 | attrs = Map.put(properties, :workflow, key) 46 | Api.post(client, "/schedules", attrs) 47 | end 48 | 49 | @doc """ 50 | Updates schedule instances with argument properties. 51 | 52 | Expected properties: 53 | - actor: actor to be used when trigger the target workflow 54 | - repeats: repeat rules to specify when the workflow must be triggered 55 | - data: data to be used as variables when the workflow runs 56 | - tenant: tenant to be used for when the workflow runs 57 | """ 58 | @spec update_schedules(Knock.Client.t(), [String.t()], map()) :: Api.response() 59 | def update_schedules(client, schedule_ids, properties \\ %{}) do 60 | attrs = Map.put(properties, :schedule_ids, schedule_ids) 61 | Api.put(client, "/schedules", attrs) 62 | end 63 | 64 | @doc """ 65 | Returns paginated schedules for the provided environment 66 | 67 | # Available optional parameters: 68 | # 69 | # - page_size: specify size of the page to be returned by the api. (max limit: 50) 70 | # - after: after cursor for pagination 71 | # - before: before cursor for pagination 72 | # - tenant: tenant_id to filter schedues with 73 | # - recipients: list of recipients to filter schedules with 74 | 75 | """ 76 | @spec list_schedules(Client.t(), String.t(), Keyword.t()) :: Api.response() 77 | def list_schedules(client, key, options \\ []) do 78 | options = Keyword.put(options, :workflow, key) 79 | Api.get(client, "/schedules", query: options) 80 | end 81 | 82 | @doc """ 83 | Delete schedule instances. 84 | """ 85 | @spec delete_schedules(Knock.Client.t(), [String.t()]) :: Api.response() 86 | def delete_schedules(client, schedule_ids) do 87 | Api.delete(client, "/schedules", body: %{schedule_ids: schedule_ids}) 88 | end 89 | 90 | @doc """ 91 | Creates schedule instances in bulk. 92 | 93 | Accepts a list of schedules and creates them asynchronously. 94 | The endpoint returns a BulkOperation. 95 | 96 | Each schedule in the list should contain: 97 | - recipient: recipient for the schedule to be created for 98 | - actor: actor to be used when trigger the target workflow 99 | - repeats: repeat rules to specify when the workflow must be triggered 100 | - data: data to be used as variables when the workflow runs 101 | - tenant: tenant to be used for when the workflow runs 102 | - scheduled_at: ISO-8601 formatted date time for when the schedule should start 103 | - ending_at: ISO-8601 formatted date time for when the schedule should end 104 | """ 105 | @spec bulk_create_schedules(Knock.Client.t(), [map()]) :: Api.response() 106 | def bulk_create_schedules(client, schedules) do 107 | Api.post(client, "/schedules/bulk/create", %{schedules: schedules}) 108 | end 109 | end 110 | -------------------------------------------------------------------------------- /lib/knock/response.ex: -------------------------------------------------------------------------------- 1 | defmodule Knock.Response do 2 | @moduledoc """ 3 | Represents a response back from Knock 4 | """ 5 | 6 | @enforce_keys [:body, :headers, :status, :url] 7 | 8 | defstruct [ 9 | :url, 10 | :body, 11 | :headers, 12 | :status 13 | ] 14 | 15 | @type t :: %__MODULE__{ 16 | url: String.t(), 17 | body: map(), 18 | headers: [], 19 | status: pos_integer() 20 | } 21 | end 22 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Knock.MixProject do 2 | use Mix.Project 3 | 4 | @source_url "https://github.com/knocklabs/knock-elixir" 5 | @version "0.4.18" 6 | 7 | def project do 8 | [ 9 | app: :knock, 10 | version: @version, 11 | elixir: "~> 1.10", 12 | start_permanent: Mix.env() == :prod, 13 | description: description(), 14 | package: package(), 15 | name: "Knock", 16 | deps: deps(), 17 | docs: docs(), 18 | source_url: @source_url 19 | ] 20 | end 21 | 22 | # Run "mix help compile.app" to learn about applications. 23 | def application do 24 | [ 25 | extra_applications: [:logger] 26 | ] 27 | end 28 | 29 | # Run "mix help deps" to learn about dependencies. 30 | defp deps do 31 | [ 32 | {:tesla, "~> 1.4"}, 33 | {:hackney, "~> 1.18"}, 34 | {:jason, "~> 1.1"}, 35 | {:ex_doc, "~> 0.14", only: :dev, runtime: false} 36 | ] 37 | end 38 | 39 | defp description do 40 | "Official Elixir SDK for interacting with the Knock API." 41 | end 42 | 43 | defp package do 44 | [ 45 | maintainers: ["Knock Team"], 46 | files: ~w(lib .formatter.exs mix.exs README* LICENSE*), 47 | licenses: ["MIT"], 48 | links: %{"GitHub" => @source_url} 49 | ] 50 | end 51 | 52 | defp docs do 53 | [ 54 | main: "readme", 55 | source_url: @source_url, 56 | source_ref: "v#{@version}", 57 | extras: ["README.md", "LICENSE"] 58 | ] 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "certifi": {:hex, :certifi, "2.14.0", "ed3bef654e69cde5e6c022df8070a579a79e8ba2368a00acf3d75b82d9aceeed", [:rebar3], [], "hexpm", "ea59d87ef89da429b8e905264fdec3419f84f2215bb3d81e07a18aac919026c3"}, 3 | "earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"}, 4 | "ex_doc": {:hex, :ex_doc, "0.37.3", "f7816881a443cd77872b7d6118e8a55f547f49903aef8747dbcb345a75b462f9", [:mix], [{:earmark_parser, "~> 1.4.42", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "e6aebca7156e7c29b5da4daa17f6361205b2ae5f26e5c7d8ca0d3f7e18972233"}, 5 | "hackney": {:hex, :hackney, "1.23.0", "55cc09077112bcb4a69e54be46ed9bc55537763a96cd4a80a221663a7eafd767", [:rebar3], [{:certifi, "~> 2.14.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "6cd1c04cd15c81e5a493f167b226a15f0938a84fc8f0736ebe4ddcab65c0b44e"}, 6 | "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, 7 | "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, 8 | "joken": {:hex, :joken, "2.3.0", "62a979c46f2c81dcb8ddc9150453b60d3757d1ac393c72bb20fc50a7b0827dc6", [:mix], [{:jose, "~> 1.10", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "57b263a79c0ec5d536ac02d569c01e6b4de91bd1cb825625fe90eab4feb7bc1e"}, 9 | "jose": {:hex, :jose, "1.11.1", "59da64010c69aad6cde2f5b9248b896b84472e99bd18f246085b7b9fe435dcdb", [:mix, :rebar3], [], "hexpm", "078f6c9fb3cd2f4cfafc972c814261a7d1e8d2b3685c0a76eb87e158efff1ac5"}, 10 | "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, 11 | "makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"}, 12 | "makeup_erlang": {:hex, :makeup_erlang, "1.0.2", "03e1804074b3aa64d5fad7aa64601ed0fb395337b982d9bcf04029d68d51b6a7", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "af33ff7ef368d5893e4a267933e7744e46ce3cf1f61e2dccf53a111ed3aa3727"}, 13 | "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, 14 | "mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"}, 15 | "mimerl": {:hex, :mimerl, "1.3.0", "d0cd9fc04b9061f82490f6581e0128379830e78535e017f7780f37fea7545726", [:rebar3], [], "hexpm", "a1e15a50d1887217de95f0b9b0793e32853f7c258a5cd227650889b38839fe9d"}, 16 | "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"}, 17 | "parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"}, 18 | "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"}, 19 | "tesla": {:hex, :tesla, "1.14.1", "71c5b031b4e089c0fbfb2b362e24b4478465773ae4ef569760a8c2899ad1e73c", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:finch, "~> 0.13", [hex: :finch, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, ">= 1.0.0", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.21", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.2", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:mox, "~> 1.0", [hex: :mox, repo: "hexpm", optional: true]}, {:msgpax, "~> 2.3", [hex: :msgpax, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "c1dde8140a49a3bef5bb622356e77ac5a24ad0c8091f12c3b7fc1077ce797155"}, 20 | "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, 21 | } 22 | -------------------------------------------------------------------------------- /test/knock_test.exs: -------------------------------------------------------------------------------- 1 | defmodule KnockTest do 2 | use ExUnit.Case 3 | doctest Knock 4 | 5 | defmodule TestClient do 6 | use Knock, otp_app: :knock 7 | end 8 | 9 | describe "using a module to create a client" do 10 | test "it can have configuration set along with the defaults" do 11 | knock = TestClient.client(api_key: "sk_test_12345") 12 | 13 | assert knock.api_key == "sk_test_12345" 14 | assert knock.adapter == Tesla.Adapter.Hackney 15 | assert knock.json_client == Jason 16 | assert knock.host == "https://api.knock.app" 17 | end 18 | 19 | test "it will default to reading the api key from env vars" do 20 | System.put_env("KNOCK_API_KEY", "sk_test_12345") 21 | 22 | knock = TestClient.client() 23 | 24 | assert knock.api_key == "sk_test_12345" 25 | end 26 | 27 | test "it can read from application config" do 28 | Application.put_env(:knock, KnockTest.TestClient, 29 | api_key: "sk_test_12345", 30 | foo: "bar" 31 | ) 32 | 33 | knock = TestClient.client() 34 | 35 | assert knock.api_key == "sk_test_12345" 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | --------------------------------------------------------------------------------