automatic 26.08 → 26.09
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/README.md +80 -46
- data/VERSION +1 -1
- data/assets/siteinfo/items_all.json +60300 -52138
- data/automatic.gemspec +4 -3
- data/bin/automatic +1 -1
- data/doc/AI_TUTORIAL.md +64 -40
- data/doc/BASIC_DESIGN.md +31 -0
- data/doc/DEPLOYMENT.md +53 -47
- data/doc/PLUGINS.md +231 -91
- data/doc/POLICY.md +149 -44
- data/doc/QUICKSTART.md +19 -15
- data/doc/RELEASING.md +20 -7
- data/doc/REQUIREMENTS.md +40 -10
- data/doc/VERSIONS +112 -54
- data/lib/automatic/cli.rb +40 -18
- data/lib/automatic/environment.rb +1 -1
- data/lib/automatic/feed_maker.rb +57 -18
- data/lib/automatic/feed_parser.rb +1 -1
- data/lib/automatic/http.rb +1 -1
- data/lib/automatic/log.rb +1 -1
- data/lib/automatic/pipeline.rb +15 -5
- data/lib/automatic/recipe.rb +45 -1
- data/lib/automatic/version.rb +3 -3
- data/lib/automatic.rb +18 -3
- data/plugins/custom_feed/web.rb +1 -1
- data/plugins/filter/absolute_uri.rb +1 -1
- data/plugins/filter/batch.rb +97 -0
- data/plugins/filter/claude.rb +1 -1
- data/plugins/filter/clear.rb +1 -1
- data/plugins/filter/description_link.rb +20 -3
- data/plugins/filter/full_feed.rb +28 -16
- data/plugins/filter/gemini.rb +1 -1
- data/plugins/filter/ignore.rb +1 -1
- data/plugins/filter/image.rb +1 -1
- data/plugins/filter/image_source.rb +20 -3
- data/plugins/filter/join.rb +4 -6
- data/plugins/filter/kimi.rb +216 -0
- data/plugins/filter/limit.rb +57 -0
- data/plugins/filter/open_ai.rb +1 -1
- data/plugins/filter/present.rb +75 -0
- data/plugins/filter/sakura_ai.rb +1 -1
- data/plugins/filter/sanitize.rb +1 -1
- data/plugins/filter/sort.rb +1 -1
- data/plugins/filter/tumblr_resize.rb +1 -1
- data/plugins/notify/ikachan.rb +1 -1
- data/plugins/provide/fluentd.rb +1 -1
- data/plugins/publish/amazon_s3.rb +9 -3
- data/plugins/publish/console.rb +1 -1
- data/plugins/publish/fluentd.rb +1 -1
- data/plugins/publish/hatena_bookmark.rb +1 -1
- data/plugins/publish/markdown.rb +1 -1
- data/plugins/publish/memcached.rb +1 -1
- data/plugins/store/digest.rb +1 -1
- data/plugins/store/file.rb +11 -3
- data/plugins/store/full_text.rb +9 -13
- data/plugins/store/permalink.rb +1 -1
- data/plugins/subscription/feed.rb +1 -1
- data/plugins/subscription/link.rb +1 -1
- data/plugins/subscription/text.rb +13 -4
- data/plugins/subscription/tumblr.rb +1 -1
- data/plugins/subscription/xml.rb +1 -1
- metadata +6 -2
data/doc/VERSIONS
CHANGED
|
@@ -1,26 +1,64 @@
|
|
|
1
1
|
automaticruby Repository Version History
|
|
2
2
|
========================================
|
|
3
3
|
|
|
4
|
+
v26.09 (2026-09-23)
|
|
5
|
+
-------------------
|
|
6
|
+
- Add FilterKimi for the Kimi API, using its OpenAI-compatible Chat Completions
|
|
7
|
+
interface.
|
|
8
|
+
- Add FilterLimit, FilterBatch, and FilterPresent for pipeline-wide limiting,
|
|
9
|
+
batching, and required-field filtering.
|
|
10
|
+
- Add per-fetch interval handling to FilterFullFeed, FilterImageSource, and
|
|
11
|
+
FilterDescriptionLink.
|
|
12
|
+
- Refresh the bundled LDRFullFeed siteinfo database from a newer upstream
|
|
13
|
+
snapshot.
|
|
14
|
+
- Make doc/PLUGINS.md section 6 the plugin catalogue's source of truth.
|
|
15
|
+
- Harden the framework's execution boundary: fix CLI contract drift, distinguish
|
|
16
|
+
dependency from load failures, and preflight-validate Recipe plugins.
|
|
17
|
+
- Preserve standard item metadata when FeedMaker rebuilds pipelines so filters
|
|
18
|
+
keep source, enclosure, and full content.
|
|
19
|
+
- Preserve nil-link pipeline items and remove SubscriptionText placeholder
|
|
20
|
+
title and link values.
|
|
21
|
+
- Preserve SubscriptionText TSV column positions and ignore empty input rows.
|
|
22
|
+
- Preserve local file paths when StoreFile hands file URIs to PublishAmazonS3.
|
|
23
|
+
- Stop StoreFullText from passing items downstream when database writes fail.
|
|
24
|
+
|
|
4
25
|
v26.08 (2026-08-22)
|
|
5
26
|
-------------------
|
|
6
|
-
- Support Ruby 3.3 through 4.0 and modernize the codebase for current Ruby and
|
|
7
|
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
27
|
+
- Support Ruby 3.3 through 4.0 and modernize the codebase for current Ruby and
|
|
28
|
+
maintained library APIs, validating representative versions in CI.
|
|
29
|
+
- Harden Recipe loading with safe YAML parsing, structural validation and
|
|
30
|
+
framework-specific errors.
|
|
31
|
+
- Restructure the CLI with help and version options, predictable error reporting
|
|
32
|
+
and documented exit statuses.
|
|
33
|
+
- Report a clear CLI error when feed inspection discovers no feed instead of
|
|
34
|
+
raising an internal exception.
|
|
10
35
|
- Reject HTTP and HTTPS URLs without a host before attempting a network request.
|
|
11
|
-
- Verify TLS certificates when publishing to Instapaper instead of accepting an
|
|
12
|
-
|
|
13
|
-
-
|
|
14
|
-
|
|
15
|
-
-
|
|
16
|
-
|
|
17
|
-
-
|
|
18
|
-
|
|
19
|
-
-
|
|
20
|
-
|
|
21
|
-
- Add
|
|
22
|
-
|
|
23
|
-
-
|
|
36
|
+
- Verify TLS certificates when publishing to Instapaper instead of accepting an
|
|
37
|
+
unverified connection.
|
|
38
|
+
- Modernize gem packaging: separate core from optional plugin dependencies so an
|
|
39
|
+
install can be minimal or extended one plugin at a time.
|
|
40
|
+
- Classify every shipped plugin by its current support status rather than
|
|
41
|
+
simulating obsolete services in tests.
|
|
42
|
+
- Modernize the bundled plugins for current Ruby, libraries and services, and
|
|
43
|
+
remove the integrations that are no longer viable.
|
|
44
|
+
- Remove the plugins for services that no longer exist: Twitter, Pocket, HipChat,
|
|
45
|
+
Google Calendar, livedoor Weather, So-net G-Guide and Chan-Toru.
|
|
46
|
+
- Add the plugin CustomFeedWeb, building one feed per HTML index page from its
|
|
47
|
+
article links, selected by CSS selector and filtered by host, pattern and count.
|
|
48
|
+
- Add the plugin StoreDigest, which drops items by content rather than by URL, on
|
|
49
|
+
a SHA-256 digest of the item fields a Recipe names.
|
|
50
|
+
- Add the plugin FilterJoin, which joins the whole pipeline into one item carrying
|
|
51
|
+
each item's title, link and body, and invents no permalink for it.
|
|
52
|
+
- Add one AI filter plugin per service (FilterOpenAI, FilterClaude, FilterGemini,
|
|
53
|
+
FilterSakuraAI), replacing an item's description with that service's answer.
|
|
54
|
+
- Rebuild the test and CI strategy for current RSpec and Ruby, with deterministic
|
|
55
|
+
isolation from user data and external services.
|
|
56
|
+
- Add Markdown as the primary service-independent publication format, with a
|
|
57
|
+
documented and tested first-run workflow.
|
|
58
|
+
- Rebuild the maintained documentation around current usage, architecture, policy,
|
|
59
|
+
plugins and deployment, and remove superseded historical documents.
|
|
60
|
+
- Offer the project under either GPLv3 or LGPLv3, retaining third-party
|
|
61
|
+
attribution and shipping both licence texts.
|
|
24
62
|
|
|
25
63
|
v14.12.2 (2015-01-04)
|
|
26
64
|
---------------------
|
|
@@ -56,9 +94,12 @@ v14.3.0 (2014-03-23)
|
|
|
56
94
|
v14.2.0 (2014-02-26)
|
|
57
95
|
--------------------
|
|
58
96
|
- Add the plugin Publish::AmazonS3.
|
|
59
|
-
- Rename Store::TargetLink to Store::File, and have it rewrite the link to a file
|
|
60
|
-
|
|
61
|
-
-
|
|
97
|
+
- Rename Store::TargetLink to Store::File, and have it rewrite the link to a file
|
|
98
|
+
URI scheme.
|
|
99
|
+
- Enable the file URI scheme in an RSS 2.0 feed, and let Store::File fetch a file
|
|
100
|
+
from Amazon S3.
|
|
101
|
+
- Read text from TSV formatted files in Subscription::Text, and add the
|
|
102
|
+
description, author and comments fields to the RSS 2.0 feed.
|
|
62
103
|
- Give the log messages more detail.
|
|
63
104
|
- Use 'double' instead of 'mock' and 'stub' for rspec-mocks.
|
|
64
105
|
- Improve CI with a network connection.
|
|
@@ -80,14 +121,17 @@ v13.7.0 (2013-07-28)
|
|
|
80
121
|
|
|
81
122
|
v13.6.0 (2013-06-28)
|
|
82
123
|
--------------------
|
|
83
|
-
- Add
|
|
84
|
-
|
|
124
|
+
- Add eleven plugins spanning Subscription, Filter and Publish, including Pocket,
|
|
125
|
+
GGuide, ChanToru, Accept, Sanitize, ConsoleLink, Hipchat, Memcached and Fluentd.
|
|
126
|
+
- Correspond to activesupport and activerecord 4.0, temporarily removing the mail
|
|
127
|
+
plugins for actionmailer 4.0.
|
|
85
128
|
- Ruby 1.8 support was outdated.
|
|
86
129
|
- And other improvements and bug fixes.
|
|
87
130
|
|
|
88
131
|
v13.5.0 (2013-05-18)
|
|
89
132
|
--------------------
|
|
90
|
-
- Add the plugins Subscription::Text, Subscription::Weather, Filter::One,
|
|
133
|
+
- Add the plugins Subscription::Text, Subscription::Weather, Filter::One,
|
|
134
|
+
Filter::Rand, Publish::Twitter and Publish::Pocket.
|
|
91
135
|
- Move the repository from id774/automaticruby to automaticruby/automaticruby.
|
|
92
136
|
|
|
93
137
|
v13.4.1 (2013-04-10)
|
|
@@ -101,7 +145,8 @@ v13.2.0 (2013-02-13)
|
|
|
101
145
|
- Implement the retry function in all subscription and download plugins.
|
|
102
146
|
- Plugin Filter::Ignore has become an item-based filter.
|
|
103
147
|
- Rename the plugin Filter::Reverse to Filter::Sort.
|
|
104
|
-
- Add the plugins Subscription::GoogleReaderStar, Filter::FullFeed and
|
|
148
|
+
- Add the plugins Subscription::GoogleReaderStar, Filter::FullFeed and
|
|
149
|
+
Publish::Instapaper.
|
|
105
150
|
- Improve the tests.
|
|
106
151
|
- And other minor bug fixes.
|
|
107
152
|
|
|
@@ -122,18 +167,25 @@ v12.9.1 (2012-09-18)
|
|
|
122
167
|
|
|
123
168
|
v12.9.0 (2012-09-18)
|
|
124
169
|
--------------------
|
|
125
|
-
- Unify the format of the pipeline to a feed object, an array of feeds; some
|
|
126
|
-
|
|
127
|
-
-
|
|
170
|
+
- Unify the format of the pipeline to a feed object, an array of feeds; some
|
|
171
|
+
plugins lost backward compatibility.
|
|
172
|
+
- Add the plugins Subscription::Link, Subscription::Twitter, Filter::Image and
|
|
173
|
+
Filter::AbsoluteURI.
|
|
174
|
+
- Remove the obsolete plugins Subscription::URI, Extract::Link, Filter::ImageLink,
|
|
175
|
+
Store::Link and Store::Target.
|
|
128
176
|
|
|
129
177
|
v12.6.0 (2012-06-18)
|
|
130
178
|
--------------------
|
|
131
|
-
- Add the plugins Subscription::URI, Extract::Link, Filter::ImageLink, Store::Link
|
|
132
|
-
|
|
133
|
-
-
|
|
179
|
+
- Add the plugins Subscription::URI, Extract::Link, Filter::ImageLink, Store::Link
|
|
180
|
+
and Store::Target.
|
|
181
|
+
- Make scaffold create the config and db directories, and add the unscaffold
|
|
182
|
+
subcommand to automatic-config.
|
|
183
|
+
- Search the user directory's config when given -c with a simple filename, and use
|
|
184
|
+
the user's db directory when it exists.
|
|
134
185
|
- Show the recipe and db name in the log.
|
|
135
186
|
- Refactor the plug-in names and the header fields.
|
|
136
|
-
- Add tests, taking RSpec code coverage over 99%, and add the all option to
|
|
187
|
+
- Add tests, taking RSpec code coverage over 99%, and add the all option to
|
|
188
|
+
script/build.
|
|
137
189
|
|
|
138
190
|
v12.4.0 (2012-04-30)
|
|
139
191
|
--------------------
|
|
@@ -144,40 +196,40 @@ v12.4.0 (2012-04-30)
|
|
|
144
196
|
v12.3.1 (2012-03-16)
|
|
145
197
|
--------------------
|
|
146
198
|
- Write the English documents doc/README and doc/PLUGINS.
|
|
147
|
-
- Let a user who installed the gem package create their own plugins, under
|
|
148
|
-
|
|
199
|
+
- Let a user who installed the gem package create their own plugins, under
|
|
200
|
+
~/.automatic/plugins.
|
|
201
|
+
- Merge the utilities into the automatic-config command, adding the subcommands
|
|
202
|
+
scaffold, autodiscovery, opmlparser, feedparser and log.
|
|
149
203
|
- Add the integration test script script/build for CI.
|
|
150
204
|
|
|
151
205
|
v12.03 (2012-03-07)
|
|
152
206
|
-------------------
|
|
153
207
|
- Distribute as a gem library.
|
|
154
|
-
- Add the plugins CustomFeed::SVNLog, Notify::Ikachan, Filter::TumblrResize,
|
|
208
|
+
- Add the plugins CustomFeed::SVNLog, Notify::Ikachan, Filter::TumblrResize,
|
|
209
|
+
Filter::Image and Store::TargetLink.
|
|
155
210
|
- Add the CI and build script script/bootstrap.
|
|
156
211
|
- Significant refactoring.
|
|
157
212
|
|
|
158
213
|
v12.02.1 (2012-02-25)
|
|
159
214
|
---------------------
|
|
160
|
-
- Point release: a large refactoring of the core, a reorganization of the
|
|
215
|
+
- Point release: a large refactoring of the core, a reorganization of the
|
|
216
|
+
namespace, and a few plugins added.
|
|
161
217
|
|
|
162
218
|
v12.02 (2012-02-18)
|
|
163
219
|
-------------------
|
|
164
|
-
-
|
|
220
|
+
- Initial release.
|
|
165
221
|
|
|
166
222
|
|
|
167
223
|
Notes on this record
|
|
168
224
|
====================
|
|
169
|
-
- Entries from v12.02 to v14.12.0 were moved here from doc/ChangeLog,
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
-
|
|
176
|
-
|
|
177
|
-
other record of them.
|
|
178
|
-
- Between v14.12.2 and v26.08 the only committed change was the addition of the
|
|
179
|
-
integration recipe test/integration/test_pdf2local.yml in April 2015. It is
|
|
180
|
-
part of the v26.08 entry's repository state and took no version of its own.
|
|
225
|
+
- Entries from v12.02 to v14.12.0 were moved here from doc/ChangeLog, unchanged in
|
|
226
|
+
date or content, one bullet per change.
|
|
227
|
+
- v14.10.1, v14.12.1 and v14.12.2 were released and recorded in the commit log but
|
|
228
|
+
never in doc/ChangeLog; their entries above are reconstructed from it.
|
|
229
|
+
- No Git tag exists for any release before v26.08; the version numbers above are
|
|
230
|
+
those recorded in the VERSION file at the time.
|
|
231
|
+
- Between v14.12.2 and v26.08 the only committed change was adding
|
|
232
|
+
test/integration/test_pdf2local.yml (April 2015), folded into v26.08.
|
|
181
233
|
|
|
182
234
|
|
|
183
235
|
Version History Guidelines
|
|
@@ -187,10 +239,11 @@ Version History Guidelines
|
|
|
187
239
|
- Omit development-time corrections and intermediate states that were reverted
|
|
188
240
|
or superseded before release; Git history retains that detail.
|
|
189
241
|
- Summarize related low-level changes under their substantive outcome.
|
|
190
|
-
- Write one coherent change per
|
|
191
|
-
-
|
|
192
|
-
names, plugin names, option names, and
|
|
193
|
-
|
|
242
|
+
- Write one coherent change per bullet, at most two physical lines.
|
|
243
|
+
- A single line at or under 80 columns is preferred whenever practical.
|
|
244
|
+
Necessary file names, module names, plugin names, option names, and
|
|
245
|
+
configuration names may pass 80 columns on the bullet's one or two lines
|
|
246
|
+
when they cannot be shortened without losing meaning.
|
|
194
247
|
- When a version history already exists, balance a new entry against the length
|
|
195
248
|
of the existing lines, so that the whole file stays consistent.
|
|
196
249
|
- When an entry becomes substantially longer, omit or abstract implementation
|
|
@@ -201,8 +254,13 @@ Version History Guidelines
|
|
|
201
254
|
same file or feature.
|
|
202
255
|
- Place closely related entries near each other.
|
|
203
256
|
- Append an independent new change to the end of the current version entry.
|
|
257
|
+
- Never create more than one repository version for the same calendar date.
|
|
258
|
+
- Independent changes may use separate bullets, but they stay in that day's
|
|
259
|
+
single version entry.
|
|
204
260
|
- Past entries and their dates are a record. Do not rewrite, renumber or merge
|
|
205
|
-
them
|
|
206
|
-
|
|
261
|
+
them. Past releases that share a date stay recorded as they were made; they
|
|
262
|
+
do not conform to the rule above and are not precedent for new releases.
|
|
207
263
|
- This file records the history of the repository. A change that is not
|
|
208
264
|
observable from outside does not need an entry here.
|
|
265
|
+
- The first entry, at the lowest version this file reaches, reads only
|
|
266
|
+
"Initial release." and nothing else.
|
data/lib/automatic/cli.rb
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
2
|
# Name:: Automatic::CLI
|
|
3
|
-
# Author: id774 (More info:
|
|
3
|
+
# Author: id774 (More info: https://id774.net)
|
|
4
4
|
# Source Code:: https://github.com/id774/automaticruby
|
|
5
5
|
# License:: The GPL version 3, or LGPL version 3 (Dual License).
|
|
6
6
|
# Contact:: idnanashi@gmail.com
|
|
7
7
|
# Created:: Aug 14, 2026
|
|
8
|
-
# Updated::
|
|
8
|
+
# Updated:: Sep 9, 2026
|
|
9
9
|
# Copyright:: Copyright (c) 2012-2026 Automatic Ruby Developers.
|
|
10
10
|
#
|
|
11
11
|
# Everything that belongs to being a command: option parsing, the subcommands,
|
|
@@ -103,11 +103,13 @@ module Automatic
|
|
|
103
103
|
EXIT_FAILURE
|
|
104
104
|
end
|
|
105
105
|
|
|
106
|
-
#
|
|
107
|
-
# naming a plugin whose optional gem is not
|
|
108
|
-
# mistake with an answer, and the answer is in
|
|
109
|
-
#
|
|
110
|
-
#
|
|
106
|
+
# Automatic::OptionalDependencyError is one of the failures reported as a
|
|
107
|
+
# message here: a Recipe naming a plugin whose optional gem is not
|
|
108
|
+
# installed is an operator's mistake with an answer, and the answer is in
|
|
109
|
+
# the message rather than in a backtrace. A LoadError raised from inside a
|
|
110
|
+
# plugin's own load -- for a different, unrelated missing file -- is not
|
|
111
|
+
# this, and is left alone along with everything else the plugin itself
|
|
112
|
+
# raises; see doc/PLUGINS.md section 2.7.
|
|
111
113
|
def run_recipe(path)
|
|
112
114
|
unless File.exist?(resolve_recipe(path))
|
|
113
115
|
@stderr.puts "automatic: no such recipe: #{path}"
|
|
@@ -116,7 +118,8 @@ module Automatic
|
|
|
116
118
|
|
|
117
119
|
Automatic.run(recipe: Automatic::Recipe.new(path), root_dir: @root_dir)
|
|
118
120
|
EXIT_SUCCESS
|
|
119
|
-
rescue Automatic::Error, Psych::Exception, SystemCallError, IOError,
|
|
121
|
+
rescue Automatic::Error, Psych::Exception, SystemCallError, IOError,
|
|
122
|
+
Automatic::OptionalDependencyError => e
|
|
120
123
|
@stderr.puts "automatic: #{e.message}"
|
|
121
124
|
EXIT_FAILURE
|
|
122
125
|
end
|
|
@@ -139,7 +142,7 @@ module Automatic
|
|
|
139
142
|
|
|
140
143
|
handler.call(argv)
|
|
141
144
|
EXIT_SUCCESS
|
|
142
|
-
rescue Automatic::Error, SystemCallError, IOError,
|
|
145
|
+
rescue Automatic::Error, SystemCallError, IOError, Automatic::OptionalDependencyError => e
|
|
143
146
|
@stderr.puts "automatic: #{e.message}"
|
|
144
147
|
EXIT_FAILURE
|
|
145
148
|
end
|
|
@@ -178,16 +181,12 @@ module Automatic
|
|
|
178
181
|
create_dir(File.join(Automatic.user_dir, 'db'))
|
|
179
182
|
|
|
180
183
|
assets = File.join(Automatic.user_dir, 'assets')
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
File.join(assets, 'siteinfo'))
|
|
184
|
-
end
|
|
184
|
+
create_dir(assets)
|
|
185
|
+
copy_bundled(File.join(@root_dir, 'assets', 'siteinfo'), File.join(assets, 'siteinfo'))
|
|
185
186
|
|
|
186
187
|
config = Automatic.user_config_dir
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
File.join(config, 'example'))
|
|
190
|
-
end
|
|
188
|
+
create_dir(config)
|
|
189
|
+
copy_bundled(File.join(@root_dir, 'config'), File.join(config, 'example'))
|
|
191
190
|
end
|
|
192
191
|
|
|
193
192
|
def unscaffold(_argv)
|
|
@@ -209,9 +208,19 @@ module Automatic
|
|
|
209
208
|
require 'automatic/feed_parser'
|
|
210
209
|
require 'pp'
|
|
211
210
|
url = argv.shift || missing_argument('feedparser')
|
|
211
|
+
validate_fetch_url(url)
|
|
212
212
|
@stdout.puts Automatic::FeedParser.get_url(url).pretty_inspect
|
|
213
213
|
end
|
|
214
214
|
|
|
215
|
+
# Converts only the URL validation failure `Automatic::Http.uri` already
|
|
216
|
+
# applies into the CLI's ordinary failure path; an unexpected error from
|
|
217
|
+
# inside FeedParser itself is left to propagate.
|
|
218
|
+
def validate_fetch_url(url)
|
|
219
|
+
Automatic::Http.uri(url)
|
|
220
|
+
rescue ArgumentError, URI::InvalidURIError => e
|
|
221
|
+
raise Automatic::Error, e.message
|
|
222
|
+
end
|
|
223
|
+
|
|
215
224
|
def inspect_url(argv)
|
|
216
225
|
require 'automatic/feed_parser'
|
|
217
226
|
Automatic.require_optional('feedbag', needed_by: 'the inspect subcommand')
|
|
@@ -221,7 +230,7 @@ module Automatic
|
|
|
221
230
|
raise Automatic::Error, "no feed found at #{url}" if feeds.empty?
|
|
222
231
|
|
|
223
232
|
@stdout.puts feeds.pretty_inspect
|
|
224
|
-
@stdout.puts Automatic::FeedParser.get_url(feeds.
|
|
233
|
+
@stdout.puts Automatic::FeedParser.get_url(feeds.first).pretty_inspect
|
|
225
234
|
end
|
|
226
235
|
|
|
227
236
|
def opmlparser(argv)
|
|
@@ -244,5 +253,18 @@ module Automatic
|
|
|
244
253
|
@stdout.puts "Creating #{path}"
|
|
245
254
|
true
|
|
246
255
|
end
|
|
256
|
+
|
|
257
|
+
# Bundled initial data (siteinfo, example config) is copied whenever its
|
|
258
|
+
# own destination is missing, independent of whether the parent user
|
|
259
|
+
# directory already existed. That is what lets a re-run of scaffold heal a
|
|
260
|
+
# partially-scaffolded user directory instead of only ever populating a
|
|
261
|
+
# brand new one. See doc/DEPLOYMENT.md and doc/POLICY.md on scaffold only
|
|
262
|
+
# adding what is missing.
|
|
263
|
+
def copy_bundled(src, dest)
|
|
264
|
+
return if File.exist?(dest)
|
|
265
|
+
|
|
266
|
+
FileUtils.cp_r(src, dest)
|
|
267
|
+
@stdout.puts "Creating #{dest}"
|
|
268
|
+
end
|
|
247
269
|
end
|
|
248
270
|
end
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
2
|
# Name:: Automatic::Environment
|
|
3
|
-
# Author: id774 (More info:
|
|
3
|
+
# Author: id774 (More info: https://id774.net)
|
|
4
4
|
# Source Code:: https://github.com/id774/automaticruby
|
|
5
5
|
# License:: The GPL version 3, or LGPL version 3 (Dual License).
|
|
6
6
|
# Contact:: idnanashi@gmail.com
|
data/lib/automatic/feed_maker.rb
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
2
|
# Name:: Automatic::FeedMaker
|
|
3
|
-
# Author: id774 (More info:
|
|
3
|
+
# Author: id774 (More info: https://id774.net)
|
|
4
4
|
# Source Code:: https://github.com/id774/automaticruby
|
|
5
5
|
# License:: The GPL version 3, or LGPL version 3 (Dual License).
|
|
6
6
|
# Contact:: idnanashi@gmail.com
|
|
7
7
|
# Created:: Feb 21, 2014
|
|
8
|
-
# Updated::
|
|
8
|
+
# Updated:: Sep 7, 2026
|
|
9
9
|
# Copyright:: Copyright (c) 2012-2026 Automatic Ruby Developers.
|
|
10
10
|
|
|
11
11
|
module Automatic
|
|
@@ -16,8 +16,8 @@ module Automatic
|
|
|
16
16
|
class FeedObject
|
|
17
17
|
attr_accessor :title, :link, :description, :author, :comments
|
|
18
18
|
def initialize
|
|
19
|
-
@link =
|
|
20
|
-
@title =
|
|
19
|
+
@link = nil
|
|
20
|
+
@title = nil
|
|
21
21
|
@description = ''
|
|
22
22
|
@author = ''
|
|
23
23
|
@comments = ''
|
|
@@ -34,6 +34,20 @@ module Automatic
|
|
|
34
34
|
feed_object
|
|
35
35
|
end
|
|
36
36
|
|
|
37
|
+
# Plain-value standard fields, copied onto the rebuilt item as-is when the
|
|
38
|
+
# item being rebuilt carries them. See doc/REQUIREMENTS.md and
|
|
39
|
+
# doc/PLUGINS.md for the standard item field contract this preserves.
|
|
40
|
+
REBUILD_SIMPLE_FIELDS = %i[title link description author comments content_encoded].freeze
|
|
41
|
+
|
|
42
|
+
# source and enclosure are RSS child elements. RSS::Maker exposes each of
|
|
43
|
+
# them on a new item as a builder with its own sub-attributes rather than
|
|
44
|
+
# accepting the finished element directly, so each is rebuilt attribute by
|
|
45
|
+
# attribute instead of by one assignment.
|
|
46
|
+
REBUILD_STRUCTURED_FIELDS = {
|
|
47
|
+
source: %i[url content],
|
|
48
|
+
enclosure: %i[url length type]
|
|
49
|
+
}.freeze
|
|
50
|
+
|
|
37
51
|
def self.create_pipeline(feeds = [])
|
|
38
52
|
RSS::Maker.make("2.0") {|maker|
|
|
39
53
|
xss = maker.xml_stylesheets.new_xml_stylesheet
|
|
@@ -44,20 +58,12 @@ module Automatic
|
|
|
44
58
|
|
|
45
59
|
unless feeds.nil?
|
|
46
60
|
feeds.each {|feed|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
item.description = feed.description
|
|
54
|
-
item.author = feed.author
|
|
55
|
-
item.comments = feed.comments
|
|
56
|
-
item.date = feed.pubDate || Time.now
|
|
57
|
-
rescue NoMethodError
|
|
58
|
-
Automatic::Log.puts("warn", "Undefined field detected in feed.")
|
|
59
|
-
end
|
|
60
|
-
end
|
|
61
|
+
Automatic::Log.puts("info", "Create Pipeline: #{feed.link}")
|
|
62
|
+
item = maker.items.new_item
|
|
63
|
+
item.date = (feed.pubDate if feed.respond_to?(:pubDate)) || Time.now
|
|
64
|
+
|
|
65
|
+
REBUILD_SIMPLE_FIELDS.each { |field| copy_rebuild_field(feed, item, field) }
|
|
66
|
+
REBUILD_STRUCTURED_FIELDS.each_key { |field| copy_rebuild_structured_field(feed, item, field) }
|
|
61
67
|
}
|
|
62
68
|
end
|
|
63
69
|
}
|
|
@@ -77,5 +83,38 @@ module Automatic
|
|
|
77
83
|
item.date = Time.now
|
|
78
84
|
}
|
|
79
85
|
end
|
|
86
|
+
|
|
87
|
+
# A field the item being rebuilt does not carry, or carries as nil, is
|
|
88
|
+
# left off the new item: that is the normal shape of an optional field,
|
|
89
|
+
# not a reason to skip copying anything else. See doc/POLICY.md on
|
|
90
|
+
# distinguishing a missing optional value from a programming error.
|
|
91
|
+
def self.copy_rebuild_field(feed, item, field)
|
|
92
|
+
return unless feed.respond_to?(field)
|
|
93
|
+
|
|
94
|
+
value = feed.public_send(field)
|
|
95
|
+
return if value.nil?
|
|
96
|
+
|
|
97
|
+
item.public_send("#{field}=", value)
|
|
98
|
+
end
|
|
99
|
+
private_class_method :copy_rebuild_field
|
|
100
|
+
|
|
101
|
+
# source and enclosure are rebuilt sub-attribute by sub-attribute, on the
|
|
102
|
+
# same missing-is-normal basis as copy_rebuild_field, so that (for
|
|
103
|
+
# example) an enclosure with no type recorded still keeps its url.
|
|
104
|
+
def self.copy_rebuild_structured_field(feed, item, field)
|
|
105
|
+
return unless feed.respond_to?(field)
|
|
106
|
+
|
|
107
|
+
value = feed.public_send(field)
|
|
108
|
+
return if value.nil?
|
|
109
|
+
|
|
110
|
+
target = item.public_send(field)
|
|
111
|
+
REBUILD_STRUCTURED_FIELDS.fetch(field).each do |sub_field|
|
|
112
|
+
next unless value.respond_to?(sub_field)
|
|
113
|
+
|
|
114
|
+
sub_value = value.public_send(sub_field)
|
|
115
|
+
target.public_send("#{sub_field}=", sub_value) unless sub_value.nil?
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
private_class_method :copy_rebuild_structured_field
|
|
80
119
|
end
|
|
81
120
|
end
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
2
|
# Name:: Automatic::FeedParser
|
|
3
|
-
# Author: id774 (More info:
|
|
3
|
+
# Author: id774 (More info: https://id774.net)
|
|
4
4
|
# Source Code:: https://github.com/id774/automaticruby
|
|
5
5
|
# License:: The GPL version 3, or LGPL version 3 (Dual License).
|
|
6
6
|
# Contact:: idnanashi@gmail.com
|
data/lib/automatic/http.rb
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
2
|
# Name:: Automatic::Http
|
|
3
|
-
# Author: id774 (More info:
|
|
3
|
+
# Author: id774 (More info: https://id774.net)
|
|
4
4
|
# Source Code:: https://github.com/id774/automaticruby
|
|
5
5
|
# License:: The GPL version 3, or LGPL version 3 (Dual License).
|
|
6
6
|
# Contact:: idnanashi@gmail.com
|
data/lib/automatic/log.rb
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
2
|
# Name:: Automatic::Log
|
|
3
|
-
# Author: id774 (More info:
|
|
3
|
+
# Author: id774 (More info: https://id774.net)
|
|
4
4
|
# Source Code:: https://github.com/id774/automaticruby
|
|
5
5
|
# License:: The GPL version 3, or LGPL version 3 (Dual License).
|
|
6
6
|
# Contact:: idnanashi@gmail.com
|
data/lib/automatic/pipeline.rb
CHANGED
|
@@ -1,16 +1,23 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
2
|
# Name:: Automatic::Pipeline
|
|
3
|
-
# Author: id774 (More info:
|
|
3
|
+
# Author: id774 (More info: https://id774.net)
|
|
4
4
|
# Source Code:: https://github.com/id774/automaticruby
|
|
5
5
|
# License:: The GPL version 3, or LGPL version 3 (Dual License).
|
|
6
6
|
# Contact:: idnanashi@gmail.com
|
|
7
7
|
# Created:: Feb 22, 2012
|
|
8
|
-
# Updated::
|
|
8
|
+
# Updated:: Sep 6, 2026
|
|
9
9
|
# Copyright:: Copyright (c) 2012-2026 Automatic Ruby Developers.
|
|
10
10
|
#
|
|
11
11
|
# The core: resolve a plugin's class name to a file, then run the Recipe's
|
|
12
12
|
# plugins in order, each receiving the previous one's output. See
|
|
13
13
|
# doc/BASIC_DESIGN.md section 4.6 and doc/PLUGINS.md section 3.
|
|
14
|
+
#
|
|
15
|
+
# Every module the Recipe names is discovered before any plugin runs, so that
|
|
16
|
+
# an unknown module later in the Recipe is refused before an earlier plugin's
|
|
17
|
+
# side effect, not after it. Discovery only registers an autoload; it does not
|
|
18
|
+
# read a plugin's own source, which stays lazy until that plugin's turn to
|
|
19
|
+
# run. See doc/PLUGINS.md on unknown plugin being refused before any plugin
|
|
20
|
+
# runs.
|
|
14
21
|
|
|
15
22
|
require 'active_support/core_ext/string/inflections'
|
|
16
23
|
|
|
@@ -51,10 +58,13 @@ module Automatic
|
|
|
51
58
|
def run(recipe)
|
|
52
59
|
raise NoRecipeError, 'no recipe given' if recipe.nil?
|
|
53
60
|
|
|
61
|
+
entries = []
|
|
62
|
+
recipe.each_plugin { |plugin| entries << [plugin, plugin.module] }
|
|
63
|
+
|
|
64
|
+
entries.each { |_plugin, mod| load_plugin(mod) }
|
|
65
|
+
|
|
54
66
|
pipeline = []
|
|
55
|
-
|
|
56
|
-
mod = plugin.module
|
|
57
|
-
load_plugin(mod)
|
|
67
|
+
entries.each do |plugin, mod|
|
|
58
68
|
klass = Automatic::Plugin.const_get(mod)
|
|
59
69
|
pipeline = klass.new(plugin.config, pipeline).run
|
|
60
70
|
end
|
data/lib/automatic/recipe.rb
CHANGED
|
@@ -5,11 +5,16 @@
|
|
|
5
5
|
# License:: The GPL version 3, or LGPL version 3 (Dual License).
|
|
6
6
|
# Contact:: idnanashi@gmail.com
|
|
7
7
|
# Created:: Feb 18, 2012
|
|
8
|
-
# Updated::
|
|
8
|
+
# Updated:: Sep 6, 2026
|
|
9
9
|
# Copyright:: Copyright (c) 2012-2026 Automatic Ruby Developers.
|
|
10
10
|
#
|
|
11
11
|
# Turns a Recipe file into something the pipeline can iterate. The format is
|
|
12
12
|
# specified in doc/PLUGINS.md section 2.
|
|
13
|
+
#
|
|
14
|
+
# Each plugin entry's documented shape -- a mapping naming a module, with an
|
|
15
|
+
# optional config mapping -- is validated here, at load time, rather than
|
|
16
|
+
# left to surface as whatever internal exception a malformed entry happens to
|
|
17
|
+
# raise once the pipeline reaches it.
|
|
13
18
|
|
|
14
19
|
require 'date'
|
|
15
20
|
require 'hashie'
|
|
@@ -45,6 +50,8 @@ module Automatic
|
|
|
45
50
|
"recipe #{resolved} has no plugins sequence"
|
|
46
51
|
end
|
|
47
52
|
|
|
53
|
+
validate_plugins(resolved)
|
|
54
|
+
|
|
48
55
|
Automatic::Log.level(@procedure.global&.log&.level)
|
|
49
56
|
Automatic::Log.puts('info', "Loading Recipe: #{resolved}")
|
|
50
57
|
@procedure
|
|
@@ -72,5 +79,42 @@ module Automatic
|
|
|
72
79
|
aliases: true
|
|
73
80
|
)
|
|
74
81
|
end
|
|
82
|
+
|
|
83
|
+
# Only the shape doc/PLUGINS.md documents is checked here: a mapping
|
|
84
|
+
# naming a module, with an optional config mapping. What a plugin does
|
|
85
|
+
# with its own config is that plugin's concern, not this one's.
|
|
86
|
+
def validate_plugins(resolved)
|
|
87
|
+
@procedure.plugins.each_with_index do |plugin, index|
|
|
88
|
+
validate_plugin_entry(resolved, plugin, index)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def validate_plugin_entry(resolved, plugin, index)
|
|
93
|
+
unless plugin.is_a?(Hash)
|
|
94
|
+
raise InvalidRecipeError,
|
|
95
|
+
"recipe #{resolved} plugins[#{index}] is not a mapping"
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
validate_module_name(resolved, plugin, index)
|
|
99
|
+
validate_plugin_config(resolved, plugin, index)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def validate_module_name(resolved, plugin, index)
|
|
103
|
+
name = plugin['module']
|
|
104
|
+
|
|
105
|
+
return if name.is_a?(String) && !name.strip.empty?
|
|
106
|
+
|
|
107
|
+
raise InvalidRecipeError,
|
|
108
|
+
"recipe #{resolved} plugins[#{index}] has no module name"
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def validate_plugin_config(resolved, plugin, index)
|
|
112
|
+
config = plugin['config']
|
|
113
|
+
|
|
114
|
+
return if config.nil? || config.is_a?(Hash)
|
|
115
|
+
|
|
116
|
+
raise InvalidRecipeError,
|
|
117
|
+
"recipe #{resolved} plugins[#{index}] has a config that is not a mapping"
|
|
118
|
+
end
|
|
75
119
|
end
|
|
76
120
|
end
|