defmodule Pushest do @moduledoc ~S""" Pushest is a Pusher library leveraging Elixir/OTP to combine server and client-side Pusher features. Abstracts un/subscription, client-side triggers, private/presence channel authorizations. Keeps track of subscribed channels and users presence when subscribed to presence channel. Pushest is meant to be used in your module where you can define callbacks for events you're interested in. A simple implementation in an OTP application would be: ``` # Add necessary pusher configuration to your application config: # simple_client/config/config.exs config :simple_client, SimpleClient, pusher_app_id: System.get_env("PUSHER_APP_ID"), pusher_key: System.get_env("PUSHER_APP_KEY"), pusher_secret: System.get_env("PUSHER_SECRET"), pusher_cluster: System.get_env("PUSHER_CLUSTER"), pusher_encrypted: true # simple_client/simple_client.ex defmodule SimpleClient do use Pushest, otp_app: :simple_client def handle_event({:ok, "public-channel", "some-event"}, frame) do # do something with public frame end def handle_event({:ok, "private-channel", "some-other-event"}, frame) do # do something with private frame end end # Now you can start your application with Pushest as a part of your supervision tree: # simple_client/lib/simple_client/application.ex def start(_type, _args) do children = [ {SimpleClient, []} ] opts = [strategy: :one_for_one, name: Sup.Supervisor] Supervisor.start_link(children, opts) end ``` You can also provide Pusher options directly via start_link/1 (without using OTP app configuration): ``` config = %{ app_id: System.get_env("PUSHER_APP_ID"), key: System.get_env("PUSHER_APP_KEY"), secret: System.get_env("PUSHER_SECRET"), cluster: System.get_env("PUSHER_CLUSTER"), encrypted: true } {:ok, pid} = SimpleClient.start_link(config) ``` Now you can interact with Pusher: ``` SimpleClient.trigger("private-channel", "event", %{message: "via api"}) SimpleClient.channels() # => %{"channels" => %{"public-channel" => %{}}} SimpleClient.subscribe("private-channel") SimpleClient.trigger("private-channel", "event", %{message: "via ws"}) SimpleClient.trigger("private-channel", "event", %{message: "via api"}, force_api: true) # ... ``` """ alias Pushest.Router @doc ~S""" Invoked when the Pusher event occurs (e.g. other client sends a message). """ @callback handle_event({atom, String.t(), String.t()}, term) :: term defmacro __using__(opts) do quote bind_quoted: [opts: opts] do @typedoc ~S""" Options for Pushest to properly communicate with Pusher server. - `:app_id` - Pusher Application ID. - `:key` - Pusher Application key. - `:secret` - Necessary to subscribe to private/presence channels and trigger events. - `:cluster` - Cluster where your Pusher app is configured. - `:encrypted` - When set to true communication with Pusher is fully encrypted. """ @type pusher_opts :: %{ app_id: String.t(), secret: String.t(), key: String.t(), cluster: String.t(), encrypted: boolean } @typedoc ~S""" Optional options for trigger function. - `:force_api` - Always triggers via Pusher REST API endpoint when set to `true` """ @type trigger_opts :: [force_api: boolean] @behaviour Pushest @config Pushest.Supervisor.config(__MODULE__, opts) @doc ~S""" Starts a Pushest Supervisor process linked to current process. Can be started as a part of host application supervision tree. Pusher options can be passed as an argument or can be provided in an OTP application config. For available pusher_opts values see `t:pusher_opts/0`. """ @spec start_link(pusher_opts) :: {:ok, pid} | {:error, term} def start_link(pusher_opts) when is_map(pusher_opts) do Pushest.Supervisor.start_link(pusher_opts, __MODULE__) end def start_link(_) do Pushest.Supervisor.start_link(@config, __MODULE__) end def child_spec(opts) do %{ id: __MODULE__, start: {__MODULE__, :start_link, [opts]}, type: :supervisor } end @doc ~S""" Subscribe to a channel with user_data as a map. When subscribing to a presence- channel user_id key with unique identifier as a value has to be provided in the user_data map. user_info key can contain a map with optional informations about user. E.g.: %{user_id: "1", user_info: %{name: "Tomas Koutsky"}} """ @spec subscribe(String.t(), map) :: term def subscribe(channel, user_data) do Router.cast({:subscribe, channel, user_data}) end @doc ~S""" Subscribe to a channel without any user data, like any public channel. """ @spec subscribe(String.t()) :: term def subscribe(channel) do Router.cast({:subscribe, channel, %{}}) end @doc ~S""" Trigger on given channel/event combination - sends given data to Pusher. data has to be a map. """ @spec trigger(String.t(), String.t(), map) :: term def trigger(channel, event, data) do Router.cast({:trigger, channel, event, data}) end @doc ~S""" Same as trigger/3 but adds a possiblity to enforce triggering via API endpoint. For enforced API trigger provide `force_api: true` as an `opts`. E.g.: `Mod.trigger("channel", "event", %{message: "m"}, force_api: true)` For trigger_opts values see `t:trigger_opts/0`. """ @spec trigger(String.t(), String.t(), map, trigger_opts) :: term def trigger(channel, event, data, opts) do Router.cast({:trigger, channel, event, data}, opts) end @doc ~S""" Returns all the channels anyone is using, calls Pusher via REST API. """ def channels do Router.call(:channels) end @doc ~S""" Returns only the channels this client is subscribed to. """ def subscribed_channels do Router.call(:subscribed_channels) end @doc ~S""" Returns information about all the users subscribed to a presence channels this client is subscribed to. """ def presence do Router.call(:presence) end @doc ~S""" Unsubscribes from a channel """ def unsubscribe(channel) do Router.cast({:unsubscribe, channel}) end @doc ~S""" Function meant to be overwritten in user module, e.g.: ``` defmodule MyMod do use Pushest, otp_app: :my_mod handle_event({:ok, "my-channel, "my-event"}, frame) do # Do something with a frame here. end end ``` """ def handle_event({status, channel, event}, frame) do require Logger Logger.error( "No #{inspect(status)} handle_event/2 clause in #{__MODULE__} provided for #{ inspect(event) }" ) end defoverridable handle_event: 2 end end end