feedbook 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +15 -0
- data/.gitignore +22 -0
- data/.rspec +3 -0
- data/.travis.yml +6 -0
- data/Gemfile +3 -0
- data/LICENSE.txt +22 -0
- data/README.md +129 -0
- data/Rakefile +7 -0
- data/bin/feedbook +73 -0
- data/feedbook.gemspec +35 -0
- data/lib/feedbook.rb +6 -0
- data/lib/feedbook/comparers/posts_comparer.rb +15 -0
- data/lib/feedbook/configuration.rb +47 -0
- data/lib/feedbook/errors.rb +10 -0
- data/lib/feedbook/errors/invalid_feed_url_error.rb +6 -0
- data/lib/feedbook/errors/invalid_interval_format_error.rb +6 -0
- data/lib/feedbook/errors/invalid_variables_format_error.rb +6 -0
- data/lib/feedbook/errors/no_configuration_file_error.rb +6 -0
- data/lib/feedbook/errors/notifier_configuration_error.rb +12 -0
- data/lib/feedbook/errors/notifier_notify_error.rb +12 -0
- data/lib/feedbook/errors/parse_feed_error.rb +11 -0
- data/lib/feedbook/errors/template_syntax_error.rb +6 -0
- data/lib/feedbook/errors/unsupported_notifier_error.rb +12 -0
- data/lib/feedbook/factories/notifiers_factory.rb +28 -0
- data/lib/feedbook/feed.rb +85 -0
- data/lib/feedbook/helpers/time_interval_parser.rb +38 -0
- data/lib/feedbook/listener.rb +122 -0
- data/lib/feedbook/notification.rb +62 -0
- data/lib/feedbook/notifiers.rb +5 -0
- data/lib/feedbook/notifiers/facebook_notifier.rb +44 -0
- data/lib/feedbook/notifiers/irc_notifier.rb +42 -0
- data/lib/feedbook/notifiers/mail_notifier.rb +52 -0
- data/lib/feedbook/notifiers/null_notifier.rb +25 -0
- data/lib/feedbook/notifiers/twitter_notifier.rb +49 -0
- data/lib/feedbook/post.rb +33 -0
- data/lib/feedbook/version.rb +3 -0
- data/spec/spec_helper.rb +30 -0
- data/spec/unit/lib/comparers/posts_comparer_spec.rb +13 -0
- data/spec/unit/lib/configuration_spec.rb +43 -0
- data/spec/unit/lib/factories/notifiers_factory_spec.rb +28 -0
- data/spec/unit/lib/feed_spec.rb +42 -0
- data/spec/unit/lib/helpers/time_interval_parser_spec.rb +26 -0
- data/spec/unit/lib/listener_spec.rb +29 -0
- data/spec/unit/lib/notification_spec.rb +44 -0
- data/spec/unit/lib/notifiers/facebook_notifier_spec.rb +34 -0
- data/spec/unit/lib/notifiers/irc_notifier_spec.rb +41 -0
- data/spec/unit/lib/notifiers/mail_notifier_spec.rb +39 -0
- data/spec/unit/lib/notifiers/null_notifier_spec.rb +19 -0
- data/spec/unit/lib/notifiers/twitter_notifier_spec.rb +36 -0
- data/spec/unit/lib/post_spec.rb +51 -0
- metadata +304 -0
@@ -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,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
|