class MailchimpKit < Kit attr_accessible :api_key, :default_list_id, :fan_subscribe_question QUEUE = "mailchimp" REQUIRED_MERGE_VARS = ["EMAIL", "FNAME", "LNAME", "ADDRESS"] acts_as_kit :with_approval => true do self.configurable = true when_active do end end after_initialize do self[:settings][:attached_lists] ||= [] end validate :check_valid_api_key? store :settings, :accessors => [ :api_key, :old_api_key, :attached_lists, :mailchimp_state, :default_list_id, :fan_subscribe_question, :count_from_mailchimp, :count_merged_mailchimp, :hide_default_list_alert ] def friendly_name "MailChimp" end def pitch "Integrate your Mailchimp lists with Artful.ly" end def configured? mailchimp_state == "configured" end def configured! settings[:mailchimp_state] = "configured" save end def valid_api_key? return unless api_key begin gibbon.helper.ping["msg"] == "Everything's Chimpy!" rescue false end end def lists gibbon.lists.list({ :start => 0, :limit => 100 })["data"].map do |list| [list["name"], list["id"]] end end def groups(list_id) gibbon.lists.interest_groupings(:id => list_id).map do |grouping| grouping["groups"] = grouping["groups"].map do |group| group.slice("name") end grouping.slice("id", "name", "groups") end rescue Gibbon::MailChimpError # The list might not have groups enabled and throw this error [] end def cache_groups_for_attached_lists attached_lists.each do |attached_lists| attached_lists[:groups] = groups(attached_lists[:list_id]) end save end def default_list_id if settings[:default_list_id].present? settings[:default_list_id] elsif attached_lists.one? attached_lists.first[:list_id] end end def fan_subscribe_question settings[:fan_subscribe_question] || "Would you like to join our email list?" end def list_attached?(list_id) attached_lists.any? { |list| list[:list_id] == list_id } end def change_lists(new_list_ids) added_list_names = [] added_list_ids = [] removed_list_names = [] mailchimp_lists = lists mailchimp_lists.each do |list| list_id = list[1] if !list_attached?(list_id) && new_list_ids.include?(list_id) add_list(list_id) added_list_ids << list_id added_list_names << find_list_name(list_id) elsif list_attached?(list_id) && !new_list_ids.include?(list_id) remove_list(list_id) removed_list_names << find_list_name(list_id) end end # Clean out old synced lists old_lists = attached_lists.select do |list| mailchimp_lists.none? { |mailchimp_list| mailchimp_list[1] == list[:list_id] } end old_lists.each do |list| remove_list(list[:list_id]) end cache_groups_for_attached_lists start_sync(added_list_names, removed_list_names) end def add_list(list_id) self.attached_lists = attached_lists.reject { |list| list.empty? } attached_lists << { :list_id => list_id, :list_name => find_list_name(list_id), :groups => groups(list_id), } save end def start_sync(added_list_names, removed_list_names) list_ids = attached_lists.map do |list| list[:list_id] end job = MailchimpSyncJob.new(self, { :type => :initial_sync, :list_ids => list_ids, :added_list_names => added_list_names, :removed_list_names => removed_list_names, }) Delayed::Job.enqueue job, :queue => QUEUE end def remove_list(list_id) self.attached_lists = attached_lists.reject { |list| list[:list_id] == list_id } save end def create_webhooks(list_id) webhook_url = Rails.application.routes.url_helpers.mailchimp_webhook_url(id, { :list_id => list_id, :host => MAILCHIMP_WEBHOOK_URL[:host], :protocol => MAILCHIMP_WEBHOOK_URL[:protocol] || "http", }) webhooks_for_list = gibbon.lists.webhooks(:id => list_id) existing_webhook = webhooks_for_list.detect do |webhook| webhook["url"] == webhook_url end return true if existing_webhook gibbon.lists.webhook_add({ :id => list_id, :url => webhook_url, }) end def destroy_webhooks(list_id) gibbon = Gibbon::API.new(old_api_key || api_key) gibbon.lists.webhook_del({ :id => list_id, :url => Rails.application.routes.url_helpers.mailchimp_webhook_url(id, :list_id => list_id, :host => MAILCHIMP_WEBHOOK_URL[:host], :protocol => MAILCHIMP_WEBHOOK_URL[:protocol] || "http") })["completed"] end def unsubscribe_old_members(list_id) organization.people.each do |person| person.subscribed_lists.where(:list_id => list_id).each do |subscribed_list| subscribed_list.sync = false subscribed_list.destroy end end Sunspot.delay.commit end def sync_mailchimp_to_artfully_new_members(list_id) members = mailchimp_to_artfully_new_members(list_id) members.each do |member| person = organization.people.create({ :first_name => member["first name"], :last_name => member["last name"], :email => member["email address"], :skip_sync_to_mailchimp => true, }) do |person| person.skip_commit = true end note = person.notes.build({ :text => "Imported from MailChimp", :occurred_at => Time.now }) note.organization_id = organization_id note.save subscribed_list = person.subscribed_lists.create({ :list_id => list_id, :sync => false, :confirmed => true, }) list = attached_lists.detect { |list| list[:list_id] == list_id } create_groups_for_mailchimp_member(list, subscribed_list, member) end set_count_from_mailchimp(members.count) end def mailchimp_to_artfully_new_members(list_id) mailchimp_list_members(list_id).reject do |member| organization_people_emails.include?(member["email address"]) end end def sync_mailchimp_to_artfully_update_members(list_id) members = mailchimp_to_artfully_update_members(list_id) members.each do |member| person = organization.people.find_by_email(member["email address"]) next if person.do_not_email? member.each do |attribute, value| attribute = mailchimp_attributes_to_artfully[attribute] if attribute && person.send(attribute).blank? person.send("#{attribute}=", value) end end person.skip_sync_to_mailchimp = true person.save subscribed_list = person.subscribed_lists.find_or_initialize_by_list_id(list_id) subscribed_list.sync = false subscribed_list.confirmed = true subscribed_list.bounced = false subscribed_list.save list = attached_lists.detect { |list| list[:list_id] == list_id } create_groups_for_mailchimp_member(list, subscribed_list, member) end set_count_merged_mailchimp(members.count) end def create_groups_for_mailchimp_member(list, subscribed_list, member) list.fetch(:groups, []).each do |grouping| groups = member[grouping["name"].downcase] || "" groups = groups.split(",").map(&:strip) groups.each do |group| subscribed_list.groupings.create({ :mailchimp_id => grouping["id"], :name => grouping["name"], :group => group, }) end end end def mailchimp_to_artfully_update_members(list_id) mailchimp_list_members(list_id).select do |member| organization_people_emails.include?(member["email address"]) end end def sync_merged_loser_to_mailchimp(email) unsubscribe_email(email) end def sync_merged_winner_to_mailchimp(person_id, new_lists) person = Person.find(person_id) new_lists.each do |list_id| gibbon.lists.subscribe({ :id => list_id, :email => { :email => person.email, }, :merge_vars => { "FNAME" => person.first_name, "LNAME" => person.last_name, }, :double_optin => false }) end end def sync_mailchimp_webhook_new_subscriber(list_id, data) if person = organization.people.find_by_email(data["email"]) subscribed_list = person.subscribed_lists.where(:list_id => list_id).first subscribed_list ||= person.subscribed_lists.create(:list_id => list_id, :sync => false) subscribed_list.confirmed = true subscribed_list.bounced = false subscribed_list.save return sync_mailchimp_webhook_update_person(list_id, data) end person = organization.people.create({ :first_name => data["merges"]["FNAME"], :last_name => data["merges"]["LNAME"], :email => data["email"], :skip_sync_to_mailchimp => true, }) subscribed_list = person.subscribed_lists.create(:list_id => list_id, :sync => false) subscribed_list.confirmed = true subscribed_list.save note = person.notes.build({ :text => "Imported from MailChimp", :occurred_at => Time.now }) note.organization_id = organization_id note.save person end def sync_mailchimp_webhook_update_person(list_id, data) person = organization.people.find_by_email(data["email"]) if person.nil? Rails.logger.warn "WARNING: Mailchimp sent an update webhook with an out of date email: #{data["email"]}" return end return if person.do_not_email? data["merges"].each do |attribute, value| attribute = mailchimp_merges_to_artfully[attribute] person.send("#{attribute}=", value) if attribute end subscribed_list = person.subscribed_lists.where(:list_id => list_id).first if subscribed_list data["merges"].fetch("GROUPINGS", {}).each do |_, grouping| # Clear out all groupings so we only have an up to date list subscribed_list.groupings.where(:name => grouping["name"]).delete_all groups = grouping["groups"].split(",").map(&:strip) groups.each do |group| subscribed_list.groupings.create({ :mailchimp_id => grouping["id"], :name => grouping["name"], :group => group, }) end end end person.subscribed_lists.not_bounced.each do |subscribed_list| next if subscribed_list.list_id == list_id gibbon.lists.update_member({ :id => subscribed_list.list_id, :email => { :email => person.email, }, :merge_vars => { "FNAME" => person.first_name, "LNAME" => person.last_name, }, }) end person.skip_sync_to_mailchimp = true person.save person end def sync_mailchimp_webhook_cleaned(list_id, data) person = organization.people.find_by_email(data["email"]) return if person.nil? reason = (data["reason"] == "hard" ? "a hard bounce" : "abuse") person.subscribed_lists.where(:list_id => list_id).each do |subscribed_list| subscribed_list.bounced = true subscribed_list.confirmed = true subscribed_list.save end person.new_note("MailChimp cleaned #{person.email} from #{list_name(list_id)} because of #{reason}.", Time.now, nil, organization_id) person.save end def sync_mailchimp_webhook_update_person_email(list_id, data) person = organization.people.find_by_email(data["old_email"]) return if person.nil? || person.do_not_email? person.update_attributes(:email => data["new_email"], :skip_sync_to_mailchimp => true) person.subscribed_lists.each do |subscribed_list| next if subscribed_list.list_id == list_id gibbon.lists.update_member({ :id => subscribed_list.list_id, :email => { :email => data["old_email"], }, :merge_vars => { "EMAIL" => data["new_email"] } }) end end def list_name(list_id) attached_lists.find { |list| list[:list_id] == list_id }[:list_name] end def sync_mailchimp_webhook_member_unsubscribe(list_id, data) person = organization.people.find_by_email(data["email"]) return unless person person.new_note("Unsubscribed in MailChimp from #{list_name(list_id)}", Time.now, nil, organization_id) person.subscribed_lists.where(:list_id => list_id).each do |subscribed_list| subscribed_list.sync = false subscribed_list.destroy end person.opted_out_lists << list_id person.opted_out_lists.uniq! person.save end def list_attached?(list_id) attached_lists.detect do |list| list[:list_id] == list_id end end def sync_mailchimp_webhook_campaign_sent(list_id, data) return unless list_attached?(list_id) Rails.logger.debug("MAILCHIMP sync_mailchimp_webhook_campaign_sent: Starting sync") start = Time.now occurred_at = Time.now emails = [] page = 0 begin response = gibbon.reports.sent_to(:cid => data["id"], :opts => {:start => page, :limit => 100}) response["data"].each do |user| emails << user.fetch("member").fetch("email") end page += 1 end while response["total"] > emails.count Rails.logger.debug("MAILCHIMP sync_mailchimp_webhook_campaign_sent: [#{emails.count}] emails found") system_action = SystemAction.for_organization(organization) system_action.details = %{"#{data["subject"]}" delivered to #{emails.count} subscribers of #{list_name(list_id)}.} system_action.subtype = "Mailchimp (Sent)" system_action.save people_ids = organization.people .joins(:subscribed_lists) .where(:deleted_at => nil) .where(:email => emails) .where(:do_not_email => false) .where('subscribed_lists.list_id = ?', list_id) .pluck(:id) Rails.logger.debug("MAILCHIMP sync_mailchimp_webhook_campaign_sent: [#{people_ids.length}] people found") hear_actions = [] people_ids.each_with_index do |person_id, index| hear_action = MailchimpHearAction.for_organization(organization) hear_action.details = %{"#{data["subject"].truncate(25)}" via #{list_name(list_id)} MailChimp list.} hear_action.occurred_at = occurred_at hear_action.subtype = "Mailchimp (Sent)" hear_action.person_id = person_id hear_action.hide_on_recent_activity = true hear_action.external_reference = data["id"] hear_actions << hear_action if ((index+1) % 100 == 0) hear_actions = flush_actions(hear_actions) end end # Final flush hear_actions = flush_actions(hear_actions) Rails.logger.debug("MAILCHIMP sync_mailchimp_webhook_campaign_sent: Finished with people") schedule_open_check(data["id"], 1.hour.from_now) finish = Time.now Rails.logger.debug("MAILCHIMP sync_mailchimp_webhook_campaign_sent: took #{finish-start} seconds") AdminMailer.mailchimp_sent(organization, data, list_id, emails, people_ids.length, finish - start).deliver end def flush_actions(hear_actions) if !hear_actions.empty? Rails.logger.debug("MAILCHIMP sync_mailchimp_webhook_campaign_sent: Importing [#{hear_actions.length}] actions") HearAction.import hear_actions Rails.logger.debug("MAILCHIMP sync_mailchimp_webhook_campaign_sent: Flushing actions") hear_actions = [] end hear_actions end def sync_artfully_open_check(campaign_id) members = [] page = 0 begin response = gibbon.reports.opened(:cid => campaign_id, :opts => {:start => page}) response["data"].each do |member| members << { :email => member.fetch("member").fetch("email"), :opened? => member.fetch("opens").to_i > 0, } end page += 1 end while response["total"].to_i > members.count response = gibbon.campaigns.list(:filters => { :campaign_id => campaign_id }) campaign = response["data"].first occurred_at = Time.now utc = ActiveSupport::TimeZone.new("UTC") send_time = utc.parse(campaign["send_time"]) members.each do |member| next unless member[:opened?] person = organization.people.find_by_email(member[:email]) next unless person action = person.actions.where({ :type => MailchimpHearAction, :external_reference => campaign_id, }).first next unless action next unless action.subtype == "Mailchimp (Sent)" action.details = %{"#{campaign["subject"].truncate(25)}" via the #{list_name(campaign["list_id"])} MailChimp list.} action.subtype = "Mailchimp (Opened)" action.occurred_at = occurred_at action.save end difference = Time.now.utc - send_time if difference < 1.week schedule_open_check(campaign_id, 1.hour.from_now) elsif 1.week < difference && difference < 2.weeks schedule_open_check(campaign_id, 1.day.from_now) end end def schedule_open_check(campaign_id, time) job = MailchimpSyncJob.new(self, { :type => "open_check", :campaign_id => campaign_id, }) Delayed::Job.enqueue(job, :queue => QUEUE, :run_at => time) end def sync_artfully_grouping_update(person_id) person = Person.find_by_id(person_id) return unless person person.subscribed_lists.each do |subscribed_list| list = attached_lists.detect do |list| list[:list_id] == subscribed_list.list_id end next unless list groupings = list.fetch(:groups, []).map do |group| groups = subscribed_list.groupings.where(:name => group["name"]) { :id => group["id"], :groups => groups.map(&:group), } end gibbon.lists.update_member({ :id => subscribed_list.list_id, :email => { :email => person.email, }, :merge_vars => { "GROUPINGS" => groupings, }, }) end end def sync_artfully_person_subscribe(person_id, new_list_id, single_optin) person = organization.people.find_by_id(person_id) return unless person subscribe_email(person.email, person.first_name, person.last_name, new_list_id, !single_optin) end def sync_artfully_person_unsubscribe(person_id, new_list_id) person = organization.people.find_by_id(person_id) return unless person unsubscribe_email(person.email, new_list_id) end # # The API here is unclear. Sometimes this returns true, sometimes nothing # and sometimes the array of subscribed lists # def sync_artfully_person_update(person_id, person_changes, bounced_list_ids = []) person = organization.people.find_by_id(person_id) return unless person merge_vars = {} merge_vars["FNAME"] = person_changes["first_name"][1] if person_changes["first_name"] merge_vars["LNAME"] = person_changes["last_name"][1] if person_changes["last_name"] email = person_changes["email"] ? person_changes["email"][0] : person.email if person_changes.has_key?("do_not_email") && person_changes["do_not_email"][1] return attached_lists.all? do |list| unsubscribe_email(email, list[:list_id]) end end return unless sync_person?(person) merge_vars["EMAIL"] = person_changes["email"][1] if person_changes["email"] # go over the person's subscribed lists and update them person.subscribed_lists.each do |subscribed_list| next unless attached_lists.any? { |list| list[:list_id] == subscribed_list.list_id } # Bounced emails are no longer subscribed so we should # resubscribe them without sending a notification if merge_vars.has_key?("EMAIL") && bounced_list_ids.include?(subscribed_list.id) subscribe_email(merge_vars["EMAIL"], person.first_name, person.last_name, subscribed_list.list_id, false) next end gibbon.lists.update_member({ :id => subscribed_list.list_id, :email => { :email => email, }, :merge_vars => merge_vars }) end end def sync_artfully_segment(segment_id, list_id) segment = organization.segments.find(segment_id) return unless segment sync_artfully_people_to_mailchimp(segment.people, list_id) end def sync_artfully_search(search_id, list_id) search = organization.searches.find(search_id) return unless search sync_artfully_people_to_mailchimp(search.people, list_id) end def sync_artfully_advanced_search_segment(advanced_search_segment_id, list_id) advanced_search_segment = organization.advanced_search_segments.find(advanced_search_segment_id) return unless advanced_search_segment sync_artfully_people_to_mailchimp(advanced_search_segment.all_people, list_id) end def sync_artfully_advanced_search(advanced_search_id, list_id) advanced_search = organization.advanced_searches.find(advanced_search_id) return unless advanced_search sync_artfully_people_to_mailchimp(advanced_search.all_people, list_id) end def sync_artfully_people_to_mailchimp(people, list_id) mailchimp_emails = mailchimp_list_members(list_id).map { |member| member["email address"] } # find everyone not in mailchimp list subscribe_people = people.select do |person| person.email.present? && !mailchimp_emails.include?(person.email) end subscribe_people.each do |person| subscribe_email(person.email, person.first_name, person.last_name, list_id) person.subscribed_lists.create(:list_id => list_id, :sync => false) end end def sync_artfully_segment_add_group(segment_id, grouping) segment = organization.segments.find(segment_id) return unless segment sync_artfully_people_add_group(segment.people, grouping) end def sync_artfully_search_add_group(search_id, grouping) search = organization.searches.find(search_id) return unless search sync_artfully_people_add_group(search.people, grouping) end def sync_artfully_advanced_search_segment_add_group(advanced_search_segment_id, grouping) advanced_search_segment = organization.advanced_search_segments.find(advanced_search_segment_id) return unless advanced_search_segment sync_artfully_people_add_group(advanced_search_segment.all_people, grouping) end def sync_artfully_advanced_search_add_group(advanced_search_id, grouping) advanced_search = organization.advanced_searches.find(advanced_search_id) return unless advanced_search sync_artfully_people_add_group(advanced_search.all_people, grouping) end def sync_artfully_people_add_group(people, grouping) grouping_id, group = grouping.split("|") list = attached_lists.detect do |list| list.fetch(:groups, []).any? do |grouping| grouping["id"] == grouping_id.to_i end end return unless list grouping = list.fetch(:groups, []).detect do |grouping| grouping["id"] == grouping_id.to_i end return unless grouping mailchimp_emails = mailchimp_list_members(list[:list_id]).map { |member| member["email address"] } # find everyone in mailchimp list subscribed_people = people.select do |person| person.email.present? && mailchimp_emails.include?(person.email) end subscribed_people.each do |person| subscribed_list = person.subscribed_lists.where(:list_id => list[:list_id]).first next unless subscribed_list list_grouping = subscribed_list.groupings.create({ :mailchimp_id => grouping_id, :name => grouping["name"], :group => group, }) sync_artfully_grouping_update(person.id) if list_grouping.persisted? end end def mailchimp_list_members(list_id) response = mailchimp_exporter.list(:id => list_id).to_a headers = JSON.parse(response.shift) headers.map!(&:downcase) members = response.map { |line| JSON.parse(line) } merge_variables = list_merge_variables(list_id) headers = headers.map do |header| if merge_variables.has_key?(header) merge_variables[header] else header end end grouping_headers = attached_lists.flat_map do |list| list.fetch(:groups, []).map { |group| group["name"] } end.map(&:downcase) members.collect do |member| member_hash = {} attributes = mailchimp_attributes + grouping_headers attributes.inject({}) do |member_hash, attribute| unless headers.index(attribute).nil? member_hash[attribute] = member[headers.index(attribute)] end member_hash end end end def list_merge_variables(list_id) response = gibbon.lists.merge_vars(:id => [list_id]) list = response["data"].first variables = list["merge_vars"] variables.inject({}) do |hash, variable| case variable["tag"] when "EMAIL" hash[variable["name"].downcase] = "email address" when "FNAME" hash[variable["name"].downcase] = "first name" when "LNAME" hash[variable["name"].downcase] = "last name" end hash end end def default_list? default_list_id.present? end def display_default_list_alert? api_key.present? && !default_list? && !hide_default_list_alert end private def gibbon @gibbon ||= Gibbon::API.new(api_key) end def mailchimp_exporter @mailchimp_exporter ||= gibbon.get_exporter end def check_valid_api_key? return unless api_key return if valid_api_key? errors.add(:api_key, "is invalid") end def mailchimp_attributes ["email address", "first name", "last name"] end def mailchimp_attributes_to_artfully { "email address" => "email", "first name" => "first_name", "last name" => "last_name" } end def mailchimp_merges_to_artfully { "EMAIL" => "email", "FNAME" => "first_name", "LNAME" => "last_name", } end def organization_people_emails organization.people.pluck(:email) end def kit_cancelled self.old_api_key = api_key self.api_key = nil save Delayed::Job.enqueue(MailchimpSyncJob.new(self, :type => "kit_cancelled"), :queue => QUEUE) end def set_count_from_mailchimp(count) self.count_from_mailchimp = (count_from_mailchimp || 0) + count save end def set_count_merged_mailchimp(count) self.count_merged_mailchimp = (count_merged_mailchimp || 0) + count save end def sync_person?(person) !person.do_not_email && !person.subscribed_lists.empty? end def unsubscribe_email(email, list_id = nil) if list_id lists = [list_id] else lists = attached_lists.map { |list| list[:list_id] } end lists.each do |list_id| gibbon.lists.unsubscribe({ :id => list_id, :email => { :email => email, }, :send_goodbye => false, :send_notify => false, :delete_member => true, }) end end def subscribe_email(email, first_name, last_name, list_id, send_optin = true) response = gibbon.lists.subscribe({ :id => list_id, :email => { :email => email, }, :double_optin => send_optin, :merge_vars => { "FNAME" => first_name, "LNAME" => last_name, }, }) end def find_list_name(list_id) lists.find { |list| list[1] == list_id }[0] end end