feedigest 0.0.1

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: ab1e04866f9a432849b17e607d1c90366e248a7d42ea3d44aec0e6749dd1405e
4
+ data.tar.gz: f0948b0ddd6910cee3bd237e0b2a0a74abcf6167af8e7e1f92a2ec3a978867d1
5
+ SHA512:
6
+ metadata.gz: 57b418b0d2d9ddb580b3ec746012ffc2b00fe5125b3c2a7b9553614ff1db5d79a7b4440df4e13849f7913a736135ecc94af3529f402030d379cadaa7f1353d33
7
+ data.tar.gz: 9ca032d6a782bd4b9a3589a000651df5f660383b79f0f1e51d123150f6f71b9f799e9434a7428d3d70f4cb96a6c0ace5f53843a1ddfc57bea8c28b17a8e5e4e7
data/LICENSE.txt ADDED
@@ -0,0 +1,20 @@
1
+ The MIT License
2
+
3
+ Copyright (c) 2018 Angelos Orfanakos
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
6
+ this software and associated documentation files (the "Software"), to deal in
7
+ the Software without restriction, including without limitation the rights to
8
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9
+ the Software, and to permit persons to whom the Software is furnished to do so,
10
+ subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'feedigest'
4
+
5
+ puts 'Please provide a list of feed URLs (one per line) and press Ctrl-D'
6
+
7
+ feed_urls = $stdin.readlines.map(&:strip)
8
+
9
+ puts Feedigest.new(feed_urls)
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'feedigest'
4
+
5
+ if ENV['FEEDIGEST_EMAIL_RECIPIENT'].nil?
6
+ puts 'Please set $FEEDIGEST_EMAIL_RECIPIENT and try again'
7
+ exit 1
8
+ end
9
+
10
+ puts 'Please provide a list of feed URLs (one per line) and press Ctrl-D'
11
+
12
+ feed_urls = $stdin.readlines.map(&:strip)
13
+
14
+ Feedigest.new(feed_urls).send_email
data/lib/feedigest.rb ADDED
@@ -0,0 +1,192 @@
1
+ require 'dotenv/load'
2
+ require 'feedjira'
3
+ require 'mail'
4
+ require 'nokogiri'
5
+ require 'reverse_markdown'
6
+
7
+ class Feedigest
8
+ ENTRY_WINDOW = ENV.fetch('FEEDIGEST_ENTRY_WINDOW', 60 * 60 * 24) # Seconds
9
+ EMAIL_SENDER = ENV.fetch(
10
+ 'FEEDIGEST_EMAIL_SENDER',
11
+ "feedigest@#{`hostname`.strip}"
12
+ )
13
+ EMAIL_RECIPIENT = ENV.fetch('FEEDIGEST_EMAIL_RECIPIENT')
14
+ DELIVERY_METHOD = ENV.fetch('FEEDIGEST_DELIVERY_METHOD', 'sendmail').to_sym
15
+
16
+ Feed = Struct.new(:url, :title, :entries, :error)
17
+
18
+ attr_reader :feed_urls
19
+
20
+ def initialize(feed_urls)
21
+ @feed_urls = feed_urls
22
+ end
23
+
24
+ def send_email
25
+ build_email.deliver if send_email?
26
+ end
27
+
28
+ def to_s
29
+ build_email.to_s
30
+ end
31
+
32
+ private
33
+
34
+ def send_email?
35
+ feeds.any?
36
+ end
37
+
38
+ def build_email
39
+ return @build_email if @build_email
40
+
41
+ mail = Mail.new
42
+ mail.from = EMAIL_SENDER
43
+ mail.to = EMAIL_RECIPIENT
44
+ mail.subject = email_subject
45
+ mail.text_part = build_email_text_part
46
+ mail.html_part = build_email_html_part
47
+
48
+ setup_delivery_method(mail)
49
+
50
+ @build_email = mail
51
+ end
52
+
53
+ def setup_delivery_method(mail)
54
+ mail.delivery_method(DELIVERY_METHOD, delivery_method_options)
55
+ end
56
+
57
+ def delivery_method_options
58
+ case DELIVERY_METHOD
59
+ when :smtp
60
+ {
61
+ address: ENV.fetch('FEEDIGEST_SMTP_HOST'),
62
+ port: ENV.fetch('FEEDIGEST_SMTP_PORT', '587').to_i,
63
+ user_name: ENV.fetch('FEEDIGEST_SMTP_USERNAME'),
64
+ password: ENV.fetch('FEEDIGEST_SMTP_PASSWORD'),
65
+ authentication: ENV.fetch('FEEDIGEST_SMTP_AUTH', 'plain'),
66
+ enable_starttls: ENV.fetch('FEEDIGEST_SMTP_STARTTLS', 'true') == 'true'
67
+ }
68
+ else
69
+ {}
70
+ end
71
+ end
72
+
73
+ def email_subject
74
+ sprintf(
75
+ 'Digest for %s in %s',
76
+ pluralize(entries_count, 'entry', 'entries'),
77
+ pluralize(feeds_without_error.size, 'feed')
78
+ )
79
+ end
80
+
81
+ def build_email_text_part
82
+ Mail::Part.new.tap do |p|
83
+ p.content_type 'text/plain; charset=utf-8'
84
+ p.body email_body_text
85
+ end
86
+ end
87
+
88
+ def build_email_html_part
89
+ Mail::Part.new.tap do |p|
90
+ p.content_type 'text/html; charset=utf-8'
91
+ p.body email_body_html
92
+ end
93
+ end
94
+
95
+ def email_body_html
96
+ @email_body_html ||=
97
+ Nokogiri::HTML::Builder.new(encoding: 'utf-8') { |builder|
98
+ builder.div do
99
+ feeds.each do |feed|
100
+ feed_html(builder, feed)
101
+ end
102
+ end
103
+ }.to_html
104
+ end
105
+
106
+ def email_body_text
107
+ ReverseMarkdown.convert(email_body_html)
108
+ end
109
+
110
+ def feed_html(builder, feed)
111
+ builder.div do
112
+ if feed.error
113
+ builder.h2 feed.url
114
+ builder.p "Error: #{feed.error}"
115
+ else
116
+ builder.h2 feed.title
117
+ entries_html(builder, feed.entries)
118
+ end
119
+ end
120
+ end
121
+
122
+ def entries_html(builder, entries)
123
+ entries.group_by { |e| e.published.to_date }.each do |date, date_entries|
124
+ builder.h3 date
125
+
126
+ date_entries.each do |entry|
127
+ entry_html(builder, entry)
128
+ end
129
+ end
130
+ end
131
+
132
+ def entry_html(builder, entry)
133
+ builder.p do
134
+ builder.a(entry.title, href: entry.url)
135
+ end
136
+ end
137
+
138
+ def feeds
139
+ @feeds ||= all_feeds.select { |f| process_feed?(f) }
140
+ end
141
+
142
+ def feeds_without_error
143
+ feeds.select { |feed| feed[:error].nil? }
144
+ end
145
+
146
+ def process_feed?(feed)
147
+ feed[:error] || feed[:entries].any?
148
+ end
149
+
150
+ def all_feeds
151
+ feed_urls.map { |url| feed_from_url(url) }
152
+ end
153
+
154
+ def feed_from_url(url)
155
+ feed, error = fetch_and_parse_feed(url)
156
+ Feed.new(
157
+ url,
158
+ feed&.title,
159
+ error ? [] : feed_entries(feed),
160
+ error
161
+ )
162
+ end
163
+
164
+ def fetch_and_parse_feed(url)
165
+ feed = Feedjira::Feed.fetch_and_parse(url)
166
+ [feed, nil]
167
+ rescue StandardError => e
168
+ [nil, e.message]
169
+ end
170
+
171
+ def feed_entries(feed)
172
+ feed.entries.
173
+ select { |e| process_entry?(e) }.
174
+ sort_by { |e| -e.published.to_i }
175
+ end
176
+
177
+ def process_entry?(entry)
178
+ !entry.published.nil? && entry.published >= window_start
179
+ end
180
+
181
+ def window_start
182
+ @window_start ||= Time.now - ENTRY_WINDOW
183
+ end
184
+
185
+ def entries_count
186
+ feeds.reduce(0) { |s, f| s + f.entries.size }
187
+ end
188
+
189
+ def pluralize(count, singular, plural = singular + 's')
190
+ [count, count == 1 ? singular : plural].join(' ')
191
+ end
192
+ end
metadata ADDED
@@ -0,0 +1,119 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: feedigest
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Angelos Orfanakos
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-06-12 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: dotenv
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.4'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.4'
27
+ - !ruby/object:Gem::Dependency
28
+ name: feedjira
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '2.1'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '2.1'
41
+ - !ruby/object:Gem::Dependency
42
+ name: mail
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '2.7'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '2.7'
55
+ - !ruby/object:Gem::Dependency
56
+ name: nokogiri
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.8'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.8'
69
+ - !ruby/object:Gem::Dependency
70
+ name: reverse_markdown
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '1.1'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '1.1'
83
+ description:
84
+ email: me@agorf.gr
85
+ executables:
86
+ - feedigest-send
87
+ - feedigest-generate
88
+ extensions: []
89
+ extra_rdoc_files: []
90
+ files:
91
+ - LICENSE.txt
92
+ - bin/feedigest-generate
93
+ - bin/feedigest-send
94
+ - lib/feedigest.rb
95
+ homepage: https://github.com/agorf/feedigest
96
+ licenses:
97
+ - MIT
98
+ metadata: {}
99
+ post_install_message:
100
+ rdoc_options: []
101
+ require_paths:
102
+ - lib
103
+ required_ruby_version: !ruby/object:Gem::Requirement
104
+ requirements:
105
+ - - ">="
106
+ - !ruby/object:Gem::Version
107
+ version: 2.3.3
108
+ required_rubygems_version: !ruby/object:Gem::Requirement
109
+ requirements:
110
+ - - ">="
111
+ - !ruby/object:Gem::Version
112
+ version: '0'
113
+ requirements: []
114
+ rubyforge_project:
115
+ rubygems_version: 2.7.3
116
+ signing_key:
117
+ specification_version: 4
118
+ summary: RSS/Atom feed updates as a digest email
119
+ test_files: []