2019-07-30 21:29:24 +00:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
2020-06-12 20:03:43 +00:00
|
|
|
# License: AGPL-3.0-or-later WITH WTO-AP-3.0-or-later
|
|
|
|
# Full license explanation at https://github.com/houdiniproject/houdini/blob/master/LICENSE
|
2018-03-25 17:30:42 +00:00
|
|
|
require 'httparty'
|
|
|
|
require 'digest/md5'
|
|
|
|
|
|
|
|
module Mailchimp
|
2019-07-30 21:29:24 +00:00
|
|
|
include HTTParty
|
|
|
|
format :json
|
2018-03-25 17:30:42 +00:00
|
|
|
|
|
|
|
def self.base_uri(key)
|
|
|
|
dc = get_datacenter(key)
|
2019-07-30 21:29:24 +00:00
|
|
|
"https://#{dc}.api.mailchimp.com/3.0"
|
2018-03-25 17:30:42 +00:00
|
|
|
end
|
|
|
|
|
2019-07-30 21:29:24 +00:00
|
|
|
# Run the configuration from an initializer
|
|
|
|
# data: {:api_key => String}
|
|
|
|
def self.config(hash)
|
|
|
|
@options = {
|
|
|
|
headers: {
|
|
|
|
'Content-Type' => 'application/json',
|
|
|
|
'Accept' => 'application/json'
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@body = {
|
|
|
|
apikey: hash[:api_key]
|
|
|
|
}
|
|
|
|
end
|
2018-03-25 17:30:42 +00:00
|
|
|
|
|
|
|
# Given a nonprofit mailchimp oauth2 key, return its current datacenter
|
|
|
|
def self.get_datacenter(key)
|
2019-07-30 21:29:24 +00:00
|
|
|
metadata = HTTParty.get('https://login.mailchimp.com/oauth2/metadata',
|
|
|
|
headers: {
|
|
|
|
'User-Agent' => 'oauth2-draft-v10',
|
|
|
|
'Host' => 'login.mailchimp.com',
|
|
|
|
'Accept' => 'application/json',
|
|
|
|
'Authorization' => "OAuth #{key}"
|
|
|
|
})
|
|
|
|
metadata['dc']
|
2018-03-25 17:30:42 +00:00
|
|
|
end
|
|
|
|
|
2019-07-30 21:29:24 +00:00
|
|
|
def self.signup(email, mailchimp_list_id)
|
|
|
|
body_hash = @body.merge(
|
|
|
|
id: mailchimp_list_id,
|
|
|
|
email: { email: email }
|
|
|
|
)
|
|
|
|
post('/lists/subscribe', @options.merge(body: body_hash.to_json)).parsed_response
|
2018-03-25 17:30:42 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
def self.get_mailchimp_token(npo_id)
|
|
|
|
mailchimp_token = QueryNonprofitKeys.get_key(npo_id, 'mailchimp_token')
|
|
|
|
throw RuntimeError.new("No Mailchimp connection for this nonprofit: #{npo_id}") if mailchimp_token.nil?
|
2019-07-30 21:29:24 +00:00
|
|
|
mailchimp_token
|
2018-03-25 17:30:42 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
# Given a nonprofit id and a list of tag master ids that they make into email lists,
|
|
|
|
# create those email lists on mailchimp and return an array of hashes of mailchimp list ids, names, and tag_master_id
|
2019-07-30 21:29:24 +00:00
|
|
|
def self.create_mailchimp_lists(npo_id, tag_master_ids)
|
2018-03-25 17:30:42 +00:00
|
|
|
mailchimp_token = get_mailchimp_token(npo_id)
|
|
|
|
uri = base_uri(mailchimp_token)
|
|
|
|
puts "URI #{uri}"
|
|
|
|
puts "KEY #{mailchimp_token}"
|
|
|
|
|
|
|
|
npo = Qx.fetch(:nonprofits, npo_id).first
|
2019-07-30 21:29:24 +00:00
|
|
|
tags = Qx.select('DISTINCT(tag_masters.name) AS tag_name, tag_masters.id')
|
|
|
|
.from(:tag_masters)
|
|
|
|
.where('tag_masters.nonprofit_id' => npo_id)
|
|
|
|
.and_where('tag_masters.id IN ($ids)', ids: tag_master_ids)
|
|
|
|
.join(:nonprofits, 'tag_masters.nonprofit_id = nonprofits.id')
|
|
|
|
.execute
|
2018-03-25 17:30:42 +00:00
|
|
|
|
|
|
|
tags.map do |h|
|
2019-07-30 21:29:24 +00:00
|
|
|
list = post(uri + '/lists',
|
|
|
|
basic_auth: { username: '', password: mailchimp_token },
|
|
|
|
headers: { 'Content-Type' => 'application/json' },
|
|
|
|
body: {
|
|
|
|
name: 'CommitChange-' + h['tag_name'],
|
|
|
|
contact: {
|
|
|
|
company: npo['name'],
|
|
|
|
address1: npo['address'] || '',
|
|
|
|
city: npo['city'] || '',
|
|
|
|
state: npo['state_code'] || '',
|
|
|
|
zip: npo['zip_code'] || '',
|
|
|
|
country: npo['state_code'] || '',
|
|
|
|
phone: npo['phone'] || ''
|
|
|
|
},
|
|
|
|
permission_reminder: 'You are a registered supporter of our nonprofit.',
|
|
|
|
campaign_defaults: {
|
|
|
|
from_name: npo['name'] || '',
|
|
|
|
from_email: npo['email'].blank? ? 'support@commichange.com' : npo['email'],
|
|
|
|
subject: 'Enter your subject here...',
|
|
|
|
language: 'en'
|
|
|
|
},
|
|
|
|
email_type_option: false,
|
|
|
|
visibility: 'prv'
|
|
|
|
}.to_json)
|
|
|
|
raise Exception, "Failed to create list: #{list}" if list.code != 200
|
|
|
|
|
|
|
|
{ id: list['id'], name: list['name'], tag_master_id: h['id'] }
|
2018-03-25 17:30:42 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
# Given a nonprofit id and post_data, which is an array of batch operation hashes
|
|
|
|
# See here: http://developer.mailchimp.com/documentation/mailchimp/guides/how-to-use-batch-operations/
|
2019-07-30 21:29:24 +00:00
|
|
|
# Perform all the batch operations and return a status report
|
2018-03-25 17:30:42 +00:00
|
|
|
def self.perform_batch_operations(npo_id, post_data)
|
|
|
|
return if post_data.empty?
|
2019-07-30 21:29:24 +00:00
|
|
|
|
2018-03-25 17:30:42 +00:00
|
|
|
mailchimp_token = get_mailchimp_token(npo_id)
|
|
|
|
uri = base_uri(mailchimp_token)
|
2019-07-30 21:29:24 +00:00
|
|
|
batch_job_id = post(uri + '/batches',
|
|
|
|
basic_auth: { username: 'CommitChange', password: mailchimp_token },
|
|
|
|
headers: { 'Content-Type' => 'application/json' },
|
|
|
|
body: { operations: post_data }.to_json)['id']
|
2018-03-25 17:30:42 +00:00
|
|
|
check_batch_status(npo_id, batch_job_id)
|
|
|
|
end
|
|
|
|
|
|
|
|
def self.check_batch_status(npo_id, batch_job_id)
|
|
|
|
mailchimp_token = get_mailchimp_token(npo_id)
|
|
|
|
uri = base_uri(mailchimp_token)
|
2019-07-30 21:29:24 +00:00
|
|
|
batch_status = get(uri + '/batches/' + batch_job_id,
|
|
|
|
basic_auth: { username: 'CommitChange', password: mailchimp_token },
|
|
|
|
headers: { 'Content-Type' => 'application/json' })
|
2018-03-25 17:30:42 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
def self.delete_mailchimp_lists(npo_id, mailchimp_list_ids)
|
|
|
|
mailchimp_token = get_mailchimp_token(npo_id)
|
|
|
|
uri = base_uri(mailchimp_token)
|
|
|
|
mailchimp_list_ids.map do |id|
|
2019-07-30 21:29:24 +00:00
|
|
|
delete(uri + "/lists/#{id}", basic_auth: { username: 'CommitChange', password: mailchimp_token })
|
2018-03-25 17:30:42 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
# `removed` and `added` are arrays of tag join ids that have been added or removed to a supporter
|
|
|
|
def self.sync_supporters_to_list_from_tag_joins(npo_id, supporter_ids, tag_data)
|
2019-07-30 21:29:24 +00:00
|
|
|
emails = Qx.select(:email).from(:supporters).where('id IN ($ids)', ids: supporter_ids).execute.map { |h| h['email'] }
|
|
|
|
to_add = get_mailchimp_list_ids(tag_data.select { |h| h['selected'] }.map { |h| h['tag_master_id'] })
|
|
|
|
to_remove = get_mailchimp_list_ids(tag_data.reject { |h| h['selected'] }.map { |h| h['tag_master_id'] })
|
2018-03-25 17:30:42 +00:00
|
|
|
return if to_add.empty? && to_remove.empty?
|
|
|
|
|
2019-07-30 21:29:24 +00:00
|
|
|
bulk_post = emails.map { |em| to_add.map { |ml_id| { method: 'POST', path: "lists/#{ml_id}/members", body: { email_address: em, status: 'subscribed' }.to_json } } }.flatten
|
|
|
|
bulk_delete = emails.map { |em| to_remove.map { |ml_id| { method: 'DELETE', path: "lists/#{ml_id}/members/#{Digest::MD5.hexdigest(em.downcase)}" } } }.flatten
|
2018-03-25 17:30:42 +00:00
|
|
|
perform_batch_operations(npo_id, bulk_post.concat(bulk_delete))
|
|
|
|
end
|
|
|
|
|
|
|
|
def self.get_mailchimp_list_ids(tag_master_ids)
|
|
|
|
return [] if tag_master_ids.empty?
|
|
|
|
|
2019-07-30 21:29:24 +00:00
|
|
|
to_insert_data = Qx.select('email_lists.mailchimp_list_id')
|
|
|
|
.from(:tag_masters)
|
|
|
|
.where('tag_masters.id IN ($ids)', ids: tag_master_ids)
|
|
|
|
.join('email_lists', 'email_lists.tag_master_id=tag_masters.id')
|
|
|
|
.execute.map { |h| h['mailchimp_list_id'] }
|
|
|
|
end
|
2018-10-24 16:36:02 +00:00
|
|
|
|
|
|
|
# @param [Nonprofit] nonprofit
|
|
|
|
def self.hard_sync_lists(nonprofit)
|
2019-07-30 21:29:24 +00:00
|
|
|
return unless nonprofit
|
2018-10-24 16:36:02 +00:00
|
|
|
|
|
|
|
nonprofit.tag_masters.not_deleted.each do |i|
|
2019-07-30 21:29:24 +00:00
|
|
|
hard_sync_list(i.email_list) if i.email_list
|
2018-10-24 16:36:02 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
# @param [EmailList] email_list
|
2019-08-07 18:15:47 +00:00
|
|
|
# Notably, if a supporter unsubscribed on Mailchimp, this will not
|
|
|
|
# resubscribe them. This is the correct behavior.
|
2018-10-24 16:36:02 +00:00
|
|
|
def self.hard_sync_list(email_list)
|
|
|
|
ops = generate_batch_ops_for_hard_sync(email_list)
|
|
|
|
perform_batch_operations(email_list.nonprofit.id, ops)
|
2019-07-30 21:29:24 +00:00
|
|
|
end
|
2018-10-24 16:36:02 +00:00
|
|
|
|
|
|
|
def self.generate_batch_ops_for_hard_sync(email_list)
|
2019-07-30 21:29:24 +00:00
|
|
|
# get the subscribers from mailchimp
|
2018-10-24 16:36:02 +00:00
|
|
|
mailchimp_subscribers = get_list_mailchimp_subscribers(email_list)
|
2019-07-30 21:29:24 +00:00
|
|
|
# get our subscribers
|
|
|
|
our_supporters = email_list.tag_master.tag_joins.map(&:supporter)
|
2018-10-24 16:36:02 +00:00
|
|
|
|
2019-07-30 21:29:24 +00:00
|
|
|
# split them as follows:
|
2018-10-24 16:36:02 +00:00
|
|
|
# on both lists, on our list, on the mailchimp list
|
|
|
|
in_both, in_mailchimp_only = mailchimp_subscribers.partition do |mc_sub|
|
2019-07-30 21:29:24 +00:00
|
|
|
our_supporters.any? { |s| s.email.casecmp(mc_sub[:email_address]).zero? }
|
2018-10-24 16:36:02 +00:00
|
|
|
end
|
2019-07-30 21:29:24 +00:00
|
|
|
|
2018-10-24 16:36:02 +00:00
|
|
|
_, in_our_side_only = our_supporters.partition do |s|
|
2019-07-30 21:29:24 +00:00
|
|
|
mailchimp_subscribers.any? { |mc_sub| s.email.casecmp(mc_sub[:email_address]).zero? }
|
2018-10-24 16:36:02 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
# if on our list, add to mailchimp
|
2019-07-30 21:29:24 +00:00
|
|
|
output = in_our_side_only.map do |i|
|
|
|
|
{ method: 'POST', path: "lists/#{email_list.mailchimp_list_id}/members", body: { email_address: i.email, status: 'subscribed' }.to_json }
|
|
|
|
end
|
2018-10-24 16:36:02 +00:00
|
|
|
|
|
|
|
# if on mailchimp list, delete from mailchimp
|
2019-07-30 21:29:24 +00:00
|
|
|
output = output.concat(in_mailchimp_only.map { |i| { method: 'DELETE', path: "lists/#{email_list.mailchimp_list_id}/members/#{i[:id]}" } })
|
2018-10-24 16:36:02 +00:00
|
|
|
|
2019-07-30 21:29:24 +00:00
|
|
|
output
|
2018-10-24 16:36:02 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
def self.get_list_mailchimp_subscribers(email_list)
|
|
|
|
mailchimp_token = get_mailchimp_token(email_list.tag_master.nonprofit.id)
|
|
|
|
uri = base_uri(mailchimp_token)
|
2019-07-30 21:29:24 +00:00
|
|
|
result = get(uri + "/lists/#{email_list.mailchimp_list_id}/members?count=1000000000",
|
|
|
|
basic_auth: { username: 'CommitChange', password: mailchimp_token },
|
|
|
|
headers: { 'Content-Type' => 'application/json' })
|
|
|
|
members = result['members'].map do |i|
|
|
|
|
{ id: i['id'], email_address: i['email_address'] }
|
|
|
|
end.to_a
|
2018-10-24 16:36:02 +00:00
|
|
|
end
|
2018-10-18 01:47:26 +00:00
|
|
|
|
|
|
|
def self.get_email_lists(nonprofit)
|
|
|
|
mailchimp_token = get_mailchimp_token(nonprofit.id)
|
|
|
|
uri = base_uri(mailchimp_token)
|
2019-07-30 21:29:24 +00:00
|
|
|
result = get(uri + '/lists',
|
|
|
|
basic_auth: { username: 'CommitChange', password: mailchimp_token },
|
|
|
|
headers: { 'Content-Type' => 'application/json' })
|
2018-10-18 01:47:26 +00:00
|
|
|
result['lists']
|
2019-07-30 21:29:24 +00:00
|
|
|
end
|
2018-03-25 17:30:42 +00:00
|
|
|
end
|