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
@@ -1,49 +1,44 @@
1
1
  # -*- coding: utf-8 -*-
2
- # Name:: Automatic::Plugin::Filter::Sanitize
3
- # Author:: 774 <http://id774.net>
4
- # Created:: Jun 20, 2013
5
- # Updated:: Feb 21, 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::Filter::Sanitize
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:: Jun 20, 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 FilterSanitize
11
- require 'sanitize'
13
+ Automatic.require_optional('sanitize', needed_by: 'FilterSanitize')
12
14
 
13
- def initialize(config, pipeline=[])
14
- @config = config
15
+ MODES = {
16
+ 'basic' => Sanitize::Config::BASIC,
17
+ 'relaxed' => Sanitize::Config::RELAXED
18
+ }.freeze
19
+
20
+ def initialize(config, pipeline = [])
21
+ @config = config || {}
15
22
  @pipeline = pipeline
16
- case @config['mode']
17
- when "basic"
18
- @mode = Sanitize::Config::BASIC
19
- when "relaxed"
20
- @mode = Sanitize::Config::RELAXED
21
- else
22
- @mode = Sanitize::Config::RESTRICTED
23
- end
23
+ @mode = MODES.fetch(@config['mode'].to_s, Sanitize::Config::RESTRICTED)
24
24
  end
25
25
 
26
+ # Strips HTML from descriptions.
26
27
  def run
27
- @return_feeds = []
28
- @pipeline.each {|feeds|
29
- unless feeds.nil?
30
- feeds.items.each {|feed|
31
- feed = sanitize(feed)
32
- }
33
- @return_feeds << feeds
34
- end
35
- }
36
- @return_feeds
28
+ @pipeline.each_with_object([]) do |feeds, returned|
29
+ next if feeds.nil?
30
+
31
+ feeds.items.each { |item| sanitize(item) }
32
+ returned << feeds
33
+ end
37
34
  end
38
35
 
39
36
  private
40
- def sanitize(feed)
41
- begin
42
- feed.description = Sanitize.clean(feed.description, @mode) unless feed.description.nil?
43
- rescue
44
- Automatic::Log.puts("warn", "Undefined field detected in feed.")
45
- end
46
- feed
37
+
38
+ def sanitize(item)
39
+ item.description = Sanitize.fragment(item.description, @mode) unless item.description.nil?
40
+ rescue StandardError => e
41
+ Automatic::Log.puts('warn', "Undefined field detected in feed: #{e.message}")
47
42
  end
48
43
  end
49
44
  end
@@ -1,37 +1,30 @@
1
1
  # -*- coding: utf-8 -*-
2
- # Name:: Automatic::Plugin::Filter::Sort
3
- # Author:: 774 <http://id774.net>
4
- # Created:: Mar 23, 2012
5
- # Updated:: Jan 23, 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::Filter::Sort
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:: Mar 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 FilterSort
11
-
12
- def initialize(config, pipeline=[])
13
- @config = config
14
- @pipeline = pipeline
13
+ def initialize(config, pipeline = [])
14
+ @config = config || {}
15
+ @pipeline = pipeline
16
+ @ascending = @config['sort'].to_s == 'asc'
15
17
  end
16
18
 
19
+ # Sorts each feed's items by date. Items must carry one; a feed built from
20
+ # a source without dates fails here.
17
21
  def run
18
- @return_feeds = []
19
- @pipeline.each { |feeds|
20
- return_feed_items = []
21
- unless feeds.nil?
22
- if @config['sort'] == "asc"
23
- feeds.items.sort!{|a,b|
24
- a.date <=> b.date
25
- }
26
- else
27
- feeds.items.sort!{|a,b|
28
- - (a.date <=> b.date)
29
- }
30
- end
31
- @return_feeds << feeds
32
- end
33
- }
34
- @return_feeds
22
+ @pipeline.each_with_object([]) do |feeds, returned|
23
+ next if feeds.nil?
24
+
25
+ feeds.items.sort! { |a, b| @ascending ? a.date <=> b.date : b.date <=> a.date }
26
+ returned << feeds
27
+ end
35
28
  end
36
29
  end
37
30
  end
@@ -1,40 +1,48 @@
1
1
  # -*- coding: utf-8 -*-
2
- # Name:: Automatic::Plugin::Filter::TumblrResize
3
- # Author:: 774 <http://id774.net>
4
- # Created:: Feb 28, 2012
5
- # Updated:: Apr 5, 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::Filter::TumblrResize
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 28, 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 FilterTumblrResize
13
+ # Tumblr has served images under two URL schemes. The older one carries
14
+ # the size as a suffix on the file name -- tumblr_xxx_500.jpg -- and is
15
+ # what images uploaded before 2019 still use. The newer one carries it as
16
+ # a path segment -- /s540x810/ -- and is what everything since uses.
17
+ # Both are rewritten to the largest variant the scheme offers.
18
+ LEGACY_SIZE = /_(?:75sq|100|250|400|500)(?=\.[a-zA-Z0-9]+\z)/
19
+ LEGACY_LARGEST = '_1280'
11
20
 
12
- def initialize(config, pipeline=[])
13
- @config = config
21
+ PATH_SIZE = %r{/s\d+x\d+/}
22
+ PATH_LARGEST = '/s1280x1920/'
23
+
24
+ def initialize(config, pipeline = [])
25
+ @config = config || {}
14
26
  @pipeline = pipeline
15
27
  end
16
28
 
29
+ # Assumes FilterImage or FilterImageSource has already put an image URL in
30
+ # the link.
17
31
  def run
18
- @return_feeds = []
19
- @pipeline.each {|feeds|
20
- img_url = ""
32
+ @pipeline.each_with_object([]) do |feeds, returned|
21
33
  unless feeds.nil?
22
- feeds.items.each {|feed|
23
- feed.link = resize(feed.link) unless feed.link.nil?
24
- }
34
+ feeds.items.each do |item|
35
+ item.link = resize(item.link) unless item.link.nil?
36
+ end
25
37
  end
26
- @return_feeds << feeds
27
- }
28
- @return_feeds
38
+ returned << feeds
39
+ end
29
40
  end
30
41
 
31
42
  private
32
- def resize(string)
33
- string = string.gsub("_75sq\.", "_1280\.")
34
- string = string.gsub("_100\.", "_1280\.")
35
- string = string.gsub("_250\.", "_1280\.")
36
- string = string.gsub("_400\.", "_1280\.")
37
- string = string.gsub("_500\.", "_1280\.")
43
+
44
+ def resize(link)
45
+ link.sub(LEGACY_SIZE, LEGACY_LARGEST).sub(PATH_SIZE, PATH_LARGEST)
38
46
  end
39
47
  end
40
48
  end
@@ -1,87 +1,125 @@
1
1
  # -*- coding: utf-8 -*-
2
+ # Name:: Automatic::Plugin::Notify::Ikachan
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:: 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: 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:: 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,91 @@
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: 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 24, 2014
8
+ # Updated:: Aug 15, 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
+ def initialize(config, pipeline = [])
16
+ @config = config || {}
16
17
  @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']
18
+ @mode = @config['mode']
23
19
  end
24
20
 
21
+ # Uploads the files whose link is a file URI, normally after StoreFile.
25
22
  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
- }
23
+ @pipeline.each do |feeds|
24
+ next if feeds.nil?
25
+
26
+ feeds.items.each { |feed| publish(feed) }
27
+ end
44
28
  @pipeline
45
29
  end
46
30
 
47
31
  private
48
32
 
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
33
+ def publish(feed)
34
+ return if feed.link.nil?
35
+
36
+ uri = URI.parse(feed.link)
37
+ if uri.scheme == 'file'
38
+ upload(uri.path)
39
+ else
40
+ Automatic::Log.puts('warn', 'Skip feed due to uri scheme is not file.')
41
+ end
42
+ rescue StandardError => e
43
+ Automatic::Log.puts('error',
44
+ "Error detected with #{feed.link} in uploading AmazonS3: #{e.message}")
45
+ end
46
+
47
+ def upload(path)
48
+ key = target_key(path)
49
+ File.open(path, 'rb') { |body| s3.put_object(bucket: bucket, key: key, body: body) } unless test?
50
+ Automatic::Log.puts('info', "Uploaded: file #{path} to the key #{key} on #{bucket}.")
51
+ [path, key]
52
+ end
53
+
54
+ def target_key(path)
55
+ File.join(@config['target_path'].to_s, File.basename(path)).sub(%r{\A/}, '')
56
+ end
57
+
58
+ def bucket
59
+ @config['bucket_name'].to_s
60
+ end
61
+
62
+ def test?
63
+ @mode == 'test'
64
+ end
65
+
66
+ # AWS SDK for Ruby v3, which is the SDK AWS publishes and maintains. The
67
+ # gem is required here rather than at the top of the file, so that a
68
+ # Recipe running this plugin in `mode: test` -- and this plugin's own
69
+ # specs -- need neither the gem nor an account.
70
+ #
71
+ # Credentials are the Recipe's where it carries them, and the SDK's
72
+ # default chain -- environment, shared profile, instance role -- where it
73
+ # does not, which is the way to run this without a secret in a file.
74
+ def s3
75
+ @s3 ||= begin
76
+ Automatic.require_optional('aws-sdk-s3', needed_by: 'PublishAmazonS3')
77
+ Aws::S3::Client.new(**client_options)
78
+ end
79
+ end
80
+
81
+ def client_options
82
+ options = {}
83
+ options[:region] = @config['region'].to_s unless @config['region'].nil?
84
+ unless @config['access_key'].nil?
85
+ options[:access_key_id] = @config['access_key'].to_s
86
+ options[:secret_access_key] = @config['secret_key'].to_s
87
+ end
88
+ options
56
89
  end
57
90
  end
58
91
  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: 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 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