misarpost 1.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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: ed242bb7e51c5f0dc6ca950f1c79d10cbef43efd93da2eb8b5650f9418a7499d
4
+ data.tar.gz: daec82825bf4f1e66fdc557ec61c5994a4e71b029444b139ca58cfb951b1ccf0
5
+ SHA512:
6
+ metadata.gz: 48709887643f1f548d4f9bc4183de0a4558cdfcff761e473923820a735d0876161d24b48f7ddadb19f790acd237e6e6b80a6ee8ef8142c00a6962ce7c033bb36
7
+ data.tar.gz: 1f140c8d6ef3bc78d8f4500dc2772617c0efe0cd23d16e60ee27fbeae41bfddc5c6206cba62ef89a831ecf7f58b92a91dd2f48604074690fb4158ff3072f2881
data/CHANGELOG.md ADDED
@@ -0,0 +1,17 @@
1
+ # Changelog
2
+
3
+ All notable changes to this SDK are documented here. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project uses
5
+ [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [1.0.0] — 2026-10-16
8
+
9
+ First release of `misarpost (RubyGems)`.
10
+
11
+ - Every key-authenticated MisarPost endpoint, generated from `sdks/manifest.json` —
12
+ the same surface in all twelve MisarPost SDKs and the CLI.
13
+ - Canonical base URL `https://api.misar.io/post` (also served at `https://post.misar.io/api`).
14
+ - Plan refusals (`code: "plan_limit_exceeded"`, HTTP 402) surface as a typed plan-limit
15
+ error carrying the server's upgrade offer; detected by code before the status and never retried.
16
+ - Retries for throttles, 5xx and network failures with `Retry-After` support.
17
+ - Pagination helper for `?page=` list endpoints and `X-MisarPost-Signature` webhook verification.
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Misar.AI
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,250 @@
1
+ # MisarPost Ruby SDK
2
+
3
+ > Schedule, publish, generate and analyze social content from Ruby — standard library only.
4
+
5
+ [![Gem](https://img.shields.io/gem/v/misarpost)](https://rubygems.org/gems/misarpost)
6
+ [![license](https://img.shields.io/badge/license-MIT-green)](./LICENSE)
7
+
8
+ **17 resource groups · 102 methods · zero runtime dependencies**
9
+
10
+ MisarPost is the AI social command center: publish to X, LinkedIn, Instagram, Threads, Bluesky, TikTok and more from one API, repurpose long-form content with ContentRipple, research trends with TrendPulse and write in your own voice with PersonaForge.
11
+
12
+ The resource layer is generated from the API's own route tree, so it always matches the server. Ruby 2.6+, `Net::HTTP` under the hood.
13
+
14
+ ---
15
+
16
+ ## Install
17
+
18
+ ### Bundler
19
+
20
+ ```ruby
21
+ gem "misarpost"
22
+ ```
23
+
24
+ ### gem
25
+
26
+ ```bash
27
+ gem install misarpost
28
+ ```
29
+
30
+ ---
31
+
32
+ ## Authentication
33
+
34
+ Create an API key at https://post.misar.io/dashboard/api-keys. It starts with `mpk_` and is
35
+ sent as `Authorization: Bearer mpk_…`. Each key carries scopes (`posts:read`,
36
+ `schedule:write`, `generate:write`, …); a call outside them answers **403** with
37
+ `code: "insufficient_scope"`. New keys get every scope unless you narrow them.
38
+
39
+ ```ruby
40
+ require "misarpost"
41
+
42
+ post = MisarPost::Client.new(ENV.fetch("MISARPOST_API_KEY"))
43
+ ```
44
+
45
+ ---
46
+
47
+ ## Resources
48
+
49
+ | Resource | Methods | Scopes |
50
+ | --- | --- | --- |
51
+ | `post.analytics` | `stats`, `trends` | `analytics:read` |
52
+ | `post.composer` | `discord_channels`, `hashnode_publications`, `hashnode_tags`, `lemmy_communities`, `listmonk_lists`, `listmonk_templates`, `medium_publications`, `pinterest_boards`, `reddit_flairs`, `skool_groups`, `slack_channels`, `whop_forums` | `connections:read` |
53
+ | `post.connections` | `list`, `plugs`, `update_plugs`, `wordpress_taxonomies` | `connections:read`, `connections:write` |
54
+ | `post.contentripple` | `delete_history`, `feed`, `history`, `parse`, `repurpose`, `schedule`, `update_variants`, `usage` | `posts:write`, `generate:write`, `posts:read`, `schedule:write` |
55
+ | `post.drafts` | `create`, `list` | `posts:write`, `posts:read` |
56
+ | `post.generate` | `create`, `enhance`, `hashtags`, `image`, `optimize`, `plan` | `generate:write` |
57
+ | `post.growth` | `next_opportunity`, `opportunities`, `overview`, `playbooks` | `analytics:read` |
58
+ | `post.media` | `delete`, `list` | `media:write`, `media:read` |
59
+ | `post.notifications` | `list`, `read_all`, `update` | `notifications:read`, `notifications:write` |
60
+ | `post.personaforge` | `approve_replies`, `bio`, `create_competitors`, `create_persona`, `create_replies`, `create_voice`, `delete_persona`, `generate`, `get_persona`, `get_voice`, `list_competitors`, `list_personas`, `list_replies`, `persona_status`, `update_persona` | `personas:write`, `generate:write`, `personas:read` |
61
+ | `post.posts` | `calendar`, `cancel`, `delete`, `list`, `reply`, `revert`, `revert_batch`, `schedule`, `share`, `unshare`, `update` | `posts:read`, `posts:write`, `schedule:write` |
62
+ | `post.post_tags` | `create`, `delete`, `list`, `update` | `posts:write`, `posts:read` |
63
+ | `post.post_templates` | `create`, `delete`, `get`, `list`, `update` | `posts:write`, `posts:read` |
64
+ | `post.trendpulse` | `create_platform_content`, `digest`, `fetch`, `hooks`, `list`, `list_platform_content`, `performance`, `pipeline`, `refresh`, `use_platform_content` | `trends:write`, `trends:read`, `analytics:read`, `schedule:write` |
65
+ | `post.video` | `avatars`, `generate`, `script`, `status`, `voices` | `media:read`, `generate:write` |
66
+ | `post.webhooks` | `create_outbound`, `delete_outbound`, `list_outbound`, `update_outbound` | `webhooks:write`, `webhooks:read` |
67
+ | `post.workspaces` | `add_member`, `create`, `list`, `members`, `remove_member` | `workspaces:write`, `workspaces:read` |
68
+
69
+ Methods take path parameters positionally, then `body` (a Hash, when the endpoint reads one), then `query:`.
70
+
71
+ ---
72
+
73
+ ## Client
74
+
75
+ | Thing | Detail |
76
+ | --- | --- |
77
+ | Entry point | `MisarPost::Client.new(api_key, base_url:, max_retries: 3, timeout: 30, http: adapter)` |
78
+ | `base_url` | `https://api.misar.io/post` (also served at `https://post.misar.io/api`) |
79
+ | Transport | `Net::HTTP`; inject any `#call(method, uri, headers, body, timeout)` adapter with `http:`. |
80
+ | Retried | `429` throttles, `500`, `502`, `503`, `504` and network failures — exponential backoff from 200 ms honouring `Retry-After`. |
81
+ | Never retried | Plan refusals (`MisarPost::PlanLimitError`). |
82
+ | Errors | `MisarPost::Error`, `MisarPost::NetworkError`, `MisarPost::PlanLimitError` |
83
+ | Also exported | `MisarPost.paginate`, `MisarPost.verify_webhook_signature` |
84
+
85
+ ---
86
+
87
+ ## Quick start
88
+
89
+ ```ruby
90
+ post = MisarPost::Client.new("mpk_your_key")
91
+
92
+ accounts = post.connections.list["accounts"]
93
+ post.generate.create("prompt" => "Announce our v2 launch", "platforms" => ["linkedin"])
94
+ post.posts.schedule(
95
+ "accountIds" => [accounts.first["id"]],
96
+ "content" => "We just shipped v2",
97
+ "scheduledAt" => "2026-11-01T09:00:00Z"
98
+ )
99
+ ```
100
+
101
+ ## Pagination
102
+
103
+ ```ruby
104
+ MisarPost.paginate { |n| post.posts.list(query: { "status" => "queued", "page" => n }) }.each do |page|
105
+ page["posts"].each { |p| puts p["id"] }
106
+ end
107
+ ```
108
+
109
+ ## Plan limits
110
+
111
+ When a monthly allowance is spent, or a feature is not on the plan, the API answers
112
+ **402** with `code: "plan_limit_exceeded"` and an `upgrade` offer. The SDK detects the
113
+ code before it looks at the status, raises `MisarPost::PlanLimitError`, and **never retries
114
+ it**. All copy comes from the server.
115
+
116
+ ```ruby
117
+ begin
118
+ post.generate.enhance("content" => "…")
119
+ rescue MisarPost::PlanLimitError => e
120
+ puts e.offer["headline"], e.offer["cta"]
121
+ puts "Upgrade: #{e.upgrade_url} (resets in #{e.retry_after}s)"
122
+ end
123
+ ```
124
+
125
+ ## Webhooks
126
+
127
+ ```ruby
128
+ ok = MisarPost.verify_webhook_signature(secret, request.raw_post, request.headers["X-MisarPost-Signature"])
129
+ ```
130
+
131
+ ## Method reference
132
+
133
+ | Call | HTTP | Scope |
134
+ | --- | --- | --- |
135
+ | `post.analytics.stats` | `GET /stats` | `analytics:read` |
136
+ | `post.analytics.trends` | `GET /analytics/trends` | `analytics:read` |
137
+ | `post.composer.discord_channels` | `GET /composer/discord/channels` | `connections:read` |
138
+ | `post.composer.hashnode_publications` | `GET /composer/hashnode/publications` | `connections:read` |
139
+ | `post.composer.hashnode_tags` | `GET /composer/hashnode/tags` | `connections:read` |
140
+ | `post.composer.lemmy_communities` | `GET /composer/lemmy/communities` | `connections:read` |
141
+ | `post.composer.listmonk_lists` | `GET /composer/listmonk/lists` | `connections:read` |
142
+ | `post.composer.listmonk_templates` | `GET /composer/listmonk/templates` | `connections:read` |
143
+ | `post.composer.medium_publications` | `GET /composer/medium/publications` | `connections:read` |
144
+ | `post.composer.pinterest_boards` | `GET /composer/pinterest/boards` | `connections:read` |
145
+ | `post.composer.reddit_flairs` | `GET /composer/reddit/flairs` | `connections:read` |
146
+ | `post.composer.skool_groups` | `GET /composer/skool/groups` | `connections:read` |
147
+ | `post.composer.slack_channels` | `GET /composer/slack/channels` | `connections:read` |
148
+ | `post.composer.whop_forums` | `GET /composer/whop/forums` | `connections:read` |
149
+ | `post.connections.list` | `GET /connections` | `connections:read` |
150
+ | `post.connections.plugs` | `GET /connections/:accountId/plugs` | `connections:read` |
151
+ | `post.connections.update_plugs` | `PATCH /connections/:accountId/plugs` | `connections:write` |
152
+ | `post.connections.wordpress_taxonomies` | `GET /connections/:accountId/wordpress-taxonomies` | `connections:read` |
153
+ | `post.contentripple.delete_history` | `DELETE /contentripple/history` | `posts:write` |
154
+ | `post.contentripple.feed` | `POST /contentripple/feed` | `generate:write` |
155
+ | `post.contentripple.history` | `GET /contentripple/history` | `posts:read` |
156
+ | `post.contentripple.parse` | `POST /contentripple/parse` | `generate:write` |
157
+ | `post.contentripple.repurpose` | `POST /contentripple` | `generate:write` |
158
+ | `post.contentripple.schedule` | `POST /contentripple/schedule` | `schedule:write` |
159
+ | `post.contentripple.update_variants` | `PATCH /contentripple/variants` | `posts:write` |
160
+ | `post.contentripple.usage` | `GET /contentripple` | `posts:read` |
161
+ | `post.drafts.create` | `POST /drafts` | `posts:write` |
162
+ | `post.drafts.list` | `GET /drafts` | `posts:read` |
163
+ | `post.generate.create` | `POST /generate` | `generate:write` |
164
+ | `post.generate.enhance` | `POST /generate/enhance` | `generate:write` |
165
+ | `post.generate.hashtags` | `POST /generate/hashtags` | `generate:write` |
166
+ | `post.generate.image` | `POST /generate/image` | `generate:write` |
167
+ | `post.generate.optimize` | `GET /generate/optimize` | `generate:write` |
168
+ | `post.generate.plan` | `POST /generate/plan` | `generate:write` |
169
+ | `post.growth.next_opportunity` | `GET /growth/next-opportunity` | `analytics:read` |
170
+ | `post.growth.opportunities` | `GET /growth/opportunities` | `analytics:read` |
171
+ | `post.growth.overview` | `GET /growth/overview` | `analytics:read` |
172
+ | `post.growth.playbooks` | `GET /growth/playbooks` | `analytics:read` |
173
+ | `post.media.delete` | `DELETE /media/:id` | `media:write` |
174
+ | `post.media.list` | `GET /media` | `media:read` |
175
+ | `post.notifications.list` | `GET /notifications` | `notifications:read` |
176
+ | `post.notifications.read_all` | `POST /notifications/read-all` | `notifications:write` |
177
+ | `post.notifications.update` | `PATCH /notifications/:id` | `notifications:write` |
178
+ | `post.personaforge.approve_replies` | `POST /personaforge/replies/approve` | `personas:write` |
179
+ | `post.personaforge.bio` | `POST /personaforge/bio` | `generate:write` |
180
+ | `post.personaforge.create_competitors` | `POST /personaforge/competitors` | `personas:write` |
181
+ | `post.personaforge.create_persona` | `POST /personaforge/personas` | `personas:write` |
182
+ | `post.personaforge.create_replies` | `POST /personaforge/replies` | `personas:write` |
183
+ | `post.personaforge.create_voice` | `POST /personaforge/voice` | `personas:write` |
184
+ | `post.personaforge.delete_persona` | `DELETE /personaforge/personas/:id` | `personas:write` |
185
+ | `post.personaforge.generate` | `POST /personaforge/generate` | `generate:write` |
186
+ | `post.personaforge.get_persona` | `GET /personaforge/personas/:id` | `personas:read` |
187
+ | `post.personaforge.get_voice` | `GET /personaforge/voice` | `personas:read` |
188
+ | `post.personaforge.list_competitors` | `GET /personaforge/competitors` | `personas:read` |
189
+ | `post.personaforge.list_personas` | `GET /personaforge/personas` | `personas:read` |
190
+ | `post.personaforge.list_replies` | `GET /personaforge/replies` | `personas:read` |
191
+ | `post.personaforge.persona_status` | `GET /personaforge/personas/status/:jobId` | `personas:read` |
192
+ | `post.personaforge.update_persona` | `PATCH /personaforge/personas/:id` | `personas:write` |
193
+ | `post.posts.calendar` | `GET /calendar` | `posts:read` |
194
+ | `post.posts.cancel` | `DELETE /posts` | `posts:write` |
195
+ | `post.posts.delete` | `DELETE /posts/:id` | `posts:write` |
196
+ | `post.posts.list` | `GET /posts` | `posts:read` |
197
+ | `post.posts.reply` | `POST /posts/:id/reply` | `posts:write` |
198
+ | `post.posts.revert` | `POST /posts/:id/revert` | `posts:write` |
199
+ | `post.posts.revert_batch` | `POST /posts/revert-batch` | `posts:write` |
200
+ | `post.posts.schedule` | `POST /schedule` | `schedule:write` |
201
+ | `post.posts.share` | `POST /posts/:id/share` | `posts:write` |
202
+ | `post.posts.unshare` | `DELETE /posts/:id/share` | `posts:write` |
203
+ | `post.posts.update` | `PATCH /posts/:id` | `posts:write` |
204
+ | `post.post_tags.create` | `POST /post-tags` | `posts:write` |
205
+ | `post.post_tags.delete` | `DELETE /post-tags/:id` | `posts:write` |
206
+ | `post.post_tags.list` | `GET /post-tags` | `posts:read` |
207
+ | `post.post_tags.update` | `PATCH /post-tags/:id` | `posts:write` |
208
+ | `post.post_templates.create` | `POST /post-templates` | `posts:write` |
209
+ | `post.post_templates.delete` | `DELETE /post-templates/:id` | `posts:write` |
210
+ | `post.post_templates.get` | `GET /post-templates/:id` | `posts:read` |
211
+ | `post.post_templates.list` | `GET /post-templates` | `posts:read` |
212
+ | `post.post_templates.update` | `PATCH /post-templates/:id` | `posts:write` |
213
+ | `post.trendpulse.create_platform_content` | `POST /trendpulse/platform-content` | `trends:write` |
214
+ | `post.trendpulse.digest` | `POST /trendpulse/digest` | `trends:write` |
215
+ | `post.trendpulse.fetch` | `POST /trendpulse/fetch` | `trends:write` |
216
+ | `post.trendpulse.hooks` | `GET /trendpulse/hooks` | `trends:read` |
217
+ | `post.trendpulse.list` | `GET /trendpulse` | `trends:read` |
218
+ | `post.trendpulse.list_platform_content` | `GET /trendpulse/platform-content` | `trends:read` |
219
+ | `post.trendpulse.performance` | `GET /trendpulse/performance` | `analytics:read` |
220
+ | `post.trendpulse.pipeline` | `POST /trendpulse/pipeline` | `schedule:write` |
221
+ | `post.trendpulse.refresh` | `POST /trendpulse` | `trends:write` |
222
+ | `post.trendpulse.use_platform_content` | `POST /trendpulse/platform-content/use` | `trends:write` |
223
+ | `post.video.avatars` | `GET /video/avatars` | `media:read` |
224
+ | `post.video.generate` | `POST /video/generate` | `generate:write` |
225
+ | `post.video.script` | `POST /video/script` | `generate:write` |
226
+ | `post.video.status` | `GET /video/status/:jobId` | `media:read` |
227
+ | `post.video.voices` | `GET /video/voices` | `media:read` |
228
+ | `post.webhooks.create_outbound` | `POST /webhooks/outbound` | `webhooks:write` |
229
+ | `post.webhooks.delete_outbound` | `DELETE /webhooks/outbound/:id` | `webhooks:write` |
230
+ | `post.webhooks.list_outbound` | `GET /webhooks/outbound` | `webhooks:read` |
231
+ | `post.webhooks.update_outbound` | `PATCH /webhooks/outbound/:id` | `webhooks:write` |
232
+ | `post.workspaces.add_member` | `POST /workspaces/:id/members` | `workspaces:write` |
233
+ | `post.workspaces.create` | `POST /workspaces` | `workspaces:write` |
234
+ | `post.workspaces.list` | `GET /workspaces` | `workspaces:read` |
235
+ | `post.workspaces.members` | `GET /workspaces/:id/members` | `workspaces:read` |
236
+ | `post.workspaces.remove_member` | `DELETE /workspaces/:id/members` | `workspaces:write` |
237
+
238
+ ---
239
+
240
+ ## Links
241
+
242
+ - Website — https://www.misarpost.com
243
+ - App — https://post.misar.io
244
+ - Parent — https://misar.io
245
+ - Documentation — https://docs.misar.io/post
246
+ - API reference — https://api.misar.io/post/openapi.json
247
+ - Source — https://github.com/Misar-AI/misarpost-sdks
248
+ - RubyGems — https://rubygems.org/gems/misarpost
249
+
250
+ MIT © [Misar.AI](https://misar.io)
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MisarPost
4
+ # MisarPost API client.
5
+ #
6
+ # post = MisarPost::Client.new(ENV.fetch("MISARPOST_API_KEY"))
7
+ # post.posts.list(query: { "status" => "queued" })
8
+ #
9
+ # Every resource and method is generated from the API's own route tree
10
+ # (sdks/manifest.json).
11
+ class Client < Generated::Resources
12
+ attr_reader :transport
13
+
14
+ def initialize(api_key, base_url: DEFAULT_BASE_URL, max_retries: 3, timeout: 30, http: nil, sleeper: nil)
15
+ @transport = Transport.new(api_key, base_url: base_url, max_retries: max_retries,
16
+ timeout: timeout, http: http, sleeper: sleeper)
17
+ super(@transport)
18
+ end
19
+
20
+ def base_url
21
+ @transport.base_url
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MisarPost
4
+ # Page through MisarPost's ?page= list endpoints. Yields page 1, 2, … until
5
+ # the response's totalPages is reached, or — when an endpoint reports no
6
+ # totals — until a page comes back empty.
7
+ #
8
+ # MisarPost.paginate { |n| post.posts.list(query: { "status" => "queued", "page" => n }) }.each { |page| ... }
9
+ def self.paginate(start_page: 1, max_pages: nil, &fetch_page)
10
+ Enumerator.new do |yielder|
11
+ page_no = start_page
12
+ seen = 0
13
+ loop do
14
+ break if max_pages && seen >= max_pages
15
+ result = fetch_page.call(page_no)
16
+ yielder << result
17
+ total = result.is_a?(Hash) ? result["totalPages"] : nil
18
+ if total.is_a?(Integer)
19
+ break if page_no >= total
20
+ else
21
+ items = result.is_a?(Hash) ? result.values.find { |v| v.is_a?(Array) } : nil
22
+ break if items.nil? || items.empty?
23
+ end
24
+ page_no += 1
25
+ seen += 1
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,148 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "net/http"
5
+ require "uri"
6
+ require "erb"
7
+
8
+ module MisarPost
9
+ SDK_VERSION = "1.0.0"
10
+ DEFAULT_BASE_URL = "https://api.misar.io/post"
11
+
12
+ # Default HTTP adapter over Net::HTTP. Any object responding to
13
+ # call(method, uri, headers, body, timeout) -> [status, headers_hash, body_string]
14
+ # can replace it (tests inject a fake).
15
+ class NetHttpAdapter
16
+ def call(method, uri, headers, body, timeout)
17
+ http = Net::HTTP.new(uri.host, uri.port)
18
+ http.use_ssl = uri.scheme == "https"
19
+ http.open_timeout = timeout
20
+ http.read_timeout = timeout
21
+ klass = Net::HTTP.const_get(method.capitalize)
22
+ request = klass.new(uri.request_uri, headers)
23
+ request.body = body if body
24
+ response = http.request(request)
25
+ hdrs = {}
26
+ response.each_header { |k, v| hdrs[k.downcase] = v }
27
+ [response.code.to_i, hdrs, response.body.to_s]
28
+ end
29
+ end
30
+
31
+ # HTTP transport the generated resource layer is written against. One
32
+ # credential, one path in: the account API key (mpk_…) as a bearer token.
33
+ class Transport
34
+ RETRYABLE = [429, 500, 502, 503, 504].freeze
35
+
36
+ attr_reader :base_url
37
+
38
+ def initialize(api_key, base_url: DEFAULT_BASE_URL, max_retries: 3, timeout: 30, http: nil, sleeper: nil)
39
+ if api_key.nil? || api_key.empty?
40
+ raise ArgumentError, "A MisarPost API key is required. Create one at " \
41
+ "https://post.misar.io/dashboard/api-keys (it starts with mpk_)."
42
+ end
43
+ @api_key = api_key
44
+ @base_url = base_url.sub(%r{/+\z}, "")
45
+ @max_retries = [max_retries, 1].max
46
+ @timeout = timeout
47
+ @http = http || NetHttpAdapter.new
48
+ @sleeper = sleeper || ->(seconds) { sleep(seconds) }
49
+ end
50
+
51
+ def self.enc(value)
52
+ ERB::Util.url_encode(value.to_s)
53
+ end
54
+
55
+ # The JSON body of a call. Ruby 3 binds a brace-less trailing hash
56
+ # (+update(id, "contentText" => "x")+) to keyword arguments, not to the
57
+ # positional +body+, so every body method also takes +**fields+ and they
58
+ # land here. Both spellings, and +update(id, contentText: "x")+, therefore
59
+ # send the same body.
60
+ def self.body(body, fields)
61
+ return body if fields.empty?
62
+ unless body.nil? || body.is_a?(Hash)
63
+ raise ArgumentError, "pass the request body as one Hash or as keyword fields, not both"
64
+ end
65
+
66
+ (body || {}).transform_keys(&:to_s).merge(fields.transform_keys(&:to_s))
67
+ end
68
+
69
+ def request(method, path, query: nil, body: nil)
70
+ uri = URI("#{@base_url}#{path}#{build_query(query)}")
71
+ headers = {
72
+ "Authorization" => "Bearer #{@api_key}",
73
+ "Accept" => "application/json",
74
+ "User-Agent" => "misarpost-ruby/#{SDK_VERSION}"
75
+ }
76
+ payload = nil
77
+ unless body.nil?
78
+ headers["Content-Type"] = "application/json"
79
+ payload = JSON.generate(body)
80
+ end
81
+
82
+ attempt = 0
83
+ loop do
84
+ begin
85
+ status, resp_headers, raw = @http.call(method, uri, headers, payload, @timeout)
86
+ rescue StandardError => e
87
+ raise if e.is_a?(MisarPost::Error)
88
+ if attempt + 1 < @max_retries
89
+ @sleeper.call(backoff(attempt, nil))
90
+ attempt += 1
91
+ next
92
+ end
93
+ raise NetworkError.new(e.message, e)
94
+ end
95
+
96
+ data = decode(raw)
97
+ return normalize(data) if status >= 200 && status < 300
98
+
99
+ # Plan refusals first, by code — never retried.
100
+ if MisarPost.plan_limit_body?(data)
101
+ message = data["error"] || (data["upgrade"] || {})["headline"] || "Plan limit exceeded"
102
+ raise PlanLimitError.new(status, message, data, resp_headers)
103
+ end
104
+
105
+ if RETRYABLE.include?(status) && attempt + 1 < @max_retries
106
+ @sleeper.call(backoff(attempt, resp_headers))
107
+ attempt += 1
108
+ next
109
+ end
110
+
111
+ body_hash = data.is_a?(Hash) ? data : {}
112
+ message = body_hash["error"] || body_hash["message"] || (data.is_a?(String) && !data.empty? ? data : "HTTP #{status}")
113
+ raise Error.new(status, message.to_s, (body_hash["code"] || "api_error").to_s, data)
114
+ end
115
+ end
116
+
117
+ private
118
+
119
+ def build_query(query)
120
+ return "" if query.nil? || query.empty?
121
+ pairs = []
122
+ query.each do |k, v|
123
+ next if v.nil?
124
+ Array(v).each { |item| pairs << [k.to_s, item.to_s] }
125
+ end
126
+ pairs.empty? ? "" : "?#{URI.encode_www_form(pairs)}"
127
+ end
128
+
129
+ # Exponential backoff from 200 ms, honouring Retry-After (capped at 60 s).
130
+ def backoff(attempt, headers)
131
+ retry_after = headers && headers["retry-after"]
132
+ return [retry_after.to_f, 60.0].min if retry_after.to_s =~ /\A\d+(\.\d+)?\z/
133
+ 0.2 * (2**attempt)
134
+ end
135
+
136
+ def decode(raw)
137
+ return nil if raw.nil? || raw.empty?
138
+ JSON.parse(raw)
139
+ rescue JSON::ParserError
140
+ raw
141
+ end
142
+
143
+ def normalize(data)
144
+ return {} if data.nil?
145
+ data.is_a?(Hash) ? data : { "data" => data }
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "openssl"
4
+
5
+ module MisarPost
6
+ # Outbound webhook deliveries carry X-MisarPost-Signature: sha256=<hex> — the
7
+ # HMAC-SHA256 of the RAW request body keyed with the endpoint's mpwh_ secret.
8
+ WEBHOOK_SIGNATURE_HEADER = "X-MisarPost-Signature"
9
+
10
+ def self.compute_webhook_signature(secret, raw_body)
11
+ "sha256=" + OpenSSL::HMAC.hexdigest("SHA256", secret, raw_body)
12
+ end
13
+
14
+ def self.verify_webhook_signature(secret, raw_body, signature_header)
15
+ return false if secret.nil? || secret.empty? || signature_header.nil?
16
+ expected = compute_webhook_signature(secret, raw_body)
17
+ given = signature_header.strip.downcase
18
+ return false unless expected.bytesize == given.bytesize
19
+ diff = 0
20
+ expected.bytes.zip(given.bytes) { |a, b| diff |= a ^ b }
21
+ diff.zero?
22
+ end
23
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MisarPost
4
+ # Any non-2xx answer from the MisarPost API.
5
+ class Error < StandardError
6
+ attr_reader :status, :code, :body
7
+
8
+ def initialize(status, message, code = "api_error", body = nil)
9
+ @status = status
10
+ @code = code
11
+ @body = body
12
+ super("misarpost: API error #{status} (#{code}): #{message}")
13
+ end
14
+
15
+ # The key was rejected outright: missing, revoked, expired, or another product's.
16
+ def unauthorized?
17
+ status == 401
18
+ end
19
+
20
+ # The key is valid but lacks the scope this endpoint requires.
21
+ def forbidden?
22
+ status == 403
23
+ end
24
+
25
+ def retryable?
26
+ status == 429 || (status >= 500 && status < 600)
27
+ end
28
+ end
29
+
30
+ class NetworkError < Error
31
+ attr_reader :cause_error
32
+
33
+ def initialize(message, cause_error = nil)
34
+ @cause_error = cause_error
35
+ super(0, message, "network_error")
36
+ end
37
+ end
38
+
39
+ # The plan behind the API key blocks this call: HTTP 402 with
40
+ # code "plan_limit_exceeded" and an upgrade offer. Never retried.
41
+ class PlanLimitError < Error
42
+ attr_reader :offer, :plan, :upgrade_url, :retry_after, :feature
43
+
44
+ def initialize(status, message, body = nil, headers = {})
45
+ h = (headers || {}).each_with_object({}) { |(k, v), acc| acc[k.to_s.downcase] = v }
46
+ data = body.is_a?(Hash) ? body : {}
47
+ @offer = data["upgrade"].is_a?(Hash) ? data["upgrade"] : nil
48
+ offer = @offer || {}
49
+ @plan = h["x-misar-plan"] || (offer["current_plan"] || {})["slug"]
50
+ @upgrade_url = h["x-misar-upgrade-url"] || (offer["urls"] || {})["pricing"]
51
+ retry_after = h["retry-after"].to_s
52
+ @retry_after = retry_after =~ /\A\d+\z/ ? retry_after.to_i : nil
53
+ @feature = offer["feature"]
54
+ super(status, message, "plan_limit_exceeded", body)
55
+ end
56
+ end
57
+
58
+ # True when an error body carries the API's plan-refusal marker.
59
+ def self.plan_limit_body?(data)
60
+ data.is_a?(Hash) && (data["code"] == "plan_limit_exceeded" || data["upgrade"].is_a?(Hash))
61
+ end
62
+ end
@@ -0,0 +1,663 @@
1
+ # AUTO-GENERATED by scripts/generate-sdks.mjs from sdks/manifest.json.
2
+ # Do not edit by hand — changes are overwritten. To change this surface,
3
+ # change the API route, re-run scripts/sync-sdk-manifest.mjs, then
4
+ # re-run scripts/generate-sdks.mjs.
5
+ # frozen_string_literal: true
6
+
7
+ module MisarPost
8
+ module Generated
9
+ # The `analytics` resource group.
10
+ class AnalyticsResource
11
+ def initialize(transport)
12
+ @transport = transport
13
+ end
14
+
15
+ # GET /stats (scope: analytics:read)
16
+ def stats(query: nil)
17
+ @transport.request("GET", "/stats", query: query)
18
+ end
19
+
20
+ # GET /analytics/trends (scope: analytics:read)
21
+ def trends(query: nil)
22
+ @transport.request("GET", "/analytics/trends", query: query)
23
+ end
24
+ end
25
+
26
+ # The `composer` resource group.
27
+ class ComposerResource
28
+ def initialize(transport)
29
+ @transport = transport
30
+ end
31
+
32
+ # GET /composer/discord/channels (scope: connections:read)
33
+ def discord_channels(query: nil)
34
+ @transport.request("GET", "/composer/discord/channels", query: query)
35
+ end
36
+
37
+ # GET /composer/hashnode/publications (scope: connections:read)
38
+ def hashnode_publications(query: nil)
39
+ @transport.request("GET", "/composer/hashnode/publications", query: query)
40
+ end
41
+
42
+ # GET /composer/hashnode/tags (scope: connections:read)
43
+ def hashnode_tags(query: nil)
44
+ @transport.request("GET", "/composer/hashnode/tags", query: query)
45
+ end
46
+
47
+ # GET /composer/lemmy/communities (scope: connections:read)
48
+ def lemmy_communities(query: nil)
49
+ @transport.request("GET", "/composer/lemmy/communities", query: query)
50
+ end
51
+
52
+ # GET /composer/listmonk/lists (scope: connections:read)
53
+ def listmonk_lists(query: nil)
54
+ @transport.request("GET", "/composer/listmonk/lists", query: query)
55
+ end
56
+
57
+ # GET /composer/listmonk/templates (scope: connections:read)
58
+ def listmonk_templates(query: nil)
59
+ @transport.request("GET", "/composer/listmonk/templates", query: query)
60
+ end
61
+
62
+ # GET /composer/medium/publications (scope: connections:read)
63
+ def medium_publications(query: nil)
64
+ @transport.request("GET", "/composer/medium/publications", query: query)
65
+ end
66
+
67
+ # GET /composer/pinterest/boards (scope: connections:read)
68
+ def pinterest_boards(query: nil)
69
+ @transport.request("GET", "/composer/pinterest/boards", query: query)
70
+ end
71
+
72
+ # GET /composer/reddit/flairs (scope: connections:read)
73
+ def reddit_flairs(query: nil)
74
+ @transport.request("GET", "/composer/reddit/flairs", query: query)
75
+ end
76
+
77
+ # GET /composer/skool/groups (scope: connections:read)
78
+ def skool_groups(query: nil)
79
+ @transport.request("GET", "/composer/skool/groups", query: query)
80
+ end
81
+
82
+ # GET /composer/slack/channels (scope: connections:read)
83
+ def slack_channels(query: nil)
84
+ @transport.request("GET", "/composer/slack/channels", query: query)
85
+ end
86
+
87
+ # GET /composer/whop/forums (scope: connections:read)
88
+ def whop_forums(query: nil)
89
+ @transport.request("GET", "/composer/whop/forums", query: query)
90
+ end
91
+ end
92
+
93
+ # The `connections` resource group.
94
+ class ConnectionsResource
95
+ def initialize(transport)
96
+ @transport = transport
97
+ end
98
+
99
+ # GET /connections (scope: connections:read)
100
+ def list(query: nil)
101
+ @transport.request("GET", "/connections", query: query)
102
+ end
103
+
104
+ # GET /connections/:accountId/plugs (scope: connections:read)
105
+ def plugs(account_id, query: nil)
106
+ @transport.request("GET", "/connections/#{Transport.enc(account_id)}/plugs", query: query)
107
+ end
108
+
109
+ # PATCH /connections/:accountId/plugs (scope: connections:write)
110
+ def update_plugs(account_id, body = {}, query: nil, **fields)
111
+ @transport.request("PATCH", "/connections/#{Transport.enc(account_id)}/plugs", query: query, body: Transport.body(body, fields))
112
+ end
113
+
114
+ # GET /connections/:accountId/wordpress-taxonomies (scope: connections:read)
115
+ def wordpress_taxonomies(account_id, query: nil)
116
+ @transport.request("GET", "/connections/#{Transport.enc(account_id)}/wordpress-taxonomies", query: query)
117
+ end
118
+ end
119
+
120
+ # The `contentripple` resource group.
121
+ class ContentrippleResource
122
+ def initialize(transport)
123
+ @transport = transport
124
+ end
125
+
126
+ # DELETE /contentripple/history (scope: posts:write)
127
+ def delete_history(body = {}, query: nil, **fields)
128
+ @transport.request("DELETE", "/contentripple/history", query: query, body: Transport.body(body, fields))
129
+ end
130
+
131
+ # POST /contentripple/feed (scope: generate:write)
132
+ def feed(body = {}, query: nil, **fields)
133
+ @transport.request("POST", "/contentripple/feed", query: query, body: Transport.body(body, fields))
134
+ end
135
+
136
+ # GET /contentripple/history (scope: posts:read)
137
+ def history(query: nil)
138
+ @transport.request("GET", "/contentripple/history", query: query)
139
+ end
140
+
141
+ # POST /contentripple/parse (scope: generate:write)
142
+ def parse(body = {}, query: nil, **fields)
143
+ @transport.request("POST", "/contentripple/parse", query: query, body: Transport.body(body, fields))
144
+ end
145
+
146
+ # POST /contentripple (scope: generate:write)
147
+ def repurpose(body = {}, query: nil, **fields)
148
+ @transport.request("POST", "/contentripple", query: query, body: Transport.body(body, fields))
149
+ end
150
+
151
+ # POST /contentripple/schedule (scope: schedule:write)
152
+ def schedule(body = {}, query: nil, **fields)
153
+ @transport.request("POST", "/contentripple/schedule", query: query, body: Transport.body(body, fields))
154
+ end
155
+
156
+ # PATCH /contentripple/variants (scope: posts:write)
157
+ def update_variants(body = {}, query: nil, **fields)
158
+ @transport.request("PATCH", "/contentripple/variants", query: query, body: Transport.body(body, fields))
159
+ end
160
+
161
+ # GET /contentripple (scope: posts:read)
162
+ def usage(query: nil)
163
+ @transport.request("GET", "/contentripple", query: query)
164
+ end
165
+ end
166
+
167
+ # The `drafts` resource group.
168
+ class DraftsResource
169
+ def initialize(transport)
170
+ @transport = transport
171
+ end
172
+
173
+ # POST /drafts (scope: posts:write)
174
+ def create(body = {}, query: nil, **fields)
175
+ @transport.request("POST", "/drafts", query: query, body: Transport.body(body, fields))
176
+ end
177
+
178
+ # GET /drafts (scope: posts:read)
179
+ def list(query: nil)
180
+ @transport.request("GET", "/drafts", query: query)
181
+ end
182
+ end
183
+
184
+ # The `generate` resource group.
185
+ class GenerateResource
186
+ def initialize(transport)
187
+ @transport = transport
188
+ end
189
+
190
+ # POST /generate (scope: generate:write)
191
+ def create(body = {}, query: nil, **fields)
192
+ @transport.request("POST", "/generate", query: query, body: Transport.body(body, fields))
193
+ end
194
+
195
+ # POST /generate/enhance (scope: generate:write)
196
+ def enhance(body = {}, query: nil, **fields)
197
+ @transport.request("POST", "/generate/enhance", query: query, body: Transport.body(body, fields))
198
+ end
199
+
200
+ # POST /generate/hashtags (scope: generate:write)
201
+ def hashtags(body = {}, query: nil, **fields)
202
+ @transport.request("POST", "/generate/hashtags", query: query, body: Transport.body(body, fields))
203
+ end
204
+
205
+ # POST /generate/image (scope: generate:write)
206
+ def image(body = {}, query: nil, **fields)
207
+ @transport.request("POST", "/generate/image", query: query, body: Transport.body(body, fields))
208
+ end
209
+
210
+ # GET /generate/optimize (scope: generate:write)
211
+ def optimize(query: nil)
212
+ @transport.request("GET", "/generate/optimize", query: query)
213
+ end
214
+
215
+ # POST /generate/plan (scope: generate:write)
216
+ def plan(body = {}, query: nil, **fields)
217
+ @transport.request("POST", "/generate/plan", query: query, body: Transport.body(body, fields))
218
+ end
219
+ end
220
+
221
+ # The `growth` resource group.
222
+ class GrowthResource
223
+ def initialize(transport)
224
+ @transport = transport
225
+ end
226
+
227
+ # GET /growth/next-opportunity (scope: analytics:read)
228
+ def next_opportunity(query: nil)
229
+ @transport.request("GET", "/growth/next-opportunity", query: query)
230
+ end
231
+
232
+ # GET /growth/opportunities (scope: analytics:read)
233
+ def opportunities(query: nil)
234
+ @transport.request("GET", "/growth/opportunities", query: query)
235
+ end
236
+
237
+ # GET /growth/overview (scope: analytics:read)
238
+ def overview(query: nil)
239
+ @transport.request("GET", "/growth/overview", query: query)
240
+ end
241
+
242
+ # GET /growth/playbooks (scope: analytics:read)
243
+ def playbooks(query: nil)
244
+ @transport.request("GET", "/growth/playbooks", query: query)
245
+ end
246
+ end
247
+
248
+ # The `media` resource group.
249
+ class MediaResource
250
+ def initialize(transport)
251
+ @transport = transport
252
+ end
253
+
254
+ # DELETE /media/:id (scope: media:write)
255
+ def delete(id, query: nil)
256
+ @transport.request("DELETE", "/media/#{Transport.enc(id)}", query: query)
257
+ end
258
+
259
+ # GET /media (scope: media:read)
260
+ def list(query: nil)
261
+ @transport.request("GET", "/media", query: query)
262
+ end
263
+ end
264
+
265
+ # The `notifications` resource group.
266
+ class NotificationsResource
267
+ def initialize(transport)
268
+ @transport = transport
269
+ end
270
+
271
+ # GET /notifications (scope: notifications:read)
272
+ def list(query: nil)
273
+ @transport.request("GET", "/notifications", query: query)
274
+ end
275
+
276
+ # POST /notifications/read-all (scope: notifications:write)
277
+ def read_all(query: nil)
278
+ @transport.request("POST", "/notifications/read-all", query: query)
279
+ end
280
+
281
+ # PATCH /notifications/:id (scope: notifications:write)
282
+ def update(id, query: nil)
283
+ @transport.request("PATCH", "/notifications/#{Transport.enc(id)}", query: query)
284
+ end
285
+ end
286
+
287
+ # The `personaforge` resource group.
288
+ class PersonaforgeResource
289
+ def initialize(transport)
290
+ @transport = transport
291
+ end
292
+
293
+ # POST /personaforge/replies/approve (scope: personas:write)
294
+ def approve_replies(body = {}, query: nil, **fields)
295
+ @transport.request("POST", "/personaforge/replies/approve", query: query, body: Transport.body(body, fields))
296
+ end
297
+
298
+ # POST /personaforge/bio (scope: generate:write)
299
+ def bio(body = {}, query: nil, **fields)
300
+ @transport.request("POST", "/personaforge/bio", query: query, body: Transport.body(body, fields))
301
+ end
302
+
303
+ # POST /personaforge/competitors (scope: personas:write)
304
+ def create_competitors(body = {}, query: nil, **fields)
305
+ @transport.request("POST", "/personaforge/competitors", query: query, body: Transport.body(body, fields))
306
+ end
307
+
308
+ # POST /personaforge/personas (scope: personas:write)
309
+ def create_persona(body = {}, query: nil, **fields)
310
+ @transport.request("POST", "/personaforge/personas", query: query, body: Transport.body(body, fields))
311
+ end
312
+
313
+ # POST /personaforge/replies (scope: personas:write)
314
+ def create_replies(body = {}, query: nil, **fields)
315
+ @transport.request("POST", "/personaforge/replies", query: query, body: Transport.body(body, fields))
316
+ end
317
+
318
+ # POST /personaforge/voice (scope: personas:write)
319
+ def create_voice(query: nil)
320
+ @transport.request("POST", "/personaforge/voice", query: query)
321
+ end
322
+
323
+ # DELETE /personaforge/personas/:id (scope: personas:write)
324
+ def delete_persona(id, query: nil)
325
+ @transport.request("DELETE", "/personaforge/personas/#{Transport.enc(id)}", query: query)
326
+ end
327
+
328
+ # POST /personaforge/generate (scope: generate:write)
329
+ def generate(body = {}, query: nil, **fields)
330
+ @transport.request("POST", "/personaforge/generate", query: query, body: Transport.body(body, fields))
331
+ end
332
+
333
+ # GET /personaforge/personas/:id (scope: personas:read)
334
+ def get_persona(id, query: nil)
335
+ @transport.request("GET", "/personaforge/personas/#{Transport.enc(id)}", query: query)
336
+ end
337
+
338
+ # GET /personaforge/voice (scope: personas:read)
339
+ def get_voice(query: nil)
340
+ @transport.request("GET", "/personaforge/voice", query: query)
341
+ end
342
+
343
+ # GET /personaforge/competitors (scope: personas:read)
344
+ def list_competitors(query: nil)
345
+ @transport.request("GET", "/personaforge/competitors", query: query)
346
+ end
347
+
348
+ # GET /personaforge/personas (scope: personas:read)
349
+ def list_personas(query: nil)
350
+ @transport.request("GET", "/personaforge/personas", query: query)
351
+ end
352
+
353
+ # GET /personaforge/replies (scope: personas:read)
354
+ def list_replies(query: nil)
355
+ @transport.request("GET", "/personaforge/replies", query: query)
356
+ end
357
+
358
+ # GET /personaforge/personas/status/:jobId (scope: personas:read)
359
+ def persona_status(job_id, query: nil)
360
+ @transport.request("GET", "/personaforge/personas/status/#{Transport.enc(job_id)}", query: query)
361
+ end
362
+
363
+ # PATCH /personaforge/personas/:id (scope: personas:write)
364
+ def update_persona(id, body = {}, query: nil, **fields)
365
+ @transport.request("PATCH", "/personaforge/personas/#{Transport.enc(id)}", query: query, body: Transport.body(body, fields))
366
+ end
367
+ end
368
+
369
+ # The `posts` resource group.
370
+ class PostsResource
371
+ def initialize(transport)
372
+ @transport = transport
373
+ end
374
+
375
+ # GET /calendar (scope: posts:read)
376
+ def calendar(query: nil)
377
+ @transport.request("GET", "/calendar", query: query)
378
+ end
379
+
380
+ # DELETE /posts (scope: posts:write)
381
+ def cancel(body = {}, query: nil, **fields)
382
+ @transport.request("DELETE", "/posts", query: query, body: Transport.body(body, fields))
383
+ end
384
+
385
+ # DELETE /posts/:id (scope: posts:write)
386
+ def delete(id, query: nil)
387
+ @transport.request("DELETE", "/posts/#{Transport.enc(id)}", query: query)
388
+ end
389
+
390
+ # GET /posts (scope: posts:read)
391
+ def list(query: nil)
392
+ @transport.request("GET", "/posts", query: query)
393
+ end
394
+
395
+ # POST /posts/:id/reply (scope: posts:write)
396
+ def reply(id, body = {}, query: nil, **fields)
397
+ @transport.request("POST", "/posts/#{Transport.enc(id)}/reply", query: query, body: Transport.body(body, fields))
398
+ end
399
+
400
+ # POST /posts/:id/revert (scope: posts:write)
401
+ def revert(id, query: nil)
402
+ @transport.request("POST", "/posts/#{Transport.enc(id)}/revert", query: query)
403
+ end
404
+
405
+ # POST /posts/revert-batch (scope: posts:write)
406
+ def revert_batch(body = {}, query: nil, **fields)
407
+ @transport.request("POST", "/posts/revert-batch", query: query, body: Transport.body(body, fields))
408
+ end
409
+
410
+ # POST /schedule (scope: schedule:write)
411
+ def schedule(body = {}, query: nil, **fields)
412
+ @transport.request("POST", "/schedule", query: query, body: Transport.body(body, fields))
413
+ end
414
+
415
+ # POST /posts/:id/share (scope: posts:write)
416
+ def share(id, query: nil)
417
+ @transport.request("POST", "/posts/#{Transport.enc(id)}/share", query: query)
418
+ end
419
+
420
+ # DELETE /posts/:id/share (scope: posts:write)
421
+ def unshare(id, query: nil)
422
+ @transport.request("DELETE", "/posts/#{Transport.enc(id)}/share", query: query)
423
+ end
424
+
425
+ # PATCH /posts/:id (scope: posts:write)
426
+ def update(id, body = {}, query: nil, **fields)
427
+ @transport.request("PATCH", "/posts/#{Transport.enc(id)}", query: query, body: Transport.body(body, fields))
428
+ end
429
+ end
430
+
431
+ # The `post_tags` resource group.
432
+ class PostTagsResource
433
+ def initialize(transport)
434
+ @transport = transport
435
+ end
436
+
437
+ # POST /post-tags (scope: posts:write)
438
+ def create(body = {}, query: nil, **fields)
439
+ @transport.request("POST", "/post-tags", query: query, body: Transport.body(body, fields))
440
+ end
441
+
442
+ # DELETE /post-tags/:id (scope: posts:write)
443
+ def delete(id, query: nil)
444
+ @transport.request("DELETE", "/post-tags/#{Transport.enc(id)}", query: query)
445
+ end
446
+
447
+ # GET /post-tags (scope: posts:read)
448
+ def list(query: nil)
449
+ @transport.request("GET", "/post-tags", query: query)
450
+ end
451
+
452
+ # PATCH /post-tags/:id (scope: posts:write)
453
+ def update(id, body = {}, query: nil, **fields)
454
+ @transport.request("PATCH", "/post-tags/#{Transport.enc(id)}", query: query, body: Transport.body(body, fields))
455
+ end
456
+ end
457
+
458
+ # The `post_templates` resource group.
459
+ class PostTemplatesResource
460
+ def initialize(transport)
461
+ @transport = transport
462
+ end
463
+
464
+ # POST /post-templates (scope: posts:write)
465
+ def create(body = {}, query: nil, **fields)
466
+ @transport.request("POST", "/post-templates", query: query, body: Transport.body(body, fields))
467
+ end
468
+
469
+ # DELETE /post-templates/:id (scope: posts:write)
470
+ def delete(id, query: nil)
471
+ @transport.request("DELETE", "/post-templates/#{Transport.enc(id)}", query: query)
472
+ end
473
+
474
+ # GET /post-templates/:id (scope: posts:read)
475
+ def get(id, query: nil)
476
+ @transport.request("GET", "/post-templates/#{Transport.enc(id)}", query: query)
477
+ end
478
+
479
+ # GET /post-templates (scope: posts:read)
480
+ def list(query: nil)
481
+ @transport.request("GET", "/post-templates", query: query)
482
+ end
483
+
484
+ # PATCH /post-templates/:id (scope: posts:write)
485
+ def update(id, body = {}, query: nil, **fields)
486
+ @transport.request("PATCH", "/post-templates/#{Transport.enc(id)}", query: query, body: Transport.body(body, fields))
487
+ end
488
+ end
489
+
490
+ # The `trendpulse` resource group.
491
+ class TrendpulseResource
492
+ def initialize(transport)
493
+ @transport = transport
494
+ end
495
+
496
+ # POST /trendpulse/platform-content (scope: trends:write)
497
+ def create_platform_content(body = {}, query: nil, **fields)
498
+ @transport.request("POST", "/trendpulse/platform-content", query: query, body: Transport.body(body, fields))
499
+ end
500
+
501
+ # POST /trendpulse/digest (scope: trends:write)
502
+ def digest(query: nil)
503
+ @transport.request("POST", "/trendpulse/digest", query: query)
504
+ end
505
+
506
+ # POST /trendpulse/fetch (scope: trends:write)
507
+ def fetch(query: nil)
508
+ @transport.request("POST", "/trendpulse/fetch", query: query)
509
+ end
510
+
511
+ # GET /trendpulse/hooks (scope: trends:read)
512
+ def hooks(query: nil)
513
+ @transport.request("GET", "/trendpulse/hooks", query: query)
514
+ end
515
+
516
+ # GET /trendpulse (scope: trends:read)
517
+ def list(query: nil)
518
+ @transport.request("GET", "/trendpulse", query: query)
519
+ end
520
+
521
+ # GET /trendpulse/platform-content (scope: trends:read)
522
+ def list_platform_content(query: nil)
523
+ @transport.request("GET", "/trendpulse/platform-content", query: query)
524
+ end
525
+
526
+ # GET /trendpulse/performance (scope: analytics:read)
527
+ def performance(query: nil)
528
+ @transport.request("GET", "/trendpulse/performance", query: query)
529
+ end
530
+
531
+ # POST /trendpulse/pipeline (scope: schedule:write)
532
+ def pipeline(body = {}, query: nil, **fields)
533
+ @transport.request("POST", "/trendpulse/pipeline", query: query, body: Transport.body(body, fields))
534
+ end
535
+
536
+ # POST /trendpulse (scope: trends:write)
537
+ def refresh(body = {}, query: nil, **fields)
538
+ @transport.request("POST", "/trendpulse", query: query, body: Transport.body(body, fields))
539
+ end
540
+
541
+ # POST /trendpulse/platform-content/use (scope: trends:write)
542
+ def use_platform_content(body = {}, query: nil, **fields)
543
+ @transport.request("POST", "/trendpulse/platform-content/use", query: query, body: Transport.body(body, fields))
544
+ end
545
+ end
546
+
547
+ # The `video` resource group.
548
+ class VideoResource
549
+ def initialize(transport)
550
+ @transport = transport
551
+ end
552
+
553
+ # GET /video/avatars (scope: media:read)
554
+ def avatars(query: nil)
555
+ @transport.request("GET", "/video/avatars", query: query)
556
+ end
557
+
558
+ # POST /video/generate (scope: generate:write)
559
+ def generate(body = {}, query: nil, **fields)
560
+ @transport.request("POST", "/video/generate", query: query, body: Transport.body(body, fields))
561
+ end
562
+
563
+ # POST /video/script (scope: generate:write)
564
+ def script(body = {}, query: nil, **fields)
565
+ @transport.request("POST", "/video/script", query: query, body: Transport.body(body, fields))
566
+ end
567
+
568
+ # GET /video/status/:jobId (scope: media:read)
569
+ def status(job_id, query: nil)
570
+ @transport.request("GET", "/video/status/#{Transport.enc(job_id)}", query: query)
571
+ end
572
+
573
+ # GET /video/voices (scope: media:read)
574
+ def voices(query: nil)
575
+ @transport.request("GET", "/video/voices", query: query)
576
+ end
577
+ end
578
+
579
+ # The `webhooks` resource group.
580
+ class WebhooksResource
581
+ def initialize(transport)
582
+ @transport = transport
583
+ end
584
+
585
+ # POST /webhooks/outbound (scope: webhooks:write)
586
+ def create_outbound(body = {}, query: nil, **fields)
587
+ @transport.request("POST", "/webhooks/outbound", query: query, body: Transport.body(body, fields))
588
+ end
589
+
590
+ # DELETE /webhooks/outbound/:id (scope: webhooks:write)
591
+ def delete_outbound(id, query: nil)
592
+ @transport.request("DELETE", "/webhooks/outbound/#{Transport.enc(id)}", query: query)
593
+ end
594
+
595
+ # GET /webhooks/outbound (scope: webhooks:read)
596
+ def list_outbound(query: nil)
597
+ @transport.request("GET", "/webhooks/outbound", query: query)
598
+ end
599
+
600
+ # PATCH /webhooks/outbound/:id (scope: webhooks:write)
601
+ def update_outbound(id, body = {}, query: nil, **fields)
602
+ @transport.request("PATCH", "/webhooks/outbound/#{Transport.enc(id)}", query: query, body: Transport.body(body, fields))
603
+ end
604
+ end
605
+
606
+ # The `workspaces` resource group.
607
+ class WorkspacesResource
608
+ def initialize(transport)
609
+ @transport = transport
610
+ end
611
+
612
+ # POST /workspaces/:id/members (scope: workspaces:write)
613
+ def add_member(id, body = {}, query: nil, **fields)
614
+ @transport.request("POST", "/workspaces/#{Transport.enc(id)}/members", query: query, body: Transport.body(body, fields))
615
+ end
616
+
617
+ # POST /workspaces (scope: workspaces:write)
618
+ def create(body = {}, query: nil, **fields)
619
+ @transport.request("POST", "/workspaces", query: query, body: Transport.body(body, fields))
620
+ end
621
+
622
+ # GET /workspaces (scope: workspaces:read)
623
+ def list(query: nil)
624
+ @transport.request("GET", "/workspaces", query: query)
625
+ end
626
+
627
+ # GET /workspaces/:id/members (scope: workspaces:read)
628
+ def members(id, query: nil)
629
+ @transport.request("GET", "/workspaces/#{Transport.enc(id)}/members", query: query)
630
+ end
631
+
632
+ # DELETE /workspaces/:id/members (scope: workspaces:write)
633
+ def remove_member(id, member_id, query: nil)
634
+ @transport.request("DELETE", "/workspaces/#{Transport.enc(id)}/members", query: (query || {}).merge("memberId" => member_id))
635
+ end
636
+ end
637
+
638
+ # Every resource group, generated from sdks/manifest.json.
639
+ class Resources
640
+ attr_reader :analytics, :composer, :connections, :contentripple, :drafts, :generate, :growth, :media, :notifications, :personaforge, :posts, :post_tags, :post_templates, :trendpulse, :video, :webhooks, :workspaces
641
+
642
+ def initialize(transport)
643
+ @analytics = AnalyticsResource.new(transport)
644
+ @composer = ComposerResource.new(transport)
645
+ @connections = ConnectionsResource.new(transport)
646
+ @contentripple = ContentrippleResource.new(transport)
647
+ @drafts = DraftsResource.new(transport)
648
+ @generate = GenerateResource.new(transport)
649
+ @growth = GrowthResource.new(transport)
650
+ @media = MediaResource.new(transport)
651
+ @notifications = NotificationsResource.new(transport)
652
+ @personaforge = PersonaforgeResource.new(transport)
653
+ @posts = PostsResource.new(transport)
654
+ @post_tags = PostTagsResource.new(transport)
655
+ @post_templates = PostTemplatesResource.new(transport)
656
+ @trendpulse = TrendpulseResource.new(transport)
657
+ @video = VideoResource.new(transport)
658
+ @webhooks = WebhooksResource.new(transport)
659
+ @workspaces = WorkspacesResource.new(transport)
660
+ end
661
+ end
662
+ end
663
+ end
data/lib/misarpost.rb ADDED
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ # MisarPost Ruby SDK — https://docs.misar.io/post
4
+ require_relative "misarpost/errors"
5
+ require_relative "misarpost/core/transport"
6
+ require_relative "misarpost/core/pagination"
7
+ require_relative "misarpost/core/webhooks"
8
+ require_relative "misarpost/generated/resources"
9
+ require_relative "misarpost/client"
metadata ADDED
@@ -0,0 +1,75 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: misarpost
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Misar.AI
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-10-17 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rspec
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '3.12'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '3.12'
27
+ description: Schedule and publish posts across every connected channel, generate and
28
+ repurpose content with AI, research trends, run PersonaForge and read analytics
29
+ — every MisarPost API endpoint, with retries and plan-limit upgrade offers. Standard
30
+ library only.
31
+ email:
32
+ - hello@misar.io
33
+ executables: []
34
+ extensions: []
35
+ extra_rdoc_files: []
36
+ files:
37
+ - CHANGELOG.md
38
+ - LICENSE
39
+ - README.md
40
+ - lib/misarpost.rb
41
+ - lib/misarpost/client.rb
42
+ - lib/misarpost/core/pagination.rb
43
+ - lib/misarpost/core/transport.rb
44
+ - lib/misarpost/core/webhooks.rb
45
+ - lib/misarpost/errors.rb
46
+ - lib/misarpost/generated/resources.rb
47
+ homepage: https://www.misarpost.com
48
+ licenses:
49
+ - MIT
50
+ metadata:
51
+ homepage_uri: https://www.misarpost.com
52
+ documentation_uri: https://docs.misar.io/post
53
+ source_code_uri: https://github.com/Misar-AI/misarpost-sdks/tree/main/ruby
54
+ changelog_uri: https://github.com/Misar-AI/misarpost-sdks/blob/main/ruby/CHANGELOG.md
55
+ bug_tracker_uri: https://github.com/Misar-AI/misarpost-sdks/issues
56
+ post_install_message:
57
+ rdoc_options: []
58
+ require_paths:
59
+ - lib
60
+ required_ruby_version: !ruby/object:Gem::Requirement
61
+ requirements:
62
+ - - ">="
63
+ - !ruby/object:Gem::Version
64
+ version: '2.6'
65
+ required_rubygems_version: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ requirements: []
71
+ rubygems_version: 3.5.22
72
+ signing_key:
73
+ specification_version: 4
74
+ summary: MisarPost social publishing API client for Ruby.
75
+ test_files: []