···11# Used by "mix format"
22[
33- inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"],
33+ inputs: ["{mix,.formatter}.exs", "{config,examples,lib,test}/**/*.{ex,exs}"],
44 import_deps: [:typedstruct]
55]
+8
README.md
···2233Drinkup is an ELixir library for listening to events from an ATProtocol
44firehose.
55+66+## Roadmap
77+88+- Support for different subscriptions other than
99+ `com.atproto.sync.subscribeRepo'
1010+- Support for multiple instances at once, each with unique consumers (for
1111+ listening to multiple subscriptions at once)
1212+- Tests
+33
examples/record_consumer.ex
···11+defmodule ExampleRecordConsumer do
22+ use Drinkup.RecordConsumer, collections: [~r/app\.bsky\.graph\..+/, "app.bsky.feed.post"]
33+44+ def handle_create(record) do
55+ IO.inspect(record, label: "create")
66+ end
77+88+ def handle_update(record) do
99+ IO.inspect(record, label: "update")
1010+ end
1111+1212+ def handle_delete(record) do
1313+ IO.inspect(record, label: "delete")
1414+ end
1515+end
1616+1717+defmodule ExampleSupervisor do
1818+ use Supervisor
1919+2020+ def start_link(args \\ []) do
2121+ Supervisor.start_link(__MODULE__, arg, name: __MODULE__)
2222+ end
2323+2424+ @immpl true
2525+ def init(_arg) do
2626+ children = [
2727+ Drinkup,
2828+ ExampleRecordConsumer
2929+ ]
3030+3131+ Supervisor.init(children, strategy: :one_for_one)
3232+ end
3333+end
+85
lib/record_consumer.ex
···11+defmodule Drinkup.RecordConsumer do
22+ @moduledoc """
33+ An opinionated consumer of the Firehose that eats consumers
44+ """
55+66+ @callback handle_create(any()) :: any()
77+ @callback handle_update(any()) :: any()
88+ @callback handle_delete(any()) :: any()
99+1010+ defmacro __using__(opts) do
1111+ {collections, _opts} = Keyword.pop(opts, :collections, [])
1212+1313+ quote location: :keep do
1414+ use Drinkup.Consumer
1515+ @behaviour Drinkup.RecordConsumer
1616+1717+ def handle_event(%Drinkup.Event.Commit{} = event) do
1818+ event.ops
1919+ |> Enum.filter(fn %{path: path} ->
2020+ path |> String.split("/") |> Enum.at(0) |> matches_collections?()
2121+ end)
2222+ |> Enum.map(&Drinkup.RecordConsumer.Record.from(&1, event.repo))
2323+ |> Enum.each(&apply(__MODULE__, :"handle_#{&1.action}", [&1]))
2424+ end
2525+2626+ def handle_event(_event), do: :noop
2727+2828+ unquote(
2929+ if collections == [] do
3030+ quote do
3131+ def matches_collections?(_type), do: true
3232+ end
3333+ else
3434+ quote do
3535+ def matches_collections?(nil), do: false
3636+3737+ def matches_collections?(type) when is_binary(type),
3838+ do:
3939+ Enum.any?(unquote(collections), fn
4040+ matcher when is_binary(matcher) -> type == matcher
4141+ matcher -> Regex.match?(matcher, type)
4242+ end)
4343+ end
4444+ end
4545+ )
4646+4747+ @impl true
4848+ def handle_create(_record), do: nil
4949+ @impl true
5050+ def handle_update(_record), do: nil
5151+ @impl true
5252+ def handle_delete(_record), do: nil
5353+5454+ defoverridable handle_create: 1, handle_update: 1, handle_delete: 1
5555+ end
5656+ end
5757+5858+ defmodule Record do
5959+ alias Drinkup.Event.Commit.RepoOp
6060+ use TypedStruct
6161+6262+ typedstruct do
6363+ field :type, String.t()
6464+ field :rkey, String.t()
6565+ field :did, String.t()
6666+ field :action, :create | :update | :delete
6767+ field :cid, binary() | nil
6868+ field :record, map() | nil
6969+ end
7070+7171+ @spec from(RepoOp.t(), String.t()) :: t()
7272+ def from(%RepoOp{action: action, path: path, cid: cid, record: record}, did) do
7373+ [type, rkey] = String.split(path, "/")
7474+7575+ %__MODULE__{
7676+ type: type,
7777+ rkey: rkey,
7878+ did: did,
7979+ action: action,
8080+ cid: cid,
8181+ record: record
8282+ }
8383+ end
8484+ end
8585+end