Elixir ATProtocol firehose & subscription listener

docs: add README and LICENSE

ovyerus.com e99f36f5 905d880b

verified
Changed files
+158 -6
+18
LICENSE
···
+
Copyright 2025 comet.sh
+
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
+
this software and associated documentation files (the “Software”), to deal in
+
the Software without restriction, including without limitation the rights to
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+
the Software, and to permit persons to whom the Software is furnished to do so,
+
subject to the following conditions:
+
+
The above copyright notice and this permission notice shall be included in all
+
copies or substantial portions of the Software.
+
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+104 -4
README.md
···
# Drinkup
-
Drinkup is an ELixir library for listening to events from an ATProtocol
-
firehose.
+
An Elixir library for listening to events from an ATProtocol relay
+
(firehose/`com.atproto.sync.subscribeRepos`). Eventually aiming to support any
+
ATProtocol subscription.
-
## Roadmap
+
## TODO
- Support for different subscriptions other than
-
`com.atproto.sync.subscribeRepo'
+
`com.atproto.sync.subscribeRepo'.
+
- Validation (signatures, making sure to only track handle active accounts,
+
etc.) (see
+
[Firehose Validation Best Practices](https://atproto.com/specs/sync#firehose-validation-best-practices))
+
- Look into backfilling? See if there's better ways to do it.
+
- Built-in solutions for tracking resumption? (probably a pluggable solution to
+
allow for different things like Mnesia, Postgres, etc.)
+
- Testing of multi-node/distribution.
- Tests
- Documentation
+
+
## Installation
+
+
Add `drinkup` to your `mix.exs`.
+
+
```elixir
+
def deps do
+
[
+
{:drinkup, "~> 0.1"}
+
]
+
end
+
```
+
+
Documentation can be found on HexDocs at https://hexdocs.pm/drinkup.
+
+
## Example Usage
+
+
First, create a module implementing the `Drinkup.Consumer` behaviour (only
+
requires a `handle_event/1` function):
+
+
```elixir
+
defmodule ExampleConsumer do
+
@behaviour Drinkup.Consumer
+
+
def handle_event(%Drinkup.Event.Commit{} = event) do
+
IO.inspect(event, label: "Got commit event")
+
end
+
+
def handle_event(_), do: :noop
+
end
+
```
+
+
Then add Drinkup and your consumer to your application's supervision tree:
+
+
```elixir
+
defmodule MyApp.Application do
+
use Application
+
+
def start(_type, _args) do
+
children = [{Drinkup, %{consumer: ExampleConsumer}}]
+
Supervisor.start_link(children, strategy: :one_for_one)
+
end
+
end
+
```
+
+
You should then be able to start your application and start seeing
+
`Got commit event: ...` in the terminal.
+
+
### Record Consumer
+
+
One of the main reasons for listening to an ATProto relay is to synchronise a
+
database with records. As a result, Drinkup provides a light extension around a
+
basic consumer, the `RecordConsumer`, which only listens to commit events, and
+
transforms them into a slightly nicer structure to work around, calling your
+
`handle_create/1`, `handle_update/1`, and `handle_delete/1` functions for each
+
record it comes across. It also allows for filtering of specific types of
+
records either by full name or with a
+
[Regex](https://hexdocs.pm/elixir/1.18.4/Regex.html) match.
+
+
```elixir
+
defmodule ExampleRecordConsumer do
+
# Will respond to any events either `app.bsky.feed.post` records, or anything under `app.bsky.graph`.
+
use Drinkup.RecordConsumer, collections: [~r/app\.bsky\.graph\..+/, "app.bsky.feed.post"]
+
alias Drinkup.RecordConsumer.Record
+
+
def handle_create(%Record{type: "app.bsky.feed.post"} = record) do
+
IO.inspect(record, label: "Bluesky post created")
+
end
+
+
def handle_create(%Record{type: "app.bsky.graph" <> _} = record) do
+
IO.inspect(record, label: "Bluesky graph updated")
+
end
+
+
def handle_update(record) do
+
# ...
+
end
+
+
def handle_delete(record) do
+
# ...
+
end
+
end
+
```
+
+
## Special thanks
+
+
The process structure used in Drinkup is heavily inspired by the work done on
+
[Nostrum](https://github.com/Kraigie/nostrum), an incredible Elixir library for
+
Discord.
+
+
## License
+
+
This project is licensed under the [MIT License](./LICENSE)
+30 -2
mix.exs
···
defmodule Drinkup.MixProject do
use Mix.Project
+
@version "0.1.0"
+
@source_url "https://github.com/cometsh/drinkup"
+
def project do
[
app: :drinkup,
-
version: "0.1.0",
+
version: @version,
elixir: "~> 1.18",
start_permanent: Mix.env() == :prod,
-
deps: deps()
+
deps: deps(),
+
name: "Drinkup",
+
description: "ATProtocol firehose & subscription listener",
+
package: package(),
+
docs: docs()
]
end
···
{:cbor, "~> 1.0.0"},
{:certifi, "~> 2.15"},
{:credo, "~> 1.7", only: [:dev, :test], runtime: false},
+
{:ex_doc, "~> 0.34", only: :dev, runtime: false},
{:gun, "~> 2.2"},
{:typedstruct, "~> 0.5"}
+
]
+
end
+
+
defp package do
+
[
+
licenses: ["MIT"],
+
links: %{"GitHub" => @source_url}
+
]
+
end
+
+
defp docs do
+
[
+
extras: [
+
LICENSE: [title: "License"],
+
"README.md": [title: "Overview"]
+
],
+
main: "readme",
+
source_url: @source_url,
+
source_ref: "v#{@version}",
+
formatters: ["html"]
]
end
end
+6
mix.lock
···
"certifi": {:hex, :certifi, "2.15.0", "0e6e882fcdaaa0a5a9f2b3db55b1394dba07e8d6d9bcad08318fb604c6839712", [:rebar3], [], "hexpm", "b147ed22ce71d72eafdad94f055165c1c182f61a2ff49df28bcc71d1d5b94a60"},
"cowlib": {:hex, :cowlib, "2.15.0", "3c97a318a933962d1c12b96ab7c1d728267d2c523c25a5b57b0f93392b6e9e25", [:make, :rebar3], [], "hexpm", "4f00c879a64b4fe7c8fcb42a4281925e9ffdb928820b03c3ad325a617e857532"},
"credo": {:hex, :credo, "1.7.12", "9e3c20463de4b5f3f23721527fcaf16722ec815e70ff6c60b86412c695d426c1", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8493d45c656c5427d9c729235b99d498bd133421f3e0a683e5c1b561471291e5"},
+
"earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"},
+
"ex_doc": {:hex, :ex_doc, "0.38.2", "504d25eef296b4dec3b8e33e810bc8b5344d565998cd83914ffe1b8503737c02", [:mix], [{:earmark_parser, "~> 1.4.44", [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", "732f2d972e42c116a70802f9898c51b54916e542cc50968ac6980512ec90f42b"},
"file_system": {:hex, :file_system, "1.1.0", "08d232062284546c6c34426997dd7ef6ec9f8bbd090eb91780283c9016840e8f", [:mix], [], "hexpm", "bfcf81244f416871f2a2e15c1b515287faa5db9c6bcf290222206d120b3d43f6"},
"gun": {:hex, :gun, "2.2.0", "b8f6b7d417e277d4c2b0dc3c07dfdf892447b087f1cc1caff9c0f556b884e33d", [:make, :rebar3], [{:cowlib, ">= 2.15.0 and < 3.0.0", [hex: :cowlib, repo: "hexpm", optional: false]}], "hexpm", "76022700c64287feb4df93a1795cff6741b83fb37415c40c34c38d2a4645261a"},
"jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"},
+
"makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"},
+
"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"},
+
"makeup_erlang": {:hex, :makeup_erlang, "1.0.2", "03e1804074b3aa64d5fad7aa64601ed0fb395337b982d9bcf04029d68d51b6a7", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "af33ff7ef368d5893e4a267933e7744e46ce3cf1f61e2dccf53a111ed3aa3727"},
+
"nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"},
"typedstruct": {:hex, :typedstruct, "0.5.3", "d68ae424251a41b81a8d0c485328ab48edbd3858f3565bbdac21b43c056fc9b4", [:make, :mix], [], "hexpm", "b53b8186701417c0b2782bf02a2db5524f879b8488f91d1d83b97d84c2943432"},
"varint": {:hex, :varint, "1.5.1", "17160c70d0428c3f8a7585e182468cac10bbf165c2360cf2328aaa39d3fb1795", [:mix], [], "hexpm", "24f3deb61e91cb988056de79d06f01161dd01be5e0acae61d8d936a552f1be73"},
}