feed2email 0.8.0 → 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +21 -10
- data/README.md +138 -86
- data/bin/f2e +6 -4
- data/bin/feed2email +6 -4
- data/bin/feed2email-migrate +23 -0
- data/lib/feed2email.rb +6 -9
- data/lib/feed2email/cli.rb +112 -75
- data/lib/feed2email/config.rb +72 -52
- data/lib/feed2email/core_ext.rb +10 -0
- data/lib/feed2email/database.rb +75 -0
- data/lib/feed2email/entry.rb +149 -15
- data/lib/feed2email/feed.rb +111 -130
- data/lib/feed2email/feed_autodiscoverer.rb +8 -10
- data/lib/feed2email/migrate/convert_feeds_migration.rb +29 -0
- data/lib/feed2email/migrate/feeds_import_migration.rb +42 -0
- data/lib/feed2email/migrate/history_import_migration.rb +42 -0
- data/lib/feed2email/migrate/migration.rb +42 -0
- data/lib/feed2email/migrate/split_history_migration.rb +32 -0
- data/lib/feed2email/open-uri.rb +7 -0
- data/lib/feed2email/opml_exporter.rb +109 -0
- data/lib/feed2email/opml_importer.rb +52 -0
- data/lib/feed2email/redirection_checker.rb +2 -2
- data/lib/feed2email/smtp_connection.rb +59 -0
- data/lib/feed2email/version.rb +1 -1
- metadata +55 -30
- data/bin/feed2email-migrate-feedlist +0 -36
- data/bin/feed2email-migrate-history +0 -29
- data/lib/feed2email/feed_history.rb +0 -82
- data/lib/feed2email/feed_list.rb +0 -147
- data/lib/feed2email/lazy_smtp_connection.rb +0 -35
- data/lib/feed2email/mail.rb +0 -84
data/lib/feed2email/cli.rb
CHANGED
@@ -1,126 +1,155 @@
|
|
1
1
|
require 'thor'
|
2
|
-
require 'feed2email'
|
3
|
-
require 'feed2email/feed_autodiscoverer'
|
4
|
-
require 'feed2email/redirection_checker'
|
5
2
|
|
6
3
|
module Feed2Email
|
7
4
|
class Cli < Thor
|
8
|
-
desc 'add URL', '
|
5
|
+
desc 'add URL', 'Subscribe to feed at URL'
|
9
6
|
def add(uri)
|
10
|
-
|
11
|
-
|
7
|
+
require 'feed2email/feed'
|
8
|
+
require 'feed2email/feed_autodiscoverer'
|
12
9
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
abort
|
10
|
+
uri = autodiscover_feeds(uri)
|
11
|
+
|
12
|
+
if feed = Feed[uri: uri]
|
13
|
+
abort "Feed already exists: #{feed}"
|
17
14
|
end
|
18
15
|
|
19
|
-
|
20
|
-
|
16
|
+
feed = Feed.new(uri: uri)
|
17
|
+
|
18
|
+
if feed.save(raise_on_failure: false)
|
19
|
+
puts "Added feed: #{feed}"
|
21
20
|
else
|
22
21
|
abort 'Failed to add feed'
|
23
22
|
end
|
24
23
|
end
|
25
24
|
|
26
|
-
desc '
|
27
|
-
def
|
28
|
-
|
29
|
-
|
25
|
+
desc 'backend', 'Open an SQLite console to the database'
|
26
|
+
def backend
|
27
|
+
require 'feed2email/database'
|
28
|
+
exec('sqlite3', Feed2Email.database_path)
|
29
|
+
end
|
30
30
|
|
31
|
-
|
32
|
-
|
31
|
+
desc 'config', 'Open configuration file with $EDITOR'
|
32
|
+
def config
|
33
|
+
if ENV['EDITOR']
|
34
|
+
require 'feed2email'
|
35
|
+
exec(ENV['EDITOR'], Feed2Email.config_path)
|
33
36
|
else
|
34
|
-
abort
|
37
|
+
abort 'EDITOR not set'
|
35
38
|
end
|
36
39
|
end
|
37
40
|
|
38
|
-
desc '
|
39
|
-
def
|
40
|
-
|
41
|
+
desc 'export PATH', 'Export feed subscriptions as OPML to PATH'
|
42
|
+
def export(path)
|
43
|
+
require 'feed2email/feed'
|
41
44
|
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
exec(ENV['EDITOR'], history_path)
|
46
|
-
end
|
45
|
+
if Feed.empty?
|
46
|
+
abort 'No feeds to export'
|
47
|
+
end
|
47
48
|
|
48
|
-
|
49
|
-
|
50
|
-
index = check_feed_index(index, in: (0...feed_list.size))
|
51
|
-
deleted = feed_list.delete_at(index)
|
49
|
+
unless File.exist?(path)
|
50
|
+
require 'feed2email/opml_exporter'
|
52
51
|
|
53
|
-
|
54
|
-
puts "Removed feed at index #{index}"
|
52
|
+
puts 'This may take a bit. Please wait...'
|
55
53
|
|
56
|
-
if
|
57
|
-
puts '
|
54
|
+
if n = OPMLExporter.export(path)
|
55
|
+
puts "Exported #{'feed subscription'.pluralize(n)} to #{path}"
|
56
|
+
else
|
57
|
+
abort 'Failed to export feed subscriptions'
|
58
58
|
end
|
59
59
|
else
|
60
|
-
abort
|
60
|
+
abort 'File already exists'
|
61
61
|
end
|
62
62
|
end
|
63
63
|
|
64
|
-
desc '
|
65
|
-
def
|
66
|
-
|
67
|
-
|
68
|
-
enabled = feed_list[index][:enabled]
|
64
|
+
desc 'import PATH', 'Import feed subscriptions as OPML from PATH'
|
65
|
+
def import(path)
|
66
|
+
if File.exist?(path)
|
67
|
+
require 'feed2email/opml_importer'
|
69
68
|
|
70
|
-
|
71
|
-
|
69
|
+
puts 'Importing...'
|
70
|
+
|
71
|
+
if n = OPMLImporter.import(path)
|
72
|
+
if n > 0
|
73
|
+
puts "Imported #{'feed subscription'.pluralize(n)} from #{path}"
|
74
|
+
end
|
75
|
+
else
|
76
|
+
abort 'Failed to import feed subscriptions'
|
77
|
+
end
|
72
78
|
else
|
73
|
-
abort
|
79
|
+
abort 'File does not exist'
|
74
80
|
end
|
75
81
|
end
|
76
82
|
|
77
|
-
desc 'list', '
|
83
|
+
desc 'list', 'List feed subscriptions'
|
78
84
|
def list
|
79
|
-
|
85
|
+
require 'feed2email/feed'
|
86
|
+
|
87
|
+
if Feed.any?
|
88
|
+
puts Feed.by_smallest_id.to_a
|
89
|
+
else
|
90
|
+
puts 'No feeds'
|
91
|
+
end
|
80
92
|
end
|
81
93
|
|
82
|
-
desc 'process', '
|
94
|
+
desc 'process', 'Process feed subscriptions'
|
83
95
|
def process
|
84
|
-
|
96
|
+
require 'feed2email/feed'
|
97
|
+
Feed.enabled.by_smallest_id.each(&:process)
|
85
98
|
end
|
86
99
|
|
87
|
-
desc '
|
88
|
-
def
|
89
|
-
require 'feed2email/
|
90
|
-
puts "feed2email #{Feed2Email::VERSION}"
|
91
|
-
end
|
100
|
+
desc 'remove ID', 'Unsubscribe from feed with id ID'
|
101
|
+
def remove(id)
|
102
|
+
require 'feed2email/feed'
|
92
103
|
|
93
|
-
|
94
|
-
def check_feed_index(index, options = {})
|
95
|
-
if index.to_i.to_s != index ||
|
96
|
-
(options[:in] && !options[:in].include?(index.to_i))
|
97
|
-
puts if index.nil? # Ctrl-D
|
98
|
-
abort 'Invalid index'
|
99
|
-
end
|
104
|
+
feed = Feed[id]
|
100
105
|
|
101
|
-
|
106
|
+
if feed && feed.delete
|
107
|
+
puts "Removed feed: #{feed}"
|
108
|
+
else
|
109
|
+
abort "Failed to remove feed. Is #{id} a valid id?"
|
102
110
|
end
|
111
|
+
end
|
112
|
+
|
113
|
+
desc 'toggle ID', 'Enable/disable feed with id ID'
|
114
|
+
def toggle(id)
|
115
|
+
require 'feed2email/feed'
|
116
|
+
|
117
|
+
feed = Feed[id]
|
103
118
|
|
104
|
-
|
105
|
-
|
119
|
+
if feed && feed.toggle
|
120
|
+
puts "Toggled feed: #{feed}"
|
121
|
+
else
|
122
|
+
abort "Failed to toggle feed. Is #{id} a valid id?"
|
106
123
|
end
|
124
|
+
end
|
107
125
|
|
108
|
-
|
109
|
-
|
126
|
+
desc 'uncache ID', 'Clear fetch cache for feed with id ID'
|
127
|
+
def uncache(id)
|
128
|
+
require 'feed2email/feed'
|
110
129
|
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
130
|
+
feed = Feed[id]
|
131
|
+
|
132
|
+
if feed && feed.uncache
|
133
|
+
puts "Uncached feed: #{feed}"
|
134
|
+
else
|
135
|
+
abort "Failed to uncache feed. Is #{id} a valid id?"
|
117
136
|
end
|
137
|
+
end
|
118
138
|
|
119
|
-
|
139
|
+
desc 'version', 'Show feed2email version'
|
140
|
+
def version
|
141
|
+
require 'feed2email/version'
|
142
|
+
puts "feed2email #{Feed2Email::VERSION}"
|
143
|
+
end
|
144
|
+
|
145
|
+
no_commands do
|
146
|
+
def autodiscover_feeds(uri)
|
120
147
|
discoverer = FeedAutodiscoverer.new(uri)
|
121
148
|
|
149
|
+
# Exclude already subscribed feeds from results
|
150
|
+
subscribed_feed_uris = Feed.select_map(:uri)
|
122
151
|
discovered_feeds = discoverer.feeds.reject {|feed|
|
123
|
-
|
152
|
+
subscribed_feed_uris.include?(feed[:uri])
|
124
153
|
}
|
125
154
|
|
126
155
|
if discovered_feeds.empty?
|
@@ -150,8 +179,16 @@ module Feed2Email
|
|
150
179
|
exit
|
151
180
|
end
|
152
181
|
|
153
|
-
|
154
|
-
|
182
|
+
unless response.numeric? &&
|
183
|
+
(0...discovered_feeds.size).include?(response.to_i)
|
184
|
+
abort 'Invalid index'
|
185
|
+
end
|
186
|
+
|
187
|
+
feed = discovered_feeds[response.to_i]
|
188
|
+
|
189
|
+
abort 'Invalid index' unless feed && feed[:uri]
|
190
|
+
|
191
|
+
feed[:uri]
|
155
192
|
end
|
156
193
|
end
|
157
194
|
end
|
data/lib/feed2email/config.rb
CHANGED
@@ -1,52 +1,62 @@
|
|
1
|
+
require 'forwardable'
|
1
2
|
require 'yaml'
|
2
3
|
|
3
4
|
module Feed2Email
|
4
5
|
class Config
|
5
|
-
class
|
6
|
-
class
|
7
|
-
class
|
8
|
-
class
|
9
|
-
class
|
6
|
+
class ConfigError < StandardError; end
|
7
|
+
class MissingConfigError < ConfigError; end
|
8
|
+
class InvalidConfigPermissionsError < ConfigError; end
|
9
|
+
class InvalidConfigSyntaxError < ConfigError; end
|
10
|
+
class InvalidConfigDataTypeError < ConfigError; end
|
11
|
+
class MissingConfigOptionError < ConfigError; end
|
12
|
+
class InvalidConfigOptionError < ConfigError; end
|
10
13
|
|
11
|
-
|
12
|
-
@path = File.expand_path(path)
|
13
|
-
check
|
14
|
-
end
|
14
|
+
SEND_METHODS = %w{file sendmail smtp}
|
15
15
|
|
16
|
-
|
17
|
-
|
18
|
-
|
16
|
+
extend Forwardable
|
17
|
+
|
18
|
+
delegate [:[], :keys, :slice] => :config
|
19
19
|
|
20
|
-
def
|
21
|
-
|
22
|
-
config['smtp_pass']
|
20
|
+
def initialize(path)
|
21
|
+
@path = path
|
23
22
|
end
|
24
23
|
|
25
24
|
private
|
26
25
|
|
27
|
-
def
|
28
|
-
|
26
|
+
def check_data_type
|
27
|
+
if !data.is_a?(Hash)
|
28
|
+
raise InvalidConfigDataTypeError,
|
29
|
+
"Invalid data type (not a Hash) for config file #{path}"
|
30
|
+
end
|
29
31
|
end
|
30
32
|
|
31
|
-
def
|
32
|
-
|
33
|
+
def check_existence
|
34
|
+
if !File.exist?(path)
|
35
|
+
raise MissingConfigError, "Missing config file #{path}"
|
36
|
+
end
|
33
37
|
end
|
34
38
|
|
35
|
-
def
|
39
|
+
def check_file
|
36
40
|
check_existence
|
37
41
|
check_permissions
|
38
42
|
check_syntax
|
39
43
|
check_data_type
|
40
|
-
check_recipient_existence
|
41
|
-
check_sender_existence
|
42
44
|
end
|
43
45
|
|
44
|
-
def
|
45
|
-
if
|
46
|
-
raise
|
46
|
+
def check_option(option)
|
47
|
+
if config[option].nil?
|
48
|
+
raise MissingConfigOptionError,
|
49
|
+
"Option #{option} missing from config file #{path}"
|
47
50
|
end
|
48
51
|
end
|
49
52
|
|
53
|
+
def check_options
|
54
|
+
check_recipient
|
55
|
+
check_sender
|
56
|
+
check_send_method
|
57
|
+
check_smtp_options if config['send_method'] == 'smtp'
|
58
|
+
end
|
59
|
+
|
50
60
|
def check_permissions
|
51
61
|
if '%o' % (File.stat(path).mode & 0777) != '600'
|
52
62
|
raise InvalidConfigPermissionsError,
|
@@ -54,40 +64,53 @@ module Feed2Email
|
|
54
64
|
end
|
55
65
|
end
|
56
66
|
|
57
|
-
def
|
58
|
-
|
59
|
-
load_yaml
|
60
|
-
rescue Psych::SyntaxError
|
61
|
-
raise InvalidConfigSyntaxError,
|
62
|
-
"Invalid YAML syntax for config file #{path}"
|
63
|
-
end
|
67
|
+
def check_recipient
|
68
|
+
check_option('recipient')
|
64
69
|
end
|
65
70
|
|
66
|
-
def
|
67
|
-
|
68
|
-
raise
|
69
|
-
"
|
71
|
+
def check_send_method
|
72
|
+
unless SEND_METHODS.include?(config['send_method'])
|
73
|
+
raise InvalidConfigOptionError,
|
74
|
+
"Option send_method not one of: #{SEND_METHODS.join(' ')}"
|
70
75
|
end
|
71
76
|
end
|
72
77
|
|
73
|
-
def
|
74
|
-
|
78
|
+
def check_sender
|
79
|
+
check_option('sender')
|
75
80
|
end
|
76
81
|
|
77
|
-
def
|
78
|
-
|
82
|
+
def check_smtp_options
|
83
|
+
check_option('smtp_host')
|
84
|
+
check_option('smtp_port')
|
85
|
+
check_option('smtp_user')
|
86
|
+
check_option('smtp_pass')
|
79
87
|
end
|
80
88
|
|
81
|
-
def
|
82
|
-
|
89
|
+
def check_syntax
|
90
|
+
begin
|
91
|
+
data
|
92
|
+
rescue Psych::SyntaxError
|
93
|
+
raise InvalidConfigSyntaxError,
|
94
|
+
"Invalid YAML syntax for config file #{path}"
|
95
|
+
end
|
83
96
|
end
|
84
97
|
|
85
|
-
def
|
86
|
-
|
98
|
+
def config
|
99
|
+
return @config if @config
|
100
|
+
|
101
|
+
begin
|
102
|
+
check_file
|
103
|
+
@config = defaults.merge(data)
|
104
|
+
check_options
|
105
|
+
rescue ConfigError => e
|
106
|
+
abort e.message
|
107
|
+
end
|
108
|
+
|
109
|
+
@config
|
87
110
|
end
|
88
111
|
|
89
|
-
def
|
90
|
-
@
|
112
|
+
def data
|
113
|
+
@data ||= YAML.load(File.read(path))
|
91
114
|
end
|
92
115
|
|
93
116
|
def defaults
|
@@ -96,19 +119,16 @@ module Feed2Email
|
|
96
119
|
'log_path' => true,
|
97
120
|
'log_shift_age' => 0,
|
98
121
|
'log_shift_size' => 1, # megabyte
|
122
|
+
'mail_path' => File.join(ENV['HOME'], 'Mail'),
|
99
123
|
'max_entries' => 20,
|
100
124
|
'send_delay' => 10,
|
125
|
+
'send_method' => 'file',
|
101
126
|
'sendmail_path' => '/usr/sbin/sendmail',
|
102
127
|
'smtp_auth' => 'login',
|
103
128
|
'smtp_starttls' => true,
|
104
129
|
}
|
105
130
|
end
|
106
131
|
|
107
|
-
def
|
108
|
-
if data[option].nil?
|
109
|
-
raise MissingConfigOptionError,
|
110
|
-
"Option #{option} missing from config file #{path}"
|
111
|
-
end
|
112
|
-
end
|
132
|
+
def path; @path end
|
113
133
|
end
|
114
134
|
end
|
data/lib/feed2email/core_ext.rb
CHANGED
@@ -2,6 +2,12 @@ require 'cgi'
|
|
2
2
|
require 'reverse_markdown'
|
3
3
|
require 'sanitize'
|
4
4
|
|
5
|
+
class Hash
|
6
|
+
def slice(*keys)
|
7
|
+
Hash[values_at(*keys).each_with_index.map {|v, i| [keys[i], v] }]
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
5
11
|
class Numeric
|
6
12
|
def megabytes
|
7
13
|
self * 1024 * 2014
|
@@ -13,6 +19,10 @@ class String
|
|
13
19
|
CGI.escapeHTML(self)
|
14
20
|
end
|
15
21
|
|
22
|
+
def numeric?
|
23
|
+
to_i.to_s == self
|
24
|
+
end
|
25
|
+
|
16
26
|
def pluralize(count, plural = self + 's')
|
17
27
|
"#{count} #{count == 1 ? self : plural}"
|
18
28
|
end
|