feedbook 0.9.1 → 1.0.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 CHANGED
@@ -1,15 +1,15 @@
1
1
  ---
2
2
  !binary "U0hBMQ==":
3
3
  metadata.gz: !binary |-
4
- YmU1YzkwNDUwN2I0YTI4YWI0NDc3Y2I5ZGRiYmY2OTM0NmEzYjJkZg==
4
+ NWM2NzUzZWNhOTIwZTE2MDhkZTQ5NTU3NGNhNTYwZmQ4ZDNiOThjOA==
5
5
  data.tar.gz: !binary |-
6
- MzlkYTkyMjllOGE4ZWIwNDM2ZTdmMDkzOTUzM2U5NzFkYjUzYjRlMQ==
6
+ ZmIzOTgxMmU5MWNmNzJiMjFjN2RlNzFlOGQ4MjY4NTg3Y2EzYzQ1Mw==
7
7
  SHA512:
8
8
  metadata.gz: !binary |-
9
- Y2E4MTAzZjBhYjdkOGZmZDc4MTY0MjcyMmY3YTZmNDI3MGUwNjc5MTE4MjU5
10
- NTAxNzc1N2QzOTZlZDZkYjMwZTBhYTQzY2YxNmYyMGU3ZjMyZGU3ZjYzZjMx
11
- OWMyMDJjOGNlZWZhMjYyYjA0NjlkMmJmZDUwNTU0ZmY3Yzc4YjQ=
9
+ MmM1NDJlYTY4MDUxYjM5YmFiY2RjNzNlZjhmNjRiN2Q5MTNiZTAxODEyOTUy
10
+ NDY5MjI4ZTVkMmZkZWY5Y2RlNjJjM2Y3Yjg3MmFhOTUyYzk5YzFiYThmMjNi
11
+ NWVlNGRmNzZlYTljMTM4YzhjNjIwNmNmMTcxMGI4Nzk0YjliOTU=
12
12
  data.tar.gz: !binary |-
13
- OTQ2OWZkZTI5Njc3N2VkZjE4NDdjNTY0Zjk4NjUzOGFjM2IzYTA3NGI3YzNj
14
- MTIzMzc5ZDg0NmU1NWZiMjhlYzFiNTFjOWUxOGNhYTliYTI2Y2NmMzYyZDlk
15
- ZTMxMDM2OWM4MTJlMDVmYjBiYTA1ZmJkNWJjMTdlODg5NmI3MjE=
13
+ MWI2YjRlODRkNTJmNzFmMDE4ZTBiYjdlYTdkZjBlZjE0MGVhMDcwODlmZmE1
14
+ MjUxYTFmMWJjMzYyMWVjZjc5ZTYyZmRkOGU5ZGZmNjNiOTQ0OGU4N2VjZjhi
15
+ NGU4ODNkMzU1ZTg3MjBlODc4YjdhYzRlMmVkNWFkZjNkMWZmMzM=
data/README.md CHANGED
@@ -112,13 +112,11 @@ Feedbook uses Liquid for generating output. If you know Mustache it will not be
112
112
  Because Feedbook is still in development there is many to do:
113
113
 
114
114
  * add Google+ Notifier
115
- Unfortunately Google+ does not provides API with ability to push messages to streams. Because of that I need to find a tool that will provide that feature without use of Google+.
116
- * add links shortener (bit.ly or goo.gl) as a Liquid tag/filter
117
- Twitter limits Tweet length to 140 characters, why not to shorten links and rescue too long message.
118
- * add ability to add own Notifier without forking Feedbook project
119
- I would like to give ability to users to create own Notifier plugins without any special development to Feedbook.
115
+ * Unfortunately Google+ does not provides API with ability to push messages to streams. Because of that I need to find a tool that will provide that feature without use of Google+.
120
116
  * add more tests
121
- I would like to add integration tests to make sure that all features work.
117
+ * I would like to add integration tests to make sure that all features work.
118
+ * add offline mode
119
+ * I would like to add ability to run Feedbook with cron (start Feedbook, look for changes, save list of already notified posts and turn off)
122
120
 
123
121
  ## Contributing
124
122
 
@@ -10,28 +10,42 @@ include GLI::App
10
10
 
11
11
  program_desc 'Feedbook is a simple gem and application that notifies about new posts and Atom/RSS updates on your social media sites (like Facebook and Twitter), e-mail or IRC.'
12
12
 
13
- flag [:c,:config], default_value: 'feedbook.yml', desc: 'Feedbook configuration file location'
14
-
15
13
  desc 'Parses feedbook.yml file for feeds and configuration and listen for changes in RSS/Atom feeds to notify about updates on social media sites'
16
14
  command :start do |c|
17
15
 
18
- c.action do |global_options, _, _|
19
- Feedbook::Listener.start(global_options[:config])
16
+ c.flag [:c, :config], default_value: 'feedbook.yml', desc: 'Feedbook configuration file location'
17
+ c.flag [:p, :plugins], default_value: './plugins', desc: 'Plugins directory'
18
+ c.flag [:a, :archive], default_value: 'feedbook_offline.yml', desc: 'Feedbook archive file'
19
+
20
+ c.default_desc 'Parses feedbook.yml file for feeds and configuration and listen for changes in RSS/Atom feeds to notify about updates on social media sites'
21
+ c.action do |_, options, _|
22
+ Feedbook::Listener.start(options[:config], options[:plugins])
23
+ end
24
+
25
+ c.desc 'Parses feedbook.yml file for feeds and configuration and listen for changes in RSS/Atom feeds to notify about updates on social media sites'
26
+ c.command :offline do |cc|
27
+
28
+ cc.action do |_, options, _|
29
+ Feedbook::Listener.start_offline(options[:config], options[:archive], options[:plugins])
30
+ end
20
31
  end
21
32
  end
22
33
 
23
34
  desc 'Creates basic feedbook config file'
24
35
  command :init do |c|
25
- c.action do |global_options, _, _|
26
- abort 'There is already a feedbook configuration file' if File.exist?(global_options[:config])
27
- File.open(global_options[:config], 'w') { |f| f.write(Box::FILES['feedbook']) }
28
- puts "=> Created default feedbook configuration file: #{global_options[:config]}"
36
+
37
+ c.flag [:c,:config], default_value: 'feedbook.yml', desc: 'Feedbook configuration file location'
38
+
39
+ c.action do |_, options, _|
40
+ abort 'There is already a feedbook configuration file' if File.exist?(options[:config])
41
+ File.open(options[:config], 'w') { |f| f.write(Box::FILES['feedbook']) }
42
+ puts "=> Created default feedbook configuration file: #{options[:config]}"
29
43
  end
30
44
  end
31
45
 
32
46
  desc 'Outputs feedbook gem version'
33
47
  command :version do |c|
34
- c.action do |global_options, _, _|
48
+ c.action do |_, _, _|
35
49
  puts "feedbook version: #{Feedbook::VERSION}"
36
50
  end
37
51
  end
@@ -67,7 +81,7 @@ configuration:
67
81
  facebook:
68
82
  token: SECRET_FACEBOOK_TOKEN
69
83
  twitter:
70
- consumer_key: SECRET_CONSUMER_KEY
71
- consumer_secret: SECRET_TWITTER_CONSUMER_SECRET
84
+ api_key: SECRET_API_KEY
85
+ api_secret: SECRET_API_SECRET
72
86
  access_token: SECRET_TWITTER_ACCESS_TOKEN
73
87
  access_token_secret: SECRET_TWITTER_ACCESS_TOKEN_SECRET
@@ -26,11 +26,11 @@ Gem::Specification.new do |spec|
26
26
  spec.add_runtime_dependency 'box', '~> 0.1'
27
27
  spec.add_runtime_dependency 'twitter', '~> 5.1'
28
28
  spec.add_runtime_dependency 'mail', '~> 2.6'
29
+ spec.add_runtime_dependency 'googl', '~> 0.6'
29
30
  spec.add_runtime_dependency 'irc-notify'
30
31
 
31
32
  spec.add_development_dependency 'bundler', '~> 1.6'
32
33
  spec.add_development_dependency 'rake', '~> 10.3'
33
34
  spec.add_development_dependency 'rspec'
34
35
  spec.add_development_dependency 'coveralls'
35
- spec.add_development_dependency 'pry-debugger'
36
36
  end
@@ -1,9 +1,9 @@
1
- require 'feedbook/notifiers'
1
+ require 'feedbook/factories/notifiers_factory'
2
2
  require 'feedbook/helpers/time_interval_parser'
3
3
 
4
4
  module Feedbook
5
5
  class Configuration
6
- attr_reader :interval
6
+ attr_reader :interval, :options
7
7
 
8
8
  INTERVAL_FORMAT = /\A(\d+)(s|m|h|d)\z/
9
9
 
@@ -12,36 +12,18 @@ module Feedbook
12
12
  #
13
13
  # @return [NilClass] nil
14
14
  def initialize(opts = {})
15
- @interval = Helpers::TimeIntervalParser.parse(opts.fetch(:interval, ''))
16
- @twitter = opts.fetch(:twitter, nil)
17
- @facebook = opts.fetch(:facebook, nil)
18
- @irc = opts.fetch(:irc, nil)
19
- @mail = opts.fetch(:mail, nil)
15
+ @interval = Helpers::TimeIntervalParser.parse opts.delete('interval')
16
+ @options = opts
20
17
  end
21
18
 
22
19
  # Load notifiers configuration
23
20
  #
24
21
  # @return [NilClass] nil
25
22
  def load_notifiers
26
- unless twitter.nil?
27
- Notifiers::TwitterNotifier.instance.load_configuration(twitter)
28
- end
29
-
30
- unless facebook.nil?
31
- Notifiers::FacebookNotifier.instance.load_configuration(facebook)
32
- end
33
-
34
- unless irc.nil?
35
- Notifiers::IRCNotifier.instance.load_configuration(irc)
36
- end
37
-
38
- unless mail.nil?
39
- Notifiers::MailNotifier.instance.load_configuration(mail)
23
+ options.each do |name, config|
24
+ notifier = Factories::NotifiersFactory.create(name)
25
+ notifier.load_configuration(config) unless notifier.nil?
40
26
  end
41
27
  end
42
-
43
- private
44
- attr_reader :twitter, :facebook, :irc, :mail
45
-
46
28
  end
47
29
  end
@@ -7,4 +7,3 @@ require 'feedbook/errors/no_configuration_file_error'
7
7
  require 'feedbook/errors/template_syntax_error'
8
8
  require 'feedbook/errors/unsupported_notifier_error'
9
9
  require 'feedbook/errors/notifier_configuration_error'
10
- require 'feedbook/errors/notifier_notify_error'
@@ -19,8 +19,16 @@ module Feedbook
19
19
  Notifiers::FacebookNotifier.instance
20
20
  when :irc, 'irc'
21
21
  Notifiers::IRCNotifier.instance
22
+ when :mail, 'mail'
23
+ Notifiers::MailNotifier.instance
22
24
  else
23
- raise Errors::UnsupportedNotifierError.new(type)
25
+ if Notifiers.const_defined?("#{type.capitalize}Notifier")
26
+ Notifiers.const_get("#{type.capitalize}Notifier").instance
27
+ elsif Notifiers.const_defined?("#{type.upcase}Notifier")
28
+ Notifiers.const_get("#{type.upcase}Notifier").instance
29
+ else
30
+ puts "notifier #{type} is not supported by Feedbook."
31
+ end
24
32
  end
25
33
  end
26
34
  end
@@ -0,0 +1 @@
1
+ require 'feedbook/liquid_extensions/filters/googl_shortener_filter'
@@ -0,0 +1,22 @@
1
+ require 'googl'
2
+ require 'liquid'
3
+
4
+ module Feedbook
5
+ module LiquidExtensions
6
+ module Filters
7
+ module GooglShortenerFilter
8
+
9
+ # Shorten links with goo.gl
10
+ # @param input [String] url
11
+ #
12
+ # @return [String] url in shorten form
13
+ def googl(input)
14
+ url = Googl.shorten(input)
15
+ url.short_url
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+
22
+ Liquid::Template.register_filter(Feedbook::LiquidExtensions::Filters::GooglShortenerFilter)
@@ -1,6 +1,7 @@
1
1
  require 'yaml'
2
2
  require 'timeloop'
3
3
  require 'feedbook/feed'
4
+ require 'feedbook/plugins_manager'
4
5
  require 'feedbook/errors'
5
6
  require 'feedbook/configuration'
6
7
  require 'feedbook/comparers/posts_comparer'
@@ -10,23 +11,16 @@ module Feedbook
10
11
 
11
12
  # Starts listening on feeds and notifies if there is new post.
12
13
  # @param path [String] configuration file path
13
- def self.start(path)
14
+ # @param plugins_path [String] plugins directory path
15
+ def self.start(path, plugins_path)
14
16
  handle_exceptions do
15
- print "Loading configuration from file #{path}... "
16
- feeds, configuration = load_configuration(path)
17
- feeds.each { |feed| feed.valid? }
18
- puts 'completed.'
19
-
20
- puts 'Loading notifiers... '
21
- configuration.load_notifiers
22
- puts 'completed.'
17
+ feeds, configuration = initialize_listener(path, plugins_path)
23
18
 
24
19
  print 'Fetching feeds for the first use... '
25
20
  observed_feeds = feeds.map do |feed|
26
21
  {
27
- feed: feed,
28
- old_posts: feed.fetch,
29
- new_posts: []
22
+ feed: feed,
23
+ posts: feed.fetch
30
24
  }
31
25
  end
32
26
  puts 'completed.'
@@ -36,34 +30,104 @@ module Feedbook
36
30
 
37
31
  puts 'Fetching feeds...'
38
32
  observed_feeds.each do |feed|
39
- new_posts = feed[:feed].fetch
40
-
41
- difference = Comparers::PostsComparer.get_new_posts(feed[:old_posts], new_posts)
42
-
43
- if difference.empty?
44
- puts 'No new posts found.'
45
- else
46
- puts "#{difference.size} new posts found."
47
- print 'Started sending notifications... '
48
- end
49
-
50
- difference.each do |post|
51
- feed[:feed].notifications.each do |notification|
52
- notification.notify(post)
53
- end
54
- end
55
-
56
- unless difference.empty?
57
- puts 'completed.'
58
- end
59
-
60
- feed[:old_posts] = new_posts
61
- feed[:new_posts] = []
33
+ observe_and_notify(feed)
62
34
  end
63
35
  end
64
36
  end
65
37
  end
66
38
 
39
+ # Starts listening on feeds and notifies if there is new post (offline mode).
40
+ # @param path [String] configuration file path
41
+ # @param plugins_path [String] plugins directory path
42
+ def self.start_offline(path, archive_path, plugins_path)
43
+ handle_exceptions do
44
+ feeds, configuration = initialize_listener(path, plugins_path)
45
+
46
+ observed_feeds = load_feeds_archive(archive_path)
47
+
48
+ if observed_feeds.blank?
49
+ print 'Fetching feeds for the first use... '
50
+ observed_feeds = feeds.map do |feed|
51
+ {
52
+ feed: feed,
53
+ posts: feed.fetch
54
+ }
55
+ end
56
+ puts 'completed.'
57
+ end
58
+
59
+ puts 'Fetching feeds...'
60
+ observed_feeds.each do |feed|
61
+ observe_and_notify(feed)
62
+ end
63
+
64
+ save_feeds_archive(archive_path, observed_feeds)
65
+ end
66
+ end
67
+
68
+ # Load feeds from serialized YAML file.
69
+ # @param [String] path to YAML file with serialized objects
70
+ #
71
+ # @return [Array] Array of feeds with archived posts
72
+ def self.load_feeds_archive(path)
73
+ print 'Reading feeds from file... '
74
+
75
+ if File.exist? path
76
+ puts 'completed.'
77
+ YAML::load(File.read(path))
78
+ else
79
+ puts 'canceled. File does not exist.'
80
+ []
81
+ end
82
+ end
83
+
84
+ # Saves feeds into serialized YAML file
85
+ # @param path [String] path to file
86
+ # @param feeds [Array] Array with feeds to be saved
87
+ #
88
+ # @return [NilClass] nil
89
+ def self.save_feeds_archive(path, feeds)
90
+ print 'Saving feeds to file... '
91
+
92
+ File.open(path, 'w') do |f|
93
+ f.write YAML::dump(feeds)
94
+ end
95
+
96
+ puts 'completed.'
97
+ end
98
+
99
+
100
+ # Single run of loop for listening for changes in RSS feeds and notifies about updates
101
+ # @param feed [Feedbook::Feed] requested feed to be observed (hash with feed and posts)
102
+ #
103
+ # @return [NilClass] nil
104
+ def self.observe_and_notify(feed)
105
+ new_posts = feed[:feed].fetch
106
+
107
+ difference = Comparers::PostsComparer.get_new_posts(feed[:posts], new_posts)
108
+
109
+ if difference.empty?
110
+ puts 'No new posts found.'
111
+ else
112
+ puts "#{difference.size} new posts found."
113
+ print 'Started sending notifications... '
114
+ end
115
+
116
+ difference.each do |post|
117
+ feed[:feed].notifications.each do |notification|
118
+ notification.notify(post)
119
+ end
120
+ end
121
+
122
+ unless difference.empty?
123
+ puts 'completed.'
124
+ end
125
+
126
+ feed[:posts] = new_posts
127
+
128
+ feed
129
+ end
130
+
67
131
  private
68
132
 
69
133
  # Handle exceptions rescued from given block
@@ -72,23 +136,39 @@ module Feedbook
72
136
  rescue Errors::InvalidFeedUrlError
73
137
  abort 'feed url collection is not valid (contains empty or invalid urls)'
74
138
  rescue Errors::InvalidIntervalFormatError
75
- abort 'interval value in configuration is not valud (should be in format: "(Number)(TimeType)" where TimeType is s, m, h or d)'
139
+ abort 'interval value in configuration is not valid (should be in format: "(Number)(TimeType)" where TimeType is s, m, h or d)'
76
140
  rescue Errors::InvalidVariablesFormatError
77
141
  abort 'invalid variables format in configuration (should be a key-value pairs)'
78
142
  rescue Errors::NoConfigurationFileError => e
79
143
  abort "configuration file could not be loaded: #{e.message}"
80
144
  rescue Errors::NotifierConfigurationError => e
81
145
  abort "notifier #{e.notifier} has invalid configuration (#{e.message})."
82
- rescue Errors::NotifierNotifyError => e
83
- p "notifier #{e.notifier} did not notify because of client error (#{e.message})."
84
146
  rescue Errors::ParseFeedError => e
85
147
  p "feed on #{e.url} could not be parsed because of fetching/parsing error."
86
- rescue Errors::UnsupportedNotifierError => e
87
- abort "notifier #{e.notifier} is not supported by Feedbook."
88
148
  rescue Errors::TemplateSyntaxError
89
149
  abort "one of your templates in configuration file is not valid."
90
150
  end
91
151
 
152
+ # Initializes listener configuration, loads plugins and notifiers.
153
+ # @param path [String] config file path
154
+ # @param plugins_path [String] plugins directory path
155
+ #
156
+ # @return [[Array, Feedbook::Configuration]] feeds and Configuration instance
157
+ def self.initialize_listener(path, plugins_path)
158
+ print "Loading configuration from file #{path}... "
159
+ feeds, configuration = load_configuration(path)
160
+ feeds.each { |feed| feed.valid? }
161
+ puts 'completed.'
162
+
163
+ Feedbook::PluginsManager.load_plugins(plugins_path)
164
+
165
+ puts 'Loading notifiers... '
166
+ configuration.load_notifiers
167
+ puts 'completed.'
168
+
169
+ [feeds, configuration]
170
+ end
171
+
92
172
  # Load configuration from given path
93
173
  # @param path [String] configuration file path
94
174
  #
@@ -107,16 +187,11 @@ module Feedbook
107
187
 
108
188
  configuration_hash = config.fetch('configuration', {})
109
189
 
110
- configuration = Configuration.new(
111
- twitter: configuration_hash['twitter'],
112
- facebook: configuration_hash['facebook'],
113
- interval: configuration_hash['interval'],
114
- )
190
+ configuration = Configuration.new(configuration_hash)
115
191
 
116
192
  [feeds, configuration]
117
193
  rescue Errno::ENOENT => e
118
194
  raise Errors::NoConfigurationFileError.new(e)
119
195
  end
120
-
121
196
  end
122
- end
197
+ end
@@ -1,4 +1,5 @@
1
1
  require 'liquid'
2
+ require 'feedbook/liquid_extensions'
2
3
  require 'feedbook/factories/notifiers_factory'
3
4
  require 'feedbook/errors/template_syntax_error'
4
5
  require 'feedbook/errors/invalid_variables_format_error'
@@ -1,7 +1,6 @@
1
1
  require 'singleton'
2
2
  require 'koala'
3
3
  require 'feedbook/errors/notifier_configuration_error'
4
- require 'feedbook/errors/notifier_notify_error'
5
4
 
6
5
  module Feedbook
7
6
  module Notifiers
@@ -21,17 +20,17 @@ module Feedbook
21
20
  puts "New message has been notified on Facebook: #{message}"
22
21
  end
23
22
  rescue Koala::KoalaError => e
24
- raise Errors::NotifierNotifyError.new(:facebook, e.message)
23
+ p "FacebookNotifier did not notify because of client error (#{e.message})."
25
24
  end
26
25
 
27
26
  # Load configuration for FacebookNotifier
28
- # @param configuration = {} [Hash] Configuration hash (required: token)
29
- #
27
+ # @param configuration = {} [Hash] Configuration hash (required: token, optional: app_secret)
28
+ #
30
29
  # @return [NilClass] nil
31
30
  # @raise [Feedbook::Errors::NotifierConfigurationError] if notifier configuration fails
32
31
  def load_configuration(configuration = {})
33
- @client = Koala::Facebook::API.new(configuration.fetch('token'))
34
-
32
+ @client = Koala::Facebook::API.new(configuration.fetch('token'), configuration.fetch('app_secret', nil))
33
+
35
34
  puts 'Configuration loaded for FacebookNotifier'
36
35
  rescue Koala::KoalaError => e
37
36
  raise Errors::NotifierConfigurationError.new(:facebook, e.message)
@@ -17,7 +17,7 @@ module Feedbook
17
17
  # @param configuration = {} [Hash] Configuration hash
18
18
  #
19
19
  # @return [NilClass] nil
20
- def load_configuration(configuration = {})
20
+ def load_configuration(_)
21
21
  puts 'Configuration loaded for NullNotifier'
22
22
  end
23
23
  end
@@ -1,7 +1,6 @@
1
1
  require 'singleton'
2
2
  require 'twitter'
3
3
  require 'feedbook/errors/notifier_configuration_error'
4
- require 'feedbook/errors/notifier_notify_error'
5
4
 
6
5
  module Feedbook
7
6
  module Notifiers
@@ -10,9 +9,9 @@ module Feedbook
10
9
 
11
10
  # Sends notification to Twitter
12
11
  # @param message [String] message to be send to Twitter
13
- #
12
+ #
14
13
  # @return [NilClass] nil
15
- # @raise [Feedbook::Errors::NotifierNotifyError] if notify method fails
14
+ # @raise [Feedbook::Errors::NotifierNotifyError] if notify method fails
16
15
  def notify(message)
17
16
  if client.nil?
18
17
  puts "Message has not been notified on Twitter: #{message} because of invalid client configuration"
@@ -21,18 +20,18 @@ module Feedbook
21
20
  puts "New message has been notified on Twitter: #{message}"
22
21
  end
23
22
  rescue Twitter::Error => e
24
- raise Errors::NotifierNotifyError.new(:twitter, e.message)
23
+ p "TwitterNotifier did not notify because of client error (#{e.message})."
25
24
  end
26
25
 
27
26
  # Load configuration for TwitterNotifier
28
27
  # @param configuration = {} [Hash] Configuration hash (required: consumer_key, consumer_secret, access_token, access_token_secret)
29
- #
28
+ #
30
29
  # @return [NilClass] nil
31
- # @raise [Feedbook::Errors::NotifierConfigurationError] if notifier configuration fails
30
+ # @raise [Feedbook::Errors::NotifierConfigurationError] if notifier configuration fails
32
31
  def load_configuration(configuration = {})
33
32
  @client = Twitter::REST::Client.new do |config|
34
- config.consumer_key = configuration.fetch('consumer_key')
35
- config.consumer_secret = configuration.fetch('consumer_secret')
33
+ config.consumer_key = configuration.fetch('api_key')
34
+ config.consumer_secret = configuration.fetch('api_secret')
36
35
  config.access_token = configuration.fetch('access_token')
37
36
  config.access_token_secret = configuration.fetch('access_token_secret')
38
37
  end
@@ -46,4 +45,4 @@ module Feedbook
46
45
  attr_reader :client
47
46
  end
48
47
  end
49
- end
48
+ end
@@ -0,0 +1,20 @@
1
+ module Feedbook
2
+ class PluginsManager
3
+
4
+ # Loads plugins and extensions from given path.
5
+ # @param path [String] plugins directory path
6
+ #
7
+ # @return [NilClass] nil
8
+ def self.load_plugins(path)
9
+ print "Loading plugins from #{path}... "
10
+ if File.directory? path
11
+ Dir[File.join(path, '**', '*.rb')].sort.each do |f|
12
+ require f
13
+ end
14
+ puts 'completed.'
15
+ else
16
+ puts "Plugins directory could not be found in #{path}."
17
+ end
18
+ end
19
+ end
20
+ end
@@ -1,3 +1,3 @@
1
1
  module Feedbook
2
- VERSION = '0.9.1'
2
+ VERSION = '1.0.0'
3
3
  end
@@ -4,16 +4,16 @@ describe Feedbook::Configuration do
4
4
 
5
5
  let(:hash) do
6
6
  {
7
- twitter: {},
8
- facebook: {},
9
- irc: {},
10
- interval: '5m'
7
+ 'twitter' => {},
8
+ 'facebook' => {},
9
+ 'irc' => {},
10
+ 'interval' => '5m'
11
11
  }
12
12
  end
13
13
 
14
14
  before :each do
15
15
  allow(Feedbook::Helpers::TimeIntervalParser).to receive(:parse).with('5m').and_return(300)
16
- allow(Feedbook::Helpers::TimeIntervalParser).to receive(:parse).with('').and_raise(Feedbook::Errors::InvalidIntervalFormatError.new)
16
+ allow(Feedbook::Helpers::TimeIntervalParser).to receive(:parse).with(nil).and_raise(Feedbook::Errors::InvalidIntervalFormatError.new)
17
17
  end
18
18
 
19
19
  subject { Feedbook::Configuration.new(hash) }
@@ -14,6 +14,11 @@ describe Feedbook::Factories::NotifiersFactory do
14
14
  it { expect(Feedbook::Factories::NotifiersFactory.create(:irc)).to be(Feedbook::Notifiers::IRCNotifier.instance) }
15
15
  end
16
16
 
17
+ context 'MailNotifier' do
18
+ it { expect(Feedbook::Factories::NotifiersFactory.create('mail')).to be(Feedbook::Notifiers::MailNotifier.instance) }
19
+ it { expect(Feedbook::Factories::NotifiersFactory.create(:mail)).to be(Feedbook::Notifiers::MailNotifier.instance) }
20
+ end
21
+
17
22
  context 'NullNotifier' do
18
23
  it { expect(Feedbook::Factories::NotifiersFactory.create('null')).to be(Feedbook::Notifiers::NullNotifier.instance) }
19
24
  it { expect(Feedbook::Factories::NotifiersFactory.create(:null)).to be(Feedbook::Notifiers::NullNotifier.instance) }
@@ -24,5 +29,41 @@ describe Feedbook::Factories::NotifiersFactory do
24
29
  it { expect(Feedbook::Factories::NotifiersFactory.create(:twitter)).to be(Feedbook::Notifiers::TwitterNotifier.instance) }
25
30
  end
26
31
 
32
+ context 'Notifier is not defined in Factory' do
33
+ context 'and Notifier exists as TestNotifier' do
34
+ before :each do
35
+ expect(Feedbook::Notifiers).to receive(:const_defined?).with('TestNotifier').and_return(true)
36
+ expect(Feedbook::Notifiers).to receive(:const_get).with('TestNotifier').and_return(Feedbook::Notifiers::NullNotifier)
37
+ end
38
+
39
+ it 'returns instance of given Notifier' do
40
+ expect(Feedbook::Factories::NotifiersFactory.create('test')).to be(Feedbook::Notifiers::NullNotifier.instance)
41
+ end
42
+ end
43
+
44
+ context 'and Notifier exists as TESTNotifier' do
45
+ before :each do
46
+ expect(Feedbook::Notifiers).to receive(:const_defined?).with('TestNotifier').and_return(false)
47
+ expect(Feedbook::Notifiers).to receive(:const_defined?).with('TESTNotifier').and_return(true)
48
+ expect(Feedbook::Notifiers).to receive(:const_get).with('TESTNotifier').and_return(Feedbook::Notifiers::NullNotifier)
49
+ end
50
+
51
+ it 'returns instance of given Notifier' do
52
+ expect(Feedbook::Factories::NotifiersFactory.create('test')).to be(Feedbook::Notifiers::NullNotifier.instance)
53
+ end
54
+ end
55
+
56
+ context 'and Notifier does not exist' do
57
+ before :each do
58
+ expect(Feedbook::Notifiers).to receive(:const_defined?).with('TestNotifier').and_return(false)
59
+ expect(Feedbook::Notifiers).to receive(:const_defined?).with('TESTNotifier').and_return(false)
60
+ end
61
+
62
+ it 'does nothing' do
63
+ expect(Feedbook::Factories::NotifiersFactory.create('test')).to be(nil)
64
+ end
65
+ end
66
+ end
67
+
27
68
  end
28
69
  end
@@ -0,0 +1,18 @@
1
+ require 'spec_helper'
2
+
3
+ describe Feedbook::LiquidExtensions::Filters::GooglShortenerFilter do
4
+
5
+ describe '#googl' do
6
+ before :each do
7
+ allow(Googl).to receive(:shorten).with('https://github.com/pinoss').and_return(OpenStruct.new(short_url: 'http://goo.gl/jExQ6W'))
8
+ end
9
+
10
+ it 'converts given link into shorten form by Googl API service' do
11
+ class TestClass
12
+ include Feedbook::LiquidExtensions::Filters::GooglShortenerFilter
13
+ end
14
+
15
+ expect(TestClass.new.googl('https://github.com/pinoss')).to eq('http://goo.gl/jExQ6W')
16
+ end
17
+ end
18
+ end
@@ -15,7 +15,10 @@ describe Feedbook::Listener do
15
15
  end
16
16
 
17
17
  it 'parses hash and creates Feed instance' do
18
+ expect(Feedbook::PluginsManager).to receive(:load_plugins).with('./plugins')
19
+
18
20
  allow(configuration).to receive(:load_notifiers)
21
+
19
22
  feeds.each do |feed|
20
23
  expect(feed).to receive(:fetch)
21
24
  expect(feed).to receive(:valid?)
@@ -23,7 +26,120 @@ describe Feedbook::Listener do
23
26
 
24
27
  expect(Object).to receive(:every).with(300).and_return(300)
25
28
 
26
- subject.start('feedbook.yml')
29
+ subject.start('feedbook.yml', './plugins')
30
+ end
31
+ end
32
+
33
+ describe '#start_offline' do
34
+
35
+ context 'there is no file with archived feeds' do
36
+ let(:configuration) { double }
37
+ let(:feeds) { [double, double, double] }
38
+ let(:feeds_with_posts) do
39
+ feeds.map do |feed|
40
+ {
41
+ feed: feed,
42
+ posts: nil
43
+ }
44
+ end
45
+ end
46
+
47
+ before :each do
48
+ allow(subject).to receive(:load_configuration).with('feedbook.yml').and_return([feeds, configuration])
49
+ allow(subject).to receive(:load_feeds_archive).with('feedbook_offline.yml').and_return([])
50
+ end
51
+
52
+ it 'parses hash and creates Feed instance' do
53
+ expect(Feedbook::PluginsManager).to receive(:load_plugins).with('./plugins')
54
+
55
+ allow(configuration).to receive(:load_notifiers)
56
+
57
+ feeds.each do |feed|
58
+ expect(feed).to receive(:fetch)
59
+ expect(feed).to receive(:valid?)
60
+ expect(subject).to receive(:observe_and_notify).with({
61
+ feed: feed,
62
+ posts: nil
63
+ })
64
+ end
65
+
66
+ expect(subject).to receive(:save_feeds_archive).with('feedbook_offline.yml', feeds_with_posts)
67
+
68
+ subject.start_offline('feedbook.yml', 'feedbook_offline.yml', './plugins')
69
+ end
70
+ end
71
+
72
+ context 'there is file with archived feeds' do
73
+ let(:configuration) { double }
74
+ let(:feeds) { [double, double, double] }
75
+
76
+ before :each do
77
+ allow(subject).to receive(:load_configuration).with('feedbook.yml').and_return([feeds, configuration])
78
+ allow(subject).to receive(:load_feeds_archive).with('feedbook_offline.yml').and_return(feeds)
79
+ end
80
+
81
+ it 'parses hash and creates Feed instance' do
82
+ expect(Feedbook::PluginsManager).to receive(:load_plugins).with('./plugins')
83
+
84
+ allow(configuration).to receive(:load_notifiers)
85
+
86
+ feeds.each do |feed|
87
+ expect(feed).to receive(:valid?)
88
+ expect(subject).to receive(:observe_and_notify).with(feed)
89
+ end
90
+
91
+ expect(subject).to receive(:save_feeds_archive).with('feedbook_offline.yml', feeds)
92
+
93
+ subject.start_offline('feedbook.yml', 'feedbook_offline.yml', './plugins')
94
+ end
95
+ end
96
+ end
97
+
98
+ describe '#observe_and_notify' do
99
+
100
+ context 'there are new posts' do
101
+ let(:feed) do
102
+ {
103
+ posts: [OpenStruct.new(url: '1'), OpenStruct.new(url: '2')],
104
+ feed: double
105
+ }
106
+ end
107
+
108
+ let(:notifications) { [double, double] }
109
+
110
+ before :each do
111
+ expect(feed[:feed]).to receive(:fetch).and_return([OpenStruct.new(url: '1'), OpenStruct.new(url: '2'), OpenStruct.new(url: '3'), OpenStruct.new(url: '4')])
112
+ expect(feed[:feed]).to receive(:notifications).twice.and_return(notifications)
113
+ end
114
+
115
+ it 'parses hash and creates Feed instance' do
116
+ expect(Feedbook::Comparers::PostsComparer).to receive(:get_new_posts).with([OpenStruct.new(url: '1'), OpenStruct.new(url: '2')], [OpenStruct.new(url: '1'), OpenStruct.new(url: '2'), OpenStruct.new(url: '3'), OpenStruct.new(url: '4')]).and_return([OpenStruct.new(url: '3'), OpenStruct.new(url: '4')])
117
+ notifications.each do |notification|
118
+ expect(notification).to receive(:notify).with(OpenStruct.new(url: '3')).and_return(nil)
119
+ expect(notification).to receive(:notify).with(OpenStruct.new(url: '4')).and_return(nil)
120
+ end
121
+
122
+ expect(subject.observe_and_notify(feed)[:posts]).to eq([OpenStruct.new(url: '1'), OpenStruct.new(url: '2'), OpenStruct.new(url: '3'), OpenStruct.new(url: '4')])
123
+ end
124
+ end
125
+
126
+ context 'there are no new posts' do
127
+ let(:feed) do
128
+ {
129
+ posts: [OpenStruct.new(url: '1'), OpenStruct.new(url: '2')],
130
+ feed: double
131
+ }
132
+ end
133
+
134
+ before :each do
135
+ expect(feed[:feed]).to receive(:fetch).and_return([OpenStruct.new(url: '1'), OpenStruct.new(url: '2')])
136
+ end
137
+
138
+ it 'parses hash and creates Feed instance' do
139
+ expect(Feedbook::Comparers::PostsComparer).to receive(:get_new_posts).with([OpenStruct.new(url: '1'), OpenStruct.new(url: '2')], [OpenStruct.new(url: '1'), OpenStruct.new(url: '2')]).and_return([])
140
+
141
+ expect(subject.observe_and_notify(feed)[:posts]).to eq([OpenStruct.new(url: '1'), OpenStruct.new(url: '2')])
142
+ end
27
143
  end
28
144
  end
29
145
  end
@@ -8,13 +8,25 @@ describe Feedbook::Notifiers::FacebookNotifier do
8
8
  }
9
9
  end
10
10
 
11
+ let(:hash_with_app_token) do
12
+ {
13
+ 'token' => 'token123',
14
+ 'app_secret' => 'app_token_123'
15
+ }
16
+ end
17
+
11
18
  subject { Feedbook::Notifiers::FacebookNotifier.instance }
12
19
 
13
20
  describe '#load_configuration' do
14
21
  it 'parses configuration hash and creates client instance' do
15
- expect(Koala::Facebook::API).to receive(:new).with('token123')
22
+ expect(Koala::Facebook::API).to receive(:new).with('token123', nil)
16
23
  subject.load_configuration(hash)
17
24
  end
25
+
26
+ it 'parses configuration hash and creates client instance with app secret' do
27
+ expect(Koala::Facebook::API).to receive(:new).with('token123', 'app_token_123')
28
+ subject.load_configuration(hash_with_app_token)
29
+ end
18
30
  end
19
31
 
20
32
  describe '#notify' do
@@ -0,0 +1,31 @@
1
+ require 'spec_helper'
2
+
3
+ describe Feedbook::PluginsManager do
4
+
5
+ describe '#load_plugins' do
6
+
7
+ context 'when plugins directory exists' do
8
+ before :each do
9
+ expect(File).to receive(:directory?).with('./plugins').and_return(true)
10
+ expect(File).to receive(:join).with('./plugins', '**', '*.rb').and_return('./plugins/**/.fb')
11
+ expect(Dir).to receive(:[]).with('./plugins/**/.fb').and_return(['time'])
12
+ expect(Object).to receive(:require).with('time')
13
+ end
14
+
15
+ it 'requires all file inside plugins directory' do
16
+ Feedbook::PluginsManager.load_plugins('./plugins')
17
+ end
18
+ end
19
+
20
+ context 'when plugins directory do not exists' do
21
+ before :each do
22
+ expect(File).to receive(:directory?).with('./plugins').and_return(false)
23
+ end
24
+
25
+ it 'does nothing' do
26
+ Feedbook::PluginsManager.load_plugins('./plugins')
27
+ end
28
+
29
+ end
30
+ end
31
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: feedbook
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.1
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Maciej Paruszewski
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-07-26 00:00:00.000000000 Z
11
+ date: 2014-07-31 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: feedjira
@@ -122,6 +122,20 @@ dependencies:
122
122
  - - ~>
123
123
  - !ruby/object:Gem::Version
124
124
  version: '2.6'
125
+ - !ruby/object:Gem::Dependency
126
+ name: googl
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ~>
130
+ - !ruby/object:Gem::Version
131
+ version: '0.6'
132
+ type: :runtime
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ~>
137
+ - !ruby/object:Gem::Version
138
+ version: '0.6'
125
139
  - !ruby/object:Gem::Dependency
126
140
  name: irc-notify
127
141
  requirement: !ruby/object:Gem::Requirement
@@ -192,20 +206,6 @@ dependencies:
192
206
  - - ! '>='
193
207
  - !ruby/object:Gem::Version
194
208
  version: '0'
195
- - !ruby/object:Gem::Dependency
196
- name: pry-debugger
197
- requirement: !ruby/object:Gem::Requirement
198
- requirements:
199
- - - ! '>='
200
- - !ruby/object:Gem::Version
201
- version: '0'
202
- type: :development
203
- prerelease: false
204
- version_requirements: !ruby/object:Gem::Requirement
205
- requirements:
206
- - - ! '>='
207
- - !ruby/object:Gem::Version
208
- version: '0'
209
209
  description: Feedbook is a simple console application that allows to notify on IRC,
210
210
  mail and social media (like Twitter and Facebook) about RSS/Atom updates.
211
211
  email:
@@ -233,13 +233,14 @@ files:
233
233
  - lib/feedbook/errors/invalid_variables_format_error.rb
234
234
  - lib/feedbook/errors/no_configuration_file_error.rb
235
235
  - lib/feedbook/errors/notifier_configuration_error.rb
236
- - lib/feedbook/errors/notifier_notify_error.rb
237
236
  - lib/feedbook/errors/parse_feed_error.rb
238
237
  - lib/feedbook/errors/template_syntax_error.rb
239
238
  - lib/feedbook/errors/unsupported_notifier_error.rb
240
239
  - lib/feedbook/factories/notifiers_factory.rb
241
240
  - lib/feedbook/feed.rb
242
241
  - lib/feedbook/helpers/time_interval_parser.rb
242
+ - lib/feedbook/liquid_extensions.rb
243
+ - lib/feedbook/liquid_extensions/filters/googl_shortener_filter.rb
243
244
  - lib/feedbook/listener.rb
244
245
  - lib/feedbook/notification.rb
245
246
  - lib/feedbook/notifiers.rb
@@ -248,6 +249,7 @@ files:
248
249
  - lib/feedbook/notifiers/mail_notifier.rb
249
250
  - lib/feedbook/notifiers/null_notifier.rb
250
251
  - lib/feedbook/notifiers/twitter_notifier.rb
252
+ - lib/feedbook/plugins_manager.rb
251
253
  - lib/feedbook/post.rb
252
254
  - lib/feedbook/version.rb
253
255
  - spec/spec_helper.rb
@@ -256,6 +258,7 @@ files:
256
258
  - spec/unit/lib/factories/notifiers_factory_spec.rb
257
259
  - spec/unit/lib/feed_spec.rb
258
260
  - spec/unit/lib/helpers/time_interval_parser_spec.rb
261
+ - spec/unit/lib/liquid_extensions/filters/googl_shortener_filter_spec.rb
259
262
  - spec/unit/lib/listener_spec.rb
260
263
  - spec/unit/lib/notification_spec.rb
261
264
  - spec/unit/lib/notifiers/facebook_notifier_spec.rb
@@ -263,6 +266,7 @@ files:
263
266
  - spec/unit/lib/notifiers/mail_notifier_spec.rb
264
267
  - spec/unit/lib/notifiers/null_notifier_spec.rb
265
268
  - spec/unit/lib/notifiers/twitter_notifier_spec.rb
269
+ - spec/unit/lib/plugins_manager_spec.rb
266
270
  - spec/unit/lib/post_spec.rb
267
271
  homepage: https://github.com/pinoss/feedbook
268
272
  licenses:
@@ -296,6 +300,7 @@ test_files:
296
300
  - spec/unit/lib/factories/notifiers_factory_spec.rb
297
301
  - spec/unit/lib/feed_spec.rb
298
302
  - spec/unit/lib/helpers/time_interval_parser_spec.rb
303
+ - spec/unit/lib/liquid_extensions/filters/googl_shortener_filter_spec.rb
299
304
  - spec/unit/lib/listener_spec.rb
300
305
  - spec/unit/lib/notification_spec.rb
301
306
  - spec/unit/lib/notifiers/facebook_notifier_spec.rb
@@ -303,4 +308,5 @@ test_files:
303
308
  - spec/unit/lib/notifiers/mail_notifier_spec.rb
304
309
  - spec/unit/lib/notifiers/null_notifier_spec.rb
305
310
  - spec/unit/lib/notifiers/twitter_notifier_spec.rb
311
+ - spec/unit/lib/plugins_manager_spec.rb
306
312
  - spec/unit/lib/post_spec.rb
@@ -1,12 +0,0 @@
1
- module Feedbook
2
- module Errors
3
- class NotifierNotifyError < StandardError
4
- attr_reader :notifier
5
-
6
- def initialize(notifier, message = {})
7
- @notifier = notifier
8
- super(message)
9
- end
10
- end
11
- end
12
- end