feedbook 0.9.0

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