Elixir ATProtocol firehose & subscription listener

docs: add README and LICENSE

ovyerus.com e99f36f5 905d880b

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