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.
Files changed (177) hide show
  1. checksums.yaml +5 -5
  2. data/README.md +669 -83
  3. data/VERSION +1 -1
  4. data/assets/siteinfo/items_all.json +60300 -52138
  5. data/automatic.gemspec +110 -248
  6. data/bin/automatic +20 -139
  7. data/config/feed2console.yml +10 -4
  8. data/config/feed2markdown.yml +41 -0
  9. data/doc/AI_TUTORIAL.md +542 -0
  10. data/doc/BASIC_DESIGN.md +547 -0
  11. data/doc/COPYING.LESSER +165 -0
  12. data/doc/DEPLOYMENT.md +830 -0
  13. data/doc/LICENSE.md +14 -0
  14. data/doc/PLUGINS.md +2015 -0
  15. data/doc/PLUGIN_DEVELOPMENT.md +86 -0
  16. data/doc/POLICY.md +962 -0
  17. data/doc/QUICKSTART.md +260 -0
  18. data/doc/RELEASING.md +394 -0
  19. data/doc/REQUIREMENTS.md +556 -0
  20. data/doc/VERSIONS +266 -0
  21. data/lib/automatic/cli.rb +270 -0
  22. data/lib/automatic/environment.rb +31 -5
  23. data/lib/automatic/feed_maker.rb +65 -25
  24. data/lib/automatic/feed_parser.rb +51 -35
  25. data/lib/automatic/http.rb +107 -0
  26. data/lib/automatic/log.rb +49 -18
  27. data/lib/automatic/opml.rb +3 -1
  28. data/lib/automatic/pipeline.rb +73 -32
  29. data/lib/automatic/recipe.rb +100 -17
  30. data/lib/automatic/version.rb +14 -1
  31. data/lib/automatic.rb +93 -20
  32. data/plugins/custom_feed/svn_log.rb +73 -32
  33. data/plugins/custom_feed/web.rb +348 -0
  34. data/plugins/filter/absolute_uri.rb +43 -27
  35. data/plugins/filter/accept.rb +38 -45
  36. data/plugins/filter/batch.rb +97 -0
  37. data/plugins/filter/claude.rb +217 -0
  38. data/plugins/filter/clear.rb +12 -8
  39. data/plugins/filter/description_link.rb +66 -51
  40. data/plugins/filter/full_feed.rb +170 -52
  41. data/plugins/filter/gemini.rb +216 -0
  42. data/plugins/filter/github_feed.rb +38 -26
  43. data/plugins/filter/ignore.rb +33 -44
  44. data/plugins/filter/image.rb +36 -25
  45. data/plugins/filter/image_source.rb +75 -52
  46. data/plugins/filter/join.rb +105 -0
  47. data/plugins/filter/kimi.rb +216 -0
  48. data/plugins/filter/limit.rb +57 -0
  49. data/plugins/filter/one.rb +19 -26
  50. data/plugins/filter/open_ai.rb +198 -0
  51. data/plugins/filter/present.rb +75 -0
  52. data/plugins/filter/rand.rb +16 -17
  53. data/plugins/filter/sakura_ai.rb +205 -0
  54. data/plugins/filter/sanitize.rb +29 -34
  55. data/plugins/filter/sort.rb +20 -27
  56. data/plugins/filter/tumblr_resize.rb +31 -23
  57. data/plugins/notify/ikachan.rb +86 -48
  58. data/plugins/provide/fluentd.rb +43 -24
  59. data/plugins/publish/amazon_s3.rb +79 -40
  60. data/plugins/publish/console.rb +19 -16
  61. data/plugins/publish/console_link.rb +20 -17
  62. data/plugins/publish/eject.rb +48 -26
  63. data/plugins/publish/fluentd.rb +50 -30
  64. data/plugins/publish/hatena_bookmark.rb +88 -71
  65. data/plugins/publish/instapaper.rb +69 -59
  66. data/plugins/publish/markdown.rb +278 -0
  67. data/plugins/publish/memcached.rb +35 -29
  68. data/plugins/store/database.rb +50 -48
  69. data/plugins/store/digest.rb +212 -0
  70. data/plugins/store/file.rb +107 -68
  71. data/plugins/store/full_text.rb +31 -28
  72. data/plugins/store/permalink.rb +18 -22
  73. data/plugins/subscription/feed.rb +34 -21
  74. data/plugins/subscription/link.rb +31 -32
  75. data/plugins/subscription/text.rb +40 -45
  76. data/plugins/subscription/tumblr.rb +55 -44
  77. data/plugins/subscription/xml.rb +40 -36
  78. metadata +112 -321
  79. data/Gemfile +0 -38
  80. data/Rakefile +0 -59
  81. data/doc/ChangeLog +0 -303
  82. data/doc/PLUGINS +0 -750
  83. data/doc/PLUGINS.ja +0 -753
  84. data/doc/README +0 -511
  85. data/doc/README.ja +0 -519
  86. data/plugins/filter/google_news.rb +0 -50
  87. data/plugins/publish/google_calendar.rb +0 -84
  88. data/plugins/publish/hipchat.rb +0 -46
  89. data/plugins/publish/pocket.rb +0 -45
  90. data/plugins/publish/twitter.rb +0 -58
  91. data/plugins/subscription/chan_toru.rb +0 -57
  92. data/plugins/subscription/g_guide.rb +0 -57
  93. data/plugins/subscription/pocket.rb +0 -51
  94. data/plugins/subscription/twitter.rb +0 -69
  95. data/plugins/subscription/twitter_search.rb +0 -50
  96. data/plugins/subscription/weather.rb +0 -33
  97. data/script/build +0 -84
  98. data/spec/fixtures/sampleFeeds.tsv +0 -1
  99. data/spec/fixtures/sampleFeeds2.tsv +0 -2
  100. data/spec/fixtures/sampleRecipe.yml +0 -24
  101. data/spec/lib/automatic/log_spec.rb +0 -32
  102. data/spec/lib/automatic/pipeline_spec.rb +0 -68
  103. data/spec/lib/automatic/recipe_spec.rb +0 -40
  104. data/spec/lib/automatic_spec.rb +0 -99
  105. data/spec/plugins/custom_feed/svn_log_spec.rb +0 -31
  106. data/spec/plugins/filter/absolute_uri_spec.rb +0 -61
  107. data/spec/plugins/filter/accept_spec.rb +0 -331
  108. data/spec/plugins/filter/clear_spec.rb +0 -49
  109. data/spec/plugins/filter/description_link_spec.rb +0 -138
  110. data/spec/plugins/filter/full_feed_spec.rb +0 -129
  111. data/spec/plugins/filter/github_feed_spec.rb +0 -55
  112. data/spec/plugins/filter/google_news_spec.rb +0 -69
  113. data/spec/plugins/filter/ignore_spec.rb +0 -328
  114. data/spec/plugins/filter/image_source_spec.rb +0 -89
  115. data/spec/plugins/filter/image_spec.rb +0 -65
  116. data/spec/plugins/filter/one_spec.rb +0 -71
  117. data/spec/plugins/filter/rand_spec.rb +0 -52
  118. data/spec/plugins/filter/sanitize_spec.rb +0 -153
  119. data/spec/plugins/filter/sort_spec.rb +0 -189
  120. data/spec/plugins/filter/tumblr_resize_spec.rb +0 -109
  121. data/spec/plugins/notify/ikachan_spec.rb +0 -58
  122. data/spec/plugins/provide/fluentd_spec.rb +0 -49
  123. data/spec/plugins/publish/amazon_s3_spec.rb +0 -40
  124. data/spec/plugins/publish/console_spec.rb +0 -30
  125. data/spec/plugins/publish/eject_spec.rb +0 -40
  126. data/spec/plugins/publish/fluentd_spec.rb +0 -40
  127. data/spec/plugins/publish/google_calendar_spec.rb +0 -83
  128. data/spec/plugins/publish/hatena_bookmark_spec.rb +0 -134
  129. data/spec/plugins/publish/hipchat_spec.rb +0 -69
  130. data/spec/plugins/publish/instapaper_spec.rb +0 -82
  131. data/spec/plugins/publish/memcached_spec.rb +0 -63
  132. data/spec/plugins/publish/pocket_spec.rb +0 -51
  133. data/spec/plugins/publish/twitter_spec.rb +0 -73
  134. data/spec/plugins/store/file_spec.rb +0 -58
  135. data/spec/plugins/store/full_text_spec.rb +0 -152
  136. data/spec/plugins/store/permalink_spec.rb +0 -206
  137. data/spec/plugins/subscription/chan_toru_spec.rb +0 -56
  138. data/spec/plugins/subscription/feed_spec.rb +0 -71
  139. data/spec/plugins/subscription/g_guide_spec.rb +0 -82
  140. data/spec/plugins/subscription/link_spec.rb +0 -72
  141. data/spec/plugins/subscription/pocket_spec.rb +0 -57
  142. data/spec/plugins/subscription/text_spec.rb +0 -84
  143. data/spec/plugins/subscription/tumblr_spec.rb +0 -74
  144. data/spec/plugins/subscription/twitter_search_spec.rb +0 -57
  145. data/spec/plugins/subscription/twitter_spec.rb +0 -73
  146. data/spec/plugins/subscription/weather_spec.rb +0 -44
  147. data/spec/plugins/subscription/xml_spec.rb +0 -84
  148. data/spec/spec_helper.rb +0 -106
  149. data/spec/user_dir/plugins/store/mock.rb +0 -16
  150. data/test/fixtures/sampleOPML.xml +0 -11
  151. data/test/integration/test_absoluteurl.yml +0 -25
  152. data/test/integration/test_activerecord.yml +0 -24
  153. data/test/integration/test_add_pocket.yml +0 -26
  154. data/test/integration/test_chan_toru.yml +0 -21
  155. data/test/integration/test_descriptionlink.yml +0 -21
  156. data/test/integration/test_fluentd.yml +0 -22
  157. data/test/integration/test_fulltext.yml +0 -30
  158. data/test/integration/test_google_news.yml +0 -21
  159. data/test/integration/test_googlealert.yml +0 -21
  160. data/test/integration/test_hatenabookmark.yml +0 -30
  161. data/test/integration/test_ignore.yml +0 -25
  162. data/test/integration/test_ignore2.yml +0 -22
  163. data/test/integration/test_image2local.yml +0 -33
  164. data/test/integration/test_instapaper.yml +0 -26
  165. data/test/integration/test_link2local.yml +0 -34
  166. data/test/integration/test_one.yml +0 -23
  167. data/test/integration/test_pocket.yml +0 -22
  168. data/test/integration/test_rand.yml +0 -21
  169. data/test/integration/test_sanitize.yml +0 -23
  170. data/test/integration/test_sort.yml +0 -36
  171. data/test/integration/test_svnlog.yml +0 -15
  172. data/test/integration/test_text2feed.yml +0 -36
  173. data/test/integration/test_tumblr2local.yml +0 -43
  174. data/test/integration/test_twitter_search.yml +0 -22
  175. data/test/integration/test_weather.yml +0 -19
  176. data/test/integration/test_xml2fluentd.yml +0 -21
  177. data/vendor/.gitkeep +0 -0
@@ -1,87 +1,125 @@
1
1
  # -*- coding: utf-8 -*-
2
+ # Name:: Automatic::Plugin::Notify::Ikachan
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:: Mar 7, 2012
8
+ # Updated:: Aug 15, 2026
9
+ # Copyright:: Copyright (c) 2012-2026 Automatic Ruby Developers.
10
+ #
11
+ # Description:: Posts each item to an IRC channel through an ikachan
12
+ # HTTP-to-IRC gateway, which the operator runs themselves.
2
13
 
3
- require 'active_support'
4
- require 'active_support/core_ext'
5
- require 'active_support/deprecation'
14
+ require 'net/http'
15
+ require 'uri'
6
16
 
7
17
  module Automatic::Plugin
8
18
  class Ikachan
9
- require 'rubygems'
10
- require 'time'
11
- require 'net/http'
12
- require 'uri'
19
+ OPEN_TIMEOUT = 10
20
+ READ_TIMEOUT = 30
13
21
 
14
22
  attr_accessor :params
15
23
 
16
24
  def initialize
17
- @params = {
18
- "url" => "",
19
- "port" => "",
20
- "channels" => [],
21
- "command" => "",
22
- }
25
+ @params = { 'url' => '', 'port' => '', 'channels' => [], 'command' => '' }
23
26
  end
24
27
 
25
- def post(link, title = "")
26
- message = build_message(link, title)
27
- uri = URI.parse(endpoint_url)
28
- proxy_class = Net::HTTP::Proxy(ENV["PROXY"], 8080)
29
- http = proxy_class.new(uri.host, uri.port)
30
- http.start do |http|
31
- @params['channels'].split(",").each do|channel|
32
- # send join command to make sure when if ikachan is not in the channel
33
- http.post("/join", "channel=#{channel}")
34
- res = http.post(uri.path, %Q(channel=#{channel}&message=#{message}))
35
- if res.code == "200"
36
- Automatic::Log.puts(:info, "Success: #{message}")
37
- else
38
- Automatic::Log.puts(:error, "#{res.code} Error: #{message}")
39
- end
28
+ def post(link, title = '')
29
+ message = build_message(link, title)
30
+ uri = endpoint
31
+
32
+ start(uri) do |http|
33
+ channels.each do |channel|
34
+ # Join first, in case the gateway is not in the channel.
35
+ http.request(form('/join', channel: channel))
36
+ response = http.request(form(uri.path, channel: channel, message: message))
37
+ log(response, message)
40
38
  end
41
39
  end
42
40
  end
43
41
 
44
42
  private
45
43
 
46
- def endpoint_url
47
- "#{@params['url']}:#{@params['port']}/#{@params['command']}"
44
+ # A gateway reached over https is spoken to over https. The earlier
45
+ # version built a plain connection whatever the URL said.
46
+ def start(uri, &block)
47
+ proxy = Net::HTTP.Proxy(ENV.fetch('PROXY', nil), 8080)
48
+ proxy.start(uri.host, uri.port,
49
+ use_ssl: uri.scheme == 'https',
50
+ open_timeout: OPEN_TIMEOUT,
51
+ read_timeout: READ_TIMEOUT, &block)
52
+ end
53
+
54
+ # Form-encoded by Net::HTTP rather than interpolated into the body, so
55
+ # that a title carrying an ampersand or a space reaches the channel as it
56
+ # was written instead of splitting the request.
57
+ def form(path, params)
58
+ request = Net::HTTP::Post.new(path)
59
+ request.set_form_data(params)
60
+ request
61
+ end
62
+
63
+ def channels
64
+ value = @params['channels']
65
+ value.is_a?(Array) ? value : value.to_s.split(',')
66
+ end
67
+
68
+ def endpoint
69
+ URI.parse("#{@params['url']}:#{@params['port']}/#{@params['command']}")
70
+ end
71
+
72
+ def log(response, message)
73
+ if response.code == '200'
74
+ Automatic::Log.puts(:info, "Success: #{message}")
75
+ else
76
+ Automatic::Log.puts(:error, "#{response.code} Error: #{message}")
77
+ end
48
78
  end
49
79
 
50
80
  def build_message(link, title)
51
- message = ""
52
- message += "#{title.to_s} - " unless title.blank?
53
- message += link.to_s
54
- message
81
+ message = ''
82
+ message += "#{title} - " unless title.nil? || title.to_s.empty?
83
+ message + link.to_s
55
84
  end
56
85
  end
57
86
 
58
87
  class NotifyIkachan
59
88
  attr_accessor :ikachan
60
89
 
61
- def initialize(config, pipeline=[])
62
- @config = config
90
+ def initialize(config, pipeline = [])
91
+ @config = config || {}
63
92
  @pipeline = pipeline
64
93
 
65
94
  @ikachan = Ikachan.new
66
95
  @ikachan.params = {
67
- "url" => @config['url'],
68
- "port" => @config['port'] || "4979",
69
- "channels" => @config['channels'].split(',').map{ |v| v.match(/^#/) ? v : "#" + v }, # Prifixing '#'
70
- "command" => @config['command'] || "notice",
96
+ 'url' => @config['url'],
97
+ 'port' => @config['port'] || '4979',
98
+ 'channels' => channels,
99
+ 'command' => @config['command'] || 'notice'
71
100
  }
72
101
  end
73
102
 
103
+ # Returns the pipeline unchanged: a Notify plugin sends a notification and
104
+ # nothing else.
74
105
  def run
75
- @pipeline.each {|feeds|
76
- unless feeds.nil?
77
- feeds.items.each {|feed|
78
- Automatic::Log.puts("info", %Q(Ikachan: [#{feed.link}] sending with params #{ikachan.params.to_s}...))
79
- ikachan.post(feed.link, feed.title)
80
- sleep ||= @config['interval'].to_i
81
- }
106
+ @pipeline.each do |feeds|
107
+ next if feeds.nil?
108
+
109
+ feeds.items.each do |feed|
110
+ Automatic::Log.puts('info', "Ikachan: [#{feed.link}] sending to #{channels.join(',')}...")
111
+ ikachan.post(feed.link, feed.title)
112
+ sleep(@config['interval'].to_i)
82
113
  end
83
- }
114
+ end
84
115
  @pipeline
85
116
  end
117
+
118
+ private
119
+
120
+ # Comma separated, with a leading '#' added where it is absent.
121
+ def channels
122
+ @config['channels'].to_s.split(',').map { |name| name.start_with?('#') ? name : "##{name}" }
123
+ end
86
124
  end
87
125
  end
@@ -1,37 +1,56 @@
1
1
  # -*- coding: utf-8 -*-
2
- # Name:: Automatic::Plugin::Provide::Fluentd
3
- # Author:: 774 <http://id774.net>
4
- # Created:: Jul 12, 2013
5
- # Updated:: May 16, 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::Plugin::Provide::Fluentd
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:: Jul 12, 2013
8
+ # Updated:: Aug 15, 2026
9
+ # Copyright:: Copyright (c) 2012-2026 Automatic Ruby Developers.
8
10
 
9
11
  module Automatic::Plugin
10
12
  class ProvideFluentd
11
- require 'fluent-logger'
13
+ Automatic.require_optional('fluent-logger', needed_by: 'ProvideFluentd')
12
14
 
13
- def initialize(config, pipeline=[])
14
- @config = config
15
+ def initialize(config, pipeline = [])
16
+ @config = config || {}
15
17
  @pipeline = pipeline
16
- @mode = @config['mode']
17
- @fluentd = Fluent::Logger::FluentLogger.open(nil,
18
- host = @config['host'],
19
- port = @config['port']) unless @mode == 'test'
18
+ @mode = @config['mode']
19
+ @fluentd = open_logger unless test?
20
20
  end
21
21
 
22
+ # Posts each item's content_encoded to Fluentd, which must be something
23
+ # Fluentd accepts as a record. Distinct from PublishFluentd, which posts
24
+ # the item's fields.
22
25
  def run
23
- @pipeline.each {|feeds|
24
- unless feeds.nil?
25
- feeds.items.each {|feed|
26
- begin
27
- @fluentd.post(@config['tag'], feed.content_encoded) unless @mode == 'test'
28
- rescue
29
- Automatic::Log.puts("error", "Fluent::Logger.post failed, the content_encoded of item may be not kind of Hash.")
30
- end
31
- }
32
- end
33
- }
26
+ @pipeline.each do |feeds|
27
+ next if feeds.nil?
28
+
29
+ feeds.items.each { |feed| post(feed) }
30
+ end
34
31
  @pipeline
35
32
  end
33
+
34
+ private
35
+
36
+ def open_logger
37
+ Fluent::Logger::FluentLogger.open(nil,
38
+ host: @config['host'],
39
+ port: @config['port'].to_i)
40
+ end
41
+
42
+ def post(feed)
43
+ return if test?
44
+
45
+ @fluentd.post(@config['tag'], feed.content_encoded)
46
+ rescue StandardError => e
47
+ Automatic::Log.puts('error',
48
+ 'Fluent::Logger.post failed, the content_encoded of item may ' \
49
+ "be not kind of Hash: #{e.message}")
50
+ end
51
+
52
+ def test?
53
+ @mode == 'test'
54
+ end
36
55
  end
37
56
  end
@@ -1,58 +1,97 @@
1
1
  # -*- coding: utf-8 -*-
2
- # Name:: Automatic::Plugin::Publish::AmazonS3
3
- # Author:: 774 <http://id774.net>
4
- # Created:: Feb 24, 2014
5
- # Updated:: Feb 24, 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::Plugin::Publish::AmazonS3
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 24, 2014
8
+ # Updated:: Sep 9, 2026
9
+ # Copyright:: Copyright (c) 2012-2026 Automatic Ruby Developers.
8
10
 
9
11
  module Automatic::Plugin
10
12
  class PublishAmazonS3
11
13
  require 'uri'
12
- require 'aws-sdk'
13
14
 
14
- def initialize(config, pipeline=[])
15
- @config = config
15
+ FILE_URI_ESCAPER = URI::RFC2396_Parser.new
16
+
17
+ def initialize(config, pipeline = [])
18
+ @config = config || {}
16
19
  @pipeline = pipeline
17
- s3 = AWS::S3.new(
18
- :access_key_id => @config['access_key'],
19
- :secret_access_key => @config['secret_key']
20
- )
21
- @bucket = s3.buckets[@config['bucket_name']]
22
- @mode = @config['mode']
20
+ @mode = @config['mode']
23
21
  end
24
22
 
23
+ # Uploads the files whose link is a file URI, normally after StoreFile.
25
24
  def run
26
- @pipeline.each {|feeds|
27
- unless feeds.nil?
28
- feeds.items.each {|feed|
29
- unless feed.link.nil?
30
- begin
31
- uri = URI.parse(feed.link)
32
- if uri.scheme == 'file'
33
- upload_amazons3(uri.path, @config['target_path'])
34
- else
35
- Automatic::Log.puts("warn", "Skip feed due to uri scheme is not file.")
36
- end
37
- rescue
38
- Automatic::Log.puts("error", "Error detected with #{feed.link} in uploading AmazonS3.")
39
- end
40
- end
41
- }
42
- end
43
- }
25
+ @pipeline.each do |feeds|
26
+ next if feeds.nil?
27
+
28
+ feeds.items.each { |feed| publish(feed) }
29
+ end
44
30
  @pipeline
45
31
  end
46
32
 
47
33
  private
48
34
 
49
- def upload_amazons3(filename, target_path)
50
- target = File.join(target_path, File.basename(filename))
51
- object = @bucket.objects[target]
52
- source = Pathname.new(filename)
53
- object.write(source) unless @mode == "test"
54
- Automatic::Log.puts("info", "Uploaded: file #{source} to the bucket #{target} on #{@bucket.name}.")
55
- return source, target
35
+ def publish(feed)
36
+ return if feed.link.nil?
37
+
38
+ uri = URI.parse(feed.link)
39
+ if uri.scheme == 'file'
40
+ upload(file_path(uri))
41
+ else
42
+ Automatic::Log.puts('warn', 'Skip feed due to uri scheme is not file.')
43
+ end
44
+ rescue StandardError => e
45
+ Automatic::Log.puts('error',
46
+ "Error detected with #{feed.link} in uploading AmazonS3: #{e.message}")
47
+ end
48
+
49
+ def file_path(uri)
50
+ FILE_URI_ESCAPER.unescape(uri.path.to_s)
51
+ end
52
+
53
+ def upload(path)
54
+ key = target_key(path)
55
+ File.open(path, 'rb') { |body| s3.put_object(bucket: bucket, key: key, body: body) } unless test?
56
+ Automatic::Log.puts('info', "Uploaded: file #{path} to the key #{key} on #{bucket}.")
57
+ [path, key]
58
+ end
59
+
60
+ def target_key(path)
61
+ File.join(@config['target_path'].to_s, File.basename(path)).sub(%r{\A/}, '')
62
+ end
63
+
64
+ def bucket
65
+ @config['bucket_name'].to_s
66
+ end
67
+
68
+ def test?
69
+ @mode == 'test'
70
+ end
71
+
72
+ # AWS SDK for Ruby v3, which is the SDK AWS publishes and maintains. The
73
+ # gem is required here rather than at the top of the file, so that a
74
+ # Recipe running this plugin in `mode: test` -- and this plugin's own
75
+ # specs -- need neither the gem nor an account.
76
+ #
77
+ # Credentials are the Recipe's where it carries them, and the SDK's
78
+ # default chain -- environment, shared profile, instance role -- where it
79
+ # does not, which is the way to run this without a secret in a file.
80
+ def s3
81
+ @s3 ||= begin
82
+ Automatic.require_optional('aws-sdk-s3', needed_by: 'PublishAmazonS3')
83
+ Aws::S3::Client.new(**client_options)
84
+ end
85
+ end
86
+
87
+ def client_options
88
+ options = {}
89
+ options[:region] = @config['region'].to_s unless @config['region'].nil?
90
+ unless @config['access_key'].nil?
91
+ options[:access_key_id] = @config['access_key'].to_s
92
+ options[:secret_access_key] = @config['secret_key'].to_s
93
+ end
94
+ options
56
95
  end
57
96
  end
58
97
  end
@@ -1,29 +1,32 @@
1
1
  # -*- coding: utf-8 -*-
2
- # Name:: Automatic::Plugin::Publish::Console
3
- # Author:: 774 <http://id774.net>
4
- # Created:: Feb 23, 2012
5
- # Updated:: Feb 24, 2012
6
- # Copyright:: Copyright (c) 2012-2013 Automatic Ruby Developers.
7
- # License:: Licensed under the GNU GENERAL PUBLIC LICENSE, Version 3.0.
2
+ # Name:: Automatic::Plugin::Publish::Console
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 23, 2012
8
+ # Updated:: Aug 15, 2026
9
+ # Copyright:: Copyright (c) 2012-2026 Automatic Ruby Developers.
8
10
 
9
11
  module Automatic::Plugin
10
12
  class PublishConsole
11
13
  require 'pp'
12
14
 
13
- def initialize(config, pipeline=[])
14
- @config = config
15
+ def initialize(config, pipeline = [])
16
+ @config = config || {}
15
17
  @pipeline = pipeline
16
- @output = STDOUT
18
+ # Held in an instance variable rather than written to $stdout directly,
19
+ # so that a spec can substitute it. See doc/PLUGINS.md section 3.6.
20
+ @output = $stdout
17
21
  end
18
22
 
23
+ # Prints each item, the plugin to end a Recipe with while writing it.
19
24
  def run
20
- @pipeline.each {|feeds|
21
- unless feeds.nil?
22
- feeds.items.each {|feed|
23
- @output.puts("info", feed.pretty_inspect)
24
- }
25
- end
26
- }
25
+ @pipeline.each do |feeds|
26
+ next if feeds.nil?
27
+
28
+ feeds.items.each { |feed| @output.puts(feed.pretty_inspect) }
29
+ end
27
30
  @pipeline
28
31
  end
29
32
  end
@@ -1,29 +1,32 @@
1
1
  # -*- coding: utf-8 -*-
2
- # Name:: Automatic::Plugin::Publish::ConsoleLink
3
- # Author:: soramugi <http://soramugi.net>
4
- # Created:: Jun 02, 2013
5
- # Updated:: Jun 02, 2013
6
- # Copyright:: Copyright (c) 2012-2013 Automatic Ruby Developers.
7
- # License:: Licensed under the GNU GENERAL PUBLIC LICENSE, Version 3.0.
2
+ # Name:: Automatic::Plugin::Publish::ConsoleLink
3
+ # Author: soramugi (More info: http://soramugi.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:: Jun 02, 2013
8
+ # Updated:: Aug 15, 2026
9
+ # Copyright:: Copyright (c) 2012-2026 Automatic Ruby Developers.
8
10
 
9
11
  module Automatic::Plugin
10
12
  class PublishConsoleLink
11
- require 'pp'
12
-
13
- def initialize(config, pipeline=[])
14
- @config = config
13
+ def initialize(config, pipeline = [])
14
+ @config = config || {}
15
15
  @pipeline = pipeline
16
- @output = STDOUT
16
+ @output = $stdout
17
17
  end
18
18
 
19
+ # Prints each item's link, one per line, and nothing else. Useful in a
20
+ # pipe, which is why an item whose link a filter has blanked prints
21
+ # nothing rather than an empty line.
19
22
  def run
20
- @pipeline.each {|feeds|
21
- unless feeds.nil?
22
- feeds.items.each {|feed|
23
- @output.puts("info", feed.link)
24
- }
23
+ @pipeline.each do |feeds|
24
+ next if feeds.nil?
25
+
26
+ feeds.items.each do |feed|
27
+ @output.puts(feed.link) unless feed.link.nil?
25
28
  end
26
- }
29
+ end
27
30
  @pipeline
28
31
  end
29
32
  end
@@ -1,42 +1,64 @@
1
1
  # -*- coding: utf-8 -*-
2
- # Name:: Automatic::Plugin::Publish::Eject
3
- # Author:: soramugi <http://soramugi.net>
4
- # 774 <http://id774.net>
5
- # Created:: Jun 9, 2013
6
- # Updated:: May 16, 2014
7
- # Copyright:: Copyright (c) 2012-2014 Automatic Ruby Developers.
8
- # License:: Licensed under the GNU GENERAL PUBLIC LICENSE, Version 3.0.
2
+ # Name:: Automatic::Plugin::Publish::Eject
3
+ # Author: soramugi (More info: http://soramugi.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:: Jun 9, 2013
8
+ # Updated:: Aug 15, 2026
9
+ # Copyright:: Copyright (c) 2012-2026 Automatic Ruby Developers.
9
10
 
10
11
  module Automatic::Plugin
11
12
  class PublishEject
13
+ # The command that opens the tray and the command that closes it again,
14
+ # per platform, as argument vectors. Nothing here goes through a shell.
15
+ COMMANDS = {
16
+ 'eject' => [%w[eject], %w[eject -t]], # GNU/Linux
17
+ 'drutil' => [%w[drutil tray eject], %w[drutil tray close]] # macOS
18
+ }.freeze
12
19
 
13
- def initialize(config, pipeline=[])
14
- @config = config
20
+ def initialize(config, pipeline = [])
21
+ @config = config || {}
15
22
  @pipeline = pipeline
16
23
  end
17
24
 
25
+ # Opens and closes the optical drive once per item. A physical
26
+ # notification, and the plugin that needs a machine with a tray.
18
27
  def run
19
- @pipeline.each {|feeds|
20
- unless feeds.nil?
21
- feeds.items.each {|feed|
22
- unless feed.link.nil?
23
- `#{eject_cmd}`
24
- Automatic::Log.puts('info', "Eject: #{feed.link}")
25
-
26
- interval = @config['interval'].to_i unless @config['interval'].nil? unless @config.nil?
27
- sleep ||= @config['interval'].to_i
28
- end
29
- }
28
+ @pipeline.each do |feeds|
29
+ next if feeds.nil?
30
+
31
+ feeds.items.each do |feed|
32
+ next if feed.link.nil?
33
+
34
+ eject
35
+ Automatic::Log.puts('info', "Eject: #{feed.link}")
36
+ sleep(@config['interval'].to_i)
30
37
  end
31
- }
38
+ end
32
39
  @pipeline
33
40
  end
34
41
 
35
- def eject_cmd
36
- if `which eject` != '' # linux
37
- 'eject ; eject -t'
38
- elsif `which drutil` != '' # mac
39
- 'drutil tray eject ; drutil tray close'
42
+ private
43
+
44
+ def eject
45
+ commands = COMMANDS[command_name]
46
+ if commands.nil?
47
+ Automatic::Log.puts('warn', 'No eject command found on this system.')
48
+ return
49
+ end
50
+
51
+ commands.each { |command| system(*command) }
52
+ end
53
+
54
+ # Looked up on PATH rather than by running `which` in a shell.
55
+ def command_name
56
+ @command_name ||= COMMANDS.keys.find { |name| executable?(name) }
57
+ end
58
+
59
+ def executable?(name)
60
+ ENV['PATH'].to_s.split(File::PATH_SEPARATOR).any? do |dir|
61
+ File.executable?(File.join(dir, name))
40
62
  end
41
63
  end
42
64
  end