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,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
@@ -1,43 +1,63 @@
1
1
  # -*- coding: utf-8 -*-
2
- # Name:: Automatic::Plugin::Publish::Fluentd
3
- # Author:: 774 <http://id774.net>
4
- # Created:: Jun 21, 2013
5
- # Updated:: Feb 25, 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::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:: Jun 21, 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 PublishFluentd
11
- require 'fluent-logger'
13
+ Automatic.require_optional('fluent-logger', needed_by: 'PublishFluentd')
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 title, link, description, content_encoded and a
23
+ # timestamp to Fluentd.
22
24
  def run
23
- @pipeline.each {|feeds|
24
- unless feeds.nil?
25
- feeds.items.each {|feed|
26
- begin
27
- @fluentd.post(@config['tag'], {
28
- :title => feed.title,
29
- :link => feed.link,
30
- :description => feed.description,
31
- :content => feed.content_encoded,
32
- :created_at => Time.now.strftime("%Y/%m/%d %X")
33
- }) unless @mode == 'test'
34
- rescue
35
- Automatic::Log.puts("warn", "Skip feed due to fault in forward.")
36
- end
37
- }
38
- end
39
- }
25
+ @pipeline.each do |feeds|
26
+ next if feeds.nil?
27
+
28
+ feeds.items.each { |feed| post(feed) }
29
+ end
40
30
  @pipeline
41
31
  end
32
+
33
+ private
34
+
35
+ def open_logger
36
+ Fluent::Logger::FluentLogger.open(nil,
37
+ host: @config['host'],
38
+ port: @config['port'].to_i)
39
+ end
40
+
41
+ def post(feed)
42
+ return if test?
43
+
44
+ @fluentd.post(@config['tag'], record(feed))
45
+ rescue StandardError => e
46
+ Automatic::Log.puts('warn', "Skip feed due to fault in forward: #{e.message}")
47
+ end
48
+
49
+ def record(feed)
50
+ {
51
+ title: feed.title,
52
+ link: feed.link,
53
+ description: feed.description,
54
+ content: feed.content_encoded,
55
+ created_at: Time.now.strftime('%Y/%m/%d %X')
56
+ }
57
+ end
58
+
59
+ def test?
60
+ @mode == 'test'
61
+ end
42
62
  end
43
63
  end
@@ -1,110 +1,127 @@
1
1
  # -*- coding: utf-8 -*-
2
- # Name:: Automatic::Plugin::Publish::HatenaBookmark
3
- # Author:: 774 <http://id774.net>
4
- # Created:: Feb 22, 2012
5
- # Updated:: Jan 15, 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::HatenaBookmark
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 22, 2012
8
+ # Updated:: Aug 15, 2026
9
+ # Copyright:: Copyright (c) 2012-2026 Automatic Ruby Developers.
10
+ #
11
+ # STATUS: Needs rework. This speaks the WSSE AtomPub interface, which Hatena
12
+ # has superseded with an OAuth one. The service and the bookmarking API are
13
+ # both current; this client is not, and restoring it means the current
14
+ # endpoint and the current authentication rather than a change of a few lines.
15
+ # See doc/PLUGINS.md section 6.7.
16
+ #
17
+ # The transport was corrected in the meantime: the request is made over HTTPS,
18
+ # so that an operator who runs this does not put a password digest on the wire
19
+ # in the clear. That is a defect worth not shipping whatever the plugin's
20
+ # status is; it is not a claim that the plugin works.
21
+
22
+ require 'digest/sha1'
23
+ require 'net/http'
24
+ require 'securerandom'
25
+ require 'time'
26
+ require 'uri'
8
27
 
9
28
  module Automatic::Plugin
10
29
  class HatenaBookmark
11
- require 'rubygems'
12
- require 'time'
13
- require 'digest/sha1'
14
- require 'net/http'
15
- require 'uri'
16
- #require 'nkf'
30
+ ENDPOINT = 'https://b.hatena.ne.jp/atom/post'
31
+ OPEN_TIMEOUT = 10
32
+ READ_TIMEOUT = 30
17
33
 
18
34
  attr_accessor :user
19
35
 
20
36
  def initialize
21
- @user = {
22
- "hatena_id" => "",
23
- "password" => ""
24
- }
37
+ @user = { 'hatena_id' => '', 'password' => '' }
25
38
  end
26
39
 
27
40
  def wsse(hatena_id, password)
28
- # Unique value
29
- nonce = [Time.now.to_i.to_s].pack('m').gsub(/\n/, '')
30
- now = Time.now.utc.iso8601
41
+ nonce = SecureRandom.random_bytes(16)
42
+ now = Time.now.utc.iso8601
43
+ digest = [Digest::SHA1.digest(nonce + now + password.to_s)].pack('m0')
31
44
 
32
- # Base64 encoding for SHA1 Digested strings
33
- digest = [Digest::SHA1.digest(nonce + now + password)].pack("m").gsub(/\n/, '')
45
+ { 'X-WSSE' => format('UsernameToken Username="%s", PasswordDigest="%s", ' \
46
+ 'Nonce="%s", Created="%s"',
47
+ hatena_id, digest, [nonce].pack('m0'), now) }
48
+ end
34
49
 
35
- {'X-WSSE' => sprintf(
36
- %Q<UsernameToken Username="%s", PasswordDigest="%s", Nonce="%s", Created="%s">,
37
- hatena_id, digest, nonce, now)
38
- }
50
+ def to_xml(link, summary)
51
+ <<~XML
52
+ <entry xmlns="http://purl.org/atom/ns#">
53
+ <title>dummy</title>
54
+ <link rel="related" type="text/html" href="#{link}" />
55
+ <summary type="text/plain">#{summary}</summary>
56
+ </entry>
57
+ XML
39
58
  end
40
59
 
41
- def toXml(link, summary)
42
- %Q(
43
- <entry xmlns="http://purl.org/atom/ns#">
44
- <title>dummy</title>
45
- <link rel="related" type="text/html" href="#{link}" />
46
- <summary type="text/plain">#{summary}</summary>
47
- </entry>
48
- )
60
+ def post(url, comment)
61
+ Automatic::Log.puts('info', "Bookmarking: #{url}")
62
+ uri = URI.parse(ENDPOINT)
63
+ request = Net::HTTP::Post.new(uri.path, wsse(@user['hatena_id'], @user['password']))
64
+ request.body = to_xml(url, comment)
65
+
66
+ response = start(uri) { |http| http.request(request) }
67
+ log(response, url, comment)
49
68
  end
50
69
 
51
- def post(b_url, b_comment)
52
- Automatic::Log.puts("info", "Bookmarking: #{b_url}")
53
- url = "http://b.hatena.ne.jp/atom/post"
54
- header = wsse(@user["hatena_id"], @user["password"])
55
- uri = URI.parse(url)
56
- proxy_class = Net::HTTP::Proxy(ENV["PROXY"], 8080)
57
- http = proxy_class.new(uri.host)
58
- http.start { |http|
59
- # b_url = NKF.nkf('-w', b_url)
60
- # b_comment = NKF.nkf('-w', b_comment)
61
- res = http.post(uri.path, toXml(b_url, b_comment), header)
62
- if res.code == "201" then
63
- message = "Success: #{b_url}"
64
- message += " Comment: #{b_comment}" unless b_comment.nil?
65
- Automatic::Log.puts(:info, message)
66
- else
67
- Automatic::Log.puts(:error, "#{res.code} Error: #{b_url}")
68
- end
69
- }
70
+ private
71
+
72
+ def start(uri, &block)
73
+ proxy = Net::HTTP.Proxy(ENV.fetch('PROXY', nil), 8080)
74
+ proxy.start(uri.host, uri.port,
75
+ use_ssl: true,
76
+ open_timeout: OPEN_TIMEOUT,
77
+ read_timeout: READ_TIMEOUT, &block)
78
+ end
79
+
80
+ def log(response, url, comment)
81
+ if response.code == '201'
82
+ message = "Success: #{url}"
83
+ message += " Comment: #{comment}" unless comment.nil?
84
+ Automatic::Log.puts(:info, message)
85
+ else
86
+ Automatic::Log.puts(:error, "#{response.code} Error: #{url}")
87
+ end
70
88
  end
71
89
  end
72
90
 
73
91
  class PublishHatenaBookmark
74
92
  attr_accessor :hb
75
93
 
76
- def initialize(config, pipeline=[])
77
- @config = config
94
+ def initialize(config, pipeline = [])
95
+ @config = config || {}
78
96
  @pipeline = pipeline
79
97
 
80
98
  @hb = HatenaBookmark.new
81
99
  @hb.user = {
82
- "hatena_id" => @config['username'],
83
- "password" => @config['password']
100
+ 'hatena_id' => @config['username'],
101
+ 'password' => @config['password']
84
102
  }
85
103
  end
86
104
 
87
105
  def run
88
- @pipeline.each {|feeds|
89
- unless feeds.nil?
90
- feeds.items.each {|feed|
91
- hb.post(rewrite(feed.link), nil)
92
- sleep ||= @config['interval'].to_i
93
- }
106
+ @pipeline.each do |feeds|
107
+ next if feeds.nil?
108
+
109
+ feeds.items.each do |feed|
110
+ hb.post(absolute(feed.link), nil)
111
+ sleep(@config['interval'].to_i)
94
112
  end
95
- }
113
+ end
96
114
  @pipeline
97
115
  end
98
116
 
99
117
  private
100
- def rewrite(string)
101
- if /^https?:\/\/.*$/ =~ string
102
- return string
103
- elsif /^\/\/.*$/ =~ string
104
- return "http:" + string
105
- else
106
- return "http://" + string
107
- end
118
+
119
+ def absolute(link)
120
+ string = link.to_s
121
+ return string if string.match?(%r{\Ahttps?://})
122
+ return "https:#{string}" if string.start_with?('//')
123
+
124
+ "https://#{string}"
108
125
  end
109
126
  end
110
127
  end
@@ -1,88 +1,98 @@
1
1
  # -*- coding: utf-8 -*-
2
- # Name:: Automatic::Plugin::Publish::Instapaper
3
- # Author:: soramugi <http://soramugi.net>
4
- # 774 <http://id774.net>
5
- # Created:: Feb 9, 2013
6
- # Updated:: Jan 15, 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::Instapaper
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:: Feb 9, 2013
8
+ # Updated:: Aug 15, 2026
9
+ # Copyright:: Copyright (c) 2012-2026 Automatic Ruby Developers.
9
10
 
10
11
  module Automatic::Plugin
12
+ # The Instapaper Simple API: HTTPS, basic authentication, form parameters.
13
+ # @see https://www.instapaper.com/api/simple
11
14
  class Instapaper
12
- # @see http://www.instapaper.com/api/simple
13
- require "net/http"
14
- require "openssl"
15
+ require 'net/http'
16
+ require 'openssl'
17
+ require 'uri'
18
+
19
+ HOST = 'www.instapaper.com'
20
+ OPEN_TIMEOUT = 10
21
+ READ_TIMEOUT = 30
22
+
23
+ class Error < StandardError; end
15
24
 
16
25
  def initialize(username, password = '')
17
26
  @username = username
18
- @password = password
27
+ @password = password.to_s
19
28
  request(:authenticate)
20
29
  end
21
30
 
22
31
  def add(url, title = '', selection = '')
23
- params = {
24
- :url => url,
25
- :title => title,
26
- :selection => selection
27
- }
28
- res = request(:add, params)
29
- if res.code == "201" then
30
- message = "Success: #{url}"
31
- message += " Title: #{title}" unless title.nil?
32
- Automatic::Log.puts(:info, message)
33
- else
34
- Automatic::Log.puts(:error, "#{res.code} Error: #{url}")
35
- raise
36
- end
32
+ response = request(:add, url: url, title: title, selection: selection)
33
+ raise Error, "Instapaper answered #{response.code} for #{url}" unless response.code == '201'
34
+
35
+ message = "Success: #{url}"
36
+ message += " Title: #{title}" unless title.nil?
37
+ Automatic::Log.puts(:info, message)
38
+ response
37
39
  end
38
40
 
39
41
  private
40
42
 
43
+ # TLS with the certificate verified, which is Net::HTTP's own default and
44
+ # is stated here because this plugin used to turn it off.
41
45
  def request(method, params = {})
42
- request = Net::HTTP::Post.new('/api/' + method.to_s)
43
- request.basic_auth(@username, @password)
44
- request.set_form_data(params)
45
- http = Net::HTTP.new('www.instapaper.com', 443)
46
- http.use_ssl = true
47
- http.verify_mode = OpenSSL::SSL::VERIFY_NONE
48
- http.start { http.request(request) }
46
+ post = Net::HTTP::Post.new("/api/#{method}")
47
+ post.basic_auth(@username, @password)
48
+ post.set_form_data(params)
49
+
50
+ Net::HTTP.start(HOST, 443,
51
+ use_ssl: true,
52
+ verify_mode: OpenSSL::SSL::VERIFY_PEER,
53
+ open_timeout: OPEN_TIMEOUT,
54
+ read_timeout: READ_TIMEOUT) { |http| http.request(post) }
49
55
  end
50
56
  end
51
57
 
52
58
  class PublishInstapaper
53
-
54
59
  attr_accessor :instapaper
55
60
 
56
- def initialize(config, pipeline=[])
57
- @config = config
58
- @pipeline = pipeline
59
-
60
- @instapaper = Instapaper.new(
61
- @config['email'],
62
- @config['password']
63
- )
61
+ def initialize(config, pipeline = [])
62
+ @config = config || {}
63
+ @pipeline = pipeline
64
+ @instapaper = Instapaper.new(@config['email'], @config['password'])
64
65
  end
65
66
 
67
+ # Adds each item to Instapaper.
66
68
  def run
67
- @pipeline.each {|feeds|
68
- unless feeds.nil?
69
- feeds.items.each {|feed|
70
- Automatic::Log.puts("info", "add: #{feed.link}")
71
- retries = 0
72
- retry_max = @config['retry'].to_i || 0
73
- begin
74
- instapaper.add(feed.link, feed.title, feed.description)
75
- rescue
76
- retries += 1
77
- Automatic::Log.puts("error", "ErrorCount: #{retries}, Fault in publish to instapaper.")
78
- sleep ||= @config['interval'].to_i
79
- retry if retries <= retry_max
80
- end
81
- sleep ||= @config['interval'].to_i
82
- }
83
- end
84
- }
69
+ @pipeline.each do |feeds|
70
+ next if feeds.nil?
71
+
72
+ feeds.items.each { |feed| add(feed) }
73
+ end
85
74
  @pipeline
86
75
  end
76
+
77
+ private
78
+
79
+ def add(feed)
80
+ Automatic::Log.puts('info', "add: #{feed.link}")
81
+ retries = 0
82
+ retry_max = @config['retry'].to_i
83
+ begin
84
+ instapaper.add(feed.link, feed.title, feed.description)
85
+ rescue StandardError => e
86
+ retries += 1
87
+ # The message names the item, never the account or the password.
88
+ Automatic::Log.puts('error',
89
+ "ErrorCount: #{retries}, Fault in publish to instapaper: #{e.message}")
90
+ if retries <= retry_max
91
+ sleep(@config['interval'].to_i)
92
+ retry
93
+ end
94
+ end
95
+ sleep(@config['interval'].to_i)
96
+ end
87
97
  end
88
98
  end