diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..ac053b5 --- /dev/null +++ b/LICENSE @@ -0,0 +1,18 @@ +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. diff --git a/README.md b/README.md index ad4b8ee..b660b1a 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,111 @@ # 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) diff --git a/mix.exs b/mix.exs index 7b21d68..153b0b7 100644 --- a/mix.exs +++ b/mix.exs @@ -1,13 +1,20 @@ 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 @@ -26,8 +33,29 @@ defmodule Drinkup.MixProject do {: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 diff --git a/mix.lock b/mix.lock index dad7d12..79d6db9 100644 --- a/mix.lock +++ b/mix.lock @@ -5,9 +5,15 @@ "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"}, }