hominid 1.2.1 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +4 -0
- data/{MIT-LICENSE → LICENSE} +1 -1
- data/README.textile +125 -40
- data/Rakefile +45 -16
- data/VERSION +1 -0
- data/hominid.gemspec +26 -19
- data/hominid.yml.tpl +1 -0
- data/lib/hominid.rb +33 -244
- data/lib/hominid/base.rb +85 -0
- data/lib/hominid/campaign.rb +172 -0
- data/lib/hominid/helper.rb +45 -0
- data/lib/hominid/list.rb +150 -0
- data/lib/hominid/webhook.rb +131 -0
- data/tasks/rails/hominid.rake +22 -0
- data/test/hominid_test.rb +7 -0
- data/test/test_helper.rb +10 -0
- metadata +30 -17
- data/VERSION.yml +0 -4
- data/init.rb +0 -1
- data/install.rb +0 -4
- data/pkg/hominid-1.2.1.gem +0 -0
- data/rails/init.rb +0 -1
- data/spec/hominid_spec.rb +0 -29
- data/spec/spec_helper.rb +0 -3
- data/uninstall.rb +0 -1
data/lib/hominid/base.rb
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
module Hominid
|
2
|
+
class Base
|
3
|
+
|
4
|
+
# MailChimp API Documentation: http://www.mailchimp.com/api/1.2/
|
5
|
+
MAILCHIMP_API_VERSION = "1.2"
|
6
|
+
|
7
|
+
def initialize(config = {})
|
8
|
+
if defined?(Rails.root) && (!config || config.empty?)
|
9
|
+
config = YAML.load(File.open("#{Rails.root}/config/hominid.yml"))[Rails.env].symbolize_keys
|
10
|
+
end
|
11
|
+
api_endpoint = config[:api_key].split('-').last
|
12
|
+
config.merge(:username => config[:username].to_s, :password => config[:password].to_s)
|
13
|
+
defaults = {:send_welcome => false,
|
14
|
+
:double_opt_in => false,
|
15
|
+
:update_existing => true,
|
16
|
+
:replace_interests => true,
|
17
|
+
:merge_tags => {}}
|
18
|
+
@config = defaults.merge(config).freeze
|
19
|
+
@chimpApi = XMLRPC::Client.new2("http://#{api_endpoint}.api.mailchimp.com/#{MAILCHIMP_API_VERSION}/")
|
20
|
+
end
|
21
|
+
|
22
|
+
# Security related methods
|
23
|
+
# --------------------------------
|
24
|
+
|
25
|
+
def add_api_key
|
26
|
+
@chimpApi.call("apikeyAdd", *@config.values_at(:username, :password, :api_key))
|
27
|
+
end
|
28
|
+
|
29
|
+
def expire_api_key
|
30
|
+
@chimpApi.call("apikeyExpire", *@config.values_at(:username, :password, :api_key))
|
31
|
+
end
|
32
|
+
|
33
|
+
def api_keys(include_expired = false)
|
34
|
+
username, password = *@config.values_at(:username, :password)
|
35
|
+
@chimpApi.call("apikeys", username, password, include_expired)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Used internally by Hominid
|
39
|
+
# --------------------------------
|
40
|
+
|
41
|
+
# handle common cases for which the Mailchimp API would raise Exceptions
|
42
|
+
def clean_merge_tags(merge_tags)
|
43
|
+
return {} unless merge_tags.is_a? Hash
|
44
|
+
merge_tags.each do |key, value|
|
45
|
+
if merge_tags[key].is_a? String
|
46
|
+
merge_tags[key] = value.gsub("\v", '')
|
47
|
+
elsif merge_tags[key].nil?
|
48
|
+
merge_tags[key] = ''
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def apply_defaults_to(options)
|
54
|
+
@config.merge(options)
|
55
|
+
end
|
56
|
+
|
57
|
+
def call(method, *args)
|
58
|
+
@chimpApi.call(method, @config[:api_key], *args)
|
59
|
+
rescue XMLRPC::FaultException => error
|
60
|
+
case error.faultCode
|
61
|
+
when 230
|
62
|
+
raise AlreadySubscribed.new(error)
|
63
|
+
when 231
|
64
|
+
raise AlreadyUnsubscribed.new(error)
|
65
|
+
when 232
|
66
|
+
raise NotExists.new(error)
|
67
|
+
when 233, 215
|
68
|
+
raise NotSubscribed.new(error)
|
69
|
+
else
|
70
|
+
raise HominidError.new(error)
|
71
|
+
end
|
72
|
+
rescue RuntimeError => error
|
73
|
+
if error.message =~ /Wrong type NilClass\. Not allowed!/
|
74
|
+
hashes = args.select{|a| a.is_a? Hash}
|
75
|
+
errors = hashes.select{|k, v| v.nil? }.collect{ |k, v| "#{k} is Nil." }.join(' ')
|
76
|
+
raise CommunicationError.new(errors)
|
77
|
+
else
|
78
|
+
raise error
|
79
|
+
end
|
80
|
+
rescue Exception => error
|
81
|
+
raise CommunicationError.new(error.message)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
@@ -0,0 +1,172 @@
|
|
1
|
+
module Hominid
|
2
|
+
|
3
|
+
class Campaign < Base
|
4
|
+
|
5
|
+
# Campaign related methods
|
6
|
+
# --------------------------------
|
7
|
+
|
8
|
+
attr_reader :campaign_id
|
9
|
+
attr_reader :attributes
|
10
|
+
|
11
|
+
def initialize(*args)
|
12
|
+
options = args.last.is_a?(Hash) ? args.last : {}
|
13
|
+
raise StandardError.new('Please provide a Campaign ID.') unless options[:id]
|
14
|
+
@campaign_id = options.delete(:id)
|
15
|
+
@attributes = options.delete(:attributes)
|
16
|
+
super(options)
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.all
|
20
|
+
# Get all campaigns for this mailchimp account
|
21
|
+
new(:id => 0).call("campaigns").to_a.collect { |c| Campaign.new(:id => c.delete('id'), :attributes => c) }
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.find_by_list_name(list_name)
|
25
|
+
new(:id => 0).call("campaigns", {:list_id => List.find_by_name(list_name).list_id}).to_a.collect { |c| Campaign.new(:id=> c.delete('id'), :attributes => c) }
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.find_by_list_id(list_id)
|
29
|
+
# Find all campaigns for the given list
|
30
|
+
new(:id => 0).call("campaigns", {:list_id => list_id}).to_a.collect { |c| Campaign.new(:id=> c.delete('id'), :attributes => c) }
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.find_by_title(title)
|
34
|
+
# Find campaign by title
|
35
|
+
all.find { |c| c.attributes['title'] =~ /#{title}/ }
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.find_by_type(type)
|
39
|
+
# Find campaign by type. Possible choices are:
|
40
|
+
# 'regular'
|
41
|
+
# 'plaintext'
|
42
|
+
# 'absplit'
|
43
|
+
# 'rss'
|
44
|
+
# 'inspection'
|
45
|
+
# 'trans'
|
46
|
+
# 'auto'
|
47
|
+
all.find { |campaign| campaign.attributes['type'] =~ /#{type}/ }
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.find_by_web_id(web_id)
|
51
|
+
# Find campaigns by web_id
|
52
|
+
all.find { |campaign| campaign.attributes['web_id'] =~ /#{web_id}/ }
|
53
|
+
end
|
54
|
+
|
55
|
+
def self.find_by_id(id)
|
56
|
+
# Find campaign by id
|
57
|
+
all.find { |campaign| (campaign.campaign_id == id.to_s) }
|
58
|
+
end
|
59
|
+
|
60
|
+
def self.find(id_or_web_id)
|
61
|
+
# Campaign finder method
|
62
|
+
all = self.all
|
63
|
+
campaign = self.find_by_id(id_or_web_id.to_s).to_a + self.find_by_web_id(id_or_web_id.to_i).to_a
|
64
|
+
return campaign.blank? ? nil : campaign.first
|
65
|
+
end
|
66
|
+
|
67
|
+
def self.create(type = 'regular', options = {}, content = {}, segment_options = {}, type_opts = {})
|
68
|
+
# Create a new campaign
|
69
|
+
# The options hash should be structured as follows:
|
70
|
+
#
|
71
|
+
# :list_id = (string) The ID of the list to send this campaign to.
|
72
|
+
# :subject = (string) The subject of the campaign.
|
73
|
+
# :from_email = (string) The email address this campaign will come from.
|
74
|
+
# :from_name = (string) The name that this campaign will come from.
|
75
|
+
# :to_email = (string) The To: name recipients will see.
|
76
|
+
# :template_id = (integer) The ID of the template to use for this campaign (optional).
|
77
|
+
# :folder_id = (integer) The ID of the folder to file this campaign in (optional).
|
78
|
+
# :tracking = (array) What to track for this campaign (optional).
|
79
|
+
# :title = (string) Internal title for this campaign (optional).
|
80
|
+
# :authenticate = (boolean) Set to true to authenticate campaign (optional).
|
81
|
+
# :analytics = (array) Google analytics tags (optional).
|
82
|
+
# :auto_footer = (boolean) Auto-generate the footer (optional)?
|
83
|
+
# :inline_css = (boolean) Inline the CSS styles (optional)?
|
84
|
+
# :generate_text = (boolean) Auto-generate text from HTML email (optional)?
|
85
|
+
#
|
86
|
+
# Visit http://www.mailchimp.com/api/1.2/campaigncreate.func.php for more information about creating
|
87
|
+
# campaigns via the API.
|
88
|
+
#
|
89
|
+
new(:id => 0).call("campaignCreate", type, options, content, segment_options, type_opts)
|
90
|
+
## TODO: Return the new campaign with the ID returned from the API
|
91
|
+
end
|
92
|
+
|
93
|
+
def self.templates
|
94
|
+
# Get the templates for this account
|
95
|
+
new(:id => 0).call("campaignTemplates")
|
96
|
+
end
|
97
|
+
|
98
|
+
def add_order(order)
|
99
|
+
# Attach Ecommerce Order Information to a campaign.
|
100
|
+
# The order hash should be structured as follows:
|
101
|
+
#
|
102
|
+
# :id = (string) the order id
|
103
|
+
# :email_id = (string) email id of the subscriber (mc_eid query string)
|
104
|
+
# :total = (double) Show only campaigns with this from_name.
|
105
|
+
# :shipping = (string) *optional - the total paid for shipping fees.
|
106
|
+
# :tax = (string) *optional - the total tax paid.
|
107
|
+
# :store_id = (string) a unique id for the store sending the order in
|
108
|
+
# :store_name = (string) *optional - A readable name for the store, typicaly the hostname.
|
109
|
+
# :plugin_id = (string) the MailChimp-assigned Plugin Id. Using 1214 for the moment.
|
110
|
+
# :items = (array) the individual line items for an order, using the following keys:
|
111
|
+
#
|
112
|
+
# :line_num = (integer) *optional - line number of the item on the order
|
113
|
+
# :product_id = (integer) internal product id
|
114
|
+
# :product_name = (string) the name for the product_id associated with the item
|
115
|
+
# :category_id = (integer) internal id for the (main) category associated with product
|
116
|
+
# :category_name = (string) the category name for the category id
|
117
|
+
# :qty = (double) the quantity of items ordered
|
118
|
+
# :cost = (double) the cost of a single item (i.e., not the extended cost of the line)
|
119
|
+
order.merge(:campaign_id => @campaign_id)
|
120
|
+
call("campaignEcommAddOrder", order)
|
121
|
+
end
|
122
|
+
|
123
|
+
def campaign_stats()
|
124
|
+
# Get the stats of a campaign
|
125
|
+
call("campaignStats", @campaign_id)
|
126
|
+
end
|
127
|
+
|
128
|
+
# Get the HTML & text content for a campaign
|
129
|
+
# :for_archive = (boolean) default true, true returns the content as it would appear in the archive, false returns the raw HTML/text
|
130
|
+
def campaign_content(for_archive = true)
|
131
|
+
# Get the content of a campaign
|
132
|
+
call("campaignContent", @campaign_id, for_archive)
|
133
|
+
end
|
134
|
+
|
135
|
+
def delete_campaign()
|
136
|
+
# Delete a campaign
|
137
|
+
call("campaignDelete", @campaign_id)
|
138
|
+
end
|
139
|
+
|
140
|
+
def replicate_campaign()
|
141
|
+
# Replicate a campaign (returns ID of new campaign)
|
142
|
+
call("campaignReplicate", @campaign_id)
|
143
|
+
end
|
144
|
+
|
145
|
+
def schedule_campaign(time = "#{1.day.from_now}")
|
146
|
+
# Schedule a campaign
|
147
|
+
## TODO: Add support for A/B Split scheduling
|
148
|
+
call("campaignSchedule", @campaign_id, time)
|
149
|
+
end
|
150
|
+
|
151
|
+
def send_now()
|
152
|
+
# Send a campaign
|
153
|
+
call("campaignSendNow", @campaign_id)
|
154
|
+
end
|
155
|
+
|
156
|
+
# Send a test of a campaign
|
157
|
+
def send_test(emails = {})
|
158
|
+
call("campaignSendTest", @campaign_id, emails)
|
159
|
+
end
|
160
|
+
|
161
|
+
def update(name, value)
|
162
|
+
# Update a campaign
|
163
|
+
call("campaignUpdate", @campaign_id, name, value)
|
164
|
+
end
|
165
|
+
|
166
|
+
def unschedule()
|
167
|
+
# Unschedule a campaign
|
168
|
+
call("campaignUnschedule", @campaign_id)
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Hominid
|
2
|
+
|
3
|
+
class Helper < Base
|
4
|
+
|
5
|
+
# Helper methods
|
6
|
+
# --------------------------------
|
7
|
+
|
8
|
+
def self.account_details
|
9
|
+
# Get details for this account.
|
10
|
+
new.call("getAccountDetails")
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.convert_css_to_inline(html, strip_css = false)
|
14
|
+
# Convert CSS styles to inline styles and (optionally) remove original styles
|
15
|
+
new.call("inlineCss", html, strip_css)
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.create_folder(name)
|
19
|
+
# Create a new folder to file campaigns in
|
20
|
+
new.call("createFolder", name)
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.generate_text(type, content)
|
24
|
+
# Have HTML content auto-converted to a text-only format.
|
25
|
+
# The options for text type are:
|
26
|
+
# 'html' => Expects a string of HTML(default).
|
27
|
+
# 'template' => Expects an array.
|
28
|
+
# 'url' => Expects a valid and public URL.
|
29
|
+
# 'cid' => Expects a campaign ID.
|
30
|
+
# 'tid' => Expects a template ID.
|
31
|
+
new.call("generateText", type, content)
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.html_to_text(content)
|
35
|
+
# Convert HTML content to text
|
36
|
+
new.call("generateText", 'html', content)
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.ping
|
40
|
+
# Ping the Mailchimp API
|
41
|
+
new.call("ping")
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
end
|
data/lib/hominid/list.rb
ADDED
@@ -0,0 +1,150 @@
|
|
1
|
+
module Hominid
|
2
|
+
|
3
|
+
class List < Base
|
4
|
+
|
5
|
+
# List related methods
|
6
|
+
# --------------------------------
|
7
|
+
|
8
|
+
attr_reader :list_id
|
9
|
+
attr_reader :attributes
|
10
|
+
|
11
|
+
def initialize(*args)
|
12
|
+
options = args.last.is_a?(Hash) ? args.last : {}
|
13
|
+
raise StandardError.new('Please provide a List ID.') unless options[:id]
|
14
|
+
@list_id = options.delete(:id)
|
15
|
+
@attributes = options.delete(:attributes)
|
16
|
+
super(options)
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.all
|
20
|
+
# Get all lists for this mailchimp account
|
21
|
+
new(:id => 0).call("lists").to_a.collect { |list| List.new(:id => list.delete('id'), :attributes => list) }
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.find_by_name(name)
|
25
|
+
# Find list by name
|
26
|
+
all.find { |list| list.attributes['name'] =~ /#{name}/ }
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.find_by_web_id(web_id)
|
30
|
+
# Find list by name
|
31
|
+
all.find { |list| (list.attributes['web_id'] == web_id.to_i) }
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.find_by_id(id)
|
35
|
+
# Find list by id
|
36
|
+
all.find { |list| (list.list_id == id.to_s) }
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.find(id_or_web_id)
|
40
|
+
# List finder method
|
41
|
+
all = self.all
|
42
|
+
list = self.find_by_id(id_or_web_id.to_s).to_a + self.find_by_web_id(id_or_web_id.to_i).to_a
|
43
|
+
return list.blank? ? nil : list.first
|
44
|
+
end
|
45
|
+
|
46
|
+
def create_group(group)
|
47
|
+
# Add an interest group to a list
|
48
|
+
call("listInterestGroupAdd", @list_id, group)
|
49
|
+
end
|
50
|
+
alias :interest_group_add :create_group
|
51
|
+
|
52
|
+
def create_tag(tag, name, required = false)
|
53
|
+
# Add a merge tag to a list
|
54
|
+
call("listMergeVarAdd", @list_id, tag, name, required)
|
55
|
+
end
|
56
|
+
alias :merge_var_add :create_tag
|
57
|
+
|
58
|
+
def delete_group(group)
|
59
|
+
# Delete an interest group for a list
|
60
|
+
call("listInterestGroupDel", @list_id, group)
|
61
|
+
end
|
62
|
+
alias :interest_group_del :delete_group
|
63
|
+
|
64
|
+
def delete_tag(tag)
|
65
|
+
# Delete a merge tag and all its members
|
66
|
+
call("listMergeVarDel", @list_id, tag)
|
67
|
+
end
|
68
|
+
alias :merge_var_del :delete_tag
|
69
|
+
|
70
|
+
def groups()
|
71
|
+
# Get the interest groups for a list
|
72
|
+
call("listInterestGroups", @list_id)
|
73
|
+
end
|
74
|
+
alias :interest_groups :groups
|
75
|
+
|
76
|
+
|
77
|
+
def member_info(email)
|
78
|
+
# Get a member of a list
|
79
|
+
call("listMemberInfo", @list_id, email)
|
80
|
+
end
|
81
|
+
|
82
|
+
def members(status = "subscribed", since = "2000-01-01 00:00:00", start = 0, limit = 100)
|
83
|
+
# Get members of a list based on status
|
84
|
+
# Select members based on one of the following statuses:
|
85
|
+
# 'subscribed'
|
86
|
+
# 'unsubscribed'
|
87
|
+
# 'cleaned'
|
88
|
+
# 'updated'
|
89
|
+
#
|
90
|
+
# Select members that have updated their status or profile by providing
|
91
|
+
# a "since" date in the format of YYYY-MM-DD HH:MM:SS
|
92
|
+
call("listMembers", @list_id, status, since, start, limit)
|
93
|
+
end
|
94
|
+
|
95
|
+
def merge_tags()
|
96
|
+
# Get the merge tags for a list
|
97
|
+
call("listMergeVars", @list_id)
|
98
|
+
end
|
99
|
+
alias :merge_vars :merge_tags
|
100
|
+
|
101
|
+
def subscribe(email, options = {})
|
102
|
+
# Subscribe a member to this list
|
103
|
+
merge_tags = clean_merge_tags options[:merge_tags]
|
104
|
+
options = apply_defaults_to({:email_type => "html"}.merge(options))
|
105
|
+
call(
|
106
|
+
"listSubscribe",
|
107
|
+
@list_id,
|
108
|
+
email,
|
109
|
+
merge_tags,
|
110
|
+
*options.values_at(
|
111
|
+
:email_type,
|
112
|
+
:double_opt_in,
|
113
|
+
:update_existing,
|
114
|
+
:replace_interests,
|
115
|
+
:send_welcome
|
116
|
+
)
|
117
|
+
)
|
118
|
+
end
|
119
|
+
|
120
|
+
def subscribe_many(subscribers, options = {})
|
121
|
+
# Subscribe an array of email addresses
|
122
|
+
# subscribers(array) = [{:EMAIL => 'example@email.com', :EMAIL_TYPE => 'html'}]
|
123
|
+
subscribers = subscribers.collect { |subscriber| clean_merge_tags(subscriber) }
|
124
|
+
options = apply_defaults_to({:update_existing => true}.merge(options))
|
125
|
+
call("listBatchSubscribe", @list_id, subscribers, *options.values_at(:double_opt_in, :update_existing, :replace_interests))
|
126
|
+
end
|
127
|
+
alias :batch_subscribe :subscribe_many
|
128
|
+
|
129
|
+
def unsubscribe(current_email, options = {})
|
130
|
+
# Unsubscribe a list member
|
131
|
+
options = apply_defaults_to({:delete_member => true}.merge(options))
|
132
|
+
call("listUnsubscribe", @list_id, current_email, *options.values_at(:delete_member, :send_goodbye, :send_notify))
|
133
|
+
end
|
134
|
+
|
135
|
+
def unsubscribe_many(emails, options = {})
|
136
|
+
# Unsubscribe an array of email addresses
|
137
|
+
# emails(array) = ['first@email.com', 'second@email.com']
|
138
|
+
options = apply_defaults_to({:delete_member => true}.merge(options))
|
139
|
+
call("listBatchUnsubscribe", @list_id, emails, *options.values_at(:delete_member, :send_goodbye, :send_notify))
|
140
|
+
end
|
141
|
+
alias :batch_unsubscribe :unsubscribe_many
|
142
|
+
|
143
|
+
def update_member(current_email, merge_tags = {}, email_type = "html")
|
144
|
+
# Update a member of this list
|
145
|
+
call("listUpdateMember", @list_id, current_email, merge_tags, email_type, true)
|
146
|
+
end
|
147
|
+
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|