+18
LICENSE
+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
+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
+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
+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
}