misarblog 1.1.0 → 5.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 (4) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +261 -51
  3. data/lib/misarblog/client.rb +6 -1
  4. metadata +16 -7
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 668bfc444b17b3b94fcf0cee95e32d9b4666c8e592f2620e4bb1ca84900d96fc
4
- data.tar.gz: 5eab33effad1e6be69d8c8c6f7585afbc87b0b8031a05f030046256f5b2b74f7
3
+ metadata.gz: c8085dfc4b323a9b12d169e1e5a5f28a45bd28511c239f1a7d318bdb8c1d0db0
4
+ data.tar.gz: 17064b11fcb64cb83755c8d13fd5475b21edad5680da643b50f6c9e41c3a0dee
5
5
  SHA512:
6
- metadata.gz: '09713e47a720fb3028f62d13fb14b9650b7f079bac11f90a9576376e7d45454d8f90e68cf04f019f680916c7a8d809589e690adeec32c5f94ef36681a3697221'
7
- data.tar.gz: e2475931a76cdbbbbd1f388e66ad9197d7f48ffd49924f321ffe5e4d7674e69b0f5732344b0bbe5e386a6a49cb8b9a7257233b2e8661daac61b93aa98b7c75c5
6
+ metadata.gz: e586d6906800b2fd4917ee3468d3e8187d4f7e991d8e57af09a7f5f30d076439acddffa1df8ef9eb0075c2a59389164c09afedecc3a74d4320c688c4bc8c9445
7
+ data.tar.gz: 8934679336eafb88534627b17d73760f8f6d2f8c2fc33766366b57d0d8ffb5ca6eec44679eba5acdc74907df5aa060a68ede2f839f0d451bf9bec31dd02cd891
data/README.md CHANGED
@@ -1,6 +1,17 @@
1
1
  # Misar.Blog Ruby SDK
2
2
 
3
- Official Ruby client for the Misar.Blog developer API. Standard library only, retry with back-off.
3
+ > The official Ruby client for the Misar.Blog developer API.
4
+
5
+ [![gem](https://img.shields.io/gem/v/misarblog)](https://rubygems.org/gems/misarblog) [![Ruby](https://img.shields.io/badge/ruby-%3E%3D2.7-CC342D)](https://www.ruby-lang.org) [![license](https://img.shields.io/badge/license-MIT-green)](./LICENSE)
6
+
7
+ **9 resource groups · 25 operations · standard library only (net/http)**
8
+
9
+ Works with any Ruby 2.7+ program that needs to drive a
10
+ [Misar.Blog](https://www.misar.blog) account: automating publishing, syncing a
11
+ blog out of CI or another CMS, or building a reader, dashboard or integration on
12
+ top of the API at `https://api.misar.io/blog/v1`.
13
+
14
+ ---
4
15
 
5
16
  ## Install
6
17
 
@@ -8,74 +19,273 @@ Official Ruby client for the Misar.Blog developer API. Standard library only, re
8
19
  gem install misarblog
9
20
  ```
10
21
 
11
- ## Quick start
22
+ Or in a `Gemfile`:
23
+
24
+ ```ruby
25
+ gem "misarblog", "~> 1.1"
26
+ ```
27
+
28
+ Ruby 2.7+.
29
+
30
+ ---
31
+
32
+ ## Authentication
33
+
34
+ Mint a key at <https://www.misar.blog/dashboard/settings/api>. Keys are prefixed
35
+ `mbk_`; an OAuth 2.1 access token works on the same header. Key management
36
+ `MisarBlog.new(api_key:, base_url:, timeout:, max_retries:)` is a shorthand that
37
+ delegates to `MisarBlog::Client.new`; the key is sent as
38
+ `Authorization: Bearer`. See the first example below. The full request/response
39
+ contract is published as an OpenAPI document at
40
+ <https://api.misar.io/blog/v1/openapi.json>.
41
+
42
+ ---
43
+
44
+ ## API surface
45
+
46
+ | Resource | Method | Endpoint | What it does |
47
+ | --- | --- | --- | --- |
48
+ | `articles` | `list` | `GET /articles` | list your articles, filtered by status/visibility/sort |
49
+ | `articles` | `get` | `GET /articles/{slug}` | fetch one article by slug or UUID, full Markdown body |
50
+ | `articles` | `publish` | `POST /articles` | publish or schedule an article from Markdown |
51
+ | `articles` | `update` | `PATCH /articles/{slug}` | update title/body/tags in place; `publish: true` flips a draft live |
52
+ | `articles` | `create_draft` | `POST /drafts` | save a draft without publishing |
53
+ | `articles` | `search` | `GET /search` | full-text search across articles, profiles and tags |
54
+ | `articles` | `recommendations` | `GET /recommendations` | related articles for an article id |
55
+ | `series` | `list` | `GET /series` | list your series |
56
+ | `series` | `create` | `POST /series` | create a series |
57
+ | `series` | `add_article` | `POST /series/{slug}/articles` | add an article to a series at a position |
58
+ | `reactions` | `get` | `GET /reactions` | reaction counts and the caller's own reactions |
59
+ | `reactions` | `add` | `POST /reactions` | add a `like` / `clap` / `bookmark` |
60
+ | `reactions` | `remove` | `DELETE /reactions` | remove a reaction |
61
+ | `comments` | `list` | `GET /comments` | an article's comment thread, newest first, replies one level deep |
62
+ | `follows` | `status` | `GET /follows` | follower/following counts and whether the key's owner follows |
63
+ | `ai` | `complete` | `POST /ai/complete` | free-form system + user completion |
64
+ | `ai` | `titles` | `POST /ai/titles` | SEO/AEO/GEO title suggestions (`seo` from a keyword, `suggest` from copy) |
65
+ | `images` | `generate` | `POST /images/generate` | AI cover image (`1024x1024`, `1792x1024`, `1024x1792`) |
66
+ | `images` | `upload` | `POST /images/upload` | upload an image to the CDN |
67
+ | `account` | `profile` | `GET /me` | the authenticated creator profile |
68
+ | `account` | `plan` | `GET /plan` | live plan and per-feature quota |
69
+ | `account` | `trial_status` | `GET /trial` | whether a self-serve trial is active |
70
+ | `account` | `start_trial` | `POST /trial` | start a self-serve trial |
71
+ | `account` | `upsell_funnel` | `GET /upsell-funnel` | per-feature upsell funnel (platform-admin keys only; a creator key gets 403) |
72
+ | `analytics` | `get` | `GET /analytics` | views, gross/net revenue, active subscribers for trailing N days |
73
+
74
+ Note the two groupings that differ from the other Misar.Blog SDKs: profile,
75
+ plan, trial and upsell all hang off `account`, and the analytics summary is
76
+ `analytics.get` rather than `analytics.summary`.
77
+
78
+ ---
79
+
80
+ ## What's in the package
81
+
82
+ | Item | What it is |
83
+ | --- | --- |
84
+ | `MisarBlog::Client` | The client. `MisarBlog.new(api_key:, base_url:, timeout: 30, max_retries: 3)` is the shorthand constructor. Resource readers: `articles`, `series`, `reactions`, `comments`, `follows`, `ai`, `images`, `account`, `analytics`. |
85
+ | Errors | `MisarBlog::ApiError`, `MisarBlog::PlanLimitError`, `MisarBlog::NetworkError`. |
86
+ | `MisarBlog.embed_url(username:, slug:, theme:)` | Pure string building for public embeds. |
87
+ | `MisarBlog::Models` | `Article`, `ArticleList`, `Series`, `SeriesList`, `Profile`, `Plan`, `PlanUsage`, `Analytics`, `ArticleReactions`, `ReactionResult`, `TrialStatus`, `TitlesResult`, `TitleSuggestion`, `AiText`, `ImageResult`. Each wraps the decoded body: named readers for the documented fields, `#[]` for string-key access, and `#raw` / `#to_h` for the untouched Hash, so a field the API adds after this release is still reachable. |
88
+ | `client.request(method, path, data)` | Public, so an endpoint this SDK does not wrap yet is still one call away. |
89
+
90
+ **Not everything is modelled.** `comments.list`, `follows.status`,
91
+ `articles.search`, `articles.recommendations`, `account.start_trial` and
92
+ `account.upsell_funnel` return the plain decoded `Hash` with string keys.
93
+
94
+ **Transport.** Standard library only — `net/http`, `uri`, `json`; no runtime
95
+ gem dependencies. Base URL `https://api.misar.io/blog/v1`; the key goes on
96
+ `Authorization: Bearer`. Statuses 429/500/502/503/504 and connection failures
97
+ are retried up to `max_retries` attempts (default 3) with exponential back-off
98
+ from 300 ms; the final attempt is always surfaced. Open timeout 10 s, read
99
+ timeout `timeout:` (default 30 s). A `204` or empty body returns `{}`.
100
+
101
+ **No streaming or webhooks.** Every operation is a single request/response. No
102
+ SSE or WebSocket endpoint accepts an API key, and the API has no webhook
103
+ registration route — `webhook_only` is an article *visibility* value, not a
104
+ subscription.
105
+
106
+ ---
107
+
108
+ ## Examples
109
+
110
+ ### Authenticate and publish
12
111
 
13
112
  ```ruby
14
113
  require "misarblog"
15
114
 
16
- blog = MisarBlog.new(api_key: "mbk_...")
115
+ blog = MisarBlog.new(api_key: ENV.fetch("MISARBLOG_API_KEY"))
17
116
 
18
117
  me = blog.account.profile
19
- thread = blog.comments.list(article_id: "article-id", limit: 50)
20
- follows = blog.follows.status(user_id: me["id"])
118
+ puts "authenticated as @#{me.username}"
21
119
 
22
- begin
23
- blog.ai.complete(prompt: "Draft an intro paragraph")
24
- rescue MisarBlog::PlanLimitError => e
25
- puts "#{e.plan} plan is out of credits — upgrade at #{e.upgrade_url}"
120
+ article = blog.articles.publish(
121
+ title: "Shipping a blog from CI",
122
+ body_markdown: "# Shipping a blog from CI\n\nMarkdown in, article out.",
123
+ tags: %w[ci automation]
124
+ )
125
+ puts article.url
126
+ ```
127
+
128
+ ### Publish (or schedule) an article
129
+
130
+ ```ruby
131
+ article = blog.articles.publish(
132
+ title: "Hello, Misar",
133
+ body_markdown: "# Hello\n\nFirst post.",
134
+ tags: ["intro"],
135
+ cover_image_url: "https://cdn.example.com/cover.png",
136
+ visibility: "public", # public | subscribers | paid | private | webhook_only
137
+ schedule_at: "2026-09-01T09:00:00Z" # omit to publish immediately
138
+ )
139
+ puts "#{article.slug} #{article.status} #{article.url}"
140
+ ```
141
+
142
+ `title:` and `body_markdown:` are required keywords; every other keyword is
143
+ dropped from the request body when left nil.
144
+
145
+ ### Save a draft
146
+
147
+ ```ruby
148
+ draft = blog.articles.create_draft(
149
+ title: "Work in progress",
150
+ body_markdown: "Notes so far…",
151
+ tags: ["draft"]
152
+ )
153
+ puts draft.editor_url # open in the Misar.Blog editor
154
+ ```
155
+
156
+ ### List your articles
157
+
158
+ ```ruby
159
+ result = blog.articles.list(status: "published", limit: 20)
160
+ result.articles.each { |a| puts "#{a.slug} #{a["view_count"]}" }
161
+ puts "#{result.articles.size} of #{result.total}"
162
+ ```
163
+
164
+ `status:` accepts `draft`, `published`, `scheduled`, `archived` or `flagged`;
165
+ `visibility:`, `webhook_only:` and `sort:` narrow it further. `Article` exposes
166
+ the common fields as readers and everything else through `#[]` or `#raw`.
167
+
168
+ ### Update an article — and publish a draft
169
+
170
+ ```ruby
171
+ updated = blog.articles.update(
172
+ "work-in-progress",
173
+ title: "Finished at last",
174
+ body_markdown: "The complete post.",
175
+ publish: true # flips a draft to published in the same call
176
+ )
177
+ puts "#{updated.status} #{updated.published_at}"
178
+ ```
179
+
180
+ The slug is positional; everything else is a keyword, and omitted keywords are
181
+ left out of the body so those fields stay unchanged.
182
+
183
+ ### Read an article's comment thread
184
+
185
+ ```ruby
186
+ thread = blog.comments.list(article_id: article.id, limit: 50, offset: 0)
187
+ thread["comments"].each do |c|
188
+ puts "@#{c["user"]["username"]}: #{c["content"]} (#{c["reply_count"]} replies)"
26
189
  end
190
+ puts "#{thread["totalCount"]} #{thread["hasMore"]}"
27
191
  ```
28
192
 
29
- ## Authentication and plan gating
193
+ This one returns a plain Hash, not a model. Leave `limit:`/`offset:` out to take
194
+ the server defaults of 20 (max 100) and 0.
30
195
 
31
- Every call goes through the metered gateway at `https://api.misar.io/blog/v1`
32
- with your developer key as a Bearer token. Mint a key in the dashboard at
33
- <https://www.misar.blog/dashboard/settings/api> — key management is a
34
- cookie-session flow and is deliberately not exposed by this SDK.
196
+ ### Read and add reactions
35
197
 
36
- Feature access and throughput follow the subscription attached to that key:
198
+ ```ruby
199
+ counts = blog.reactions.get(article_id: article.id)
200
+ puts "#{counts.counts["clap"]} #{counts.total} #{counts.user_reactions}"
37
201
 
38
- | Signal | Meaning |
39
- | --- | --- |
40
- | `401` | Missing, expired or revoked key |
41
- | `403` | The key is scoped and lacks the scope this route needs |
42
- | `429` (plain) | Rate limit — 100 requests/minute per key. The SDK retries with back-off |
43
- | `429` + `plan_limit_exceeded` | A metered allowance is spent. Retrying will not help until it resets |
44
- | `402` + `plan_limit_exceeded` | The feature is not on this plan |
202
+ blog.reactions.add(article_id: article.id, type: "clap") # like | clap | bookmark
203
+ blog.reactions.remove(article_id: article.id, type: "clap")
204
+ ```
45
205
 
46
- The last two raise ``MisarBlog::PlanLimitError`` rather than a generic error, carrying the
47
- plan slug, the pricing URL and (when the API supplies it) seconds until reset.
48
- Show the upgrade URL instead of reporting a bare failure — the SDK does not
49
- retry these, because retrying cannot change the outcome.
206
+ ### Generate SEO titles
50
207
 
51
- ## Covered operations
208
+ ```ruby
209
+ result = blog.ai.titles(
210
+ action: "seo", # "seo" from a keyword, "suggest" from existing copy
211
+ prompt: "shipping a static blog from GitHub Actions"
212
+ )
213
+ result.titles.each { |t| puts "#{t.title} — #{t.hint}" }
214
+ ```
52
215
 
53
- All 25 key-authenticated operations:
216
+ For `"suggest"`, pass the article text as `context:` instead of `prompt:`.
54
217
 
55
- | Group | Operations |
56
- | --- | --- |
57
- | Articles | list, get, create, update, create draft, search, recommendations |
58
- | Series | list, create, add article |
59
- | Reactions | get, add, remove |
60
- | Comments | list |
61
- | Follows | status |
62
- | AI | complete, titles |
63
- | Images | generate, upload |
64
- | Account | profile, plan, trial status, start trial |
65
- | Analytics | summary, upsell funnel |
66
-
67
- The API exposes no SSE or WebSocket endpoint that accepts an API key, so this
68
- SDK is request/response only. See [`openapi/blog.openapi.json`][spec] for the
69
- machine-readable contract.
70
-
71
- [spec]: https://api.misar.io/blog/v1/openapi.json
218
+ ### Read the analytics summary
72
219
 
73
- ## Links
220
+ ```ruby
221
+ summary = blog.analytics.get(days: 30)
222
+ puts "#{summary.views} #{summary.revenue_cents} #{summary.active_subscribers}"
223
+ ```
224
+
225
+ ### Generate a cover image
226
+
227
+ ```ruby
228
+ image = blog.images.generate(
229
+ prompt: "a dark editorial illustration of a printing press",
230
+ size: "1792x1024"
231
+ )
232
+ puts image.url
233
+ ```
74
234
 
75
- - API docs<https://docs.misar.io/blog>
76
- - OpenAPI spec <https://api.misar.io/blog/v1/openapi.json>
77
- - Dashboard <https://www.misar.blog/dashboard/settings/api>
235
+ `blog.images.upload(data)` posts to the CDN upload route as JSON pass the Hash
236
+ the API expects (a base64 `data` field). This SDK does not build a multipart
237
+ request for you; the Go and Python clients do.
238
+
239
+ ### Embed a public article
240
+
241
+ ```ruby
242
+ puts MisarBlog.embed_url(username: "gulshan", slug: "hello-misar", theme: "dark")
243
+ # https://misar.blog/gulshan/hello-misar/embed?theme=dark
244
+ ```
245
+
246
+ Omit `slug:` to embed the whole profile; `theme:` defaults to `"auto"`, which
247
+ adds no query parameter.
248
+
249
+ ---
250
+
251
+ ## Errors
252
+
253
+ Every failure raises. `PlanLimitError` and `NetworkError` both subclass
254
+ `ApiError`, which subclasses `StandardError` — so a single `rescue
255
+ MisarBlog::ApiError` catches everything from this SDK. Order narrowest-first.
256
+
257
+ | Type | Raised when | Readers |
258
+ | --- | --- | --- |
259
+ | `ApiError` | Any non-2xx the SDK did not classify further — `400` bad payload, `401` missing/expired/revoked key, `403` the key lacks the route's scope, `404` unknown slug, plain `429` rate limit (100 req/min per key) after retries are exhausted, `5xx` after retries | `status`, `error_type`, `body` (the decoded error Hash) |
260
+ | `PlanLimitError` | The subscription blocks the call: `429` + `code: "plan_limit_exceeded"` (a metered allowance is spent) or `402` (the feature is not on this plan). **Never retried** — retrying cannot help until the allowance resets or the plan changes | `plan`, `upgrade_url`, `retry_after`, `upgrade` |
261
+ | `NetworkError` | The request never reached the API — DNS, TLS, connection refused/reset, open or read timeout — on the final attempt | `cause_error`; `status` is `0` |
262
+
263
+ ```ruby
264
+ begin
265
+ blog.ai.complete(prompt: "Draft an intro paragraph")
266
+ rescue MisarBlog::PlanLimitError => e
267
+ # Route the reader to checkout instead of reporting a bare failure.
268
+ puts "#{e.plan} plan is out of credits — upgrade at #{e.upgrade_url}"
269
+ rescue MisarBlog::NetworkError => e
270
+ puts "could not reach the API: #{e.cause_error}"
271
+ rescue MisarBlog::ApiError => e
272
+ puts "#{e.status} #{e.body && e.body["required_scope"]}"
273
+ end
274
+ ```
275
+
276
+ The 403 scope details are not promoted to named readers here — read
277
+ `required_scope` and `granted_scopes` off `e.body`. If a failure lands in the
278
+ wrong class, file it at <https://github.com/Misar-AI/misarblog-sdks/issues>.
279
+
280
+ ---
281
+
282
+ ## Links
78
283
 
79
- ## License
284
+ - Website — https://www.misar.blog
285
+ - App — https://www.misar.blog
286
+ - Parent — https://misar.io
287
+ - Documentation — https://docs.misar.io/blog
288
+ - Source — https://github.com/Misar-AI/misarblog-sdks
289
+ - RubyGems — https://rubygems.org/gems/misarblog
80
290
 
81
- MIT see [LICENSE](LICENSE).
291
+ MIT © [Misar AI](https://misar.io)
@@ -347,7 +347,10 @@ module MisarBlog
347
347
  end
348
348
 
349
349
  def parse_response(resp, status)
350
- return {} if status == 204 || resp.body.nil? || resp.body.empty?
350
+ # Only a *successful* empty body means "no content". An error with an empty
351
+ # body — a bare 401, or anything a proxy stripped — must still raise, or the
352
+ # caller reads a failure as an empty result set.
353
+ return {} if status < 400 && (status == 204 || resp.body.nil? || resp.body.empty?)
351
354
 
352
355
  decoded = begin
353
356
  JSON.parse(resp.body)
@@ -357,6 +360,8 @@ module MisarBlog
357
360
 
358
361
  if status >= 400
359
362
  msg = decoded.is_a?(Hash) ? (decoded["error"] || decoded["message"] || resp.body) : resp.body
363
+ # A stripped body leaves nothing to report; fall back to the reason phrase.
364
+ msg = resp.message.to_s if msg.nil? || msg.empty?
360
365
  if decoded.is_a?(Hash) && decoded["code"] == "plan_limit_exceeded"
361
366
  raise PlanLimitError.new(status, msg, decoded, response_headers(resp))
362
367
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: misarblog
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 5.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Misar AI
@@ -38,9 +38,14 @@ dependencies:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
40
  version: '3.23'
41
- description: Full-featured Ruby SDK for the Misar.Blog developer API (api.misar.io/blog/v1).
42
- Covers all 25 dev-API operations with typed models, mbk_ bearer auth, and retry
43
- with exponential backoff.
41
+ description: 'Ruby client for the developer API of Misar.Blog (misar.blog), a hosted
42
+ blogging platform. Publish or schedule Markdown articles, save and update drafts,
43
+ group articles into series, read comment threads, reactions and follows, pull an
44
+ analytics summary and live plan/quota state, generate SEO/AEO/GEO title suggestions,
45
+ completions and AI cover images, search articles, profiles and tags, and build public
46
+ iframe embed URLs — all 25 key-authenticated operations. Standard library only (net/http):
47
+ mbk_ bearer auth, retry with exponential back-off, and a typed PlanLimitError carrying
48
+ the upgrade URL.'
44
49
  email:
45
50
  - hello@misar.io
46
51
  executables: []
@@ -55,13 +60,15 @@ files:
55
60
  - lib/misarblog/embed.rb
56
61
  - lib/misarblog/errors.rb
57
62
  - lib/misarblog/models.rb
58
- homepage: https://www.misar.blog/docs/sdks/ruby
63
+ homepage: https://www.misar.blog
59
64
  licenses:
60
65
  - MIT
61
66
  metadata:
62
- homepage_uri: https://www.misar.blog/docs/sdks/ruby
67
+ homepage_uri: https://www.misar.blog
63
68
  source_code_uri: https://github.com/Misar-AI/misarblog-sdks
64
69
  changelog_uri: https://github.com/Misar-AI/misarblog-sdks/releases
70
+ documentation_uri: https://docs.misar.io/blog
71
+ bug_tracker_uri: https://github.com/Misar-AI/misarblog-sdks/issues
65
72
  post_install_message:
66
73
  rdoc_options: []
67
74
  require_paths:
@@ -80,5 +87,7 @@ requirements: []
80
87
  rubygems_version: 3.5.22
81
88
  signing_key:
82
89
  specification_version: 4
83
- summary: Official Ruby SDK for Misar.Blog articles, series, reactions, AI, analytics
90
+ summary: 'Ruby client for misar.blog, a hosted blogging platform: publish and schedule
91
+ Markdown articles, manage drafts and series, read comments, reactions, follows and
92
+ analytics, and generate AI titles and covers.'
84
93
  test_files: []