e621stream 0.0.0

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.
Files changed (77) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +35 -0
  4. data/lib/e621stream/client/options.rb +20 -0
  5. data/lib/e621stream/client.rb +211 -0
  6. data/lib/e621stream/close_codes.rb +23 -0
  7. data/lib/e621stream/event.rb +24 -0
  8. data/lib/e621stream/events.rb +29 -0
  9. data/lib/e621stream/stream_error.rb +18 -0
  10. data/lib/e621stream/version.rb +5 -0
  11. data/lib/e621stream.rb +18 -0
  12. data/sorbet/config +6 -0
  13. data/sorbet/rbi/annotations/.gitattributes +1 -0
  14. data/sorbet/rbi/annotations/minitest.rbi +120 -0
  15. data/sorbet/rbi/annotations/rainbow.rbi +269 -0
  16. data/sorbet/rbi/gems/.gitattributes +1 -0
  17. data/sorbet/rbi/gems/ast@2.4.3.rbi +550 -0
  18. data/sorbet/rbi/gems/async-http@0.105.0.rbi +1649 -0
  19. data/sorbet/rbi/gems/async-pool@0.12.0.rbi +219 -0
  20. data/sorbet/rbi/gems/async-websocket@0.30.2.rbi +271 -0
  21. data/sorbet/rbi/gems/async@2.46.0.rbi +2193 -0
  22. data/sorbet/rbi/gems/benchmark@0.5.0.rbi +495 -0
  23. data/sorbet/rbi/gems/console@1.38.0.rbi +1552 -0
  24. data/sorbet/rbi/gems/erb@6.0.7.rbi +1581 -0
  25. data/sorbet/rbi/gems/erubi@1.13.1.rbi +153 -0
  26. data/sorbet/rbi/gems/fiber-annotation@0.2.0.rbi +62 -0
  27. data/sorbet/rbi/gems/fiber-local@1.1.0.rbi +36 -0
  28. data/sorbet/rbi/gems/fiber-storage@1.0.1.rbi +43 -0
  29. data/sorbet/rbi/gems/io-console@0.9.5.rbi +9 -0
  30. data/sorbet/rbi/gems/io-endpoint@0.19.0.rbi +1079 -0
  31. data/sorbet/rbi/gems/io-event@1.22.3.rbi +971 -0
  32. data/sorbet/rbi/gems/io-stream@0.14.0.rbi +805 -0
  33. data/sorbet/rbi/gems/json@3.1.0.rbi +1929 -0
  34. data/sorbet/rbi/gems/language_server-protocol@3.17.0.6.rbi +9 -0
  35. data/sorbet/rbi/gems/lint_roller@1.1.0.rbi +189 -0
  36. data/sorbet/rbi/gems/logger@1.7.0.rbi +896 -0
  37. data/sorbet/rbi/gems/minitest@5.27.0.rbi +1499 -0
  38. data/sorbet/rbi/gems/netrc@0.11.0.rbi +147 -0
  39. data/sorbet/rbi/gems/openssl@4.0.2.rbi +4366 -0
  40. data/sorbet/rbi/gems/parallel@2.3.0.rbi +332 -0
  41. data/sorbet/rbi/gems/parser@3.3.13.0.rbi +5146 -0
  42. data/sorbet/rbi/gems/pp@0.6.4.rbi +373 -0
  43. data/sorbet/rbi/gems/prettyprint@0.2.1.rbi +476 -0
  44. data/sorbet/rbi/gems/prism@1.9.0.rbi +42213 -0
  45. data/sorbet/rbi/gems/protocol-hpack@1.5.1.rbi +418 -0
  46. data/sorbet/rbi/gems/protocol-http1@0.41.1.rbi +894 -0
  47. data/sorbet/rbi/gems/protocol-http2@0.29.2.rbi +2277 -0
  48. data/sorbet/rbi/gems/protocol-http@0.72.1.rbi +3801 -0
  49. data/sorbet/rbi/gems/protocol-rack@0.23.0.rbi +9 -0
  50. data/sorbet/rbi/gems/protocol-url@0.19.0.rbi +9 -0
  51. data/sorbet/rbi/gems/protocol-websocket@0.21.3.rbi +1087 -0
  52. data/sorbet/rbi/gems/racc@1.8.1.rbi +169 -0
  53. data/sorbet/rbi/gems/rack@3.2.8.rbi +9 -0
  54. data/sorbet/rbi/gems/rainbow@3.1.1.rbi +362 -0
  55. data/sorbet/rbi/gems/rake@13.4.2.rbi +3127 -0
  56. data/sorbet/rbi/gems/rbi@0.5.0.rbi +5875 -0
  57. data/sorbet/rbi/gems/rbs@4.2.0.rbi +7133 -0
  58. data/sorbet/rbi/gems/rdoc@8.1.0.rbi +9 -0
  59. data/sorbet/rbi/gems/regexp_parser@2.13.1.rbi +3500 -0
  60. data/sorbet/rbi/gems/reline@0.7.0.rbi +2449 -0
  61. data/sorbet/rbi/gems/require-hooks@0.5.1.rbi +152 -0
  62. data/sorbet/rbi/gems/rexml@3.4.4.rbi +4905 -0
  63. data/sorbet/rbi/gems/rubocop-ast@1.51.0.rbi +7230 -0
  64. data/sorbet/rbi/gems/rubocop-rake@0.7.1.rbi +314 -0
  65. data/sorbet/rbi/gems/rubocop@1.92.2.rbi +69094 -0
  66. data/sorbet/rbi/gems/ruby-progressbar@1.13.0.rbi +988 -0
  67. data/sorbet/rbi/gems/rubydex@0.4.4.rbi +1620 -0
  68. data/sorbet/rbi/gems/spoom@1.8.13.rbi +7380 -0
  69. data/sorbet/rbi/gems/tapioca@0.20.0.rbi +3752 -0
  70. data/sorbet/rbi/gems/thor@1.5.0.rbi +3870 -0
  71. data/sorbet/rbi/gems/tsort@0.2.0.rbi +389 -0
  72. data/sorbet/rbi/gems/unicode-display_width@3.3.0.rbi +130 -0
  73. data/sorbet/rbi/gems/unicode-emoji@4.3.0.rbi +254 -0
  74. data/sorbet/rbi/gems/zeitwerk@2.8.3.rbi +1178 -0
  75. data/sorbet/tapioca/config.yml +13 -0
  76. data/sorbet/tapioca/require.rb +4 -0
  77. metadata +175 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: b16a81259148bd2d44ea56a857d2aeefbf15029660f8bd79d219198e3231711e
4
+ data.tar.gz: d24d99e59eb909cbf80f39108c9d90835022f2d886b4f89060245e8c6ceb86c7
5
+ SHA512:
6
+ metadata.gz: d8b860f937490e74e90af663866c043b9923321ef85a6d2a84a7a8ba57571605079c71fcaf9153f28343262583647ceb1bd4b3c659cb1aa0346e8d2581c1cd47
7
+ data.tar.gz: aa52fe6a7af0ac4f3bf7e0adff246b800b16b8350a12ea27feb55c4d389c06bdacd54e17c03e62f84d11bc40cbb7c2d49b5b1b62dd9df16b894b5e82fcabd83c
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) undefined Donovan_DMC
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,35 @@
1
+ # e621stream
2
+
3
+ A Ruby client for [E621Stream](https://e621.stream), a live WebSocket stream of e621 activity. It's built on [async-websocket](https://github.com/socketry/async-websocket) and typed with Sorbet.
4
+
5
+ ```bash
6
+ gem install e621stream
7
+ ```
8
+
9
+ ```ruby
10
+ require "e621stream"
11
+
12
+ client = E621Stream::Client.new(
13
+ username: "your_name",
14
+ api_key: "your e621 api key",
15
+ subscriptions: ["post.create", { "type" => "post.update", "filter" => { "tags" => "fox -solo" } }, "comment.*"],
16
+ )
17
+
18
+ client.on("post.create") { |event| puts(event.data["post"]["id"]) }
19
+ client.on("post.update") { |event| p(event.data["changes"]) }
20
+ client.on(:error) { |error| warn(error.message) }
21
+
22
+ client.run
23
+ ```
24
+
25
+ - **Events:** `on("<event type>")` for one type, or `on(:dispatch)` for everything. Each is an `E621Stream::Event` with `id`, `type`, `time` and `data`, which is the record as e621's api returns it. See [Events](https://e621.stream/events/) for what each one is.
26
+ - **Running:** `run` blocks. Inside a reactor already, it runs as a task, so `client.close` from another task stops it.
27
+ - **Reconnecting:** the connection is reopened with backoff when it drops, and resumes from the last event it got. It isn't reopened after a close retrying won't fix, like bad credentials. `reconnect: false` turns it off.
28
+ - **Resuming:** pass `cursor:` (an event id, or a millisecond timestamp) to replay from there. `client.cursor` is the last event received.
29
+ - **Changing subscriptions:** `client.subscribe([...])`.
30
+ - **Private events:** like `dmail.create`, these are polled with your own credentials and have to be subscribed to by name.
31
+ - **Other events:** `:ready`, `:replay_complete`, `:subscribed`, `:reconnecting`, `:close` and `:error`.
32
+
33
+ ```bash
34
+ bundle exec rake # rubocop, zeitwerk, tests and sorbet
35
+ ```
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+ # typed: strict
3
+
4
+ module E621Stream
5
+ class Client
6
+ class Options < T::Struct
7
+ const(:subscriptions, T::Array[Subscription])
8
+ # Your e621 username and an api key, from Account > Manage API Access.
9
+ const(:username, T.nilable(String), default: nil)
10
+ const(:api_key, T.nilable(String), default: nil)
11
+ const(:url, String, default: "wss://e621.stream/ws")
12
+ # A name for your client, shown in the server's logs.
13
+ const(:client, T.nilable(String), default: nil)
14
+ # An event id (or a millisecond timestamp) to replay from. After that, reconnecting resumes from the last event.
15
+ const(:cursor, T.nilable(T.any(String, Integer)), default: nil)
16
+ # Reconnects with backoff when the connection drops, unless the server closed it for something retrying won't fix.
17
+ const(:reconnect, T::Boolean, default: true)
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,211 @@
1
+ # frozen_string_literal: true
2
+ # typed: strict
3
+
4
+ require("async")
5
+ require("async/http/endpoint")
6
+ require("async/websocket/client")
7
+
8
+ module E621Stream
9
+ # A connection to E621Stream. Listeners run inside the client's reactor, between reading messages.
10
+ #
11
+ # client = E621Stream::Client.new(username: "...", api_key: "...", subscriptions: ["post.create"])
12
+ # client.on("post.create") { |event| puts(event.data["post"]["id"]) }
13
+ # client.run
14
+ class Client
15
+ extend(T::Sig)
16
+
17
+ Listener = T.type_alias { T.proc.params(arg0: T.untyped).void }
18
+ # An event type, `<resource>.*`, `*`, or `{ "type" => ..., "filter" => { ... } }`.
19
+ Subscription = T.type_alias { T.any(String, T::Hash[T.any(String, Symbol), T.untyped]) }
20
+
21
+ # Opens the real connection, tests swap it for a fake one.
22
+ class Connector
23
+ extend(T::Sig)
24
+
25
+ sig { params(url: String, block: T.proc.params(connection: T.untyped).void).void }
26
+ def connect(url, &block)
27
+ endpoint = Async::HTTP::Endpoint.parse(url, alpn_protocols: Async::HTTP::Protocol::HTTP11.names)
28
+ Async::WebSocket::Client.connect(endpoint, &block)
29
+ end
30
+ end
31
+
32
+ HEARTBEAT_SECONDS = 30
33
+ # The server answers heartbeats and pings, nothing for this long means the connection is gone without having closed.
34
+ SILENT_SECONDS = 75
35
+ MAX_DELAY_SECONDS = 60
36
+
37
+ sig { returns(Options) }
38
+ attr_reader(:options)
39
+
40
+ # The newest event received, which reconnecting resumes from.
41
+ sig { returns(T.nilable(String)) }
42
+ attr_reader(:cursor)
43
+
44
+ sig do
45
+ params(
46
+ subscriptions: T::Array[Subscription],
47
+ username: T.nilable(String),
48
+ api_key: T.nilable(String),
49
+ url: String,
50
+ client: T.nilable(String),
51
+ cursor: T.nilable(T.any(String, Integer)),
52
+ reconnect: T::Boolean,
53
+ connector: Connector,
54
+ ).void
55
+ end
56
+ def initialize(subscriptions:, username: nil, api_key: nil, url: "wss://e621.stream/ws", client: nil, cursor: nil, reconnect: true, connector: Connector.new)
57
+ @options = T.let(Options.new(subscriptions: subscriptions, username: username, api_key: api_key, url: url, client: client, cursor: cursor, reconnect: reconnect), Options)
58
+ @connector = connector
59
+ @cursor = T.let(cursor&.to_s, T.nilable(String))
60
+ @listeners = T.let(Hash.new { |hash, key| hash[key] = [] }, T::Hash[String, T::Array[Listener]])
61
+ @connection = T.let(nil, T.untyped)
62
+ @closing = T.let(false, T::Boolean)
63
+ @attempt = T.let(0, Integer)
64
+ @last_message = T.let(0.0, Float)
65
+ @last_error = T.let(nil, T.nilable(StreamError))
66
+ end
67
+
68
+ # Listens for an event type (like "post.create"), or one of :dispatch (every event), :ready, :replay_complete,
69
+ # :subscribed, :error, :close and :reconnecting.
70
+ sig { params(name: T.any(String, Symbol), block: Listener).returns(T.self_type) }
71
+ def on(name, &block)
72
+ T.must(@listeners[name.to_s]) << block
73
+ self
74
+ end
75
+
76
+ # Connects and keeps the connection going until #close is called, or the server closes it for something
77
+ # reconnecting won't fix. Blocks, or runs as a task when called inside a reactor already.
78
+ sig { void }
79
+ def run
80
+ @closing = false
81
+ Sync do
82
+ loop do
83
+ code, reason = connect_once
84
+ error = @last_error
85
+ @last_error = nil
86
+ reconnecting = !closing? && options.reconnect && !CloseCodes::FATAL.include?(code)
87
+ emit("error", error) if error
88
+ emit("close", { code: code, reason: error&.message || reason, reconnecting: reconnecting })
89
+ break unless reconnecting
90
+
91
+ @attempt += 1
92
+ # a server that's rate limiting this connection wants a longer break
93
+ base = code == CloseCodes::RATE_LIMITED ? 30 : 1
94
+ delay = [base * (2**(@attempt - 1)), MAX_DELAY_SECONDS].min * rand(0.75..1.25)
95
+ emit("reconnecting", { attempt: @attempt, delay: delay })
96
+ sleep(delay)
97
+ end
98
+ end
99
+ end
100
+
101
+ # Replaces the current subscriptions, and keeps them for reconnecting.
102
+ sig { params(subscriptions: T::Array[Subscription]).void }
103
+ def subscribe(subscriptions)
104
+ @options = Options.new(**options.serialize.transform_keys(&:to_sym), subscriptions: subscriptions)
105
+ send_message({ op: "subscribe", d: { subscriptions: subscriptions } })
106
+ end
107
+
108
+ # Closes the connection for good.
109
+ sig { void }
110
+ def close
111
+ @closing = true
112
+ @connection&.close(1000, "closed by client")
113
+ end
114
+
115
+ private
116
+
117
+ # resolves to the close code and reason
118
+ sig { returns([Integer, String]) }
119
+ def connect_once
120
+ result = T.let([1000, ""], [Integer, String])
121
+ @connector.connect(options.url) do |connection|
122
+ @connection = connection
123
+ @last_message = now
124
+ heartbeat = Async { heartbeat_loop(connection) }
125
+ begin
126
+ while (message = connection.read)
127
+ @last_message = now
128
+ handle(JSON.parse(message.buffer))
129
+ end
130
+ ensure
131
+ heartbeat.stop
132
+ @connection = nil
133
+ end
134
+ end
135
+ result
136
+ rescue Protocol::WebSocket::ClosedError => e
137
+ [e.code || 1006, e.message]
138
+ rescue StandardError => e
139
+ # closing tears the connection down under the read, which isn't worth reporting
140
+ return [1000, "closed by client"] if closing?
141
+
142
+ emit("error", e)
143
+ [1006, e.message]
144
+ end
145
+
146
+ sig { params(connection: T.untyped).void }
147
+ def heartbeat_loop(connection)
148
+ loop do
149
+ sleep(HEARTBEAT_SECONDS)
150
+ if now - @last_message > SILENT_SECONDS
151
+ connection.close(4999, "no response from the server")
152
+ break
153
+ end
154
+ send_message({ op: "heartbeat" })
155
+ end
156
+ end
157
+
158
+ sig { params(message: T::Hash[String, T.untyped]).void }
159
+ def handle(message)
160
+ data = message["d"]
161
+ case message["op"]
162
+ when "hello"
163
+ identify = { subscriptions: options.subscriptions }
164
+ identify[:auth] = { username: options.username, api_key: options.api_key } if options.username && options.api_key
165
+ identify[:client] = options.client if options.client
166
+ identify[:cursor] = @cursor if @cursor
167
+ send_message({ op: "identify", d: identify })
168
+ when "ready"
169
+ @attempt = 0
170
+ emit("ready", data)
171
+ when "dispatch"
172
+ event = Event.new(id: message["id"], type: message["type"], time: message["time"], data: message["data"], private: message["private"] == true)
173
+ # private events aren't in the stream, so they can't be resumed from
174
+ @cursor = event.id unless event.private?
175
+ emit("dispatch", event)
176
+ emit(event.type, event)
177
+ when "replay_complete"
178
+ emit("replay_complete", data)
179
+ when "subscribed"
180
+ emit("subscribed", data["subscriptions"])
181
+ when "error"
182
+ @last_error = StreamError.new(data["code"], data["message"])
183
+ end
184
+ end
185
+
186
+ sig { params(payload: T::Hash[Symbol, T.untyped]).void }
187
+ def send_message(payload)
188
+ connection = @connection
189
+ return unless connection
190
+
191
+ connection.write(Protocol::WebSocket::TextMessage.generate(payload))
192
+ connection.flush
193
+ end
194
+
195
+ sig { params(name: String, value: T.untyped).void }
196
+ def emit(name, value)
197
+ @listeners[name]&.each { |listener| listener.call(value) }
198
+ end
199
+
200
+ # read through a method, since #close changes it from another fiber while #run is waiting
201
+ sig { returns(T::Boolean) }
202
+ def closing?
203
+ @closing
204
+ end
205
+
206
+ sig { returns(Float) }
207
+ def now
208
+ Process.clock_gettime(Process::CLOCK_MONOTONIC).to_f
209
+ end
210
+ end
211
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+ # typed: strict
3
+
4
+ module E621Stream
5
+ # Why the server closed the connection, see https://e621.stream/close-codes/
6
+ module CloseCodes
7
+ UNKNOWN_ERROR = 4000
8
+ UNKNOWN_OP = 4001
9
+ DECODE_ERROR = 4002
10
+ NOT_IDENTIFIED = 4003
11
+ AUTHENTICATION_FAILED = 4004
12
+ ALREADY_IDENTIFIED = 4005
13
+ INVALID_SUBSCRIPTION = 4006
14
+ INVALID_CURSOR = 4007
15
+ RATE_LIMITED = 4008
16
+ SLOW_CONSUMER = 4009
17
+ TOO_MANY_CONNECTIONS = 4010
18
+ AUTHENTICATION_EXPIRED = 4011
19
+
20
+ # Closing for any of these again would just get the same result, so they aren't reconnected from.
21
+ FATAL = T.let([UNKNOWN_OP, DECODE_ERROR, AUTHENTICATION_FAILED, ALREADY_IDENTIFIED, INVALID_SUBSCRIPTION, INVALID_CURSOR, AUTHENTICATION_EXPIRED].freeze, T::Array[Integer])
22
+ end
23
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+ # typed: strict
3
+
4
+ module E621Stream
5
+ # One event from the stream. `data` is the record as e621's api returns it, or for update events
6
+ # `{ "<resource>" => record, "changes" => ... }`, see https://e621.stream/events/
7
+ class Event < T::Struct
8
+ extend(T::Sig)
9
+
10
+ # The event's cursor, pass it as `cursor:` to resume after it. Private events have no cursor.
11
+ const(:id, String)
12
+ const(:type, String)
13
+ # When it happened on e621.
14
+ const(:time, String)
15
+ const(:data, T::Hash[String, T.untyped])
16
+ # Only for private events, which are polled with your own credentials and never stored.
17
+ const(:private, T::Boolean, default: false)
18
+
19
+ sig { returns(T::Boolean) }
20
+ def private?
21
+ private
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+ # typed: strict
3
+
4
+ module E621Stream
5
+ module Events
6
+ # Kept identical to the server's src/events.ts, the repository's tests check it.
7
+ EVENT_TYPES = %w[
8
+ artist.create artist.delete artist.update artist_url.create artist_version.create avoid_posting.create
9
+ avoid_posting.delete avoid_posting.destroy avoid_posting.undelete avoid_posting.update
10
+ avoid_posting_version.create ban.create ban.delete ban.update blip.create bulk_update_request.create
11
+ bulk_update_request.update comment.create comment.delete comment.hide comment.unhide comment.update
12
+ forum_post.create forum_post.delete forum_post.hide forum_post.unhide forum_post.update forum_topic.create
13
+ forum_topic.delete forum_topic.hide forum_topic.unhide forum_topic.update help_page.create help_page.update
14
+ mod_action.create news_update.create news_update.update note.create note.update note_version.create
15
+ pool.add_posts pool.create pool.delete pool.remove_posts pool.update pool_version.create post.create post.update
16
+ post_approval.create post_event.create post_flag.create post_flag.update post_replacement.create
17
+ post_set.add_posts post_set.create post_set.delete post_set.remove_posts post_set.update post_version.create
18
+ post_version.hide post_version.unhide tag.create tag.delete tag.update tag_alias.create tag_alias.update
19
+ tag_implication.create tag_implication.update tag_type_version.create takedown.create takedown.delete
20
+ takedown.update upload_karma_event.create user.create user.delete user.update user_feedback.create
21
+ user_feedback.delete user_feedback.destroy user_feedback.undelete user_feedback.update wiki_page.create
22
+ wiki_page.delete wiki_page.update wiki_page_version.create
23
+ ].freeze
24
+
25
+ # Polled with your own credentials and only sent to you. They have to be subscribed to by name, `*` doesn't include
26
+ # them.
27
+ PRIVATE_EVENT_TYPES = %w[dmail.create].freeze
28
+ end
29
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+ # typed: strict
3
+
4
+ module E621Stream
5
+ # Sent by the server right before it closes the connection, `code` is one of CloseCodes.
6
+ class StreamError < StandardError
7
+ extend(T::Sig)
8
+
9
+ sig { returns(Integer) }
10
+ attr_reader(:code)
11
+
12
+ sig { params(code: Integer, message: String).void }
13
+ def initialize(code, message)
14
+ super(message)
15
+ @code = code
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module E621Stream
4
+ VERSION = "0.0.0"
5
+ end
data/lib/e621stream.rb ADDED
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require("json")
4
+ require("zeitwerk")
5
+ require("sorbet-runtime")
6
+
7
+ # Loaded eagerly (rather than left to Zeitwerk) because the gemspec already requires version.rb to read the version,
8
+ # which defines E621Stream before Zeitwerk::Loader#setup runs.
9
+ require_relative("e621stream/version")
10
+
11
+ loader = Zeitwerk::Loader.for_gem
12
+ loader.inflector.inflect({ "e621stream" => "E621Stream" })
13
+ loader.ignore("#{__dir__}/e621stream/version.rb")
14
+ loader.setup
15
+
16
+ # A client for E621Stream, a live WebSocket stream of e621 activity. See https://e621.stream
17
+ module E621Stream
18
+ end
data/sorbet/config ADDED
@@ -0,0 +1,6 @@
1
+ --dir
2
+ .
3
+ --ignore=test/
4
+ --ignore=tmp/
5
+ --ignore=vendor/
6
+ --parser=prism
@@ -0,0 +1 @@
1
+ **/*.rbi linguist-vendored=true
@@ -0,0 +1,120 @@
1
+ # typed: true
2
+
3
+ # DO NOT EDIT MANUALLY
4
+ # This file was pulled from a central RBI files repository.
5
+ # Please run `bin/tapioca annotations` to update it.
6
+
7
+ module Minitest::Assertions
8
+ sig { params(test: T.anything, msg: T.anything).returns(TrueClass) }
9
+ def assert(test, msg = nil); end
10
+
11
+ sig { params(obj: T.anything, msg: T.anything).returns(TrueClass) }
12
+ def assert_empty(obj, msg = nil); end
13
+
14
+ sig { params(exp: T.anything, act: T.anything, msg: T.anything).returns(TrueClass) }
15
+ def assert_equal(exp, act, msg = nil); end
16
+
17
+ sig { params(exp: T.anything, act: T.anything, delta: Numeric, msg: T.anything).returns(TrueClass) }
18
+ def assert_in_delta(exp, act, delta = T.unsafe(nil), msg = nil); end
19
+
20
+ sig { params(a: T.anything, b: T.anything, epsilon: Numeric, msg: T.anything).returns(TrueClass) }
21
+ def assert_in_epsilon(a, b, epsilon = T.unsafe(nil), msg = nil); end
22
+
23
+ sig { params(collection: T.anything, obj: T.anything, msg: T.anything).returns(TrueClass) }
24
+ def assert_includes(collection, obj, msg = nil); end
25
+
26
+ sig { params(cls: T.anything, obj: T.anything, msg: T.anything).returns(TrueClass) }
27
+ def assert_instance_of(cls, obj, msg = nil); end
28
+
29
+ sig { params(cls: T.anything, obj: T.anything, msg: T.anything).returns(TrueClass) }
30
+ def assert_kind_of(cls, obj, msg = nil); end
31
+
32
+ sig { params(matcher: T.any(String, Regexp), obj: T.anything, msg: T.anything).returns(MatchData) }
33
+ def assert_match(matcher, obj, msg = nil); end
34
+
35
+ sig { params(obj: T.anything, msg: T.anything).returns(TrueClass) }
36
+ def assert_nil(obj, msg = nil); end
37
+
38
+ sig { params(o1: T.anything, op: T.any(Symbol, String), o2: T.anything, msg: T.anything).returns(TrueClass) }
39
+ def assert_operator(o1, op, o2 = T.unsafe(nil), msg = nil); end
40
+
41
+ sig { params(stdout: T.nilable(T.any(String, Regexp)), stderr: T.nilable(T.any(String, Regexp)), block: T.proc.void).returns(T::Boolean) }
42
+ def assert_output(stdout = nil, stderr = nil, &block); end
43
+
44
+ sig { params(path: T.any(String, Pathname), msg: T.anything).returns(TrueClass) }
45
+ def assert_path_exists(path, msg = nil); end
46
+
47
+ sig { params(block: T.proc.void).returns(TrueClass) }
48
+ def assert_pattern(&block); end
49
+
50
+ sig { params(o1: T.anything, op: T.any(String, Symbol), msg: T.anything).returns(TrueClass) }
51
+ def assert_predicate(o1, op, msg = nil); end
52
+
53
+ sig { params(exp: NilClass, block: T.proc.void).returns(StandardError) }
54
+ sig { type_parameters(:T).params(exp: T.any(T::Class[T.type_parameter(:T)], Regexp, String), block: T.proc.void).returns(T.type_parameter(:T)) }
55
+ def assert_raises(*exp, &block); end
56
+
57
+ sig { params(obj: T.anything, meth: T.any(String, Symbol), msg: T.anything, include_all: T::Boolean).returns(TrueClass) }
58
+ def assert_respond_to(obj, meth, msg = nil, include_all: false); end
59
+
60
+ sig { params(exp: T.anything, act: T.anything, msg: T.anything).returns(TrueClass) }
61
+ def assert_same(exp, act, msg = nil); end
62
+
63
+ # @version < 6.0.0
64
+ sig { params(send_ary: T::Array[T.anything], m: T.anything).returns(T::Boolean) }
65
+ def assert_send(send_ary, m = nil); end
66
+
67
+ sig { params(block: T.proc.void).returns(T::Boolean) }
68
+ def assert_silent(&block); end
69
+
70
+ sig { params(sym: Symbol, msg: T.anything, block: T.proc.void).returns(T.anything) }
71
+ def assert_throws(sym, msg = nil, &block); end
72
+
73
+ sig { params(test: T.anything, msg: T.anything).returns(TrueClass) }
74
+ def refute(test, msg = nil); end
75
+
76
+ sig { params(obj: T.anything, msg: T.anything).returns(TrueClass) }
77
+ def refute_empty(obj, msg = nil); end
78
+
79
+ sig { params(exp: T.anything, act: T.anything, msg: T.anything).returns(TrueClass) }
80
+ def refute_equal(exp, act, msg = nil); end
81
+
82
+ sig { params(exp: T.anything, act: T.anything, delta: Numeric, msg: T.anything).returns(TrueClass) }
83
+ def refute_in_delta(exp, act, delta = T.unsafe(nil), msg = nil); end
84
+
85
+ sig { params(a: T.anything, b: T.anything, epsilon: Numeric, msg: T.anything).returns(TrueClass) }
86
+ def refute_in_epsilon(a, b, epsilon = T.unsafe(nil), msg = nil); end
87
+
88
+ sig { params(collection: T.anything, obj: T.anything, msg: T.anything).returns(TrueClass) }
89
+ def refute_includes(collection, obj, msg = nil); end
90
+
91
+ sig { params(cls: T.anything, obj: T.anything, msg: T.anything).returns(TrueClass) }
92
+ def refute_instance_of(cls, obj, msg = nil); end
93
+
94
+ sig { params(cls: T.anything, obj: T.anything, msg: T.anything).returns(TrueClass) }
95
+ def refute_kind_of(cls, obj, msg = nil); end
96
+
97
+ sig { params(matcher: T.any(String, Regexp), obj: T.anything, msg: T.anything).returns(TrueClass) }
98
+ def refute_match(matcher, obj, msg = nil); end
99
+
100
+ sig { params(obj: T.anything, msg: T.anything).returns(TrueClass) }
101
+ def refute_nil(obj, msg = nil); end
102
+
103
+ sig { params(block: T.proc.void).returns(TrueClass) }
104
+ def refute_pattern(&block); end
105
+
106
+ sig { params(o1: T.anything, op: T.any(Symbol, String), o2: T.anything, msg: T.anything).returns(TrueClass) }
107
+ def refute_operator(o1, op, o2 = T.unsafe(nil), msg = nil); end
108
+
109
+ sig { params(path: T.any(String, Pathname), msg: T.anything).returns(TrueClass) }
110
+ def refute_path_exists(path, msg = nil); end
111
+
112
+ sig { params(o1: T.anything, op: T.any(String, Symbol), msg: T.anything).returns(TrueClass) }
113
+ def refute_predicate(o1, op, msg = nil); end
114
+
115
+ sig { params(obj: T.anything, meth: T.any(String, Symbol), msg: T.anything, include_all: T::Boolean).returns(TrueClass) }
116
+ def refute_respond_to(obj, meth, msg = nil, include_all: false); end
117
+
118
+ sig { params(exp: T.anything, act: T.anything, msg: T.anything).returns(TrueClass) }
119
+ def refute_same(exp, act, msg = nil); end
120
+ end