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.
- 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
|