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.
- checksums.yaml +5 -5
- data/README.md +635 -83
- data/VERSION +1 -1
- data/automatic.gemspec +109 -248
- data/bin/automatic +20 -139
- data/config/feed2console.yml +10 -4
- data/config/feed2markdown.yml +41 -0
- data/doc/AI_TUTORIAL.md +518 -0
- data/doc/BASIC_DESIGN.md +516 -0
- data/doc/COPYING.LESSER +165 -0
- data/doc/DEPLOYMENT.md +824 -0
- data/doc/LICENSE.md +14 -0
- data/doc/PLUGINS.md +1875 -0
- data/doc/PLUGIN_DEVELOPMENT.md +86 -0
- data/doc/POLICY.md +857 -0
- data/doc/QUICKSTART.md +256 -0
- data/doc/RELEASING.md +381 -0
- data/doc/REQUIREMENTS.md +526 -0
- data/doc/VERSIONS +208 -0
- data/lib/automatic/cli.rb +248 -0
- data/lib/automatic/environment.rb +31 -5
- data/lib/automatic/feed_maker.rb +10 -9
- 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 +63 -32
- data/lib/automatic/recipe.rb +56 -17
- data/lib/automatic/version.rb +14 -1
- data/lib/automatic.rb +78 -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/claude.rb +217 -0
- data/plugins/filter/clear.rb +12 -8
- data/plugins/filter/description_link.rb +49 -51
- data/plugins/filter/full_feed.rb +158 -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 +58 -52
- data/plugins/filter/join.rb +107 -0
- data/plugins/filter/one.rb +19 -26
- data/plugins/filter/open_ai.rb +198 -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 +73 -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 +99 -68
- data/plugins/store/full_text.rb +32 -25
- 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 +32 -46
- data/plugins/subscription/tumblr.rb +55 -44
- data/plugins/subscription/xml.rb +40 -36
- metadata +108 -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
|
@@ -1,29 +1,32 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
|
-
# Name::
|
|
3
|
-
# Author
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
def initialize(config, pipeline=[])
|
|
14
|
-
@config = config
|
|
13
|
+
def initialize(config, pipeline = [])
|
|
14
|
+
@config = config || {}
|
|
15
15
|
@pipeline = pipeline
|
|
16
|
-
@output
|
|
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
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
data/plugins/publish/eject.rb
CHANGED
|
@@ -1,42 +1,64 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
|
-
# Name::
|
|
3
|
-
# Author
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
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
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
data/plugins/publish/fluentd.rb
CHANGED
|
@@ -1,43 +1,63 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
|
-
# Name::
|
|
3
|
-
# Author
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
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
|
-
|
|
13
|
+
Automatic.require_optional('fluent-logger', needed_by: 'PublishFluentd')
|
|
12
14
|
|
|
13
|
-
def initialize(config, pipeline=[])
|
|
14
|
-
@config
|
|
15
|
+
def initialize(config, pipeline = [])
|
|
16
|
+
@config = config || {}
|
|
15
17
|
@pipeline = pipeline
|
|
16
|
-
@mode
|
|
17
|
-
@fluentd
|
|
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
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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::
|
|
3
|
-
# Author
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
33
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
uri
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
|
94
|
+
def initialize(config, pipeline = [])
|
|
95
|
+
@config = config || {}
|
|
78
96
|
@pipeline = pipeline
|
|
79
97
|
|
|
80
98
|
@hb = HatenaBookmark.new
|
|
81
99
|
@hb.user = {
|
|
82
|
-
|
|
83
|
-
|
|
100
|
+
'hatena_id' => @config['username'],
|
|
101
|
+
'password' => @config['password']
|
|
84
102
|
}
|
|
85
103
|
end
|
|
86
104
|
|
|
87
105
|
def run
|
|
88
|
-
@pipeline.each
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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::
|
|
3
|
-
# Author
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
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
|
-
|
|
13
|
-
require
|
|
14
|
-
require
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
|
58
|
-
@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
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|