fopost-rails 0.1.0 → 0.2.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 +4 -4
- data/CHANGELOG.md +15 -0
- data/README.md +47 -3
- data/app/controllers/fopost/rails/articles_controller.rb +49 -0
- data/config/routes.rb +1 -0
- data/lib/fopost/rails/configuration.rb +17 -2
- data/lib/fopost/rails/version.rb +1 -1
- data/lib/generators/fopost/install/install_generator.rb +1 -1
- data/lib/generators/fopost/install/templates/fopost.rb.tt +8 -1
- metadata +4 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 66538248d3e80cefb4a338b87150c210d74f4cb4beccb9b581b9c17be5ee78ab
|
|
4
|
+
data.tar.gz: 456c6e82e21492da699bc289602099d11d18b4ce45d55fc6304a9dacb7a5fc1f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 658fe9d5a6693fb5c283362cc468f36f0e674312c1527f2d89984c0743a36c0c9a983c3002ad98043855395e8516f05efaad80e68582ff84b7156fdfcd902422
|
|
7
|
+
data.tar.gz: 56ee14f403c563b9f78cc3b36a87bd57811ed1716583131612c14b29f7f01eca0e1be025e925d6d90c315967f27273d3306c899383b748a165d7147c5a0116a1
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,20 @@ All notable changes to this gem are documented here. The format follows
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the gem follows
|
|
5
5
|
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [0.2.0] - 2026-10-18
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- `POST /fopost/articles` on the mounted engine receives the blog's website target. The `fopost`
|
|
12
|
+
gem verifies the signature; `article.published` reaches `config.on_article_published` and the
|
|
13
|
+
`fopost.article.published` notification, and a returned URL is echoed as `{ "url": ... }`.
|
|
14
|
+
- `website_secret` setting (`FOPOST_WEBSITE_SECRET`), falling back to `webhook_secret`.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- Requires `fopost` `~> 0.3`, now resolved from RubyGems instead of the GitHub source.
|
|
19
|
+
- The README points at `fopost.com/dashboard` instead of the retired app host.
|
|
20
|
+
|
|
7
21
|
## [0.1.0] - 2026-08-30
|
|
8
22
|
|
|
9
23
|
Initial release.
|
|
@@ -17,4 +31,5 @@ Initial release.
|
|
|
17
31
|
- A mountable engine that verifies incoming webhook signatures and republishes them as
|
|
18
32
|
`ActiveSupport::Notifications` events.
|
|
19
33
|
|
|
34
|
+
[0.2.0]: https://github.com/fopost/fopost-rails/releases/tag/v0.2.0
|
|
20
35
|
[0.1.0]: https://github.com/fopost/fopost-rails/releases/tag/v0.1.0
|
data/README.md
CHANGED
|
@@ -36,7 +36,7 @@ bin/rails generate fopost:install
|
|
|
36
36
|
|
|
37
37
|
## Configure
|
|
38
38
|
|
|
39
|
-
Create an API key at [
|
|
39
|
+
Create an API key at [fopost.com/dashboard/api-keys](https://fopost.com/dashboard/api-keys) and put it
|
|
40
40
|
somewhere the app can read it:
|
|
41
41
|
|
|
42
42
|
```bash
|
|
@@ -67,6 +67,7 @@ Every setting resolves the same way — **what you set explicitly wins, then Rai
|
|
|
67
67
|
| `max_retries` | `max_retries` | `FOPOST_MAX_RETRIES` | `3` |
|
|
68
68
|
| `default_workspace_id` | `default_workspace_id` | `FOPOST_WORKSPACE_ID` | none |
|
|
69
69
|
| `webhook_secret` | `webhook_secret` | `FOPOST_WEBHOOK_SECRET` | none |
|
|
70
|
+
| `website_secret` | `website_secret` | `FOPOST_WEBSITE_SECRET` | falls back to `webhook_secret` |
|
|
70
71
|
| `queue_name` | `queue_name` | `FOPOST_QUEUE` | `default` |
|
|
71
72
|
|
|
72
73
|
Set them in the initializer:
|
|
@@ -88,7 +89,7 @@ config.fopost.default_workspace_id = 'ws_...'
|
|
|
88
89
|
|
|
89
90
|
`Fopost::Rails.client` is a configured `Fopost::Client`, memoized and safe to call from any
|
|
90
91
|
thread. The [`fopost` gem README](https://github.com/fopost/fopost-ruby) documents the full
|
|
91
|
-
resource surface — `posts`, `accounts`, `workspaces`, `labels`, `ai`.
|
|
92
|
+
resource surface — `posts`, `accounts`, `workspaces`, `labels`, `ai`, `blog`.
|
|
92
93
|
|
|
93
94
|
```ruby
|
|
94
95
|
class PostsController < ApplicationController
|
|
@@ -203,6 +204,49 @@ post '/fopost/webhooks',
|
|
|
203
204
|
}
|
|
204
205
|
```
|
|
205
206
|
|
|
207
|
+
## Receive blog articles
|
|
208
|
+
|
|
209
|
+
The same engine mount also serves `POST /fopost/articles`, the receiver for the blog's **Website**
|
|
210
|
+
target. Set that target to `https://your-app.example/fopost/articles`, copy the website secret it
|
|
211
|
+
shows you once into `config.website_secret`, and hand FoPost a callback:
|
|
212
|
+
|
|
213
|
+
```ruby
|
|
214
|
+
# config/initializers/fopost.rb
|
|
215
|
+
Fopost::Rails.configure do |config|
|
|
216
|
+
config.website_secret = Rails.application.credentials.dig(:fopost, :website_secret)
|
|
217
|
+
|
|
218
|
+
config.on_article_published = lambda do |article|
|
|
219
|
+
record = Article.create!(
|
|
220
|
+
title: article.title,
|
|
221
|
+
slug: article.slug,
|
|
222
|
+
body: article.body_html,
|
|
223
|
+
excerpt: article.excerpt,
|
|
224
|
+
tags: article.tags,
|
|
225
|
+
published_at: article.published_at
|
|
226
|
+
)
|
|
227
|
+
Rails.application.routes.url_helpers.article_url(record)
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
`article` is a `Fopost::BlogArticlePublished` from the `fopost` gem: `title`, `slug`, `excerpt`,
|
|
233
|
+
`meta_title`, `meta_description`, `body_markdown`, `body_html`, `featured_image`, `images`, `tags`,
|
|
234
|
+
`keyword`, `word_count`, `published_at`. Image URLs stay fetchable for six hours, so store your own
|
|
235
|
+
copy. Return the URL the article now lives at and the receiver answers `{ "url": "..." }`; return
|
|
236
|
+
anything else and it answers `{ "ok": true }`.
|
|
237
|
+
|
|
238
|
+
Prefer events? Every verified `article.published` also fires
|
|
239
|
+
`ActiveSupport::Notifications.instrument('fopost.article.published', ...)` with `:article`, `:data`,
|
|
240
|
+
`:timestamp` and `:delivery_id`, whether or not a callback is set.
|
|
241
|
+
|
|
242
|
+
Verification is the `fopost` gem's `Fopost::Webhooks.verify`: the timestamped
|
|
243
|
+
`X-FoPost-Signature-256` over the raw body, rejected outside a 300 second window. A bad signature
|
|
244
|
+
is a `401` and the callback never runs; a test delivery (`article.test`) answers `{ "ok": true }`.
|
|
245
|
+
To sign one yourself in a request spec, use `Fopost::Webhooks.sign("#{timestamp}.#{body}", secret)`.
|
|
246
|
+
|
|
247
|
+
Managing the blog itself (settings, keywords, articles) is `Fopost::Rails.client.blog`, documented
|
|
248
|
+
in the [`fopost` gem README](https://github.com/fopost/fopost-ruby#blog).
|
|
249
|
+
|
|
206
250
|
## Testing your app
|
|
207
251
|
|
|
208
252
|
Swap the client for one wired to your own transport and nothing touches the network:
|
|
@@ -223,7 +267,7 @@ separate on purpose and never depend on each other.
|
|
|
223
267
|
## Links
|
|
224
268
|
|
|
225
269
|
- Documentation: [fopost.com/docs](https://fopost.com/docs)
|
|
226
|
-
- API keys: [
|
|
270
|
+
- API keys: [fopost.com/dashboard/api-keys](https://fopost.com/dashboard/api-keys)
|
|
227
271
|
- The SDK this wraps: [`fopost`](https://github.com/fopost/fopost-ruby)
|
|
228
272
|
- Issues: [github.com/fopost/fopost-rails/issues](https://github.com/fopost/fopost-rails/issues)
|
|
229
273
|
- Support: [fopost.com/contact](https://fopost.com/contact)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'active_support/notifications'
|
|
5
|
+
|
|
6
|
+
module Fopost
|
|
7
|
+
module Rails
|
|
8
|
+
# Receives the blog's website target: FoPost posts here when an article is
|
|
9
|
+
# published. The signature is verified by the `fopost` gem over the raw
|
|
10
|
+
# body; a mismatch is a 401 and nothing runs.
|
|
11
|
+
#
|
|
12
|
+
# # config/initializers/fopost.rb
|
|
13
|
+
# config.on_article_published = ->(article) { Article.import!(article).url }
|
|
14
|
+
#
|
|
15
|
+
# The callback may return the URL the article now lives at, which is
|
|
16
|
+
# echoed back as `{ "url": ... }`. `fopost.article.published` also fires as
|
|
17
|
+
# an ActiveSupport::Notifications event for subscribers.
|
|
18
|
+
class ArticlesController < ActionController::API
|
|
19
|
+
def create
|
|
20
|
+
secret = Fopost::Rails.config.website_secret || Fopost::Rails.config.webhook_secret
|
|
21
|
+
return head :service_unavailable if secret.nil? || secret.to_s.empty?
|
|
22
|
+
|
|
23
|
+
event = Fopost::Webhooks.verify(request.raw_post, request.headers, secret)
|
|
24
|
+
return render json: { ok: true } unless event.event == 'article.published'
|
|
25
|
+
|
|
26
|
+
url = deliver(event)
|
|
27
|
+
render json: url.is_a?(String) && !url.empty? ? { url: url } : { ok: true }
|
|
28
|
+
rescue Fopost::WebhookSignatureError
|
|
29
|
+
head :unauthorized
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def deliver(event)
|
|
35
|
+
article = event.article
|
|
36
|
+
notification = {
|
|
37
|
+
event: event.event,
|
|
38
|
+
article: article,
|
|
39
|
+
data: event.data,
|
|
40
|
+
timestamp: event.timestamp,
|
|
41
|
+
delivery_id: request.headers[Fopost::Webhooks::DELIVERY_HEADER]
|
|
42
|
+
}
|
|
43
|
+
ActiveSupport::Notifications.instrument('fopost.article.published', notification)
|
|
44
|
+
|
|
45
|
+
Fopost::Rails.config.on_article_published&.call(article)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
data/config/routes.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Fopost::Rails::Engine.routes.draw do
|
|
4
4
|
post '/webhooks', to: 'webhooks#create', as: :webhooks
|
|
5
|
+
post '/articles', to: 'articles#create', as: :articles
|
|
5
6
|
|
|
6
7
|
# So mounting straight onto the endpoint path also works:
|
|
7
8
|
# mount Fopost::Rails::Engine => '/fopost/webhooks'
|
|
@@ -15,7 +15,7 @@ module Fopost
|
|
|
15
15
|
# end
|
|
16
16
|
class Configuration
|
|
17
17
|
SETTINGS = %i[
|
|
18
|
-
api_key base_url timeout max_retries default_workspace_id webhook_secret queue_name
|
|
18
|
+
api_key base_url timeout max_retries default_workspace_id webhook_secret website_secret queue_name
|
|
19
19
|
].freeze
|
|
20
20
|
|
|
21
21
|
ENV_KEYS = {
|
|
@@ -25,6 +25,7 @@ module Fopost
|
|
|
25
25
|
max_retries: 'FOPOST_MAX_RETRIES',
|
|
26
26
|
default_workspace_id: 'FOPOST_WORKSPACE_ID',
|
|
27
27
|
webhook_secret: 'FOPOST_WEBHOOK_SECRET',
|
|
28
|
+
website_secret: 'FOPOST_WEBSITE_SECRET',
|
|
28
29
|
queue_name: 'FOPOST_QUEUE'
|
|
29
30
|
}.freeze
|
|
30
31
|
|
|
@@ -37,6 +38,11 @@ module Fopost
|
|
|
37
38
|
|
|
38
39
|
attr_writer(*SETTINGS)
|
|
39
40
|
|
|
41
|
+
# Called with a Fopost::BlogArticlePublished for every verified
|
|
42
|
+
# `article.published` delivery. Return the URL the article now lives at
|
|
43
|
+
# and the receiver echoes it back to FoPost.
|
|
44
|
+
attr_accessor :on_article_published
|
|
45
|
+
|
|
40
46
|
def api_key
|
|
41
47
|
resolve(:api_key)
|
|
42
48
|
end
|
|
@@ -65,6 +71,12 @@ module Fopost
|
|
|
65
71
|
resolve(:webhook_secret)
|
|
66
72
|
end
|
|
67
73
|
|
|
74
|
+
# The blog's website secret, shown once when the website target is set.
|
|
75
|
+
# The article receiver falls back to `webhook_secret` when it is unset.
|
|
76
|
+
def website_secret
|
|
77
|
+
resolve(:website_secret)
|
|
78
|
+
end
|
|
79
|
+
|
|
68
80
|
def queue_name
|
|
69
81
|
resolve(:queue_name)
|
|
70
82
|
end
|
|
@@ -83,6 +95,7 @@ module Fopost
|
|
|
83
95
|
# Drop every explicit value, so the next read falls back again.
|
|
84
96
|
def reset!
|
|
85
97
|
SETTINGS.each { |key| instance_variable_set(:"@#{key}", nil) }
|
|
98
|
+
@on_article_published = nil
|
|
86
99
|
self
|
|
87
100
|
end
|
|
88
101
|
|
|
@@ -92,7 +105,9 @@ module Fopost
|
|
|
92
105
|
|
|
93
106
|
# Keeps the key out of logs and consoles.
|
|
94
107
|
def inspect
|
|
95
|
-
redacted = to_h.merge(
|
|
108
|
+
redacted = to_h.merge(
|
|
109
|
+
api_key: mask(api_key), webhook_secret: mask(webhook_secret), website_secret: mask(website_secret)
|
|
110
|
+
)
|
|
96
111
|
"#<Fopost::Rails::Configuration #{redacted.map { |k, v| "#{k}=#{v.inspect}" }.join(' ')}>"
|
|
97
112
|
end
|
|
98
113
|
|
data/lib/fopost/rails/version.rb
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
# then Rails credentials under `fopost:`, then the environment. Leave a line
|
|
7
7
|
# commented to take the fallback.
|
|
8
8
|
Fopost::Rails.configure do |config|
|
|
9
|
-
# Create a key at https://
|
|
9
|
+
# Create a key at https://fopost.com/dashboard/api-keys.
|
|
10
10
|
# Falls back to credentials `fopost: api_key:`, then ENV['FOPOST_API_KEY'].
|
|
11
11
|
# config.api_key = nil
|
|
12
12
|
|
|
@@ -20,6 +20,13 @@ Fopost::Rails.configure do |config|
|
|
|
20
20
|
# Shown once, when you create a webhook. Required by the mounted endpoint.
|
|
21
21
|
# config.webhook_secret = nil
|
|
22
22
|
|
|
23
|
+
# Shown once, when you set the blog's website target. Used by /fopost/articles;
|
|
24
|
+
# falls back to webhook_secret.
|
|
25
|
+
# config.website_secret = nil
|
|
26
|
+
|
|
27
|
+
# Receives a Fopost::BlogArticlePublished; return the URL you published it at.
|
|
28
|
+
# config.on_article_published = ->(article) { Article.import!(article).url }
|
|
29
|
+
|
|
23
30
|
# ActiveJob queue for PublishJob and CreatePostJob.
|
|
24
31
|
# config.queue_name = 'default'
|
|
25
32
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: fopost-rails
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- FoPost
|
|
@@ -36,14 +36,14 @@ dependencies:
|
|
|
36
36
|
requirements:
|
|
37
37
|
- - "~>"
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
|
-
version: '0.
|
|
39
|
+
version: '0.3'
|
|
40
40
|
type: :runtime
|
|
41
41
|
prerelease: false
|
|
42
42
|
version_requirements: !ruby/object:Gem::Requirement
|
|
43
43
|
requirements:
|
|
44
44
|
- - "~>"
|
|
45
45
|
- !ruby/object:Gem::Version
|
|
46
|
-
version: '0.
|
|
46
|
+
version: '0.3'
|
|
47
47
|
- !ruby/object:Gem::Dependency
|
|
48
48
|
name: railties
|
|
49
49
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -74,6 +74,7 @@ files:
|
|
|
74
74
|
- CHANGELOG.md
|
|
75
75
|
- LICENSE
|
|
76
76
|
- README.md
|
|
77
|
+
- app/controllers/fopost/rails/articles_controller.rb
|
|
77
78
|
- app/controllers/fopost/rails/webhooks_controller.rb
|
|
78
79
|
- config/routes.rb
|
|
79
80
|
- lib/fopost-rails.rb
|