feed2email 0.8.0 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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', 'subscribe to feed at URL'
5
+ desc 'add URL', 'Subscribe to feed at URL'
9
6
  def add(uri)
10
- uri = handle_permanent_redirection(uri)
11
- uri = perform_feed_autodiscovery(uri)
7
+ require 'feed2email/feed'
8
+ require 'feed2email/feed_autodiscoverer'
12
9
 
13
- begin
14
- feed_list << uri
15
- rescue FeedList::DuplicateFeedError => e
16
- abort e.message
10
+ uri = autodiscover_feeds(uri)
11
+
12
+ if feed = Feed[uri: uri]
13
+ abort "Feed already exists: #{feed}"
17
14
  end
18
15
 
19
- if feed_list.sync
20
- puts "Added feed #{uri} at index #{feed_list.size - 1}"
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 'fetch FEED', 'clear fetch cache for feed at index FEED'
27
- def fetch(index)
28
- index = check_feed_index(index, in: (0...feed_list.size))
29
- feed_list.clear_fetch_cache(index)
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
- if feed_list.sync
32
- puts "Cleared fetch cache for feed at index #{index}"
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 "Failed to clear fetch cache for feed at index #{index}"
37
+ abort 'EDITOR not set'
35
38
  end
36
39
  end
37
40
 
38
- desc 'history FEED', 'edit history file of feed at index FEED with $EDITOR'
39
- def history(index)
40
- abort '$EDITOR not set' unless ENV['EDITOR']
41
+ desc 'export PATH', 'Export feed subscriptions as OPML to PATH'
42
+ def export(path)
43
+ require 'feed2email/feed'
41
44
 
42
- index = check_feed_index(index, in: (0...feed_list.size))
43
- require 'feed2email/feed_history'
44
- history_path = FeedHistory.new(feed_list[index][:uri]).path
45
- exec(ENV['EDITOR'], history_path)
46
- end
45
+ if Feed.empty?
46
+ abort 'No feeds to export'
47
+ end
47
48
 
48
- desc 'remove FEED', 'unsubscribe from feed at index FEED'
49
- def remove(index)
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
- if deleted && feed_list.sync
54
- puts "Removed feed at index #{index}"
52
+ puts 'This may take a bit. Please wait...'
55
53
 
56
- if feed_list.size != index # feed was not the last
57
- puts 'Warning: Feed list indices have changed!'
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 "Failed to remove feed at index #{index}"
60
+ abort 'File already exists'
61
61
  end
62
62
  end
63
63
 
64
- desc 'toggle FEED', 'enable/disable feed at index FEED'
65
- def toggle(index)
66
- index = check_feed_index(index, in: (0...feed_list.size))
67
- toggled = feed_list.toggle(index)
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
- if toggled && feed_list.sync
71
- puts "#{enabled ? 'En' : 'Dis'}abled feed at index #{index}"
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 "Failed to #{enabled ? 'en' : 'dis'}able feed at index #{index}"
79
+ abort 'File does not exist'
74
80
  end
75
81
  end
76
82
 
77
- desc 'list', 'list feed subscriptions'
83
+ desc 'list', 'List feed subscriptions'
78
84
  def list
79
- puts feed_list
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', 'process feed subscriptions'
94
+ desc 'process', 'Process feed subscriptions'
83
95
  def process
84
- feed_list.process
96
+ require 'feed2email/feed'
97
+ Feed.enabled.by_smallest_id.each(&:process)
85
98
  end
86
99
 
87
- desc 'version', 'show feed2email version'
88
- def version
89
- require 'feed2email/version'
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
- no_commands do
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
- index.to_i
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
- def feed_list
105
- Feed2Email.feed_list # delegate
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
- def handle_permanent_redirection(uri)
109
- checker = RedirectionChecker.new(uri)
126
+ desc 'uncache ID', 'Clear fetch cache for feed with id ID'
127
+ def uncache(id)
128
+ require 'feed2email/feed'
110
129
 
111
- if checker.permanently_redirected?
112
- puts "Got permanently redirected to #{checker.location}"
113
- checker.location
114
- else
115
- uri
116
- end
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
- def perform_feed_autodiscovery(uri)
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
- feed_list.include?(feed[:uri])
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
- index = check_feed_index(response, in: (0...discovered_feeds.size))
154
- discovered_feeds[index][:uri]
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
@@ -1,52 +1,62 @@
1
+ require 'forwardable'
1
2
  require 'yaml'
2
3
 
3
4
  module Feed2Email
4
5
  class Config
5
- class MissingConfigError < StandardError; end
6
- class InvalidConfigPermissionsError < StandardError; end
7
- class InvalidConfigSyntaxError < StandardError; end
8
- class InvalidConfigDataTypeError < StandardError; end
9
- class MissingConfigOptionError < StandardError; end
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
- def initialize(path)
12
- @path = File.expand_path(path)
13
- check
14
- end
14
+ SEND_METHODS = %w{file sendmail smtp}
15
15
 
16
- def [](option)
17
- config[option] # delegate
18
- end
16
+ extend Forwardable
17
+
18
+ delegate [:[], :keys, :slice] => :config
19
19
 
20
- def smtp_configured?
21
- config['smtp_host'] && config['smtp_port'] && config['smtp_user'] &&
22
- config['smtp_pass']
20
+ def initialize(path)
21
+ @path = path
23
22
  end
24
23
 
25
24
  private
26
25
 
27
- def path
28
- @path
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 data
32
- @data
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 check
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 check_existence
45
- if !File.exist?(path)
46
- raise MissingConfigError, "Missing config file #{path}"
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 check_syntax
58
- begin
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 check_data_type
67
- if !data.is_a?(Hash)
68
- raise InvalidConfigDataTypeError,
69
- "Invalid data type (not a Hash) for config file #{path}"
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 check_recipient_existence
74
- check_option_existence('recipient')
78
+ def check_sender
79
+ check_option('sender')
75
80
  end
76
81
 
77
- def check_sender_existence
78
- check_option_existence('sender')
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 load_yaml
82
- @data = YAML.load(read_file)
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 read_file
86
- File.read(path)
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 config
90
- @config ||= defaults.merge(data)
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 check_option_existence(option)
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
@@ -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