feed2email 0.8.0 → 0.9.0
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 +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
|