automatic 14.12.2 → 26.08

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (172) hide show
  1. checksums.yaml +5 -5
  2. data/README.md +635 -83
  3. data/VERSION +1 -1
  4. data/automatic.gemspec +109 -248
  5. data/bin/automatic +20 -139
  6. data/config/feed2console.yml +10 -4
  7. data/config/feed2markdown.yml +41 -0
  8. data/doc/AI_TUTORIAL.md +518 -0
  9. data/doc/BASIC_DESIGN.md +516 -0
  10. data/doc/COPYING.LESSER +165 -0
  11. data/doc/DEPLOYMENT.md +824 -0
  12. data/doc/LICENSE.md +14 -0
  13. data/doc/PLUGINS.md +1875 -0
  14. data/doc/PLUGIN_DEVELOPMENT.md +86 -0
  15. data/doc/POLICY.md +857 -0
  16. data/doc/QUICKSTART.md +256 -0
  17. data/doc/RELEASING.md +381 -0
  18. data/doc/REQUIREMENTS.md +526 -0
  19. data/doc/VERSIONS +208 -0
  20. data/lib/automatic/cli.rb +248 -0
  21. data/lib/automatic/environment.rb +31 -5
  22. data/lib/automatic/feed_maker.rb +10 -9
  23. data/lib/automatic/feed_parser.rb +51 -35
  24. data/lib/automatic/http.rb +107 -0
  25. data/lib/automatic/log.rb +49 -18
  26. data/lib/automatic/opml.rb +3 -1
  27. data/lib/automatic/pipeline.rb +63 -32
  28. data/lib/automatic/recipe.rb +56 -17
  29. data/lib/automatic/version.rb +14 -1
  30. data/lib/automatic.rb +78 -20
  31. data/plugins/custom_feed/svn_log.rb +73 -32
  32. data/plugins/custom_feed/web.rb +348 -0
  33. data/plugins/filter/absolute_uri.rb +43 -27
  34. data/plugins/filter/accept.rb +38 -45
  35. data/plugins/filter/claude.rb +217 -0
  36. data/plugins/filter/clear.rb +12 -8
  37. data/plugins/filter/description_link.rb +49 -51
  38. data/plugins/filter/full_feed.rb +158 -52
  39. data/plugins/filter/gemini.rb +216 -0
  40. data/plugins/filter/github_feed.rb +38 -26
  41. data/plugins/filter/ignore.rb +33 -44
  42. data/plugins/filter/image.rb +36 -25
  43. data/plugins/filter/image_source.rb +58 -52
  44. data/plugins/filter/join.rb +107 -0
  45. data/plugins/filter/one.rb +19 -26
  46. data/plugins/filter/open_ai.rb +198 -0
  47. data/plugins/filter/rand.rb +16 -17
  48. data/plugins/filter/sakura_ai.rb +205 -0
  49. data/plugins/filter/sanitize.rb +29 -34
  50. data/plugins/filter/sort.rb +20 -27
  51. data/plugins/filter/tumblr_resize.rb +31 -23
  52. data/plugins/notify/ikachan.rb +86 -48
  53. data/plugins/provide/fluentd.rb +43 -24
  54. data/plugins/publish/amazon_s3.rb +73 -40
  55. data/plugins/publish/console.rb +19 -16
  56. data/plugins/publish/console_link.rb +20 -17
  57. data/plugins/publish/eject.rb +48 -26
  58. data/plugins/publish/fluentd.rb +50 -30
  59. data/plugins/publish/hatena_bookmark.rb +88 -71
  60. data/plugins/publish/instapaper.rb +69 -59
  61. data/plugins/publish/markdown.rb +278 -0
  62. data/plugins/publish/memcached.rb +35 -29
  63. data/plugins/store/database.rb +50 -48
  64. data/plugins/store/digest.rb +212 -0
  65. data/plugins/store/file.rb +99 -68
  66. data/plugins/store/full_text.rb +32 -25
  67. data/plugins/store/permalink.rb +18 -22
  68. data/plugins/subscription/feed.rb +34 -21
  69. data/plugins/subscription/link.rb +31 -32
  70. data/plugins/subscription/text.rb +32 -46
  71. data/plugins/subscription/tumblr.rb +55 -44
  72. data/plugins/subscription/xml.rb +40 -36
  73. metadata +108 -321
  74. data/Gemfile +0 -38
  75. data/Rakefile +0 -59
  76. data/doc/ChangeLog +0 -303
  77. data/doc/PLUGINS +0 -750
  78. data/doc/PLUGINS.ja +0 -753
  79. data/doc/README +0 -511
  80. data/doc/README.ja +0 -519
  81. data/plugins/filter/google_news.rb +0 -50
  82. data/plugins/publish/google_calendar.rb +0 -84
  83. data/plugins/publish/hipchat.rb +0 -46
  84. data/plugins/publish/pocket.rb +0 -45
  85. data/plugins/publish/twitter.rb +0 -58
  86. data/plugins/subscription/chan_toru.rb +0 -57
  87. data/plugins/subscription/g_guide.rb +0 -57
  88. data/plugins/subscription/pocket.rb +0 -51
  89. data/plugins/subscription/twitter.rb +0 -69
  90. data/plugins/subscription/twitter_search.rb +0 -50
  91. data/plugins/subscription/weather.rb +0 -33
  92. data/script/build +0 -84
  93. data/spec/fixtures/sampleFeeds.tsv +0 -1
  94. data/spec/fixtures/sampleFeeds2.tsv +0 -2
  95. data/spec/fixtures/sampleRecipe.yml +0 -24
  96. data/spec/lib/automatic/log_spec.rb +0 -32
  97. data/spec/lib/automatic/pipeline_spec.rb +0 -68
  98. data/spec/lib/automatic/recipe_spec.rb +0 -40
  99. data/spec/lib/automatic_spec.rb +0 -99
  100. data/spec/plugins/custom_feed/svn_log_spec.rb +0 -31
  101. data/spec/plugins/filter/absolute_uri_spec.rb +0 -61
  102. data/spec/plugins/filter/accept_spec.rb +0 -331
  103. data/spec/plugins/filter/clear_spec.rb +0 -49
  104. data/spec/plugins/filter/description_link_spec.rb +0 -138
  105. data/spec/plugins/filter/full_feed_spec.rb +0 -129
  106. data/spec/plugins/filter/github_feed_spec.rb +0 -55
  107. data/spec/plugins/filter/google_news_spec.rb +0 -69
  108. data/spec/plugins/filter/ignore_spec.rb +0 -328
  109. data/spec/plugins/filter/image_source_spec.rb +0 -89
  110. data/spec/plugins/filter/image_spec.rb +0 -65
  111. data/spec/plugins/filter/one_spec.rb +0 -71
  112. data/spec/plugins/filter/rand_spec.rb +0 -52
  113. data/spec/plugins/filter/sanitize_spec.rb +0 -153
  114. data/spec/plugins/filter/sort_spec.rb +0 -189
  115. data/spec/plugins/filter/tumblr_resize_spec.rb +0 -109
  116. data/spec/plugins/notify/ikachan_spec.rb +0 -58
  117. data/spec/plugins/provide/fluentd_spec.rb +0 -49
  118. data/spec/plugins/publish/amazon_s3_spec.rb +0 -40
  119. data/spec/plugins/publish/console_spec.rb +0 -30
  120. data/spec/plugins/publish/eject_spec.rb +0 -40
  121. data/spec/plugins/publish/fluentd_spec.rb +0 -40
  122. data/spec/plugins/publish/google_calendar_spec.rb +0 -83
  123. data/spec/plugins/publish/hatena_bookmark_spec.rb +0 -134
  124. data/spec/plugins/publish/hipchat_spec.rb +0 -69
  125. data/spec/plugins/publish/instapaper_spec.rb +0 -82
  126. data/spec/plugins/publish/memcached_spec.rb +0 -63
  127. data/spec/plugins/publish/pocket_spec.rb +0 -51
  128. data/spec/plugins/publish/twitter_spec.rb +0 -73
  129. data/spec/plugins/store/file_spec.rb +0 -58
  130. data/spec/plugins/store/full_text_spec.rb +0 -152
  131. data/spec/plugins/store/permalink_spec.rb +0 -206
  132. data/spec/plugins/subscription/chan_toru_spec.rb +0 -56
  133. data/spec/plugins/subscription/feed_spec.rb +0 -71
  134. data/spec/plugins/subscription/g_guide_spec.rb +0 -82
  135. data/spec/plugins/subscription/link_spec.rb +0 -72
  136. data/spec/plugins/subscription/pocket_spec.rb +0 -57
  137. data/spec/plugins/subscription/text_spec.rb +0 -84
  138. data/spec/plugins/subscription/tumblr_spec.rb +0 -74
  139. data/spec/plugins/subscription/twitter_search_spec.rb +0 -57
  140. data/spec/plugins/subscription/twitter_spec.rb +0 -73
  141. data/spec/plugins/subscription/weather_spec.rb +0 -44
  142. data/spec/plugins/subscription/xml_spec.rb +0 -84
  143. data/spec/spec_helper.rb +0 -106
  144. data/spec/user_dir/plugins/store/mock.rb +0 -16
  145. data/test/fixtures/sampleOPML.xml +0 -11
  146. data/test/integration/test_absoluteurl.yml +0 -25
  147. data/test/integration/test_activerecord.yml +0 -24
  148. data/test/integration/test_add_pocket.yml +0 -26
  149. data/test/integration/test_chan_toru.yml +0 -21
  150. data/test/integration/test_descriptionlink.yml +0 -21
  151. data/test/integration/test_fluentd.yml +0 -22
  152. data/test/integration/test_fulltext.yml +0 -30
  153. data/test/integration/test_google_news.yml +0 -21
  154. data/test/integration/test_googlealert.yml +0 -21
  155. data/test/integration/test_hatenabookmark.yml +0 -30
  156. data/test/integration/test_ignore.yml +0 -25
  157. data/test/integration/test_ignore2.yml +0 -22
  158. data/test/integration/test_image2local.yml +0 -33
  159. data/test/integration/test_instapaper.yml +0 -26
  160. data/test/integration/test_link2local.yml +0 -34
  161. data/test/integration/test_one.yml +0 -23
  162. data/test/integration/test_pocket.yml +0 -22
  163. data/test/integration/test_rand.yml +0 -21
  164. data/test/integration/test_sanitize.yml +0 -23
  165. data/test/integration/test_sort.yml +0 -36
  166. data/test/integration/test_svnlog.yml +0 -15
  167. data/test/integration/test_text2feed.yml +0 -36
  168. data/test/integration/test_tumblr2local.yml +0 -43
  169. data/test/integration/test_twitter_search.yml +0 -22
  170. data/test/integration/test_weather.yml +0 -19
  171. data/test/integration/test_xml2fluentd.yml +0 -21
  172. data/vendor/.gitkeep +0 -0
data/doc/VERSIONS ADDED
@@ -0,0 +1,208 @@
1
+ automaticruby Repository Version History
2
+ ========================================
3
+
4
+ v26.08 (2026-08-22)
5
+ -------------------
6
+ - Support Ruby 3.3 through 4.0 and modernize the codebase for current Ruby and maintained library APIs, validating representative versions in CI.
7
+ - Harden Recipe loading with safe YAML parsing, structural validation and framework-specific errors.
8
+ - Restructure the CLI with help and version options, predictable error reporting and documented exit statuses.
9
+ - Report a clear CLI error when feed inspection discovers no feed instead of raising an internal exception.
10
+ - 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 unverified connection.
12
+ - Modernize gem packaging and dependency policy, separating core requirements from optional plugin dependencies so that an installation can be minimal, complete or extended one plugin at a time, and excluding development and generated files.
13
+ - Classify every shipped plugin by its current support status rather than simulating obsolete services in tests.
14
+ - Modernize the bundled plugins for current Ruby, libraries and services, and remove the integrations that are no longer viable.
15
+ - Remove the plugins for Twitter, Pocket, HipChat, Google Calendar, livedoor Weather, So-net G-Guide and Chan-Toru, and Google News link rewriting; their services or APIs no longer exist, and a Recipe naming one now fails at load.
16
+ - Add the plugin CustomFeedWeb, which builds one feed per HTML index page from the article links it lists, selected with CSS selectors and filtered by host, pattern and count.
17
+ - Add the plugin StoreDigest, which drops items by content rather than by URL, on a SHA-256 digest of the item fields a Recipe names.
18
+ - Add the plugin FilterJoin, which joins the whole pipeline into one item carrying each item's title, link and body, and invents no permalink for it.
19
+ - Add the plugins FilterOpenAI, FilterClaude, FilterGemini and FilterSakuraAI, one per AI service rather than one selectable by setting, each replacing an item's description with what that service answers to the Recipe's prompt under a token the Recipe holds.
20
+ - Rebuild the test and CI strategy for current RSpec and Ruby, with deterministic isolation from user data and external services.
21
+ - Add Markdown as the primary service-independent publication format, with a documented and tested first-run workflow.
22
+ - Rebuild the maintained documentation around current usage, architecture, policy, plugins and deployment, and remove superseded historical documents.
23
+ - Offer the project under either GPLv3 or LGPLv3, retaining third-party attribution and shipping both licence texts.
24
+
25
+ v14.12.2 (2015-01-04)
26
+ ---------------------
27
+ - Pin every gem in the Gemfile to an exact minor version.
28
+
29
+ v14.12.1 (2015-01-03)
30
+ ---------------------
31
+ - Depend on activerecord 4.1.
32
+
33
+ v14.12.0 (2014-12-27)
34
+ ---------------------
35
+ - Rewrite obsolete methods.
36
+ - Update Gemfile.
37
+
38
+ v14.10.1 (2014-10-23)
39
+ ---------------------
40
+ - Version-only release. No change beyond the version number is recorded for it.
41
+
42
+ v14.10.0 (2014-10-23)
43
+ ---------------------
44
+ - Use Logger for log output.
45
+ - Store::FullText ignores an entry whose title is already stored.
46
+ - Add the plugins Filter::Clear, Filter::GoogleNews and Filter::DescriptionLink.
47
+
48
+ v14.5.0 (2014-05-31)
49
+ --------------------
50
+ - Update to activesupport and activerecord 4.1.
51
+
52
+ v14.3.0 (2014-03-23)
53
+ --------------------
54
+ - Downgrade the nokogiri version for OS X.
55
+
56
+ v14.2.0 (2014-02-26)
57
+ --------------------
58
+ - Add the plugin Publish::AmazonS3.
59
+ - Rename Store::TargetLink to Store::File, and have it rewrite the link to a file URI scheme.
60
+ - Enable the file URI scheme in an RSS 2.0 feed, and let Store::File fetch a file from Amazon S3.
61
+ - Read text from TSV formatted files in Subscription::Text, and add the description, author and comments fields to the RSS 2.0 feed.
62
+ - Give the log messages more detail.
63
+ - Use 'double' instead of 'mock' and 'stub' for rspec-mocks.
64
+ - Improve CI with a network connection.
65
+ - Divide Feed::Parser into Feed::Maker.
66
+
67
+ v14.1.0 (2014-01-24)
68
+ --------------------
69
+ - Fix a dependency conflict.
70
+ - Refactor the error retry handling.
71
+
72
+ v13.12.0 (2013-12-17)
73
+ ---------------------
74
+ - Fix the twitter gem version.
75
+
76
+ v13.7.0 (2013-07-28)
77
+ --------------------
78
+ - Add the plugins Subscription::Xml and Provide::Fluentd.
79
+ - Fix some problems.
80
+
81
+ v13.6.0 (2013-06-28)
82
+ --------------------
83
+ - Add the plugins Subscription::Pocket, Subscription::TwitterSearch, Subscription::GGuide, Subscription::ChanToru, Filter::Accept, Filter::Sanitize, Filter::GithubFeed, Publish::ConsoleLink, Publish::Hipchat, Publish::Memcached and Publish::Fluentd.
84
+ - Correspond to activesupport and activerecord 4.0, temporarily removing the mail plugins for actionmailer 4.0.
85
+ - Ruby 1.8 support was outdated.
86
+ - And other improvements and bug fixes.
87
+
88
+ v13.5.0 (2013-05-18)
89
+ --------------------
90
+ - Add the plugins Subscription::Text, Subscription::Weather, Filter::One, Filter::Rand, Publish::Twitter and Publish::Pocket.
91
+ - Move the repository from id774/automaticruby to automaticruby/automaticruby.
92
+
93
+ v13.4.1 (2013-04-10)
94
+ --------------------
95
+ - Merge the command automatic-config into automatic.
96
+ - Improve the retry functions.
97
+ - Run the tests on Ruby 2.0.
98
+
99
+ v13.2.0 (2013-02-13)
100
+ --------------------
101
+ - Implement the retry function in all subscription and download plugins.
102
+ - Plugin Filter::Ignore has become an item-based filter.
103
+ - Rename the plugin Filter::Reverse to Filter::Sort.
104
+ - Add the plugins Subscription::GoogleReaderStar, Filter::FullFeed and Publish::Instapaper.
105
+ - Improve the tests.
106
+ - And other minor bug fixes.
107
+
108
+ v13.1.0 (2013-01-08)
109
+ --------------------
110
+ - Fix the interval bugs.
111
+ - Improve the timing of logging.
112
+ - Return only a new feed.
113
+ - Refactoring.
114
+
115
+ v12.10.0 (2012-10-16)
116
+ ---------------------
117
+ - Add the plugin Subscription::Tumblr.
118
+
119
+ v12.9.1 (2012-09-18)
120
+ --------------------
121
+ - Consider the case of a feed link including nil.
122
+
123
+ v12.9.0 (2012-09-18)
124
+ --------------------
125
+ - Unify the format of the pipeline to a feed object, an array of feeds; some plugins lost backward compatibility.
126
+ - Add the plugins Subscription::Link, Subscription::Twitter, Filter::Image and Filter::AbsoluteURI.
127
+ - Remove the obsolete plugins Subscription::URI, Extract::Link, Filter::ImageLink, Store::Link and Store::Target.
128
+
129
+ v12.6.0 (2012-06-18)
130
+ --------------------
131
+ - Add the plugins Subscription::URI, Extract::Link, Filter::ImageLink, Store::Link and Store::Target.
132
+ - Make scaffold create the config and db directories, and add the unscaffold subcommand to automatic-config.
133
+ - Search the user directory's config when given -c with a simple filename, and use the user's db directory when it exists.
134
+ - Show the recipe and db name in the log.
135
+ - Refactor the plug-in names and the header fields.
136
+ - Add tests, taking RSpec code coverage over 99%, and add the all option to script/build.
137
+
138
+ v12.4.0 (2012-04-30)
139
+ --------------------
140
+ - Add the inspect subcommand to automatic-config.
141
+ - Add the plugins Filter::Reverse, Publish::SMTP and Publish::Mail.
142
+ - Run all integration tests with CI.
143
+
144
+ v12.3.1 (2012-03-16)
145
+ --------------------
146
+ - Write the English documents doc/README and doc/PLUGINS.
147
+ - Let a user who installed the gem package create their own plugins, under ~/.automatic/plugins.
148
+ - Merge the utilities into the automatic-config command, adding the subcommands scaffold, autodiscovery, opmlparser, feedparser and log.
149
+ - Add the integration test script script/build for CI.
150
+
151
+ v12.03 (2012-03-07)
152
+ -------------------
153
+ - Distribute as a gem library.
154
+ - Add the plugins CustomFeed::SVNLog, Notify::Ikachan, Filter::TumblrResize, Filter::Image and Store::TargetLink.
155
+ - Add the CI and build script script/bootstrap.
156
+ - Significant refactoring.
157
+
158
+ v12.02.1 (2012-02-25)
159
+ ---------------------
160
+ - Point release: a large refactoring of the core, a reorganization of the namespace, and a few plugins added.
161
+
162
+ v12.02 (2012-02-18)
163
+ -------------------
164
+ - First release: the scratch of this script, with a plugin existing only for Hatena Bookmark.
165
+
166
+
167
+ Notes on this record
168
+ ====================
169
+ - Entries from v12.02 to v14.12.0 were moved here from doc/ChangeLog, which this
170
+ file replaces. No entry and no date was changed in the move; the wording was
171
+ brought to one bullet per change, and nothing was added or dropped.
172
+ - v14.10.1, v14.12.1 and v14.12.2 were released and are recorded in the commit
173
+ log, but were never written into doc/ChangeLog. Their entries above were
174
+ reconstructed from the commits that bumped the version.
175
+ - No Git tag exists for any release before v26.08. The version numbers above are
176
+ those recorded in the VERSION file at the time, and the commit log is the only
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.
181
+
182
+
183
+ Version History Guidelines
184
+ ==========================
185
+ - Record externally meaningful or architecturally significant outcomes, not a
186
+ diary of implementation work.
187
+ - Omit development-time corrections and intermediate states that were reverted
188
+ or superseded before release; Git history retains that detail.
189
+ - Summarize related low-level changes under their substantive outcome.
190
+ - Write one coherent change per physical line, as a rule.
191
+ - Keep entries near 100 columns where practical. Necessary file names, module
192
+ names, plugin names, option names, and configuration names may extend an entry
193
+ to approximately 120 columns.
194
+ - When a version history already exists, balance a new entry against the length
195
+ of the existing lines, so that the whole file stays consistent.
196
+ - When an entry becomes substantially longer, omit or abstract implementation
197
+ details, examples, reasons, and secondary effects where possible.
198
+ - Preserve the changed target, externally observable behavior, compatibility
199
+ or security impact, and important option or configuration names.
200
+ - Combine related changes within the same version, especially changes to the
201
+ same file or feature.
202
+ - Place closely related entries near each other.
203
+ - Append an independent new change to the end of the current version entry.
204
+ - Past entries and their dates are a record. Do not rewrite, renumber or merge
205
+ them, and do not merge two releases that actually existed because they share
206
+ a date.
207
+ - This file records the history of the repository. A change that is not
208
+ observable from outside does not need an entry here.
@@ -0,0 +1,248 @@
1
+ # -*- coding: utf-8 -*-
2
+ # Name:: Automatic::CLI
3
+ # Author: id774 (More info: http://id774.net)
4
+ # Source Code:: https://github.com/id774/automaticruby
5
+ # License:: The GPL version 3, or LGPL version 3 (Dual License).
6
+ # Contact:: idnanashi@gmail.com
7
+ # Created:: Aug 14, 2026
8
+ # Updated:: Aug 19, 2026
9
+ # Copyright:: Copyright (c) 2012-2026 Automatic Ruby Developers.
10
+ #
11
+ # Everything that belongs to being a command: option parsing, the subcommands,
12
+ # and turning a failure into a message and an exit status.
13
+ #
14
+ # CLI.run returns an Integer and never calls exit. Deciding the process's fate
15
+ # is bin/automatic's job, and returning the status is what makes the command
16
+ # line testable without spawning a process. See doc/BASIC_DESIGN.md section 4.2.
17
+
18
+ require 'automatic'
19
+ require 'fileutils'
20
+ require 'optparse'
21
+
22
+ module Automatic
23
+ class CLI
24
+ EXIT_SUCCESS = 0
25
+ EXIT_FAILURE = 1
26
+ EXIT_USAGE = 2
27
+
28
+ # The installation root: the directory holding plugins/, config/ and db/.
29
+ DEFAULT_ROOT_DIR = File.expand_path('../..', __dir__)
30
+
31
+ def self.run(argv, **options)
32
+ new(**options).run(argv)
33
+ end
34
+
35
+ def initialize(root_dir: DEFAULT_ROOT_DIR, stdout: $stdout, stderr: $stderr)
36
+ @root_dir = root_dir
37
+ @stdout = stdout
38
+ @stderr = stderr
39
+ end
40
+
41
+ # Returns the process exit status. --help and --version, and a subcommand
42
+ # invoked without its argument, finish early by throwing :automatic_exit.
43
+ def run(argv)
44
+ catch(:automatic_exit) do
45
+ argv = argv.dup
46
+ recipe_path = nil
47
+
48
+ parser = build_parser { |path| recipe_path = path }
49
+
50
+ begin
51
+ parser.order!(argv)
52
+ rescue OptionParser::ParseError => e
53
+ @stderr.puts e.message
54
+ @stderr.puts parser.help
55
+ throw :automatic_exit, EXIT_USAGE
56
+ end
57
+
58
+ next run_recipe(recipe_path) unless recipe_path.to_s.empty?
59
+ next usage(parser) if argv.empty?
60
+
61
+ run_subcommand(argv)
62
+ end
63
+ end
64
+
65
+ private
66
+
67
+ SUBCOMMAND_USAGE = {
68
+ 'scaffold' => '',
69
+ 'unscaffold' => '',
70
+ 'autodiscovery' => '<url>',
71
+ 'feedparser' => '<url>',
72
+ 'inspect' => '<url>',
73
+ 'opmlparser' => '<opml path>',
74
+ 'log' => '<level> <message>'
75
+ }.freeze
76
+
77
+ def build_parser
78
+ OptionParser.new do |parser|
79
+ parser.version = Automatic::VERSION
80
+ parser.banner = "Automatic Ruby #{Automatic::VERSION}\n" \
81
+ " Usage: automatic [options] [subcommand]"
82
+ parser.separator 'SubCommands:'
83
+ SUBCOMMAND_USAGE.each do |name, arguments|
84
+ parser.separator " #{name} #{arguments}".rstrip
85
+ end
86
+ parser.separator 'Options:'
87
+ parser.on('-c', '--config FILE', String, 'recipe YAML file') do |path|
88
+ yield path
89
+ end
90
+ parser.on('-h', '--help', 'show this message') do
91
+ @stdout.puts parser.help
92
+ throw :automatic_exit, EXIT_SUCCESS
93
+ end
94
+ parser.on('-v', '--version', 'show the version') do
95
+ @stdout.puts Automatic::VERSION
96
+ throw :automatic_exit, EXIT_SUCCESS
97
+ end
98
+ end
99
+ end
100
+
101
+ def usage(parser)
102
+ @stdout.puts parser.help
103
+ EXIT_FAILURE
104
+ end
105
+
106
+ # A LoadError is one of the failures reported as a message here: a Recipe
107
+ # naming a plugin whose optional gem is not installed is an operator's
108
+ # mistake with an answer, and the answer is in the message rather than in a
109
+ # backtrace. What the plugin itself raises is left alone; see
110
+ # doc/PLUGINS.md section 2.7.
111
+ def run_recipe(path)
112
+ unless File.exist?(resolve_recipe(path))
113
+ @stderr.puts "automatic: no such recipe: #{path}"
114
+ return EXIT_FAILURE
115
+ end
116
+
117
+ Automatic.run(recipe: Automatic::Recipe.new(path), root_dir: @root_dir)
118
+ EXIT_SUCCESS
119
+ rescue Automatic::Error, Psych::Exception, SystemCallError, IOError, LoadError => e
120
+ @stderr.puts "automatic: #{e.message}"
121
+ EXIT_FAILURE
122
+ end
123
+
124
+ # Mirrors Automatic::Recipe's own resolution, so that a missing file is
125
+ # reported as a message rather than as a backtrace.
126
+ def resolve_recipe(path)
127
+ in_user_dir = File.join(Automatic.user_config_dir, path.to_s)
128
+ File.exist?(in_user_dir) ? in_user_dir : path.to_s
129
+ end
130
+
131
+ def run_subcommand(argv)
132
+ name = argv.shift
133
+ handler = subcommands[name]
134
+
135
+ unless handler
136
+ @stderr.puts "automatic: no such subcommand: #{name}"
137
+ return EXIT_FAILURE
138
+ end
139
+
140
+ handler.call(argv)
141
+ EXIT_SUCCESS
142
+ rescue Automatic::Error, SystemCallError, IOError, LoadError => e
143
+ @stderr.puts "automatic: #{e.message}"
144
+ EXIT_FAILURE
145
+ end
146
+
147
+ def missing_argument(name)
148
+ @stderr.puts "Usage: automatic #{name} #{SUBCOMMAND_USAGE[name]}"
149
+ throw :automatic_exit, EXIT_FAILURE
150
+ end
151
+
152
+ # Each subcommand requires what it needs, so that `automatic --version`
153
+ # loads neither a feed parser nor an OPML parser.
154
+ #
155
+ # `feedbag` is an optional dependency: two subcommands out of seven use it,
156
+ # and neither running a Recipe nor any other subcommand does. It is
157
+ # required through Automatic.require_optional, so that not having it is a
158
+ # sentence rather than a backtrace. See doc/POLICY.md section 9.1.
159
+ def subcommands
160
+ {
161
+ 'scaffold' => method(:scaffold),
162
+ 'unscaffold' => method(:unscaffold),
163
+ 'autodiscovery' => method(:autodiscovery),
164
+ 'feedparser' => method(:feedparser),
165
+ 'inspect' => method(:inspect_url),
166
+ 'opmlparser' => method(:opmlparser),
167
+ 'log' => method(:log)
168
+ }
169
+ end
170
+
171
+ def scaffold(_argv)
172
+ Dir.children(File.join(@root_dir, 'plugins')).sort.each do |category|
173
+ next unless File.directory?(File.join(@root_dir, 'plugins', category))
174
+
175
+ create_dir(File.join(Automatic.user_dir, 'plugins', category))
176
+ end
177
+
178
+ create_dir(File.join(Automatic.user_dir, 'db'))
179
+
180
+ assets = File.join(Automatic.user_dir, 'assets')
181
+ if create_dir(assets)
182
+ FileUtils.cp_r(File.join(@root_dir, 'assets', 'siteinfo'),
183
+ File.join(assets, 'siteinfo'))
184
+ end
185
+
186
+ config = Automatic.user_config_dir
187
+ if create_dir(config)
188
+ FileUtils.cp_r(File.join(@root_dir, 'config'),
189
+ File.join(config, 'example'))
190
+ end
191
+ end
192
+
193
+ def unscaffold(_argv)
194
+ dir = Automatic.user_dir
195
+ return unless File.directory?(dir)
196
+
197
+ @stdout.puts "Removing #{dir}"
198
+ FileUtils.rm_r(dir)
199
+ end
200
+
201
+ def autodiscovery(argv)
202
+ Automatic.require_optional('feedbag', needed_by: 'the autodiscovery subcommand')
203
+ require 'pp'
204
+ url = argv.shift || missing_argument('autodiscovery')
205
+ @stdout.puts Feedbag.find(url).pretty_inspect
206
+ end
207
+
208
+ def feedparser(argv)
209
+ require 'automatic/feed_parser'
210
+ require 'pp'
211
+ url = argv.shift || missing_argument('feedparser')
212
+ @stdout.puts Automatic::FeedParser.get_url(url).pretty_inspect
213
+ end
214
+
215
+ def inspect_url(argv)
216
+ require 'automatic/feed_parser'
217
+ Automatic.require_optional('feedbag', needed_by: 'the inspect subcommand')
218
+ require 'pp'
219
+ url = argv.shift || missing_argument('inspect')
220
+ feeds = Feedbag.find(url)
221
+ raise Automatic::Error, "no feed found at #{url}" if feeds.empty?
222
+
223
+ @stdout.puts feeds.pretty_inspect
224
+ @stdout.puts Automatic::FeedParser.get_url(feeds.pop).pretty_inspect
225
+ end
226
+
227
+ def opmlparser(argv)
228
+ require 'automatic/opml'
229
+ path = argv.shift || missing_argument('opmlparser')
230
+ parser = Automatic::OPML::Parser.new(File.read(path))
231
+ parser.each_outline { |_opml, outline| @stdout.puts outline.xmlUrl }
232
+ end
233
+
234
+ def log(argv)
235
+ level = argv.shift || missing_argument('log')
236
+ message = argv.shift || missing_argument('log')
237
+ Automatic::Log.puts(level, message)
238
+ end
239
+
240
+ def create_dir(path)
241
+ return false if File.exist?(path)
242
+
243
+ FileUtils.mkdir_p(path)
244
+ @stdout.puts "Creating #{path}"
245
+ true
246
+ end
247
+ end
248
+ end
@@ -1,8 +1,34 @@
1
1
  # -*- coding: utf-8 -*-
2
- require 'rubygems'
2
+ # Name:: Automatic::Environment
3
+ # Author: id774 (More info: http://id774.net)
4
+ # Source Code:: https://github.com/id774/automaticruby
5
+ # License:: The GPL version 3, or LGPL version 3 (Dual License).
6
+ # Contact:: idnanashi@gmail.com
7
+ # Created:: Feb 18, 2012
8
+ # Updated:: Aug 14, 2026
9
+ # Copyright:: Copyright (c) 2012-2026 Automatic Ruby Developers.
10
+ #
11
+ # Bundler setup for a source checkout, and nothing more.
12
+ #
13
+ # When this repository is used from a checkout, bin/automatic should resolve
14
+ # the gems the checkout's Gemfile locks. When the gem is installed normally
15
+ # there is no Gemfile beside lib/, this file does nothing, and that is correct:
16
+ # an installed library must not impose a bundle on the program requiring it.
17
+ #
18
+ # This deliberately does not call Bundler.require. Loading every gem in the
19
+ # bundle is how a plugin's dependency used to become everyone's; see
20
+ # doc/POLICY.md section 2.5.
3
21
 
4
- # Set up gems listed in the Gemfile.
5
- ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
22
+ gemfile = File.expand_path('../../Gemfile', __dir__)
6
23
 
7
- require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE'])
8
- Bundler.require :default if defined?(Bundler)
24
+ if File.exist?(gemfile)
25
+ ENV['BUNDLE_GEMFILE'] ||= gemfile
26
+ begin
27
+ require 'bundler/setup'
28
+ rescue LoadError, StandardError
29
+ # Bundler is absent, or the bundle is not installed. Fall back to whatever
30
+ # RubyGems can activate; a genuinely missing dependency will surface as a
31
+ # LoadError naming it, which is the more useful message anyway.
32
+ nil
33
+ end
34
+ end
@@ -1,16 +1,17 @@
1
1
  # -*- coding: utf-8 -*-
2
- # Name:: Automatic::FeedMaker
3
- # Author:: 774 <http://id774.net>
4
- # Created:: Feb 21, 2014
5
- # Updated:: Feb 26, 2014
6
- # Copyright:: Copyright (c) 2012-2014 Automatic Ruby Developers.
7
- # License:: Licensed under the GNU GENERAL PUBLIC LICENSE, Version 3.0.
2
+ # Name:: Automatic::FeedMaker
3
+ # Author: id774 (More info: http://id774.net)
4
+ # Source Code:: https://github.com/id774/automaticruby
5
+ # License:: The GPL version 3, or LGPL version 3 (Dual License).
6
+ # Contact:: idnanashi@gmail.com
7
+ # Created:: Feb 21, 2014
8
+ # Updated:: Aug 14, 2026
9
+ # Copyright:: Copyright (c) 2012-2026 Automatic Ruby Developers.
8
10
 
9
11
  module Automatic
10
12
  module FeedMaker
11
13
  require 'rss'
12
14
  require 'uri'
13
- require 'nokogiri'
14
15
 
15
16
  class FeedObject
16
17
  attr_accessor :title, :link, :description, :author, :comments
@@ -38,7 +39,7 @@ module Automatic
38
39
  xss = maker.xml_stylesheets.new_xml_stylesheet
39
40
  maker.channel.title = "Automatic Ruby"
40
41
  maker.channel.description = "Automatic::FeedMaker"
41
- maker.channel.link = "https://github.com/automaticruby/automaticruby"
42
+ maker.channel.link = "https://github.com/id774/automaticruby"
42
43
  maker.items.do_sort = true
43
44
 
44
45
  unless feeds.nil?
@@ -67,7 +68,7 @@ module Automatic
67
68
  xss = maker.xml_stylesheets.new_xml_stylesheet
68
69
  maker.channel.title = "Automatic Ruby"
69
70
  maker.channel.description = "Automatic::FeedMaker"
70
- maker.channel.link = "https://github.com/automaticruby/automaticruby"
71
+ maker.channel.link = "https://github.com/id774/automaticruby"
71
72
  maker.items.do_sort = true
72
73
  item = maker.items.new_item
73
74
  item.title = "Automatic Ruby"
@@ -1,51 +1,67 @@
1
1
  # -*- coding: utf-8 -*-
2
- # Name:: Automatic::FeedParser
3
- # Author:: 774 <http://id774.net>
4
- # Created:: Feb 19, 2012
5
- # Updated:: Feb 26, 2014
6
- # Copyright:: Copyright (c) 2012-2014 Automatic Ruby Developers.
7
- # License:: Licensed under the GNU GENERAL PUBLIC LICENSE, Version 3.0.
2
+ # Name:: Automatic::FeedParser
3
+ # Author: id774 (More info: http://id774.net)
4
+ # Source Code:: https://github.com/id774/automaticruby
5
+ # License:: The GPL version 3, or LGPL version 3 (Dual License).
6
+ # Contact:: idnanashi@gmail.com
7
+ # Created:: Feb 19, 2012
8
+ # Updated:: Aug 14, 2026
9
+ # Copyright:: Copyright (c) 2012-2026 Automatic Ruby Developers.
10
+ #
11
+ # Fetching and parsing, on the way into the pipeline. What leaves here has the
12
+ # shape described in doc/PLUGINS.md section 3.4.
8
13
 
9
14
  module Automatic
10
15
  module FeedParser
11
16
  require 'rss'
12
- require 'uri'
13
- require 'nokogiri'
17
+ require 'automatic/http'
14
18
 
19
+ # Fetch a URL and parse it as a feed. Validation is off, because feeds in
20
+ # the wild frequently are not valid and are still readable.
21
+ #
22
+ # Fetching goes through Automatic::Http, which is where the scheme
23
+ # allowlist, the timeouts and the redirect limit live.
15
24
  def self.get_url(url)
16
- begin
17
- unless url.nil?
18
- Automatic::Log.puts("info", "Parsing Feed: #{url}")
19
- feed = URI.parse(url).normalize
20
- open(feed) {|http|
21
- response = http.read
22
- RSS::Parser.parse(response, false)
23
- }
24
- end
25
- rescue => e
26
- raise e
27
- end
25
+ return if url.nil?
26
+
27
+ Automatic::Log.puts('info', "Parsing Feed: #{url}")
28
+ RSS::Parser.parse(Automatic::Http.read(url), false)
28
29
  end
29
30
 
31
+ # Build a feed whose items are the links of an HTML document. This is how
32
+ # a page that publishes no feed enters the pipeline.
33
+ #
34
+ # nokogiri is required here rather than at the top of the file: it is the
35
+ # only thing in the framework that wants an HTML parser, it is an optional
36
+ # dependency rather than a runtime one, and requiring `automatic` must
37
+ # neither load one nor need one installed. Only the plugins that call this
38
+ # method -- SubscriptionLink and SubscriptionTumblr -- do.
39
+ # See doc/POLICY.md sections 2.5 and 9.1.
30
40
  def self.parse_html(html)
31
- RSS::Maker.make("2.0") {|maker|
32
- xss = maker.xml_stylesheets.new_xml_stylesheet
33
- maker.channel.title = "Automatic Ruby"
34
- maker.channel.description = "Automatic::FeedParser"
35
- maker.channel.link = "https://github.com/automaticruby/automaticruby"
41
+ Automatic.require_optional(
42
+ 'nokogiri',
43
+ needed_by: 'Automatic::FeedParser.parse_html, used by SubscriptionLink ' \
44
+ 'and SubscriptionTumblr'
45
+ )
46
+
47
+ RSS::Maker.make('2.0') do |maker|
48
+ maker.xml_stylesheets.new_xml_stylesheet
49
+ maker.channel.title = 'Automatic Ruby'
50
+ maker.channel.description = 'Automatic::FeedParser'
51
+ maker.channel.link = 'https://github.com/id774/automaticruby'
36
52
  maker.items.do_sort = true
37
53
 
38
54
  doc = Nokogiri::HTML(html)
39
- (doc/:a).each {|link|
40
- unless link[:href].nil?
41
- item = maker.items.new_item
42
- item.title = "Automatic Ruby"
43
- item.link = link[:href]
44
- item.date = Time.now
45
- item.description = ""
46
- end
47
- }
48
- }
55
+ (doc / :a).each do |link|
56
+ next if link[:href].nil?
57
+
58
+ item = maker.items.new_item
59
+ item.title = 'Automatic Ruby'
60
+ item.link = link[:href]
61
+ item.date = Time.now
62
+ item.description = ''
63
+ end
64
+ end
49
65
  end
50
66
  end
51
67
  end