rocksky 0.2.0 → 0.4.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7bf46315f6919d095154453f2daa8bf5b54fdd81d32b03e1b882dac582a36965
4
- data.tar.gz: '041683204d6dbea62447f52385d6fc7d4385464a9c052eca2862f7d3f340ed28'
3
+ metadata.gz: 56d65f24108dabf8d86f3b8cac9c501d29be2e6ef8cc829f0d3105a457988080
4
+ data.tar.gz: 8c781e0d976b90fe1691fc207802a5730d99911e1da99af75909b615282e2d8a
5
5
  SHA512:
6
- metadata.gz: 0117e5bdd751d9465ff1a9a80a1d586fe8cd5bc5b28e8c7f48c6fe1ba608c5a7c0ffdcb342c760f6603391990e7a24bacc60c954b9bbe10708b5cc18f8d9b50b
7
- data.tar.gz: 4ca4d2324391a138d429c02e997efd3046d8610fb3917aa315369c918c52a28b7dd3e7bbd45e98795458dcf5438b80ac11b3b61cf0f8f2d834823b309c667f14
6
+ metadata.gz: dbc13ce587f747ea82f7dd17f76d3883fbfc3eb5f08794f26b955414f633f2811d1669c84cce525a7c137485ed8b87be204df9e4622a6fe375548127a1911e85
7
+ data.tar.gz: b2e13963888d6fc54cdf183113f627261b41f089a15846e0561ff592b6dc0d50f193448ac2a5d6531cde2e38238ff056d54f9f4f769fb20c9b429b4eefec5ccf
data/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.0
4
+
5
+ - `feed.get_stories` now accepts optional `feed:` (at-uri) and `following:`
6
+ (boolean) kwargs. `feed:` narrows results to scrobbles in that feed
7
+ generator; `following: true` restricts to users the viewer follows and
8
+ requires an authenticated client. Filters intersect when both are supplied.
9
+
3
10
  ## 0.2.0
4
11
 
5
12
  - Added lexicon-derived `Struct`s under `Rocksky::Generated::*` covering every
data/README.md CHANGED
@@ -1,229 +1,73 @@
1
1
  # rocksky (Ruby)
2
2
 
3
- Idiomatic Ruby client for the [Rocksky](https://rocksky.app) XRPC API.
4
-
5
- ```ruby
6
- client = Rocksky.new(token: ENV["ROCKSKY_TOKEN"])
7
-
8
- client.actor.get_profile(did: "tsiry-sandratraina.com")
9
- client.charts.get_top_artists(limit: 10, start_date: "2025-01-01")
10
- client.scrobble.create_scrobble(title: "In Bloom", artist: "Nirvana")
11
- ```
12
-
13
- Every XRPC NSID maps to a method on a resource object. `app.rocksky.actor.getProfile`
14
- becomes `client.actor.get_profile(...)`. `app.rocksky.scrobble.createScrobble`
15
- becomes `client.scrobble.create_scrobble(...)`. No magic — just kwargs in,
16
- parsed JSON out.
3
+ Ruby bindings to the shared Rocksky Rust core (`rocksky-sdk`) through Ruby's
4
+ stdlib `fiddle` (no `ffi` gem): AppView reads, AT Protocol PDS writes (scrobble
5
+ fan-out, like, follow, shout) and the identity hashes — the same engine behind
6
+ every Rocksky SDK.
17
7
 
18
8
  ## Installation
19
9
 
20
- Add it to your `Gemfile`:
21
-
22
- ```ruby
23
- gem "rocksky"
10
+ ```sh
11
+ gem install rocksky
24
12
  ```
25
13
 
26
- Or install directly:
14
+ The gem is pure-Ruby; the native library is fetched from the GitHub release on
15
+ first load and cached (checksum-verified). For a local checkout, build it once:
27
16
 
28
- ```bash
29
- gem install rocksky
17
+ ```sh
18
+ ./build-core.sh
30
19
  ```
31
20
 
32
- Requires Ruby 3.0+. The SDK depends only on Ruby's stdlib (`net/http`, `json`, `uri`).
33
-
34
21
  ## Quick start
35
22
 
36
23
  ```ruby
37
24
  require "rocksky"
38
25
 
39
- # Reads ROCKSKY_BASE_URL and ROCKSKY_TOKEN from the env when omitted.
40
- client = Rocksky.new
41
-
42
- profile = client.actor.get_profile(did: "tsiry-sandratraina.com")
43
- puts profile["displayName"]
44
- ```
45
-
46
- For authenticated calls, pass a Bluesky-issued Bearer token (see
47
- [lexicons documentation](https://github.com/rocksky/rocksky/blob/main/LEXICONS.md)):
48
-
49
- ```ruby
50
- client = Rocksky.new(token: "eyJ...")
51
- client.scrobble.create_scrobble(title: "In Bloom", artist: "Nirvana")
52
- ```
53
-
54
- `with_token` derives a new client without mutating the original — useful in
55
- web apps that share one base client across users:
56
-
57
- ```ruby
58
- base = Rocksky.new
59
- def for_user(base, token) = base.with_token(token)
60
- ```
61
-
62
- ## Resources
63
-
64
- | Resource | Methods |
65
- |----------|---------|
66
- | `client.actor` | `get_profile`, `get_actor_albums/artists/songs/scrobbles/playlists/loved_songs`, `get_actor_neighbours`, `get_actor_compatibility` |
67
- | `client.album` | `get_album`, `get_albums`, `get_album_tracks` |
68
- | `client.apikey` | `get_apikeys`, `create_apikey`, `update_apikey`, `remove_apikey` *(auth)* |
69
- | `client.artist` | `get_artist`, `get_artists`, `get_artist_albums/tracks/listeners/recent_listeners` |
70
- | `client.charts` | `get_scrobbles_chart`, `get_top_artists`, `get_top_tracks` |
71
- | `client.feed` | `search`, `get_feed_generators/generator/feed`, `get_stories`, `get_recommendations`, `get_artist_recommendations`, `get_album_recommendations` |
72
- | `client.graph` | `follow_account`, `unfollow_account`, `get_followers`, `get_follows`, `get_known_followers` *(auth)* |
73
- | `client.like` | `like_song`, `dislike_song`, `like_shout`, `dislike_shout` *(auth)* |
74
- | `client.mirror` | `get_mirror_sources`, `put_mirror_source` *(auth)* |
75
- | `client.player` | `play`, `pause`, `next`, `previous`, `seek`, `play_file`, `play_directory`, `add_items_to_queue`, `get_currently_playing`, `get_playback_queue` |
76
- | `client.playlist` | `get_playlist`, `get_playlists`, `create_playlist`, `remove_playlist`, `start_playlist`, `insert_files`, `insert_directory` |
77
- | `client.scrobble` | `create_scrobble`, `get_scrobble`, `get_scrobbles` |
78
- | `client.shout` | `create_shout`, `reply_shout`, `remove_shout`, `report_shout`, `get_*_shouts`, `get_shout_replies` |
79
- | `client.song` | `get_song`, `get_songs`, `get_song_recent_listeners`, `match_song`, `create_song` |
80
- | `client.spotify` | `play`, `pause`, `next`, `previous`, `seek`, `get_currently_playing` *(auth)* |
81
- | `client.stats` | `get_stats`, `get_wrapped` |
82
-
83
- For anything not covered, drop down to the raw transport:
84
-
85
- ```ruby
86
- client.query("app.rocksky.actor.getProfile", did: "did:plc:7vdlgi2bflelz7mmuxoqjfcr")
87
- client.procedure("app.rocksky.like.likeSong", body: { uri: "at://..." })
88
- ```
89
-
90
- ## Conventions
91
-
92
- - **Keyword args** for every parameter. Ruby `snake_case` names map to the
93
- lexicon's `camelCase` (e.g. `start_date:` → `startDate`).
94
- - **`nil` is dropped.** Pass `nil` for any optional param and it won't be sent.
95
- - **Arrays are CSV-joined.** Lexicon list params like `names:` accept Ruby arrays:
96
- `client.artist.get_artists(names: %w[Nirvana Pixies])`.
97
- - **Hashes in, Hashes out.** Responses come back as plain `Hash` (string keys) —
98
- no DSL, no model classes. Match the shape of the lexicon JSON 1:1.
99
-
100
- ## Error handling
101
-
102
- Every non-2xx response raises a subclass of `Rocksky::Error`:
103
-
104
- ```ruby
105
- begin
106
- client.song.get_song(uri: "at://does-not-exist")
107
- rescue Rocksky::NotFound => e
108
- puts "missing: #{e.message} (status=#{e.status}, nsid=#{e.nsid})"
109
- rescue Rocksky::RateLimited
110
- sleep 5; retry
111
- rescue Rocksky::Error => e
112
- warn "rocksky failure: #{e.class}: #{e.message}"
113
- end
26
+ # Reads unauthenticated. base: overrides https://api.rocksky.app.
27
+ stats = Rocksky.global_stats
28
+ puts stats["scrobbles"]
29
+ Rocksky.top_tracks(limit: 10).each { |t| puts "#{t["artist"]} — #{t["title"]}" }
30
+
31
+ # Writes — log in once (session persisted at the given path).
32
+ agent = Rocksky::Agent.login("session.json", "alice.bsky.social", "app-password")
33
+ out = agent.scrobble(
34
+ "title" => "Chaser", "artist" => "Calibro 35",
35
+ "album" => "Jazzploitation", "albumArtist" => "Calibro 35", "durationMs" => 182_320
36
+ )
37
+ puts out["scrobbleUri"]
38
+ agent.close
114
39
  ```
115
40
 
116
- | Class | Status |
117
- |-------|--------|
118
- | `Rocksky::BadRequest` | 400 |
119
- | `Rocksky::Unauthorized` | 401 |
120
- | `Rocksky::Forbidden` | 403 |
121
- | `Rocksky::NotFound` | 404 |
122
- | `Rocksky::RateLimited` | 429 |
123
- | `Rocksky::ServerError` | 5xx |
124
- | `Rocksky::HTTPError` | any other non-2xx |
125
- | `Rocksky::TransportError` | DNS/TCP/timeouts |
126
-
127
- ## IRB console
128
-
129
- The gem ships with a `rocksky-console` executable: an IRB session
130
- pre-loaded with a `client` bound to your environment.
131
-
132
- ```bash
133
- $ gem install rocksky
134
- $ ROCKSKY_TOKEN=eyJ... rocksky-console
135
- Rocksky 0.2.0 — interactive console
136
- base_url : https://api.rocksky.app
137
- token : present (set via ROCKSKY_TOKEN)
138
-
139
- A client is bound to `client`. Try:
140
- client.actor.get_profile(did: "did:plc:7vdlgi2bflelz7mmuxoqjfcr")
141
- client.charts.get_top_artists(limit: 10)
142
-
143
- irb> client.actor.get_profile(did: "did:plc:7vdlgi2bflelz7mmuxoqjfcr")
144
- => {"did"=>"did:plc:...", "handle"=>"tsiry-sandratraina.com", ...}
145
- ```
41
+ ## API
146
42
 
147
- ### From a checkout (development)
43
+ Reads/writes return plain Hashes/Strings (the wire shape); write verbs raise
44
+ `Rocksky::Error` on an `{"error": …}` envelope. Records are Hashes with
45
+ camelCase keys.
148
46
 
149
- If you've cloned the repo, use `bin/console` instead. It loads the local
150
- source tree, so edits to `lib/` are picked up on the next `reload!`-style
151
- restart:
47
+ ### Reads `Rocksky`
152
48
 
153
- ```bash
154
- $ cd sdk/ruby
155
- $ bundle install
156
- $ bin/console
157
- ```
49
+ `profile(actor, base:)`, `scrobbles(actor, limit:, offset:, base:)`,
50
+ `top_tracks(limit:, offset:, base:)`, `global_stats(base:)`. Every read takes an
51
+ optional `base:` to target a custom AppView.
158
52
 
159
- ### Ad-hoc IRB (no script)
53
+ ### Writes `Rocksky::Agent`
160
54
 
161
- You can always launch IRB yourself:
55
+ `Agent.login(session_path, identifier, password, appview:)` an agent. Then
56
+ `scrobble(track)` (fans out to artist/album/song/scrobble), `like(uri, cid)`,
57
+ `follow(did)`, `shout(subject_uri, subject_cid, message)`, `refresh_session`, and
58
+ `close` (release the native handle).
162
59
 
163
- ```bash
164
- $ irb -rrocksky
165
- irb> client = Rocksky.new(token: ENV["ROCKSKY_TOKEN"])
166
- irb> client.charts.get_top_tracks(limit: 5)
167
- ```
168
-
169
- ### Useful IRB recipes
170
-
171
- ```ruby
172
- # Pretty-print responses
173
- require "json"
174
- puts JSON.pretty_generate(client.actor.get_profile(did: "did:plc:7vdlgi2bflelz7mmuxoqjfcr"))
60
+ ### Identity hashes
175
61
 
176
- # Inspect what the SDK is about to send
177
- client = Rocksky.new(headers: { "X-Debug" => "1" })
62
+ `Rocksky.song_hash(title, artist, album)` lowercase-hex SHA-256, identical
63
+ to the server and every other Rocksky SDK.
178
64
 
179
- # Try things against staging without touching prod
180
- client = Rocksky.new(base_url: "https://api.staging.rocksky.app")
65
+ ## Example
181
66
 
182
- # Tighter timeouts in a script
183
- client = Rocksky.new(open_timeout: 2, read_timeout: 5)
67
+ ```sh
68
+ ruby -Ilib examples/native_core.rb
184
69
  ```
185
70
 
186
- ## Configuration
187
-
188
- | Option | Default | Env var |
189
- |----------------|-------------------------------|-----------------------|
190
- | `base_url` | `https://api.rocksky.app` | `ROCKSKY_BASE_URL` |
191
- | `token` | `nil` | `ROCKSKY_TOKEN` |
192
- | `headers` | `{}` | — |
193
- | `user_agent` | `rocksky-ruby/<version>` | — |
194
- | `open_timeout` | `10` seconds | — |
195
- | `read_timeout` | `30` seconds | — |
196
-
197
- ## Types
198
-
199
- Lexicon-derived `Struct` types are available under `Rocksky::Generated::*`, mirroring every lex `*View*` / `*Record` / `*Input` / `*Output` / `*Params` shape from [the Rocksky lexicons](https://tangled.org/rocksky.app/rocksky/tree/main/apps/api/lexicons). Regenerate with `bun run lexgen:types` at the repo root.
200
-
201
-
202
- ## Examples
203
-
204
- The `examples/` directory contains runnable scripts:
205
-
206
- ```bash
207
- bundle exec ruby examples/01_profile.rb tsiry-sandratraina.com
208
- bundle exec ruby examples/03_charts.rb
209
- ROCKSKY_TOKEN=... bundle exec ruby examples/02_scrobble.rb
210
- ```
211
-
212
- See [examples/README.md](examples/README.md) for the full list.
213
-
214
- ## Development
215
-
216
- ```bash
217
- $ bundle install
218
- $ bundle exec rake test # run the suite
219
- $ bin/console # IRB with the local source tree
220
- ```
221
-
222
- Tests use Minitest + WebMock — no live network access needed. Add a new
223
- resource by dropping a file in `lib/rocksky/resources/`, wiring it into
224
- `lib/rocksky.rb` and `lib/rocksky/client.rb`, and adding tests under
225
- `test/resources/`.
226
-
227
71
  ## License
228
72
 
229
- [MIT](LICENSE) © Tsiry Sandratraina.
73
+ MIT.
@@ -0,0 +1,5 @@
1
+ {
2
+ "repo": "tsirysndr/rocksky",
3
+ "tag": "bindings-v0.1.0",
4
+ "checksums": {}
5
+ }
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Resolves the native rocksky-uniffi library, downloading a prebuilt from the
4
+ # GitHub release on first use when it isn't already present locally.
5
+ #
6
+ # Order of preference:
7
+ # 1. $ROCKSKY_NATIVE_LIB, if set.
8
+ # 2. librocksky_uniffi.<ext> next to this file — a local ./build-core.sh build.
9
+ # 3. A checksum-verified copy in the user cache, downloaded on first load (the
10
+ # published-gem path: the native lib is not bundled in the gem).
11
+ #
12
+ # The gem ships manifest.json (repo, release tag, one sha256 per target triple)
13
+ # — filled from the release artifacts by sdk/scripts/gen-uniffi-manifest.sh.
14
+ require "json"
15
+ require "rbconfig"
16
+ require "digest"
17
+ require "fileutils"
18
+ require "net/http"
19
+ require "uri"
20
+
21
+ module Rocksky
22
+ # Raised on native-lib resolution failure or an `{"error": …}` envelope.
23
+ class Error < StandardError; end
24
+
25
+ module Native
26
+ module_function
27
+
28
+ def ext
29
+ case RbConfig::CONFIG["host_os"]
30
+ when /darwin/ then "dylib"
31
+ when /mswin|mingw|cygwin/ then "dll"
32
+ else "so"
33
+ end
34
+ end
35
+
36
+ def arch
37
+ a = RbConfig::CONFIG["host_cpu"]
38
+ case a
39
+ when /x86_64|amd64/ then "x86_64"
40
+ when /arm64|aarch64/ then "aarch64"
41
+ else a
42
+ end
43
+ end
44
+
45
+ # Canonical target triple matching the release asset names.
46
+ def triple
47
+ case RbConfig::CONFIG["host_os"]
48
+ when /darwin/ then "#{arch}-apple-darwin"
49
+ when /linux/ then "#{arch}-linux-gnu"
50
+ when /freebsd/ then "#{arch}-unknown-freebsd"
51
+ when /netbsd/ then "#{arch}-unknown-netbsd"
52
+ when /openbsd/ then "#{arch}-unknown-openbsd"
53
+ else raise Error, "unsupported platform: #{RbConfig::CONFIG["host_os"]}"
54
+ end
55
+ end
56
+
57
+ def lib_dir
58
+ __dir__ # sdk/ruby/lib/rocksky
59
+ end
60
+
61
+ def cache_dir(tag)
62
+ base = ENV["XDG_CACHE_HOME"] || File.join(Dir.home, ".cache")
63
+ File.join(base, "rocksky", tag)
64
+ end
65
+
66
+ def manifest
67
+ JSON.parse(File.read(File.join(lib_dir, "manifest.json")))
68
+ end
69
+
70
+ # Absolute path to a loadable native library, fetching it if necessary.
71
+ def resolve
72
+ env = ENV["ROCKSKY_NATIVE_LIB"]
73
+ return env if env && File.exist?(env)
74
+
75
+ local = File.join(lib_dir, "librocksky_uniffi.#{ext}")
76
+ return local if File.exist?(local)
77
+
78
+ m = manifest
79
+ t = triple
80
+ sha = m.dig("checksums", t) or
81
+ raise Error, "no prebuilt native lib for #{t} (manifest has no checksum) — run ./build-core.sh"
82
+ tag = m["tag"]
83
+ dest = File.join(cache_dir(tag), "librocksky_uniffi-#{t}.#{ext}")
84
+ return dest if File.exist?(dest) && Digest::SHA256.file(dest).hexdigest == sha
85
+
86
+ download_verify(m["repo"], tag, t, sha, dest)
87
+ dest
88
+ end
89
+
90
+ def download_verify(repo, tag, triple, sha, dest)
91
+ url = "https://github.com/#{repo}/releases/download/#{tag}/librocksky_uniffi-#{triple}.#{ext}"
92
+ body = fetch(url)
93
+ got = Digest::SHA256.hexdigest(body)
94
+ raise Error, "checksum mismatch for #{triple}: want #{sha}, got #{got}" unless got == sha
95
+
96
+ FileUtils.mkdir_p(File.dirname(dest))
97
+ tmp = "#{dest}.download"
98
+ File.binwrite(tmp, body)
99
+ File.chmod(0o755, tmp)
100
+ File.rename(tmp, dest)
101
+ end
102
+
103
+ def fetch(url, limit = 5)
104
+ raise Error, "too many redirects" if limit.zero?
105
+
106
+ res = Net::HTTP.get_response(URI(url))
107
+ case res
108
+ when Net::HTTPSuccess then res.body
109
+ when Net::HTTPRedirection then fetch(res["location"], limit - 1)
110
+ else raise Error, "download failed (#{res.code}) for #{url}"
111
+ end
112
+ end
113
+ end
114
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Rocksky
2
- VERSION = "0.2.0".freeze
4
+ VERSION = "0.4.0"
3
5
  end
data/lib/rocksky.rb CHANGED
@@ -1,33 +1,135 @@
1
- require "rocksky/version"
2
- require "rocksky/error"
3
- require "rocksky/http"
4
- require "rocksky/resources/base"
5
- require "rocksky/resources/actor"
6
- require "rocksky/resources/album"
7
- require "rocksky/resources/apikey"
8
- require "rocksky/resources/artist"
9
- require "rocksky/resources/charts"
10
- require "rocksky/resources/feed"
11
- require "rocksky/resources/graph"
12
- require "rocksky/resources/like"
13
- require "rocksky/resources/mirror"
14
- require "rocksky/resources/player"
15
- require "rocksky/resources/playlist"
16
- require "rocksky/resources/scrobble"
17
- require "rocksky/resources/shout"
18
- require "rocksky/resources/song"
19
- require "rocksky/resources/spotify"
20
- require "rocksky/resources/stats"
21
- require "rocksky/client"
22
- require "rocksky/generated/types"
1
+ # frozen_string_literal: true
2
+
3
+ # Official Ruby SDK for Rocksky.
4
+ #
5
+ # A thin binding, via Ruby's stdlib `fiddle` (no `ffi` gem), to the plain C ABI
6
+ # of the shared Rust core (`rocksky-uniffi` → `rocksky-sdk`): AppView reads, AT
7
+ # Protocol PDS writes (scrobble fan-out, like, follow, shout) and the identity
8
+ # hashes — the same engine behind every Rocksky SDK.
9
+ #
10
+ # require "rocksky"
11
+ # Rocksky.global_stats
12
+ # agent = Rocksky::Agent.login("session.json", "alice.bsky.social", "app-pw")
13
+ require "fiddle"
14
+ require "fiddle/import"
15
+ require "json"
16
+ require_relative "rocksky/native"
23
17
 
24
18
  module Rocksky
25
- # Build a new Rocksky client.
26
- #
27
- # client = Rocksky.new(token: ENV["ROCKSKY_TOKEN"])
19
+ require_relative "rocksky/version"
20
+
21
+ module C
22
+ extend Fiddle::Importer
23
+ dlload Native.resolve
24
+ [
25
+ "char* rocksky_profile(const char*, const char*)",
26
+ "char* rocksky_scrobbles(const char*, const char*, unsigned int, unsigned int)",
27
+ "char* rocksky_top_tracks(const char*, unsigned int, unsigned int)",
28
+ "char* rocksky_global_stats(const char*)",
29
+ "char* rocksky_song_hash(const char*, const char*, const char*)",
30
+ "void rocksky_string_free(void*)",
31
+ "char* rocksky_last_error()",
32
+ "void* rocksky_agent_login(const char*, const char*, const char*, const char*)",
33
+ "void rocksky_agent_free(void*)",
34
+ "char* rocksky_agent_scrobble(void*, const char*)",
35
+ "char* rocksky_agent_like(void*, const char*, const char*)",
36
+ "char* rocksky_agent_follow(void*, const char*)",
37
+ "char* rocksky_agent_shout(void*, const char*, const char*, const char*)",
38
+ "char* rocksky_agent_refresh_session(void*)"
39
+ ].each { |sig| extern sig }
40
+ end
41
+ private_constant :C
42
+
43
+ # C strings the core returns aren't length-tagged; resolve length via strlen.
44
+ STRLEN = Fiddle::Function.new(
45
+ Fiddle::Handle::DEFAULT["strlen"], [Fiddle::TYPE_VOIDP], Fiddle::TYPE_SIZE_T
46
+ )
47
+ private_constant :STRLEN
48
+
49
+ # Copy an owned C string into a Ruby string and free the original.
50
+ def self.take_string(ptr)
51
+ return nil if ptr.nil? || ptr.null?
52
+
53
+ len = STRLEN.call(ptr)
54
+ str = ptr[0, len].force_encoding("UTF-8")
55
+ C.rocksky_string_free(ptr)
56
+ str
57
+ end
58
+
59
+ # Parse a `{"ok"|"error"}` envelope, raising Error on failure.
60
+ def self.unwrap(ptr)
61
+ parsed = JSON.parse(take_string(ptr))
62
+ raise Error, parsed["error"] if parsed.key?("error")
63
+
64
+ parsed["ok"]
65
+ end
66
+
67
+ # ---- reads (unauthenticated; base: overrides the AppView URL) ----
68
+
69
+ def self.profile(actor, base: nil)
70
+ unwrap(C.rocksky_profile(base.to_s, actor))
71
+ end
72
+
73
+ def self.scrobbles(actor, limit: 50, offset: 0, base: nil)
74
+ unwrap(C.rocksky_scrobbles(base.to_s, actor, limit, offset))
75
+ end
76
+
77
+ def self.top_tracks(limit: 50, offset: 0, base: nil)
78
+ unwrap(C.rocksky_top_tracks(base.to_s, limit, offset))
79
+ end
80
+
81
+ def self.global_stats(base: nil)
82
+ unwrap(C.rocksky_global_stats(base.to_s))
83
+ end
84
+
85
+ # Identity hash — identical across every Rocksky SDK.
86
+ def self.song_hash(title, artist, album)
87
+ take_string(C.rocksky_song_hash(title, artist, album))
88
+ end
89
+
90
+ # ---- authenticated agent ----
28
91
  #
29
- # See {Rocksky::Client#initialize} for all options.
30
- def self.new(**opts)
31
- Client.new(**opts)
92
+ # Records are passed as Hashes with camelCase keys (title, artist, album,
93
+ # albumArtist, durationMs, …), matching the wire record shape.
94
+ class Agent
95
+ def self.login(session_path, identifier, password, appview: nil)
96
+ ptr = C.rocksky_agent_login(session_path, identifier, password, appview.to_s)
97
+ raise Error, (Rocksky.take_string(C.rocksky_last_error()) || "login failed") if ptr.null?
98
+
99
+ new(ptr)
100
+ end
101
+
102
+ def initialize(ptr)
103
+ @ptr = ptr
104
+ end
105
+
106
+ # Scrobble a play; fans out to artist/album/song/scrobble. Returns the URIs.
107
+ def scrobble(track)
108
+ Rocksky.unwrap(C.rocksky_agent_scrobble(@ptr, JSON.generate(track)))
109
+ end
110
+
111
+ def like(uri, cid)
112
+ Rocksky.unwrap(C.rocksky_agent_like(@ptr, uri, cid))
113
+ end
114
+
115
+ def follow(did)
116
+ Rocksky.unwrap(C.rocksky_agent_follow(@ptr, did))
117
+ end
118
+
119
+ def shout(subject_uri, subject_cid, message)
120
+ Rocksky.unwrap(C.rocksky_agent_shout(@ptr, subject_uri, subject_cid, message))
121
+ end
122
+
123
+ def refresh_session
124
+ Rocksky.unwrap(C.rocksky_agent_refresh_session(@ptr))
125
+ end
126
+
127
+ # Release the native handle. The agent is unusable afterwards.
128
+ def close
129
+ return if @ptr.null?
130
+
131
+ C.rocksky_agent_free(@ptr)
132
+ @ptr = Fiddle::Pointer.new(0)
133
+ end
32
134
  end
33
135
  end
data/rocksky.gemspec CHANGED
@@ -6,9 +6,10 @@ Gem::Specification.new do |spec|
6
6
  spec.authors = ["Rocksky"]
7
7
  spec.email = ["hi@rocksky.app"]
8
8
 
9
- spec.summary = "Ruby client for the Rocksky XRPC API."
10
- spec.description = "Idiomatic Ruby SDK for Rocksky (rocksky.app) scrobbles, " \
11
- "shouts, charts, playlists, and more."
9
+ spec.summary = "Ruby SDK for Rocksky — native bindings to the shared Rust core."
10
+ spec.description = "Ruby SDK for Rocksky (rocksky.app): native bindings to the " \
11
+ "shared Rust core — AppView reads, AT Protocol PDS writes " \
12
+ "(scrobble, like, follow, shout), and identity hashes."
12
13
  spec.homepage = "https://github.com/tsirysndr/rocksky"
13
14
  spec.license = "MIT"
14
15
 
@@ -24,6 +25,9 @@ Gem::Specification.new do |spec|
24
25
 
25
26
  spec.files = Dir[
26
27
  "lib/**/*.rb",
28
+ # native-core download manifest (the lib itself is fetched on first load,
29
+ # never bundled — only *.rb + this json ship).
30
+ "lib/rocksky/manifest.json",
27
31
  "exe/*",
28
32
  "*.gemspec",
29
33
  "LICENSE",
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rocksky
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rocksky
@@ -65,8 +65,9 @@ dependencies:
65
65
  - - "~>"
66
66
  - !ruby/object:Gem::Version
67
67
  version: '3.19'
68
- description: Idiomatic Ruby SDK for Rocksky (rocksky.app) scrobbles, shouts, charts,
69
- playlists, and more.
68
+ description: 'Ruby SDK for Rocksky (rocksky.app): native bindings to the shared Rust
69
+ core — AppView reads, AT Protocol PDS writes (scrobble, like, follow, shout), and
70
+ identity hashes.'
70
71
  email:
71
72
  - hi@rocksky.app
72
73
  executables:
@@ -79,27 +80,8 @@ files:
79
80
  - README.md
80
81
  - exe/rocksky-console
81
82
  - lib/rocksky.rb
82
- - lib/rocksky/client.rb
83
- - lib/rocksky/error.rb
84
- - lib/rocksky/generated/types.rb
85
- - lib/rocksky/http.rb
86
- - lib/rocksky/resources/actor.rb
87
- - lib/rocksky/resources/album.rb
88
- - lib/rocksky/resources/apikey.rb
89
- - lib/rocksky/resources/artist.rb
90
- - lib/rocksky/resources/base.rb
91
- - lib/rocksky/resources/charts.rb
92
- - lib/rocksky/resources/feed.rb
93
- - lib/rocksky/resources/graph.rb
94
- - lib/rocksky/resources/like.rb
95
- - lib/rocksky/resources/mirror.rb
96
- - lib/rocksky/resources/player.rb
97
- - lib/rocksky/resources/playlist.rb
98
- - lib/rocksky/resources/scrobble.rb
99
- - lib/rocksky/resources/shout.rb
100
- - lib/rocksky/resources/song.rb
101
- - lib/rocksky/resources/spotify.rb
102
- - lib/rocksky/resources/stats.rb
83
+ - lib/rocksky/manifest.json
84
+ - lib/rocksky/native.rb
103
85
  - lib/rocksky/version.rb
104
86
  - rocksky.gemspec
105
87
  homepage: https://github.com/tsirysndr/rocksky
@@ -127,5 +109,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
127
109
  requirements: []
128
110
  rubygems_version: 4.0.10
129
111
  specification_version: 4
130
- summary: Ruby client for the Rocksky XRPC API.
112
+ summary: Ruby SDK for Rocksky — native bindings to the shared Rust core.
131
113
  test_files: []