feedbook 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.
Files changed (51) hide show
  1. checksums.yaml +15 -0
  2. data/.gitignore +22 -0
  3. data/.rspec +3 -0
  4. data/.travis.yml +6 -0
  5. data/Gemfile +3 -0
  6. data/LICENSE.txt +22 -0
  7. data/README.md +129 -0
  8. data/Rakefile +7 -0
  9. data/bin/feedbook +73 -0
  10. data/feedbook.gemspec +35 -0
  11. data/lib/feedbook.rb +6 -0
  12. data/lib/feedbook/comparers/posts_comparer.rb +15 -0
  13. data/lib/feedbook/configuration.rb +47 -0
  14. data/lib/feedbook/errors.rb +10 -0
  15. data/lib/feedbook/errors/invalid_feed_url_error.rb +6 -0
  16. data/lib/feedbook/errors/invalid_interval_format_error.rb +6 -0
  17. data/lib/feedbook/errors/invalid_variables_format_error.rb +6 -0
  18. data/lib/feedbook/errors/no_configuration_file_error.rb +6 -0
  19. data/lib/feedbook/errors/notifier_configuration_error.rb +12 -0
  20. data/lib/feedbook/errors/notifier_notify_error.rb +12 -0
  21. data/lib/feedbook/errors/parse_feed_error.rb +11 -0
  22. data/lib/feedbook/errors/template_syntax_error.rb +6 -0
  23. data/lib/feedbook/errors/unsupported_notifier_error.rb +12 -0
  24. data/lib/feedbook/factories/notifiers_factory.rb +28 -0
  25. data/lib/feedbook/feed.rb +85 -0
  26. data/lib/feedbook/helpers/time_interval_parser.rb +38 -0
  27. data/lib/feedbook/listener.rb +122 -0
  28. data/lib/feedbook/notification.rb +62 -0
  29. data/lib/feedbook/notifiers.rb +5 -0
  30. data/lib/feedbook/notifiers/facebook_notifier.rb +44 -0
  31. data/lib/feedbook/notifiers/irc_notifier.rb +42 -0
  32. data/lib/feedbook/notifiers/mail_notifier.rb +52 -0
  33. data/lib/feedbook/notifiers/null_notifier.rb +25 -0
  34. data/lib/feedbook/notifiers/twitter_notifier.rb +49 -0
  35. data/lib/feedbook/post.rb +33 -0
  36. data/lib/feedbook/version.rb +3 -0
  37. data/spec/spec_helper.rb +30 -0
  38. data/spec/unit/lib/comparers/posts_comparer_spec.rb +13 -0
  39. data/spec/unit/lib/configuration_spec.rb +43 -0
  40. data/spec/unit/lib/factories/notifiers_factory_spec.rb +28 -0
  41. data/spec/unit/lib/feed_spec.rb +42 -0
  42. data/spec/unit/lib/helpers/time_interval_parser_spec.rb +26 -0
  43. data/spec/unit/lib/listener_spec.rb +29 -0
  44. data/spec/unit/lib/notification_spec.rb +44 -0
  45. data/spec/unit/lib/notifiers/facebook_notifier_spec.rb +34 -0
  46. data/spec/unit/lib/notifiers/irc_notifier_spec.rb +41 -0
  47. data/spec/unit/lib/notifiers/mail_notifier_spec.rb +39 -0
  48. data/spec/unit/lib/notifiers/null_notifier_spec.rb +19 -0
  49. data/spec/unit/lib/notifiers/twitter_notifier_spec.rb +36 -0
  50. data/spec/unit/lib/post_spec.rb +51 -0
  51. metadata +304 -0
@@ -0,0 +1,12 @@
1
+ module Feedbook
2
+ module Errors
3
+ class NotifierConfigurationError < 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
@@ -0,0 +1,12 @@
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
@@ -0,0 +1,11 @@
1
+ module Feedbook
2
+ module Errors
3
+ class ParseFeedError < StandardError
4
+ attr_reader :url
5
+ def initialize(url, message)
6
+ @url = url
7
+ super(message)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,6 @@
1
+ module Feedbook
2
+ module Errors
3
+ class TemplateSyntaxError < StandardError
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,12 @@
1
+ module Feedbook
2
+ module Errors
3
+ class UnsupportedNotifierError < 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
@@ -0,0 +1,28 @@
1
+ require 'feedbook/notifiers'
2
+ require 'feedbook/errors/unsupported_notifier_error'
3
+
4
+ module Feedbook
5
+ module Factories
6
+ class NotifiersFactory
7
+
8
+ # Returns instance of Notifier for given type.
9
+ # @param type [Symbol/String] name of requested notifier
10
+ #
11
+ # @return [Notifier] Notifier instance
12
+ def self.create(type)
13
+ case type
14
+ when :null, 'null'
15
+ Notifiers::NullNotifier.instance
16
+ when :twitter, 'twitter'
17
+ Notifiers::TwitterNotifier.instance
18
+ when :facebook, 'facebook'
19
+ Notifiers::FacebookNotifier.instance
20
+ when :irc, 'irc'
21
+ Notifiers::IRCNotifier.instance
22
+ else
23
+ raise Errors::UnsupportedNotifierError.new(type)
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,85 @@
1
+ require 'uri'
2
+ require 'feedjira'
3
+ require 'feedbook/post'
4
+ require 'feedbook/notification'
5
+ require 'feedbook/errors/parse_feed_error'
6
+ require 'feedbook/errors/invalid_feed_url_error'
7
+ require 'feedbook/errors/invalid_variables_format_error'
8
+
9
+ module Feedbook
10
+ class Feed
11
+
12
+ attr_reader :urls, :notifications, :variables
13
+
14
+ # Initializes new Feed instance for given configuration
15
+ # @param opts = {} [Hash] Hash with configuration options for feed
16
+ #
17
+ # @return [NilClass] nil
18
+ def initialize(opts = {})
19
+ @urls = opts.fetch(:urls, '').split
20
+ @variables = opts.fetch(:variables, {})
21
+ @notifications = opts.fetch(:notifications, []).map do |notification|
22
+ Notification.new(
23
+ type: notification['type'],
24
+ template: notification['template'],
25
+ variables: variables
26
+ )
27
+ end
28
+ end
29
+
30
+ # Fetches and parses all feed and merges into single array.
31
+ #
32
+ # @return [Array] array of Posts
33
+ def fetch
34
+ urls
35
+ .map do |url|
36
+ parse_feed(Feedjira::Feed.fetch_and_parse(url))
37
+ end
38
+ .inject :+
39
+ end
40
+
41
+ # Validates if given parameters are valid
42
+ #
43
+ # @return [NilClass] nil
44
+ # @raise [Feedbook::Errors::InvalidVariablesFormatError] if variables parameter is not a Hash
45
+ # @raise [Feedbook::Errors::InvalidFeedUrlError] if url collection is not a empty and contains valid urls
46
+ def valid?
47
+ if urls.empty? || urls.any? { |url| url !~ /\A#{URI::regexp}\z/ }
48
+ raise Errors::InvalidFeedUrlError.new
49
+ end
50
+
51
+ unless variables.is_a? Hash
52
+ raise Errors::InvalidVariablesFormatError.new
53
+ end
54
+
55
+ notifications.each { |notification| notification.valid? }
56
+ end
57
+
58
+ private
59
+
60
+ # Parses feetched feed into Feedbook::Post
61
+ # @param feed [Feedjira::Parser::Atom] Atom/RSS feed
62
+ #
63
+ # @return [Array] array of Posts created from feed entries
64
+ def parse_feed(feed)
65
+ feed.entries.map do |entry|
66
+ Post.new(
67
+ author: entry.author,
68
+ published: entry.published,
69
+ url: entry.url,
70
+ title: entry.title,
71
+ feed_title: feed.title
72
+ )
73
+ end
74
+ end
75
+
76
+ # Determines behavior of failure in fetching feeds
77
+ # @param url [String] requested url
78
+ #
79
+ # @raise [Feedbook::Error::ParseFeedError] if fetching and parsing feed was unsuccessful
80
+ def on_failure(url)
81
+ raise Error::ParseFeedError.new(url)
82
+ end
83
+
84
+ end
85
+ end
@@ -0,0 +1,38 @@
1
+ require 'timeloop'
2
+ require 'feedbook/errors/invalid_interval_format_error'
3
+
4
+ module Feedbook
5
+ module Helpers
6
+ class TimeIntervalParser
7
+
8
+ INTERVAL_FORMAT = /\A(\d+)(s|m|h|d)\z/
9
+
10
+ # Parses given string with interval and converts into a amount of seconds.
11
+ # @param value [String] String with interval (e.g. '10m', '100s', '20h', '10d')
12
+ #
13
+ # @return [Integer] amount of seconds that equals given interval value
14
+ # @raise [Feedbook::Errors::InvalidIntervalFormatError] if given string is not a valid format
15
+ def self.parse(value)
16
+ if value.strip =~ INTERVAL_FORMAT
17
+ number, type = INTERVAL_FORMAT.match(value).captures
18
+ case type
19
+ when 's'
20
+ Integer(number).seconds
21
+ when 'm'
22
+ Integer(number).minutes
23
+ when 'h'
24
+ Integer(number).hours
25
+ when 'd'
26
+ Integer(number).days
27
+ end
28
+ else
29
+ raise ArgmumentError.new
30
+ end
31
+
32
+ rescue
33
+ raise Errors::InvalidIntervalFormatError.new
34
+ end
35
+
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,122 @@
1
+ require 'yaml'
2
+ require 'timeloop'
3
+ require 'feedbook/feed'
4
+ require 'feedbook/errors'
5
+ require 'feedbook/configuration'
6
+ require 'feedbook/comparers/posts_comparer'
7
+
8
+ module Feedbook
9
+ class Listener
10
+
11
+ # Starts listening on feeds and notifies if there is new post.
12
+ # @param path [String] configuration file path
13
+ def self.start(path)
14
+ 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.'
23
+
24
+ print 'Fetching feeds for the first use... '
25
+ observed_feeds = feeds.map do |feed|
26
+ {
27
+ feed: feed,
28
+ old_posts: feed.fetch,
29
+ new_posts: []
30
+ }
31
+ end
32
+ puts 'completed.'
33
+
34
+ puts 'Listener started...'
35
+ every configuration.interval do
36
+
37
+ puts 'Fetching feeds...'
38
+ 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] = []
62
+ end
63
+ end
64
+ end
65
+ end
66
+
67
+ private
68
+
69
+ # Handle exceptions rescued from given block
70
+ def self.handle_exceptions
71
+ yield if block_given?
72
+ rescue Errors::InvalidFeedUrlError
73
+ abort 'feed url collection is not valid (contains empty or invalid urls)'
74
+ 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)'
76
+ rescue Errors::InvalidVariablesFormatError
77
+ abort 'invalid variables format in configuration (should be a key-value pairs)'
78
+ rescue Errors::NoConfigurationFileError => e
79
+ abort "configuration file could not be loaded: #{e.message}"
80
+ rescue Errors::NotifierConfigurationError => e
81
+ 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
+ rescue Errors::ParseFeedError => e
85
+ 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
+ rescue Errors::TemplateSyntaxError
89
+ abort "one of your templates in configuration file is not valid."
90
+ end
91
+
92
+ # Load configuration from given path
93
+ # @param path [String] configuration file path
94
+ #
95
+ # @return [[Array, Feedbook::Configuration]] feeds and Configuration instance
96
+ # @raise [Feedbook::Errors::NoConfigurationFileError] if path to config file is invalid of configuration file is missing
97
+ def self.load_configuration(path)
98
+ config = YAML.load_file(path)
99
+
100
+ feeds = config.fetch('feeds', []).map do |feed|
101
+ Feed.new(
102
+ urls: feed['url'],
103
+ variables: feed['variables'],
104
+ notifications: feed['notifications']
105
+ )
106
+ end
107
+
108
+ configuration_hash = config.fetch('configuration', {})
109
+
110
+ configuration = Configuration.new(
111
+ twitter: configuration_hash['twitter'],
112
+ facebook: configuration_hash['facebook'],
113
+ interval: configuration_hash['interval'],
114
+ )
115
+
116
+ [feeds, configuration]
117
+ rescue Errno::ENOENT => e
118
+ raise Errors::NoConfigurationFileError.new(e)
119
+ end
120
+
121
+ end
122
+ end
@@ -0,0 +1,62 @@
1
+ require 'liquid'
2
+ require 'feedbook/factories/notifiers_factory'
3
+ require 'feedbook/errors/template_syntax_error'
4
+ require 'feedbook/errors/invalid_variables_format_error'
5
+
6
+ module Feedbook
7
+ class Notification
8
+
9
+ attr_reader :type, :template, :variables
10
+
11
+ # Initializes Notification instance
12
+ # @param opts = {} [Hash] Hash with
13
+ #
14
+ # @return [type] [description]
15
+ def initialize(opts = {})
16
+ @type = opts.fetch(:type, '')
17
+ @variables = opts.fetch(:variables, {})
18
+ @template = parse_template(opts.fetch(:template, ''))
19
+ end
20
+
21
+ # Notifies selected gateway about new post
22
+ # @param object [Object] objct that respond to :to_hash method
23
+ #
24
+ # @return [NilClass] nil
25
+ def notify(object)
26
+ message = template.render(object.to_hash.merge(variables))
27
+
28
+ notifier.notify(message)
29
+ end
30
+
31
+ # Validates if given parameters are valid
32
+ #
33
+ # @return [NilClass] nil
34
+ # @raise [Feedbook::Errors::InvalidVariablesFormatError] if variables parameter is not a Hash
35
+ def valid?
36
+ unless variables.is_a? Hash
37
+ raise Errors::InvalidVariablesFormatError.new
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ # Parses template from string into a valid Liquid::Template
44
+ # @param template [String] String with valid Liquid template
45
+ #
46
+ # @return [Liquid::Template] compiled Liquid template
47
+ # @raise [Feedbook::Errors::TemplateSyntaxError] if there is a SyntaxError inside template
48
+ def parse_template(template)
49
+ Liquid::Template.parse(template)
50
+ rescue SyntaxError => e
51
+ raise Errors::TemplateSyntaxError.new(e.message)
52
+ end
53
+
54
+ # Returms Notifier instance
55
+ #
56
+ # @return [Notifier] Notifier instance for given type
57
+ def notifier
58
+ @notifier ||= Factories::NotifiersFactory.create(type)
59
+ end
60
+
61
+ end
62
+ end
@@ -0,0 +1,5 @@
1
+ require 'feedbook/notifiers/facebook_notifier'
2
+ require 'feedbook/notifiers/irc_notifier'
3
+ require 'feedbook/notifiers/mail_notifier'
4
+ require 'feedbook/notifiers/null_notifier'
5
+ require 'feedbook/notifiers/twitter_notifier'
@@ -0,0 +1,44 @@
1
+ require 'singleton'
2
+ require 'koala'
3
+ require 'feedbook/errors/notifier_configuration_error'
4
+ require 'feedbook/errors/notifier_notify_error'
5
+
6
+ module Feedbook
7
+ module Notifiers
8
+ class FacebookNotifier
9
+ include Singleton
10
+
11
+ # Sends notification to Facebook wall
12
+ # @param message [String] message to be send to Facebook wall
13
+ #
14
+ # @return [NilClass] nil
15
+ # @raise [Feedbook::Errors::NotifierNotifyError] if notify method fails
16
+ def notify(message)
17
+ if client.nil?
18
+ puts "Message has not been notified on Facebook: #{message} because of invalid client configuration"
19
+ else
20
+ client.put_wall_post(message)
21
+ puts "New message has been notified on Facebook: #{message}"
22
+ end
23
+ rescue Koala::KoalaError => e
24
+ raise Errors::NotifierNotifyError.new(:facebook, e.message)
25
+ end
26
+
27
+ # Load configuration for FacebookNotifier
28
+ # @param configuration = {} [Hash] Configuration hash (required: token)
29
+ #
30
+ # @return [NilClass] nil
31
+ # @raise [Feedbook::Errors::NotifierConfigurationError] if notifier configuration fails
32
+ def load_configuration(configuration = {})
33
+ @client = Koala::Facebook::API.new(configuration.fetch('token'))
34
+
35
+ puts 'Configuration loaded for FacebookNotifier'
36
+ rescue Koala::KoalaError => e
37
+ raise Errors::NotifierConfigurationError.new(:facebook, e.message)
38
+ end
39
+
40
+ private
41
+ attr_reader :client
42
+ end
43
+ end
44
+ end