automatic 14.12.2 → 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 +5 -5
- data/README.md +669 -83
- data/VERSION +1 -1
- data/assets/siteinfo/items_all.json +60300 -52138
- data/automatic.gemspec +110 -248
- data/bin/automatic +20 -139
- data/config/feed2console.yml +10 -4
- data/config/feed2markdown.yml +41 -0
- data/doc/AI_TUTORIAL.md +542 -0
- data/doc/BASIC_DESIGN.md +547 -0
- data/doc/COPYING.LESSER +165 -0
- data/doc/DEPLOYMENT.md +830 -0
- data/doc/LICENSE.md +14 -0
- data/doc/PLUGINS.md +2015 -0
- data/doc/PLUGIN_DEVELOPMENT.md +86 -0
- data/doc/POLICY.md +962 -0
- data/doc/QUICKSTART.md +260 -0
- data/doc/RELEASING.md +394 -0
- data/doc/REQUIREMENTS.md +556 -0
- data/doc/VERSIONS +266 -0
- data/lib/automatic/cli.rb +270 -0
- data/lib/automatic/environment.rb +31 -5
- data/lib/automatic/feed_maker.rb +65 -25
- data/lib/automatic/feed_parser.rb +51 -35
- data/lib/automatic/http.rb +107 -0
- data/lib/automatic/log.rb +49 -18
- data/lib/automatic/opml.rb +3 -1
- data/lib/automatic/pipeline.rb +73 -32
- data/lib/automatic/recipe.rb +100 -17
- data/lib/automatic/version.rb +14 -1
- data/lib/automatic.rb +93 -20
- data/plugins/custom_feed/svn_log.rb +73 -32
- data/plugins/custom_feed/web.rb +348 -0
- data/plugins/filter/absolute_uri.rb +43 -27
- data/plugins/filter/accept.rb +38 -45
- data/plugins/filter/batch.rb +97 -0
- data/plugins/filter/claude.rb +217 -0
- data/plugins/filter/clear.rb +12 -8
- data/plugins/filter/description_link.rb +66 -51
- data/plugins/filter/full_feed.rb +170 -52
- data/plugins/filter/gemini.rb +216 -0
- data/plugins/filter/github_feed.rb +38 -26
- data/plugins/filter/ignore.rb +33 -44
- data/plugins/filter/image.rb +36 -25
- data/plugins/filter/image_source.rb +75 -52
- data/plugins/filter/join.rb +105 -0
- data/plugins/filter/kimi.rb +216 -0
- data/plugins/filter/limit.rb +57 -0
- data/plugins/filter/one.rb +19 -26
- data/plugins/filter/open_ai.rb +198 -0
- data/plugins/filter/present.rb +75 -0
- data/plugins/filter/rand.rb +16 -17
- data/plugins/filter/sakura_ai.rb +205 -0
- data/plugins/filter/sanitize.rb +29 -34
- data/plugins/filter/sort.rb +20 -27
- data/plugins/filter/tumblr_resize.rb +31 -23
- data/plugins/notify/ikachan.rb +86 -48
- data/plugins/provide/fluentd.rb +43 -24
- data/plugins/publish/amazon_s3.rb +79 -40
- data/plugins/publish/console.rb +19 -16
- data/plugins/publish/console_link.rb +20 -17
- data/plugins/publish/eject.rb +48 -26
- data/plugins/publish/fluentd.rb +50 -30
- data/plugins/publish/hatena_bookmark.rb +88 -71
- data/plugins/publish/instapaper.rb +69 -59
- data/plugins/publish/markdown.rb +278 -0
- data/plugins/publish/memcached.rb +35 -29
- data/plugins/store/database.rb +50 -48
- data/plugins/store/digest.rb +212 -0
- data/plugins/store/file.rb +107 -68
- data/plugins/store/full_text.rb +31 -28
- data/plugins/store/permalink.rb +18 -22
- data/plugins/subscription/feed.rb +34 -21
- data/plugins/subscription/link.rb +31 -32
- data/plugins/subscription/text.rb +40 -45
- data/plugins/subscription/tumblr.rb +55 -44
- data/plugins/subscription/xml.rb +40 -36
- metadata +112 -321
- data/Gemfile +0 -38
- data/Rakefile +0 -59
- data/doc/ChangeLog +0 -303
- data/doc/PLUGINS +0 -750
- data/doc/PLUGINS.ja +0 -753
- data/doc/README +0 -511
- data/doc/README.ja +0 -519
- data/plugins/filter/google_news.rb +0 -50
- data/plugins/publish/google_calendar.rb +0 -84
- data/plugins/publish/hipchat.rb +0 -46
- data/plugins/publish/pocket.rb +0 -45
- data/plugins/publish/twitter.rb +0 -58
- data/plugins/subscription/chan_toru.rb +0 -57
- data/plugins/subscription/g_guide.rb +0 -57
- data/plugins/subscription/pocket.rb +0 -51
- data/plugins/subscription/twitter.rb +0 -69
- data/plugins/subscription/twitter_search.rb +0 -50
- data/plugins/subscription/weather.rb +0 -33
- data/script/build +0 -84
- data/spec/fixtures/sampleFeeds.tsv +0 -1
- data/spec/fixtures/sampleFeeds2.tsv +0 -2
- data/spec/fixtures/sampleRecipe.yml +0 -24
- data/spec/lib/automatic/log_spec.rb +0 -32
- data/spec/lib/automatic/pipeline_spec.rb +0 -68
- data/spec/lib/automatic/recipe_spec.rb +0 -40
- data/spec/lib/automatic_spec.rb +0 -99
- data/spec/plugins/custom_feed/svn_log_spec.rb +0 -31
- data/spec/plugins/filter/absolute_uri_spec.rb +0 -61
- data/spec/plugins/filter/accept_spec.rb +0 -331
- data/spec/plugins/filter/clear_spec.rb +0 -49
- data/spec/plugins/filter/description_link_spec.rb +0 -138
- data/spec/plugins/filter/full_feed_spec.rb +0 -129
- data/spec/plugins/filter/github_feed_spec.rb +0 -55
- data/spec/plugins/filter/google_news_spec.rb +0 -69
- data/spec/plugins/filter/ignore_spec.rb +0 -328
- data/spec/plugins/filter/image_source_spec.rb +0 -89
- data/spec/plugins/filter/image_spec.rb +0 -65
- data/spec/plugins/filter/one_spec.rb +0 -71
- data/spec/plugins/filter/rand_spec.rb +0 -52
- data/spec/plugins/filter/sanitize_spec.rb +0 -153
- data/spec/plugins/filter/sort_spec.rb +0 -189
- data/spec/plugins/filter/tumblr_resize_spec.rb +0 -109
- data/spec/plugins/notify/ikachan_spec.rb +0 -58
- data/spec/plugins/provide/fluentd_spec.rb +0 -49
- data/spec/plugins/publish/amazon_s3_spec.rb +0 -40
- data/spec/plugins/publish/console_spec.rb +0 -30
- data/spec/plugins/publish/eject_spec.rb +0 -40
- data/spec/plugins/publish/fluentd_spec.rb +0 -40
- data/spec/plugins/publish/google_calendar_spec.rb +0 -83
- data/spec/plugins/publish/hatena_bookmark_spec.rb +0 -134
- data/spec/plugins/publish/hipchat_spec.rb +0 -69
- data/spec/plugins/publish/instapaper_spec.rb +0 -82
- data/spec/plugins/publish/memcached_spec.rb +0 -63
- data/spec/plugins/publish/pocket_spec.rb +0 -51
- data/spec/plugins/publish/twitter_spec.rb +0 -73
- data/spec/plugins/store/file_spec.rb +0 -58
- data/spec/plugins/store/full_text_spec.rb +0 -152
- data/spec/plugins/store/permalink_spec.rb +0 -206
- data/spec/plugins/subscription/chan_toru_spec.rb +0 -56
- data/spec/plugins/subscription/feed_spec.rb +0 -71
- data/spec/plugins/subscription/g_guide_spec.rb +0 -82
- data/spec/plugins/subscription/link_spec.rb +0 -72
- data/spec/plugins/subscription/pocket_spec.rb +0 -57
- data/spec/plugins/subscription/text_spec.rb +0 -84
- data/spec/plugins/subscription/tumblr_spec.rb +0 -74
- data/spec/plugins/subscription/twitter_search_spec.rb +0 -57
- data/spec/plugins/subscription/twitter_spec.rb +0 -73
- data/spec/plugins/subscription/weather_spec.rb +0 -44
- data/spec/plugins/subscription/xml_spec.rb +0 -84
- data/spec/spec_helper.rb +0 -106
- data/spec/user_dir/plugins/store/mock.rb +0 -16
- data/test/fixtures/sampleOPML.xml +0 -11
- data/test/integration/test_absoluteurl.yml +0 -25
- data/test/integration/test_activerecord.yml +0 -24
- data/test/integration/test_add_pocket.yml +0 -26
- data/test/integration/test_chan_toru.yml +0 -21
- data/test/integration/test_descriptionlink.yml +0 -21
- data/test/integration/test_fluentd.yml +0 -22
- data/test/integration/test_fulltext.yml +0 -30
- data/test/integration/test_google_news.yml +0 -21
- data/test/integration/test_googlealert.yml +0 -21
- data/test/integration/test_hatenabookmark.yml +0 -30
- data/test/integration/test_ignore.yml +0 -25
- data/test/integration/test_ignore2.yml +0 -22
- data/test/integration/test_image2local.yml +0 -33
- data/test/integration/test_instapaper.yml +0 -26
- data/test/integration/test_link2local.yml +0 -34
- data/test/integration/test_one.yml +0 -23
- data/test/integration/test_pocket.yml +0 -22
- data/test/integration/test_rand.yml +0 -21
- data/test/integration/test_sanitize.yml +0 -23
- data/test/integration/test_sort.yml +0 -36
- data/test/integration/test_svnlog.yml +0 -15
- data/test/integration/test_text2feed.yml +0 -36
- data/test/integration/test_tumblr2local.yml +0 -43
- data/test/integration/test_twitter_search.yml +0 -22
- data/test/integration/test_weather.yml +0 -19
- data/test/integration/test_xml2fluentd.yml +0 -21
- data/vendor/.gitkeep +0 -0
data/lib/automatic/feed_maker.rb
CHANGED
|
@@ -1,22 +1,23 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
|
-
# Name::
|
|
3
|
-
# Author
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
2
|
+
# Name:: Automatic::FeedMaker
|
|
3
|
+
# Author: id774 (More info: https://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:: Sep 7, 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
|
|
17
18
|
def initialize
|
|
18
|
-
@link =
|
|
19
|
-
@title =
|
|
19
|
+
@link = nil
|
|
20
|
+
@title = nil
|
|
20
21
|
@description = ''
|
|
21
22
|
@author = ''
|
|
22
23
|
@comments = ''
|
|
@@ -33,30 +34,36 @@ module Automatic
|
|
|
33
34
|
feed_object
|
|
34
35
|
end
|
|
35
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
|
+
|
|
36
51
|
def self.create_pipeline(feeds = [])
|
|
37
52
|
RSS::Maker.make("2.0") {|maker|
|
|
38
53
|
xss = maker.xml_stylesheets.new_xml_stylesheet
|
|
39
54
|
maker.channel.title = "Automatic Ruby"
|
|
40
55
|
maker.channel.description = "Automatic::FeedMaker"
|
|
41
|
-
maker.channel.link = "https://github.com/
|
|
56
|
+
maker.channel.link = "https://github.com/id774/automaticruby"
|
|
42
57
|
maker.items.do_sort = true
|
|
43
58
|
|
|
44
59
|
unless feeds.nil?
|
|
45
60
|
feeds.each {|feed|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
item.description = feed.description
|
|
53
|
-
item.author = feed.author
|
|
54
|
-
item.comments = feed.comments
|
|
55
|
-
item.date = feed.pubDate || Time.now
|
|
56
|
-
rescue NoMethodError
|
|
57
|
-
Automatic::Log.puts("warn", "Undefined field detected in feed.")
|
|
58
|
-
end
|
|
59
|
-
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) }
|
|
60
67
|
}
|
|
61
68
|
end
|
|
62
69
|
}
|
|
@@ -67,7 +74,7 @@ module Automatic
|
|
|
67
74
|
xss = maker.xml_stylesheets.new_xml_stylesheet
|
|
68
75
|
maker.channel.title = "Automatic Ruby"
|
|
69
76
|
maker.channel.description = "Automatic::FeedMaker"
|
|
70
|
-
maker.channel.link = "https://github.com/
|
|
77
|
+
maker.channel.link = "https://github.com/id774/automaticruby"
|
|
71
78
|
maker.items.do_sort = true
|
|
72
79
|
item = maker.items.new_item
|
|
73
80
|
item.title = "Automatic Ruby"
|
|
@@ -76,5 +83,38 @@ module Automatic
|
|
|
76
83
|
item.date = Time.now
|
|
77
84
|
}
|
|
78
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
|
|
79
119
|
end
|
|
80
120
|
end
|
|
@@ -1,51 +1,67 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
|
-
# Name::
|
|
3
|
-
# Author
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
2
|
+
# Name:: Automatic::FeedParser
|
|
3
|
+
# Author: id774 (More info: https://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 '
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# Name:: Automatic::Http
|
|
3
|
+
# Author: id774 (More info: https://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 15, 2026
|
|
8
|
+
# Updated:: Aug 21, 2026
|
|
9
|
+
# Copyright:: Copyright (c) 2012-2026 Automatic Ruby Developers.
|
|
10
|
+
#
|
|
11
|
+
# One way in for everything the plugins fetch over HTTP.
|
|
12
|
+
#
|
|
13
|
+
# This exists because seven plugins were each calling `URI.open` on a string,
|
|
14
|
+
# and the four decisions that call implies -- which schemes are allowed, how
|
|
15
|
+
# long to wait, how many redirects to follow, what to send as a User-Agent --
|
|
16
|
+
# were being made seven times, mostly by omission. They are made once here.
|
|
17
|
+
# It is a helper, not a client framework: one method fetches, one builds a URI,
|
|
18
|
+
# and a plugin that wants something else still calls Ruby directly.
|
|
19
|
+
# See doc/POLICY.md section 9.2 and doc/PLUGINS.md section 3.8.
|
|
20
|
+
|
|
21
|
+
require 'open-uri'
|
|
22
|
+
require 'uri'
|
|
23
|
+
require 'automatic/version'
|
|
24
|
+
|
|
25
|
+
module Automatic
|
|
26
|
+
module Http
|
|
27
|
+
# A link in a pipeline item comes from a feed, which is to say from
|
|
28
|
+
# outside. `URI.open` on such a string will happily read `file:///etc/passwd`
|
|
29
|
+
# or run an FTP session; a plugin fetching an article body wants neither.
|
|
30
|
+
SCHEMES = %w[http https].freeze
|
|
31
|
+
|
|
32
|
+
OPEN_TIMEOUT = 10
|
|
33
|
+
READ_TIMEOUT = 30
|
|
34
|
+
|
|
35
|
+
# open-uri follows redirects itself and refuses an HTTPS-to-HTTP downgrade.
|
|
36
|
+
# This bounds the chain so that a redirect loop ends as an error rather
|
|
37
|
+
# than as an unattended run that never returns.
|
|
38
|
+
REDIRECT_LIMIT = 5
|
|
39
|
+
|
|
40
|
+
USER_AGENT = "Automatic Ruby/#{Automatic::VERSION} " \
|
|
41
|
+
'(+https://github.com/id774/automaticruby)'
|
|
42
|
+
|
|
43
|
+
# The RFC 2396 parser is named directly: `URI.escape` was removed in Ruby
|
|
44
|
+
# 3.0, and `URI::Parser` became the RFC 3986 parser in 3.4, which reports
|
|
45
|
+
# #escape as obsolete. This spelling means the same thing on every
|
|
46
|
+
# supported Ruby.
|
|
47
|
+
ESCAPER = URI::RFC2396_Parser.new
|
|
48
|
+
|
|
49
|
+
class << self
|
|
50
|
+
# Fetch a URL and return its body as a string. Raises rather than
|
|
51
|
+
# returning nil: a plugin's retry handling is built around an exception,
|
|
52
|
+
# and a body that could not be fetched is not an empty body.
|
|
53
|
+
def read(url)
|
|
54
|
+
open(url, &:read)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Fetch a URL and yield the IO, for a caller that would rather stream
|
|
58
|
+
# than hold the whole body.
|
|
59
|
+
def open(url, &block)
|
|
60
|
+
uri(url).open(
|
|
61
|
+
'User-Agent' => USER_AGENT,
|
|
62
|
+
open_timeout: OPEN_TIMEOUT,
|
|
63
|
+
read_timeout: READ_TIMEOUT,
|
|
64
|
+
redirect: true,
|
|
65
|
+
max_redirects: REDIRECT_LIMIT,
|
|
66
|
+
&block
|
|
67
|
+
)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Parse a URL into a URI this framework will fetch, or raise. A string
|
|
71
|
+
# carrying characters a URI may not (a space, a Japanese query term) is
|
|
72
|
+
# escaped and parsed again, which is what the plugins used to do for
|
|
73
|
+
# themselves before every call.
|
|
74
|
+
def uri(url)
|
|
75
|
+
string = url.to_s.strip
|
|
76
|
+
raise ArgumentError, 'no URL to fetch' if string.empty?
|
|
77
|
+
|
|
78
|
+
parsed = parse(string)
|
|
79
|
+
unless SCHEMES.include?(parsed.scheme)
|
|
80
|
+
raise ArgumentError, "not an HTTP or HTTPS URL: #{string}"
|
|
81
|
+
end
|
|
82
|
+
unless parsed.host
|
|
83
|
+
raise ArgumentError, "HTTP or HTTPS URL has no host: #{string}"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
parsed.normalize
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Whether a string is a URL this framework will fetch. For a plugin that
|
|
90
|
+
# skips an item rather than failing the run on one.
|
|
91
|
+
def fetchable?(url)
|
|
92
|
+
uri(url)
|
|
93
|
+
true
|
|
94
|
+
rescue ArgumentError, URI::InvalidURIError
|
|
95
|
+
false
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
private
|
|
99
|
+
|
|
100
|
+
def parse(string)
|
|
101
|
+
URI.parse(string)
|
|
102
|
+
rescue URI::InvalidURIError
|
|
103
|
+
URI.parse(ESCAPER.escape(string))
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
data/lib/automatic/log.rb
CHANGED
|
@@ -1,31 +1,62 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
|
-
# Name::
|
|
3
|
-
# Author
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
2
|
+
# Name:: Automatic::Log
|
|
3
|
+
# Author: id774 (More info: https://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 20, 2012
|
|
8
|
+
# Updated:: Aug 14, 2026
|
|
9
|
+
# Copyright:: Copyright (c) 2012-2026 Automatic Ruby Developers.
|
|
10
|
+
#
|
|
11
|
+
# One log, to standard output, behind a level filter set per Recipe by
|
|
12
|
+
# global.log.level. See doc/BASIC_DESIGN.md section 4.7.
|
|
8
13
|
|
|
9
14
|
require 'logger'
|
|
10
15
|
|
|
11
16
|
module Automatic
|
|
12
17
|
module Log
|
|
13
|
-
|
|
18
|
+
# In increasing order of severity. 'none' is the threshold that admits
|
|
19
|
+
# nothing; it is never used as the level of a message.
|
|
20
|
+
LOG_LEVELS = %w[info warn error none].freeze
|
|
14
21
|
|
|
15
|
-
|
|
16
|
-
@level = level
|
|
17
|
-
end
|
|
22
|
+
DEFAULT_LEVEL = 'info'
|
|
18
23
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
24
|
+
class << self
|
|
25
|
+
# Set the threshold. An unrecognised name, including nil, means the
|
|
26
|
+
# default: an unattended run should not be silenced by a typo in a
|
|
27
|
+
# Recipe, nor should it raise in the middle of a job.
|
|
28
|
+
def level(level = nil)
|
|
29
|
+
@level = normalize(level, DEFAULT_LEVEL)
|
|
30
|
+
end
|
|
22
31
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
return
|
|
32
|
+
def logger
|
|
33
|
+
@logger ||= Logger.new($stdout)
|
|
26
34
|
end
|
|
27
|
-
logger.send(level, message)
|
|
28
|
-
end
|
|
29
35
|
|
|
36
|
+
# Replace the logger. Used by the specs; a plugin has no business
|
|
37
|
+
# calling this.
|
|
38
|
+
attr_writer :logger
|
|
39
|
+
|
|
40
|
+
# Emit a message at the given level, if the threshold admits it.
|
|
41
|
+
# Both 'info' and :info are accepted, because plugins pass both.
|
|
42
|
+
def puts(level, message)
|
|
43
|
+
name = normalize(level, DEFAULT_LEVEL)
|
|
44
|
+
return if name == 'none'
|
|
45
|
+
return if LOG_LEVELS.index(name) < LOG_LEVELS.index(current_level)
|
|
46
|
+
|
|
47
|
+
logger.public_send(name, message)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def current_level
|
|
53
|
+
@level ||= DEFAULT_LEVEL
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def normalize(level, fallback)
|
|
57
|
+
name = level.to_s
|
|
58
|
+
LOG_LEVELS.include?(name) ? name : fallback
|
|
59
|
+
end
|
|
60
|
+
end
|
|
30
61
|
end
|
|
31
62
|
end
|
data/lib/automatic/opml.rb
CHANGED
|
@@ -220,7 +220,9 @@ module Automatic
|
|
|
220
220
|
end # each_outline
|
|
221
221
|
|
|
222
222
|
def read_text
|
|
223
|
-
|
|
223
|
+
# +'' rather than "": this string is appended to below, and a literal
|
|
224
|
+
# is on its way to being frozen.
|
|
225
|
+
text = +''
|
|
224
226
|
while event = @p.pull
|
|
225
227
|
case event.event_type
|
|
226
228
|
when :end_element
|
data/lib/automatic/pipeline.rb
CHANGED
|
@@ -1,43 +1,84 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
|
-
# Name::
|
|
3
|
-
# Author
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
2
|
+
# Name:: Automatic::Pipeline
|
|
3
|
+
# Author: id774 (More info: https://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 22, 2012
|
|
8
|
+
# Updated:: Sep 6, 2026
|
|
9
|
+
# Copyright:: Copyright (c) 2012-2026 Automatic Ruby Developers.
|
|
10
|
+
#
|
|
11
|
+
# The core: resolve a plugin's class name to a file, then run the Recipe's
|
|
12
|
+
# plugins in order, each receiving the previous one's output. See
|
|
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.
|
|
8
21
|
|
|
9
|
-
require 'active_support'
|
|
10
|
-
require 'active_support/core_ext'
|
|
11
|
-
require 'active_support/deprecation'
|
|
22
|
+
require 'active_support/core_ext/string/inflections'
|
|
12
23
|
|
|
13
24
|
module Automatic
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
25
|
+
# The namespace every plugin class lives in. Plugin files are registered
|
|
26
|
+
# here with autoload, so a file is read when its constant is first used.
|
|
27
|
+
module Plugin; end
|
|
17
28
|
|
|
18
29
|
module Pipeline
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
30
|
+
class << self
|
|
31
|
+
# Resolve a module name such as SubscriptionFeed to
|
|
32
|
+
# <search root>/subscription/feed.rb and register it for autoloading.
|
|
33
|
+
#
|
|
34
|
+
# The user directory is searched before the installation, so a plugin in
|
|
35
|
+
# ~/.automatic/plugins shadows a shipped plugin of the same name.
|
|
36
|
+
def load_plugin(module_name)
|
|
37
|
+
underscored = module_name.underscore
|
|
38
|
+
|
|
39
|
+
search_roots.each do |dir|
|
|
40
|
+
category = File.basename(dir)
|
|
41
|
+
next unless /\A#{Regexp.escape(category)}_(.*)\z/ =~ underscored
|
|
42
|
+
|
|
43
|
+
path = File.join(dir, "#{Regexp.last_match(1)}.rb")
|
|
44
|
+
next unless File.exist?(path)
|
|
45
|
+
|
|
46
|
+
return Automatic::Plugin.autoload(module_name.to_sym, path)
|
|
27
47
|
end
|
|
28
|
-
}
|
|
29
|
-
raise NoPluginError, "unknown plugin named #{module_name}"
|
|
30
|
-
end
|
|
31
48
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
49
|
+
raise NoPluginError, "unknown plugin named #{module_name}"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Run a Recipe: each plugin's return value is the next plugin's input.
|
|
53
|
+
#
|
|
54
|
+
# Nothing here rescues a plugin's exception. A Recipe is a sequence in
|
|
55
|
+
# which each step consumes the previous one's output, so continuing past
|
|
56
|
+
# a failed step would run the rest on a value their author never
|
|
57
|
+
# intended. See doc/REQUIREMENTS.md section 12.
|
|
58
|
+
def run(recipe)
|
|
59
|
+
raise NoRecipeError, 'no recipe given' if recipe.nil?
|
|
60
|
+
|
|
61
|
+
entries = []
|
|
62
|
+
recipe.each_plugin { |plugin| entries << [plugin, plugin.module] }
|
|
63
|
+
|
|
64
|
+
entries.each { |_plugin, mod| load_plugin(mod) }
|
|
65
|
+
|
|
66
|
+
pipeline = []
|
|
67
|
+
entries.each do |plugin, mod|
|
|
68
|
+
klass = Automatic::Plugin.const_get(mod)
|
|
69
|
+
pipeline = klass.new(plugin.config, pipeline).run
|
|
70
|
+
end
|
|
71
|
+
pipeline
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
def search_roots
|
|
77
|
+
Dir[File.join(Automatic.user_plugins_dir, '*'),
|
|
78
|
+
File.join(Automatic.plugins_dir, '*')].select { |path|
|
|
79
|
+
File.directory?(path)
|
|
80
|
+
}
|
|
81
|
+
end
|
|
41
82
|
end
|
|
42
83
|
end
|
|
43
84
|
end
|