correole 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/config/configuration.rb +70 -0
- data/config/database.yml +10 -0
- data/config/dependencies.rb +12 -0
- data/config/example.config.yml +18 -0
- data/config/production.html.erb +21 -0
- data/config/production.txt.erb +13 -0
- data/config/test.config.yml +15 -0
- data/config/test.html.erb +21 -0
- data/config/test.txt.erb +13 -0
- data/db/migrate/0001_create_database.rb +9 -0
- data/db/migrate/0002_create_items.rb +12 -0
- data/lib/correole/api.rb +118 -0
- data/lib/correole/feed.rb +37 -0
- data/lib/correole/item.rb +17 -0
- data/lib/correole/purge.rb +18 -0
- data/lib/correole/qputs.rb +3 -0
- data/lib/correole/send.rb +81 -0
- data/lib/correole/subscriber.rb +3 -0
- data/lib/correole/version.rb +4 -0
- metadata +21 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fa914631ebd1483e1e4ec3f2e72b2e6ad8b0842c
|
4
|
+
data.tar.gz: 48491bc95bdfaaf363f337dfce859af9310225a6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5ff71315753d357a1db73ca235e18d1f58c1c443e0b8f543439c00ba0210eedfb8ad5a9567fb492d362d8639273d2a2b21eec5a6712f2e87f2a6330edc4149ca
|
7
|
+
data.tar.gz: d2c0e6bbeb236291e188e6e14009cb0fb9668b50c3b0f09303239efb52a5ed76c90ccb9adb216e755ba26fc29fe121d25d799299f24de2a1ccdbb948c82c6058
|
@@ -0,0 +1,70 @@
|
|
1
|
+
DEFAULT_ENV = 'production'
|
2
|
+
DEFAULT_CONFIG_FILE = 'config.yml'
|
3
|
+
|
4
|
+
ENV['RACK_ENV'] ||= DEFAULT_ENV
|
5
|
+
ENV['CONFIG_FILE'] ||= DEFAULT_CONFIG_FILE
|
6
|
+
|
7
|
+
class Configuration
|
8
|
+
|
9
|
+
CONFIG_KEYS = [
|
10
|
+
'QUIET',
|
11
|
+
'FEED',
|
12
|
+
'CONFIRMATION_URI',
|
13
|
+
'BASE_URI',
|
14
|
+
'SUBJECT',
|
15
|
+
'FROM',
|
16
|
+
'HTML_TEMPLATE',
|
17
|
+
'PLAIN_TEMPLATE',
|
18
|
+
'SMTP_HOST',
|
19
|
+
'SMTP_PORT',
|
20
|
+
'SMTP_USER',
|
21
|
+
'SMTP_PASS',
|
22
|
+
'SMTP_AUTH',
|
23
|
+
'SMTP_TTLS' ]
|
24
|
+
|
25
|
+
class << self
|
26
|
+
CONFIG_KEYS.each do |k|
|
27
|
+
attr_accessor k.downcase.to_sym
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.load!
|
32
|
+
|
33
|
+
config_file = File.expand_path "../#{ENV['CONFIG_FILE']}", __FILE__
|
34
|
+
YAML.load_file(config_file)[ENV['RACK_ENV']].each_pair do |k, v|
|
35
|
+
ENV[k.upcase] ||= v.to_s rescue abort "Cannot load configuration key #{k}."
|
36
|
+
end rescue qputs "Could not load configuration file #{config_file}."
|
37
|
+
|
38
|
+
CONFIG_KEYS.each do |k|
|
39
|
+
case k
|
40
|
+
when 'QUIET', 'SMTP_TTLS'
|
41
|
+
# Cannot store boolean values in ENV, thus this.
|
42
|
+
self.send("#{k.downcase}=".to_sym, ENV[k] == 'true')
|
43
|
+
when 'HTML_TEMPLATE', 'PLAIN_TEMPLATE'
|
44
|
+
file = File.expand_path "../#{ENV[k]}", __FILE__
|
45
|
+
template = File.read file rescue abort "Cannot load template #{ENV[k]}."
|
46
|
+
self.send("#{k.downcase}=".to_sym, template)
|
47
|
+
when 'SMTP_USER', 'SMTP_PASS'
|
48
|
+
# For user name and password, Mail interprets '' as an input.
|
49
|
+
# It doesn't do the same with nil.
|
50
|
+
self.send("#{k.downcase}=".to_sym, ENV[k] == '' ? nil : ENV[k])
|
51
|
+
else
|
52
|
+
self.send("#{k.downcase}=".to_sym, ENV[k])
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
Mail.defaults do
|
57
|
+
delivery_method :smtp,
|
58
|
+
address: Configuration.smtp_host,
|
59
|
+
port: Configuration.smtp_port,
|
60
|
+
user_name: Configuration.smtp_user,
|
61
|
+
password: Configuration.smtp_pass,
|
62
|
+
authentication: Configuration.smtp_auth,
|
63
|
+
enable_starttls_auto: Configuration.smtp_ttls
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
69
|
+
|
70
|
+
Configuration.load!
|
data/config/database.yml
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
require 'sinatra/base'
|
2
|
+
require 'sinatra/activerecord'
|
3
|
+
require 'correole/qputs'
|
4
|
+
require 'correole/subscriber'
|
5
|
+
require 'correole/item'
|
6
|
+
require 'correole/feed'
|
7
|
+
require 'correole/api'
|
8
|
+
require 'correole/send'
|
9
|
+
require 'correole/purge'
|
10
|
+
require 'net/http'
|
11
|
+
require 'mail'
|
12
|
+
require 'configuration'
|
@@ -0,0 +1,18 @@
|
|
1
|
+
production: &prod
|
2
|
+
quiet: false
|
3
|
+
base_uri: http://newsletter.ruslanledesma.com
|
4
|
+
feed: http://ruslanledesma.com/feed.xml
|
5
|
+
confirmation_uri: http://ruslanledesma.com/unsubscribed/
|
6
|
+
subject: '<%= title %>: newsletter for <%= date %>'
|
7
|
+
from: '<%= title %> <no-reply@ruslanledesma.com>'
|
8
|
+
html_template: production.html.erb
|
9
|
+
plain_template: production.txt.erb
|
10
|
+
smtp_host:
|
11
|
+
smtp_port:
|
12
|
+
smtp_user:
|
13
|
+
smtp_pass:
|
14
|
+
smtp_auth:
|
15
|
+
smtp_ttls:
|
16
|
+
|
17
|
+
development:
|
18
|
+
<<: *prod
|
@@ -0,0 +1,21 @@
|
|
1
|
+
<html>
|
2
|
+
<body>
|
3
|
+
<h1><%= title %></h1>
|
4
|
+
<h2>New posts</h2>
|
5
|
+
<ul>
|
6
|
+
<% for item in unsent_items %>
|
7
|
+
<li>
|
8
|
+
<h3>
|
9
|
+
<a href="<%= item.link %>"><%= item.title %></a>
|
10
|
+
</h3>
|
11
|
+
<% if item.pub_date -%>
|
12
|
+
<p><i><%= item.pub_date.to_date %></i></p>
|
13
|
+
<% end -%>
|
14
|
+
<p><%= item.description %></p>
|
15
|
+
</li>
|
16
|
+
<% end %>
|
17
|
+
</ul>
|
18
|
+
|
19
|
+
<a href="<%= unsubscribe_uri %>">Unsubscribe here.</a>
|
20
|
+
</body>
|
21
|
+
</html>
|
@@ -0,0 +1,13 @@
|
|
1
|
+
<%= title %>
|
2
|
+
|
3
|
+
New posts
|
4
|
+
<% for item in unsent_items %>
|
5
|
+
- <%= item.title %>
|
6
|
+
<% if item.pub_date -%>
|
7
|
+
<%= item.pub_date.to_date %>
|
8
|
+
<% end -%>
|
9
|
+
<%= item.link %>
|
10
|
+
|
11
|
+
<%= item.description %>
|
12
|
+
<% end %>
|
13
|
+
Unsubscribe here: <%= unsubscribe_uri %>
|
@@ -0,0 +1,15 @@
|
|
1
|
+
test:
|
2
|
+
quiet: true
|
3
|
+
base_uri: http://test.ruslanledesma.com
|
4
|
+
feed: http://ruslanledesma.com/feed.xml # reset by env for end-to-end test
|
5
|
+
confirmation_uri: http://newsletter.ruslanledesma.com
|
6
|
+
subject: 'Test <%= title %> - <%= date %>'
|
7
|
+
from: '<%= title %> <no-reply@ruslanledesma.com>'
|
8
|
+
html_template: test.html.erb
|
9
|
+
plain_template: test.txt.erb
|
10
|
+
smtp_host: localhost # reset by env for end-to-end test
|
11
|
+
smtp_port: 25 # reset by env for end-to-end test
|
12
|
+
smtp_user:
|
13
|
+
smtp_pass:
|
14
|
+
smtp_auth:
|
15
|
+
smtp_ttls: false
|
@@ -0,0 +1,21 @@
|
|
1
|
+
<html>
|
2
|
+
<body>
|
3
|
+
<h1><%= title %></h1>
|
4
|
+
<h2>Items</h2>
|
5
|
+
<ul>
|
6
|
+
<% for item in unsent_items %>
|
7
|
+
<li>
|
8
|
+
<% if item.pub_date -%>
|
9
|
+
<i><%= item.pub_date.to_date %></i>
|
10
|
+
<% end -%>
|
11
|
+
<h3>
|
12
|
+
<a href="<%= item.link %>"><%= item.title %></a>
|
13
|
+
</h3>
|
14
|
+
<p><%= item.description %></p>
|
15
|
+
</li>
|
16
|
+
<% end %>
|
17
|
+
</ul>
|
18
|
+
|
19
|
+
<a href="<%= unsubscribe_uri %>">Unsubscribe here.</a>
|
20
|
+
</body>
|
21
|
+
</html>
|
data/config/test.txt.erb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
class CreateItems < ActiveRecord::Migration
|
2
|
+
def change
|
3
|
+
create_table :items do |t|
|
4
|
+
t.string :title, null: false
|
5
|
+
t.string :description, null: false
|
6
|
+
t.string :link, null: false
|
7
|
+
t.timestamp :pub_date, null: true
|
8
|
+
t.index :link, unique: true
|
9
|
+
t.timestamps null: false
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
data/lib/correole/api.rb
ADDED
@@ -0,0 +1,118 @@
|
|
1
|
+
class Api < Sinatra::Base
|
2
|
+
|
3
|
+
UNSUBSCRIBE_PATH = '/unsubscribe'
|
4
|
+
SUBSCRIBERS_ALLOWED_METHODS = 'PUT, DELETE, OPTIONS'
|
5
|
+
SUBSCRIBERS_ALLOWED_ORIGIN = '*'
|
6
|
+
UNSUBSCRIBE_ALLOWED_METHODS = 'GET, OPTIONS'
|
7
|
+
UNSUBSCRIBE_ALLOWED_ORIGIN = '*'
|
8
|
+
|
9
|
+
set :server, :thin
|
10
|
+
enable :logging
|
11
|
+
disable :show_exceptions
|
12
|
+
use ActiveRecord::ConnectionAdapters::ConnectionManagement
|
13
|
+
|
14
|
+
before do
|
15
|
+
content_type 'text/plain'
|
16
|
+
end
|
17
|
+
|
18
|
+
def subscribe(params)
|
19
|
+
response.headers['Access-Control-Allow-Origin'] = SUBSCRIBERS_ALLOWED_ORIGIN
|
20
|
+
s = Subscriber.new(email: params[:email])
|
21
|
+
return 400 if not s.valid?
|
22
|
+
begin
|
23
|
+
s.save
|
24
|
+
logger.info("Subscribed #{params[:email]}.")
|
25
|
+
rescue ActiveRecord::RecordNotUnique
|
26
|
+
logger.info("Already subscribed #{params[:email]}.")
|
27
|
+
Subscriber.find_by_email(params[:email]).touch
|
28
|
+
end
|
29
|
+
"#{params[:email]}\n"
|
30
|
+
end
|
31
|
+
|
32
|
+
def unsubscribe(params)
|
33
|
+
response.headers['Access-Control-Allow-Origin'] = UNSUBSCRIBE_ALLOWED_ORIGIN
|
34
|
+
s = Subscriber.new(email: params[:email])
|
35
|
+
return 400 if not s.valid?
|
36
|
+
s = Subscriber.find_by_email(params[:email])
|
37
|
+
if s != nil
|
38
|
+
s.delete
|
39
|
+
logger.info("Unsubscribed #{params[:email]}.")
|
40
|
+
else
|
41
|
+
logger.info("Tried to unsubscribe #{params[:email]} but address is not subscribed.")
|
42
|
+
end
|
43
|
+
"#{params[:email]}\n"
|
44
|
+
end
|
45
|
+
|
46
|
+
def subscribers_method_not_allowed
|
47
|
+
response.headers['Access-Control-Allow-Methods'] = SUBSCRIBERS_ALLOWED_METHODS
|
48
|
+
405
|
49
|
+
end
|
50
|
+
|
51
|
+
def unsubscribe_method_not_allowed
|
52
|
+
response.headers['Access-Control-Allow-Methods'] = UNSUBSCRIBE_ALLOWED_METHODS
|
53
|
+
405
|
54
|
+
end
|
55
|
+
|
56
|
+
options '/subscribers/:email' do
|
57
|
+
response.headers['Access-Control-Allow-Methods'] = SUBSCRIBERS_ALLOWED_METHODS
|
58
|
+
response.headers['Access-Control-Allow-Origin'] = SUBSCRIBERS_ALLOWED_ORIGIN
|
59
|
+
200
|
60
|
+
end
|
61
|
+
|
62
|
+
put '/subscribers/:email' do
|
63
|
+
subscribe(params)
|
64
|
+
end
|
65
|
+
|
66
|
+
delete '/subscribers/:email' do
|
67
|
+
unsubscribe(params)
|
68
|
+
end
|
69
|
+
|
70
|
+
[ :get,
|
71
|
+
:post,
|
72
|
+
:patch
|
73
|
+
].each do |verb|
|
74
|
+
send verb, '/subscribers/:email' do
|
75
|
+
subscribers_method_not_allowed
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
options "#{UNSUBSCRIBE_PATH}/:email" do
|
80
|
+
response.headers['Access-Control-Allow-Methods'] = UNSUBSCRIBE_ALLOWED_METHODS
|
81
|
+
response.headers['Access-Control-Allow-Origin'] = UNSUBSCRIBE_ALLOWED_ORIGIN
|
82
|
+
200
|
83
|
+
end
|
84
|
+
|
85
|
+
get "#{UNSUBSCRIBE_PATH}/:email" do
|
86
|
+
r = unsubscribe(params)
|
87
|
+
return r if r.is_a? Integer
|
88
|
+
response.headers['Location'] = Configuration.confirmation_uri
|
89
|
+
[302, r]
|
90
|
+
end
|
91
|
+
|
92
|
+
[ :put,
|
93
|
+
:delete,
|
94
|
+
:post,
|
95
|
+
:patch
|
96
|
+
].each do |verb|
|
97
|
+
send verb, "#{UNSUBSCRIBE_PATH}/:email" do
|
98
|
+
unsubscribe_method_not_allowed
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
not_found do
|
103
|
+
[404, "Not found\n"]
|
104
|
+
end
|
105
|
+
|
106
|
+
error 400 do
|
107
|
+
[400, "Bad request\n"]
|
108
|
+
end
|
109
|
+
|
110
|
+
error 405 do
|
111
|
+
[405, "Method not allowed\n"]
|
112
|
+
end
|
113
|
+
|
114
|
+
error 500 do
|
115
|
+
[500, "Internal server error\n"]
|
116
|
+
end
|
117
|
+
|
118
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
class Feed
|
2
|
+
|
3
|
+
def self.get
|
4
|
+
uri = URI Configuration.feed
|
5
|
+
xml = Net::HTTP.get uri
|
6
|
+
hash = Hash.from_xml xml
|
7
|
+
return {
|
8
|
+
:title => hash['rss']['channel']['title'],
|
9
|
+
:item => hash['rss']['channel']['item'].map { |i|
|
10
|
+
pub_date = nil
|
11
|
+
pub_date = Time.parse(i['pubDate']) if i.has_key? 'pubDate'
|
12
|
+
Item.new(title: i['title'],
|
13
|
+
description: i['description'],
|
14
|
+
link: i['link'],
|
15
|
+
pub_date: pub_date)
|
16
|
+
}
|
17
|
+
}
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.split_items(feed)
|
21
|
+
split_feed = {
|
22
|
+
:title => feed[:title],
|
23
|
+
:unsent_item => [],
|
24
|
+
:sent_item => []
|
25
|
+
}
|
26
|
+
feed[:item].each do |i|
|
27
|
+
if Item.where(:link => i.link).any?
|
28
|
+
split_feed[:sent_item] << i
|
29
|
+
else
|
30
|
+
split_feed[:unsent_item] << i
|
31
|
+
end
|
32
|
+
end
|
33
|
+
return split_feed
|
34
|
+
end
|
35
|
+
|
36
|
+
|
37
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
class Item < ActiveRecord::Base
|
2
|
+
validates :title, presence: true
|
3
|
+
validates :description, presence: true
|
4
|
+
validates :link, presence: true, format: /http.+/
|
5
|
+
|
6
|
+
def ==(o)
|
7
|
+
return o.class == self.class &&
|
8
|
+
o.title == self.title &&
|
9
|
+
o.description == self.description &&
|
10
|
+
o.link == self.link &&
|
11
|
+
o.pub_date == self.pub_date &&
|
12
|
+
o.id == self.id &&
|
13
|
+
o.created_at == self.created_at &&
|
14
|
+
o.updated_at == self.updated_at
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
class Purge
|
2
|
+
|
3
|
+
def self.run!
|
4
|
+
qputs "Fetch feed from #{Configuration.feed}."
|
5
|
+
feed = Feed.get
|
6
|
+
unsent_items = Feed.split_items(feed)[:unsent_item]
|
7
|
+
if unsent_items.empty?
|
8
|
+
qputs 'There are no new items, exiting.'
|
9
|
+
return
|
10
|
+
end
|
11
|
+
qputs "There are #{unsent_items.length} new items. The items are the following."
|
12
|
+
unsent_items.each_with_index { |i, j| qputs "[#{j+1}] #{i.link}" }
|
13
|
+
qputs 'Purge the new items by remembering them.'
|
14
|
+
unsent_items.each { |i| i.save }
|
15
|
+
qputs 'Done.'
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
class Send
|
2
|
+
|
3
|
+
def self.run!
|
4
|
+
qputs "Fetch feed from #{Configuration.feed}."
|
5
|
+
feed = Feed.get
|
6
|
+
split_feed = Feed.split_items feed
|
7
|
+
if split_feed[:unsent_item].empty?
|
8
|
+
qputs 'There are no new items, exiting.'
|
9
|
+
return
|
10
|
+
end
|
11
|
+
qputs "There are #{split_feed[:unsent_item].length} new items. The items are the following."
|
12
|
+
split_feed[:unsent_item].each_with_index { |i, j| qputs "[#{j+1}] #{i.link}" }
|
13
|
+
html = compose_html split_feed
|
14
|
+
plain = compose_plain split_feed
|
15
|
+
count = Subscriber.count
|
16
|
+
Subscriber.find_each.with_index do |s, i|
|
17
|
+
html_s = personalize html, s.email
|
18
|
+
plain_s = personalize plain, s.email
|
19
|
+
qputs "[#{i+1}/#{count}] Send newsletter to #{s.email}."
|
20
|
+
begin
|
21
|
+
send_out feed[:title], html_s, plain_s, s.email
|
22
|
+
rescue => exc
|
23
|
+
qputs "Could not send newsletter to #{s.email} for the following reason."
|
24
|
+
qputs exc.message
|
25
|
+
end
|
26
|
+
end
|
27
|
+
qputs 'Remember new items.'
|
28
|
+
split_feed[:unsent_item].each { |i| i.save }
|
29
|
+
qputs 'Done.'
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def self.template_bindings(split_feed)
|
35
|
+
title = split_feed[:title]
|
36
|
+
unsent_items = split_feed[:unsent_item]
|
37
|
+
sent_items = split_feed[:sent_item]
|
38
|
+
unsubscribe_uri = nil # supress unused variable warning
|
39
|
+
unsubscribe_uri = "#{Configuration.base_uri}#{Api::UNSUBSCRIBE_PATH}/<%= recipient %>"
|
40
|
+
title = '' if !title.is_a?(String)
|
41
|
+
unsent_items = [] if !unsent_items.is_a?(Array)
|
42
|
+
sent_items = [] if !unsent_items.is_a?(Array)
|
43
|
+
return binding
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.compose_html(split_feed)
|
47
|
+
template = Configuration.html_template
|
48
|
+
bindings = template_bindings(split_feed)
|
49
|
+
return ERB.new(template, nil, '-').result(bindings)
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.compose_plain(split_feed)
|
53
|
+
template = Configuration.plain_template
|
54
|
+
bindings = template_bindings(split_feed)
|
55
|
+
return ERB.new(template, nil, '-').result(bindings)
|
56
|
+
end
|
57
|
+
|
58
|
+
def self.personalize(message, recipient)
|
59
|
+
return ERB.new(message).result(binding)
|
60
|
+
end
|
61
|
+
|
62
|
+
def self.send_out(title, html, plain, recipient)
|
63
|
+
date = nil # supress unused variable warning
|
64
|
+
date = Date.today.strftime('%a, %d %b %Y')
|
65
|
+
Mail.deliver do
|
66
|
+
to recipient
|
67
|
+
from ERB.new(Configuration.from).result(binding)
|
68
|
+
subject ERB.new(Configuration.subject).result(binding)
|
69
|
+
|
70
|
+
text_part do
|
71
|
+
body plain
|
72
|
+
end
|
73
|
+
|
74
|
+
html_part do
|
75
|
+
content_type 'text/html; charset=UTF-8'
|
76
|
+
body html
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: correole
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ruslan Ledesma Garza
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-07-
|
11
|
+
date: 2016-07-26 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: sinatra
|
@@ -202,6 +202,25 @@ extensions: []
|
|
202
202
|
extra_rdoc_files: []
|
203
203
|
files:
|
204
204
|
- bin/correole
|
205
|
+
- config/configuration.rb
|
206
|
+
- config/database.yml
|
207
|
+
- config/dependencies.rb
|
208
|
+
- config/example.config.yml
|
209
|
+
- config/production.html.erb
|
210
|
+
- config/production.txt.erb
|
211
|
+
- config/test.config.yml
|
212
|
+
- config/test.html.erb
|
213
|
+
- config/test.txt.erb
|
214
|
+
- db/migrate/0001_create_database.rb
|
215
|
+
- db/migrate/0002_create_items.rb
|
216
|
+
- lib/correole/api.rb
|
217
|
+
- lib/correole/feed.rb
|
218
|
+
- lib/correole/item.rb
|
219
|
+
- lib/correole/purge.rb
|
220
|
+
- lib/correole/qputs.rb
|
221
|
+
- lib/correole/send.rb
|
222
|
+
- lib/correole/subscriber.rb
|
223
|
+
- lib/correole/version.rb
|
205
224
|
homepage: http://ruslanledesma.com/
|
206
225
|
licenses:
|
207
226
|
- MIT
|