tapfall 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 10d16e103fb9bb828f86ba932076d45f3f7d5e89b81eceb3f60292dfc5605aa7
4
+ data.tar.gz: 3087c8bbc196e557f046aaef5bfc48ac49220d2efc835996319b3b74db352aa7
5
+ SHA512:
6
+ metadata.gz: e18feb0d610d2d6e16edcb6e68fd6f8e8a949117e609e519501b4b2299aa0f2b721e7b008f8f4be5f3fdb1239f43ae8a5ce7fa3cc2d6991262fa99917e7b21d2
7
+ data.tar.gz: 91557b3f07d953e622ad190c24c0bf471ece14184e004731f30eaa104a36f78121704ee1e38b0191ec7d3e5d02549f383300002d5bb11efeaed48363c0537b3a
data/CHANGELOG.md ADDED
@@ -0,0 +1,3 @@
1
+ ## [0.0.1] - 2025-12-22
2
+
3
+ - first working version, with streaming from Tap, support for ack and admin password options, and calling two HTTP endpoints
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The zlib License
2
+
3
+ Copyright (c) 2025 Jakub Suder
4
+
5
+ This software is provided 'as-is', without any express or implied
6
+ warranty. In no event will the authors be held liable for any damages
7
+ arising from the use of this software.
8
+
9
+ Permission is granted to anyone to use this software for any purpose,
10
+ including commercial applications, and to alter it and redistribute it
11
+ freely, subject to the following restrictions:
12
+
13
+ 1. The origin of this software must not be misrepresented; you must not
14
+ claim that you wrote the original software. If you use this software
15
+ in a product, an acknowledgment in the product documentation would be
16
+ appreciated but is not required.
17
+
18
+ 2. Altered source versions must be plainly marked as such, and must not be
19
+ misrepresented as being the original software.
20
+
21
+ 3. This notice may not be removed or altered from any source distribution.
data/README.md ADDED
@@ -0,0 +1,209 @@
1
+ # Tapfall
2
+
3
+ A Ruby gem for ingesting ATProto repository data from a [Tap](https://github.com/bluesky-social/indigo/tree/main/cmd/tap) service (extension of the [Skyfall](https://tangled.org/mackuba.eu/skyfall) gem).
4
+
5
+ > [!NOTE]
6
+ > Part of ATProto Ruby SDK: [ruby.sdk.blue](https://ruby.sdk.blue)
7
+
8
+
9
+ ## What does it do
10
+
11
+ [Tap](https://github.com/bluesky-social/indigo/tree/main/cmd/tap) is a [tool made by Bluesky](https://docs.bsky.app/blog/introducing-tap), which combines a firehose client/adapter with a JSON output like [Jetstream](https://github.com/bluesky-social/jetstream) and a repository/PDS crawler and importer. It can be useful if you're building some kind of backend app that needs to import and store some set of records from the Atmosphere. It's meant to be run locally on your app's server, with only your app connecting to it, and it simplifies a lot of code for you if you need to both import & backfill existing records of some kind and also stream any new ones that are added after you start the import.
12
+
13
+ Basically, before Tap, your app code needed to:
14
+
15
+ 1) Connect to a relay or Jetstream
16
+ 2) Filter only records of selected kinds
17
+ 3) Find out which repos on which PDSes have records that are relevant to you
18
+ 4) Connect to those PDSes and get those records via listRecords or getRepo
19
+ 5) Handle possible duplicates between imported repo and the firehose
20
+
21
+ If you only want some kinds of records from some specific repos, that still leaves you with both a firehose client and a repo importer and merging the results from both somehow.
22
+
23
+ With Tap, you only need to:
24
+
25
+ 1) Run Tap, passing it a list of record types to sync in command line parameters or in env
26
+ 2) Connect to the Tap stream on localhost
27
+ 3) Save everything coming from the stream
28
+
29
+ So instead of two ways of importing the records, you only have one and it's the much less involved one. And this library also handles a lot of this for you.
30
+
31
+ **Tapfall** is an extension of [Skyfall](https://tangled.org/mackuba.eu/skyfall), which is a gem for streaming records from a relay/PDS firehose or Jetstream, and it adds support for the event format used by Tap and for some additional HTTP APIs it provides.
32
+
33
+
34
+ ## Installation
35
+
36
+ Add this to your `Gemfile`:
37
+
38
+ gem 'tapfall'
39
+
40
+
41
+ ## Usage
42
+
43
+ Create a `Tapfall::Stream` object, specifying the address of the Tap service websocket:
44
+
45
+ ```rb
46
+ require 'tapfall'
47
+
48
+ tap = Tapfall::Stream.new('ws://localhost:2480')
49
+ ```
50
+
51
+ You can also just pass a hostname, but then it's interpreted as HTTPS/WSS, which might not be what you want.
52
+
53
+ Next, set up event listeners to handle incoming messages and get notified of errors. Here are all the available listeners (you will need at least `on_message`):
54
+
55
+ ```rb
56
+ # this gives you a parsed message object, one of subclasses of Tapfall::TapMessage
57
+ tap.on_message { |msg| p msg }
58
+
59
+ # lifecycle events
60
+ tap.on_connecting { |url| puts "Connecting to #{url}..." }
61
+ tap.on_connect { puts "Connected" }
62
+ tap.on_disconnect { puts "Disconnected" }
63
+ tap.on_reconnect { puts "Connection lost, trying to reconnect..." }
64
+ tap.on_timeout { puts "Connection stalled, triggering a reconnect..." }
65
+
66
+ # handling errors (there's a default error handler that does exactly this)
67
+ tap.on_error { |e| puts "ERROR: #{e}" }
68
+ ```
69
+
70
+ You can also call these as setters accepting a `Proc` – e.g. to disable default error handling, you can do:
71
+
72
+ ```rb
73
+ tap.on_error = nil
74
+ ```
75
+
76
+ When you're ready, open the connection by calling `connect`:
77
+
78
+ ```rb
79
+ tap.connect
80
+ ```
81
+
82
+ The `#connect` method blocks until the connection is explicitly closed with `#disconnect` from an event or interrupt handler. Tapfall & Skyfall use [EventMachine](https://github.com/eventmachine/eventmachine) under the hood, so in order to run some things in parallel, you can use e.g. `EM::PeriodicTimer`.
83
+
84
+ Tapfall also supports Skyfall's `on_raw_message` handler version, but only if you use Tap in "disable acks" mode (see below), which is not recommended beyond testing, unless you're doing the acks yourself. (This is because Tapfall needs to parse the message into a JSON form in order to get the `id` of the event to send the "ack".)
85
+
86
+ > [!NOTE]
87
+ > Unlike standard firehose and Jetstream, Tap streams don't have a cursor that you store and pass when reconnecting. It's meant to be used only by one client, and it tracks internally itself which events have been sent to you and which weren't. You can think about it this way: it's not a public service like Jetstream that you can share with others, it's a microservice that you run as a component of your app.
88
+
89
+
90
+ ### Acks
91
+
92
+ Tap by default runs in a mode where it expects the client to send back an "ack" after receiving and processing each event. When it gets the ack, it marks the event as processed and will not send it again. If you don't send an ack, it tries to retransmit the event after a moment.
93
+
94
+ You can also run it with acks disabled, by passing a `--disable-acks` option or `TAP_DISABLE_ACKS=true` env var, in which case it will assume an event has been processed as soon as it's sent to you. This is not recommended to do in production, since if your process crashes during an event processing loop, that event will be lost (and you can't ask for an earlier cursor because there's no cursor).
95
+
96
+ Tapfall handles the acks for you automatically. If you want it to not send acks, pass an `:ack => false` option to the constructor:
97
+
98
+ ```rb
99
+ tap = Tapfall::Stream.new(server, { ack: false })
100
+ ```
101
+
102
+ ### Password-protected access
103
+
104
+ Tap also lets you set an admin password, which you can set with the `--admin-password` option or `TAP_ADMIN_PASSWORD` env var. This locks the stream and the API behind HTTP Basic auth (with the user `admin`). Pass the password to Tapfall constructor like this:
105
+
106
+ ```rb
107
+ tap = Tapfall::Stream.new(server { admin_password: 'abracadabra' })
108
+ ```
109
+
110
+ ### Processing messages
111
+
112
+ Each message passed to `on_message` is an instance of a subclass of `Tapfall::TapMessage`. The main event type is `Tapfall::RecordMessage`, which includes a record operation; you will also receive `Tapfall::IdentityMessage` events, which provide info about an account change like changed handle or migration to a new PDS. `UnknownMessage` might be sent if new unrecognized message types are sent in the future.
113
+
114
+ All message types share these properties:
115
+
116
+ - `type` (symbol) – the message type identifier, e.g. `:record`
117
+ - `id` (integer), aliased as `seq` – a sequential index of the message
118
+
119
+ The `:record` messages have an `operations` method, which includes an array of add/remove/edit `Operation`s done on some records. Currently Tap event format only includes one single record operation in each event, but it's returned as an array here for symmetry with the `Skyfall::Firehose` stream version.
120
+
121
+ An `Operation` has such fields (also matching the API of `Skyfall::Firehose::Operation` and `Skyfall::Jetstream::Operation`):
122
+
123
+ - `repo` or `did` (string) – DID of the repository (user account)
124
+ - `collection` (string) – name of the collection / record type, e.g. `app.bsky.feed.post` for posts
125
+ - `type` (symbol) – short name of the collection, e.g. `:bsky_post`
126
+ - `rkey` (string) – identifier of a record in a collection
127
+ - `path` (string) – the path part of the at:// URI – collection name + ID (rkey) of the item
128
+ - `uri` (string) – the complete at:// URI
129
+ - `action` (symbol) – `:create`, `:update` or `:delete`
130
+ - `cid` (CID) – CID of the operation/record (`nil` for delete operations)
131
+ - `live?` (boolean) – true if the record was received from the firehose, false if it was backfilled from the repo
132
+
133
+ Create and update operations will also have an attached record (JSON object) with details of the post, like etc. The record data is currently available as a Ruby hash via `raw_record` property (custom types may be added in future).
134
+
135
+ So for example, in order to filter only "create post" operations and print their details, you can do something like this:
136
+
137
+ ```rb
138
+ tap.on_message do |m|
139
+ next if m.type != :record
140
+
141
+ m.operations.each do |op|
142
+ next unless op.action == :create && op.type == :bsky_post
143
+
144
+ puts "#{op.repo}:"
145
+ puts op.raw_record['text']
146
+ puts
147
+ end
148
+ end
149
+ ```
150
+
151
+
152
+ ### Note on custom lexicons
153
+
154
+ Note that the `Operation` objects have two properties that tell you the kind of record they're about: `#collection`, which is a string containing the official name of the collection/lexicon, e.g. `"app.bsky.feed.post"`; and `#type`, which is a symbol meant to save you some typing, e.g. `:bsky_post`.
155
+
156
+ When Tapfall receives a message about a record type that's not on the list, whether in the `app.bsky` namespace or not, the operation `type` will be `:unknown`, while the `collection` will be the original string. So if an app like e.g. "Skygram" appears with a `zz.skygram.*` namespace that lets you share photos on ATProto, the operations will have a type `:unknown` and collection names like `zz.skygram.feed.photo`, and you can check the `collection` field for record types known to you and process them in some appropriate way, even if Tapfall doesn't recognize the record type.
157
+
158
+ Do not however check if such operations have a `type` equal to `:unknown` first – just ignore the type and only check the `collection` string. The reason is that some next version might start recognizing those records and add a new `type` value for them like e.g. `:skygram_photo`, and then they won't match your condition anymore.
159
+
160
+
161
+ ### Reconnection logic
162
+
163
+ See the section in the [Skyfall readme](https://tangled.org/mackuba.eu/skyfall#reconnection-logic) about the options for handling reconnecting to a flaky firehose – but since in this case the Tap service will run under your control, likely on the same machine, this might not be as useful in practice.
164
+
165
+
166
+ ## HTTP API
167
+
168
+ Apart from the `/channel` websocket endpoint, Tap also has [a few other endpoints](https://github.com/bluesky-social/indigo/tree/main/cmd/tap#http-api) for adding/removing repos and checking various stats.
169
+
170
+ You can call all the below methods either on the `Tapfall::Stream` instance you use for connecting to the websocket, or on a separate `Tapfall::API` object if you prefer.
171
+
172
+ Currently implemented endpoints:
173
+
174
+ ### /repos/add
175
+
176
+ Tap can work in three possible ways regarding the subset of repos it tracks:
177
+
178
+ 1) `TAP_FULL_NETWORK`, when it tracks *all repos everywhere*
179
+ 2) `TAP_SIGNAL_COLLECTION`, when it finds and tracks all repos that have some specific types of records you're interested in
180
+ 3) default mode, when it only tracks repos you've added manually
181
+
182
+ In that third mode, use this to add repos to the tracking list:
183
+
184
+ ```rb
185
+ @tap.add_repo('did:plc:uh4errluyq5thgszrwwrtpuq')
186
+
187
+ # or:
188
+ @tap.add_repos(did_list)
189
+ ```
190
+
191
+ ### /repos/remove
192
+
193
+ To remove repos from the list, use:
194
+
195
+ ```rb
196
+ @tap.remove_repo('did:plc:uh4errluyq5thgszrwwrtpuq')
197
+
198
+ # or:
199
+ @tap.remove_repos(did_list)
200
+ ```
201
+
202
+
203
+ ## Credits
204
+
205
+ Copyright © 2025 Kuba Suder ([@mackuba.eu](https://bsky.app/profile/did:plc:oio4hkxaop4ao4wz2pp3f4cr)).
206
+
207
+ The code is available under the terms of the [zlib license](https://choosealicense.com/licenses/zlib/) (permissive, similar to MIT).
208
+
209
+ Bug reports and pull requests are welcome 😎
@@ -0,0 +1,78 @@
1
+ require 'json'
2
+ require 'net/http'
3
+ require 'uri'
4
+
5
+ module Tapfall
6
+ class API
7
+ def initialize(server, options = {})
8
+ @root_url = build_root_url(server)
9
+ @options = options
10
+ end
11
+
12
+ def add_repo(did)
13
+ add_repos([did])
14
+ end
15
+
16
+ def add_repos(dids)
17
+ post_request('/repos/add', { dids: dids })
18
+ end
19
+
20
+ def remove_repo(did)
21
+ remove_repos([did])
22
+ end
23
+
24
+ def remove_repos(dids)
25
+ post_request('/repos/remove', { dids: dids })
26
+ end
27
+
28
+ private
29
+
30
+ def build_root_url(server)
31
+ if !server.is_a?(String)
32
+ raise ArgumentError, "Server parameter should be a string"
33
+ end
34
+
35
+ if server.include?('://')
36
+ uri = URI(server)
37
+
38
+ if uri.scheme != 'http' && uri.scheme != 'https'
39
+ raise ArgumentError, "Server parameter should be a hostname or a http:// or https:// URL"
40
+ elsif uri.path != ''
41
+ raise ArgumentError, "Server URL should only include a hostname, without path"
42
+ end
43
+
44
+ uri.to_s
45
+ else
46
+ server = "https://#{server}"
47
+ uri = URI(server) # raises if invalid
48
+ server
49
+ end
50
+ end
51
+
52
+ def post_request(path, json_data)
53
+ uri = URI(@root_url + path)
54
+
55
+ request = Net::HTTP::Post.new(uri)
56
+ request.body = JSON.generate(json_data)
57
+ request.content_type = "application/json"
58
+
59
+ if @options[:admin_password]
60
+ request.basic_auth('admin', @options[:admin_password])
61
+ end
62
+
63
+ response = Net::HTTP.start(uri.hostname, uri.port, :use_ssl => (uri.scheme == 'https')) do |http|
64
+ http.request(request)
65
+ end
66
+
67
+ status = response.code.to_i
68
+ message = response.message
69
+ response_body = (response.content_type == 'application/json') ? JSON.parse(response.body) : response.body
70
+
71
+ if response.is_a?(Net::HTTPSuccess)
72
+ response_body
73
+ else
74
+ raise BadResponseError.new(status, message, response_body)
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,32 @@
1
+ module Tapfall
2
+ class BadResponseError < StandardError
3
+ attr_reader :status, :data
4
+
5
+ def initialize(status, status_message, data)
6
+ @status = status
7
+ @data = data
8
+
9
+ message = if error_message
10
+ "#{status} #{status_message}: #{error_message}"
11
+ else
12
+ "#{status} #{status_message}"
13
+ end
14
+
15
+ super(message)
16
+ end
17
+
18
+ def error_type
19
+ @data['error'] if @data.is_a?(Hash)
20
+ end
21
+
22
+ def error_message
23
+ @data['message'] if @data.is_a?(Hash)
24
+ end
25
+ end
26
+
27
+ class ConfigError < StandardError
28
+ end
29
+
30
+ class DecodeError < StandardError
31
+ end
32
+ end
@@ -0,0 +1,30 @@
1
+ require_relative '../errors'
2
+ require_relative 'operation'
3
+ require_relative 'tap_message'
4
+
5
+ module Tapfall
6
+ class IdentityMessage < TapMessage
7
+ def initialize(json)
8
+ raise DecodeError.new("Missing event details") if json['identity'].nil?
9
+ @identity = json['identity']
10
+
11
+ super
12
+ end
13
+
14
+ def did
15
+ @identity['did']
16
+ end
17
+
18
+ def handle
19
+ @identity['handle']
20
+ end
21
+
22
+ def active?
23
+ @identity['isActive']
24
+ end
25
+
26
+ def status
27
+ @identity['status']&.to_sym
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,56 @@
1
+ require 'skyfall/cid'
2
+ require 'skyfall/collection'
3
+
4
+ module Tapfall
5
+ class Operation
6
+ def initialize(json)
7
+ @json = json
8
+ end
9
+
10
+ def live?
11
+ @json['live']
12
+ end
13
+
14
+ def did
15
+ @json['did']
16
+ end
17
+
18
+ alias repo did
19
+
20
+ def rev
21
+ @json['rev']
22
+ end
23
+
24
+ def path
25
+ @json['collection'] + '/' + @json['rkey']
26
+ end
27
+
28
+ def action
29
+ @json['action'].to_sym
30
+ end
31
+
32
+ def collection
33
+ @json['collection']
34
+ end
35
+
36
+ def rkey
37
+ @json['rkey']
38
+ end
39
+
40
+ def uri
41
+ "at://#{repo}/#{collection}/#{rkey}"
42
+ end
43
+
44
+ def cid
45
+ @cid ||= @json['cid'] && Skyfall::CID.from_json(@json['cid'])
46
+ end
47
+
48
+ def raw_record
49
+ @json['record']
50
+ end
51
+
52
+ def type
53
+ Skyfall::Collection.short_code(collection)
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,15 @@
1
+ require_relative '../errors'
2
+ require_relative 'operation'
3
+
4
+ module Tapfall
5
+ class RecordMessage < TapMessage
6
+ def initialize(json)
7
+ raise DecodeError.new("Missing record details") if json['record'].nil?
8
+ super
9
+ end
10
+
11
+ def operations
12
+ @operations ||= [Operation.new(json['record'])]
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,44 @@
1
+ require 'json'
2
+ require_relative '../errors'
3
+
4
+ module Tapfall
5
+ class TapMessage
6
+ attr_reader :id, :type
7
+ alias seq id
8
+
9
+ # :nodoc: - consider this as semi-private API
10
+ attr_reader :json
11
+
12
+ def self.new(data)
13
+ require_relative 'identity_message'
14
+ require_relative 'record_message'
15
+ require_relative 'unknown_message'
16
+
17
+ json = JSON.parse(data)
18
+
19
+ message_class = case json['type']
20
+ when 'record' then RecordMessage
21
+ when 'identity' then IdentityMessage
22
+ else UnknownMessage
23
+ end
24
+
25
+ message = message_class.allocate
26
+ message.send(:initialize, json)
27
+ message
28
+ end
29
+
30
+ def initialize(json)
31
+ @json = json
32
+ @type = @json['type'].to_sym
33
+ @id = @json['id']
34
+ end
35
+
36
+ def unknown?
37
+ self.is_a?(UnknownMessage)
38
+ end
39
+
40
+ def operations
41
+ []
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,4 @@
1
+ module Tapfall
2
+ class UnknownMessage < TapMessage
3
+ end
4
+ end
@@ -0,0 +1,101 @@
1
+ require 'forwardable'
2
+ require 'skyfall/stream'
3
+
4
+ require_relative 'api'
5
+ require_relative 'errors'
6
+ require_relative 'messages/tap_message'
7
+ require_relative 'version'
8
+
9
+ module Tapfall
10
+ class Tapfall::Stream < Skyfall::Stream
11
+ extend Forwardable
12
+
13
+ def_delegators :@api, :add_repo, :add_repos, :remove_repo, :remove_repos
14
+
15
+ def initialize(server, options = {})
16
+ super(server)
17
+
18
+ @options = options
19
+ @root_url = ensure_empty_path(@root_url)
20
+ @ack = true unless options[:ack] == false
21
+ @password = options[:admin_password]
22
+
23
+ @api = build_api
24
+ end
25
+
26
+ def connect
27
+ if @ack && @handlers[:message].nil?
28
+ raise ConfigError, "The on(:message) handler must be set unless :ack => false option is passed"
29
+ end
30
+
31
+ super
32
+ end
33
+
34
+ def handle_message(packet)
35
+ data = packet.data
36
+ @handlers[:raw_message]&.call(data)
37
+
38
+ if @handlers[:message]
39
+ tap_message = TapMessage.new(data)
40
+ @handlers[:message].call(tap_message)
41
+ send_ack(tap_message) if @ack
42
+ end
43
+ end
44
+
45
+ def send_ack(msg)
46
+ json = %({"type":"ack","id":#{msg.id}})
47
+ send_data(json)
48
+ end
49
+
50
+ private
51
+
52
+ # TMP
53
+ def send_data(data)
54
+ @ws.send(data)
55
+ end
56
+
57
+ def build_websocket_client(url)
58
+ Faye::WebSocket::Client.new(url, nil, { headers: { 'User-Agent' => user_agent }.merge(request_headers) })
59
+ end
60
+
61
+ def ensure_empty_path(url)
62
+ url = url.chomp('/')
63
+
64
+ if URI(url).path != ''
65
+ raise ArgumentError, "Server URL should only include a hostname, without any path"
66
+ end
67
+
68
+ url
69
+ end
70
+ # END
71
+
72
+ def basic_auth(account, password)
73
+ 'Basic ' + ["#{account}:#{password}"].pack('m0')
74
+ end
75
+
76
+ def request_headers
77
+ if @password
78
+ { 'Authorization' => basic_auth('admin', @password) }
79
+ else
80
+ {}
81
+ end
82
+ end
83
+
84
+ def build_websocket_url
85
+ @root_url + "/channel"
86
+ end
87
+
88
+ def build_api_url
89
+ if @root_url.start_with?('ws://')
90
+ @root_url.gsub(/^ws:/, 'http:')
91
+ else
92
+ @root_url.gsub(/^wss:/, 'https:')
93
+ end
94
+ end
95
+
96
+ def build_api
97
+ api_url = build_api_url
98
+ API.new(api_url, { admin_password: @password })
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tapfall
4
+ VERSION = '0.0.1'
5
+ end
data/lib/tapfall.rb ADDED
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'tapfall/stream'
4
+ require_relative 'tapfall/version'
5
+
6
+ module Tapfall
7
+ end
data/sig/tapfall.rbs ADDED
@@ -0,0 +1,4 @@
1
+ module Tapfall
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,71 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: tapfall
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Kuba Suder
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: skyfall
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '0.6'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '0.6'
26
+ email:
27
+ - jakub.suder@gmail.com
28
+ executables: []
29
+ extensions: []
30
+ extra_rdoc_files: []
31
+ files:
32
+ - CHANGELOG.md
33
+ - LICENSE.txt
34
+ - README.md
35
+ - lib/tapfall.rb
36
+ - lib/tapfall/api.rb
37
+ - lib/tapfall/errors.rb
38
+ - lib/tapfall/messages/identity_message.rb
39
+ - lib/tapfall/messages/operation.rb
40
+ - lib/tapfall/messages/record_message.rb
41
+ - lib/tapfall/messages/tap_message.rb
42
+ - lib/tapfall/messages/unknown_message.rb
43
+ - lib/tapfall/stream.rb
44
+ - lib/tapfall/version.rb
45
+ - sig/tapfall.rbs
46
+ homepage: https://ruby.sdk.blue
47
+ licenses:
48
+ - Zlib
49
+ metadata:
50
+ bug_tracker_uri: https://tangled.org/mackuba.eu/tapfall/issues
51
+ changelog_uri: https://tangled.org/mackuba.eu/tapfall/blob/master/CHANGELOG.md
52
+ source_code_uri: https://tangled.org/mackuba.eu/tapfall
53
+ rdoc_options: []
54
+ require_paths:
55
+ - lib
56
+ required_ruby_version: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: 3.2.0
61
+ required_rubygems_version: !ruby/object:Gem::Requirement
62
+ requirements:
63
+ - - ">="
64
+ - !ruby/object:Gem::Version
65
+ version: '0'
66
+ requirements: []
67
+ rubygems_version: 4.0.1
68
+ specification_version: 4
69
+ summary: A gem for ingesting ATProto repository data from a Tap service (extension
70
+ of the Skyfall gem)
71
+ test_files: []