feedbook 0.9.1 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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