Работа с MailChimp API. Часть вторая

Вступление

Как было выяснено в предыдущей части, задачу по реализации подписки на рассылку вакансий я решил не совсем верно. Казалось бы, всё самое сложное уже сделано и остается самая малость — ввести элемент градации для подписчиков и рассылки. Но как оказалось, количество потраченного времени на реализацию этой фичи равнялось количеству времени потраченному на саму рассылку.

Постановка задачи

Нужно чтобы рассылка уходила по предпочтениям пользователя и его географическому положению, предпочтения включают специализацию, а география — это город или возможность удаленной работы. iOS-разработчики из Бобруйска должны получать ios вакансии Бобруйска, а любители удаленной работы должны получать вакансии, где указана возможность удаленного сотрудничества.

Решение

Пока изучал варианты решения первой части поставленной задачи, наткнулся на пост на хабре, где автор рассказывал про сегменты и рассылку по сегментам, реализованную через MailChilm API. Был только один нюанс, он писал свой код в 2012 году, и версия API была 1.3, а сейчас уже 3.0. Там значительно всё изменилось, но некоторая обратная совместимость сохранилась, по крайней мере идеологическая =)

Основная мысль вот в чем: помимо классических листов рассылки в MailChimp можно создавать дополнительные фильтры (сегменты) по листу, при этом сегмент может быть создан по различным признакам, например, все пользователи, которые добавились в рассылку до определенной даты или после, или пользователи email которых находится на gmail.com, или пользователи с именем Александр. Вот за эту идею я и решил ухватиться.

Несмотря на то, что документация по API структурно написана достаточно интуитивно, но вот с примерами там есть некоторая беда, в примерах дается только минимально необходимый набор параметров. Т.е. если для создания компании необходимо передать только html-текст, plain-текст и id листа рассылки, то в примере будут обозначены только эти параметры, а то, что метод может принимать еще вагон дополнительных необязательных параметров, конечно, сказано, но как это сделать не всегда бывает очевидно. Поэтому многое решалось экспериментами.

Несмотря на то, что сегменты могут быть построены по огромному количеству параметров, я не нашел способа добавлять свои атрибуты, т.е. никакое дополнительное кастомное поле создать для пользователя нельзя, что меня несколько огорчило, это было бы самое крутое решение. Тогда я пошел по пути наименьшего сопротивления. При добавлении подписчика дополнительно передаются 2 поля FNAME и LNAME, куда, как логично предположить, должны быть вписаны имя и фамилия, но мы для AppJobs используем только поле FNAME, поэтому LNAME я решил задействовать для собственных нужд.

Мне нужны фильтры! Следовательно в поле LNAME буду писать некоторую служебную информацию. На сайте AppJobs введены специализации: ios-разработчик, andriod-разработчик, дизайнер и т.д — а так же регион в формате страна+город и удаленная работа. Соответственно, первой реализацией фильтров было писать в поле фамилии желаемую специализацию подписчика и регион отдельными словами (забегая вперед: сегменты поддерживают выборку по включению определенного слова), например, ios Россия-Саратов удаленно. Осталось научиться создавать сегменты.

В интерфейсе MailChimp это делается в разделе листов рассылки. Нужно перейти к одному из листов и там в верхней панели выбрать создание сегмента

Раз уж заговорили про ios-разработчика из Саратова, то попробуем создать сегмент для него

Тут все очевидно, нужно чтобы фильтр соответствовал обоим параметрам, а именно, содержал в поле фамилии слово ios и Россия-Саратов.  Contacts match может принимать 2 состояния all и any, т.е. совпадение со всеми правилами или с любым из перечисленных. Нам, конечно нужно жесткое соответствие обоим правилам. После кнопки Preview Segment, в следующем окне будут отображены подписчики, которые удовлетворяют заданным условиям, не забываем сохранить сегмент, если это нужно. Технически, можно все сегменты создать руками, если их не очень много, а потом на стороне сайта обрабатывать их id, но в моем случае сервис ориентирован на все страны СНГ, а это уже сильно больше «нескольких сегментов» — тысячи городов в каждой стране. Вариант создавать сегменты руками — не вариант. Поэтому нужны методы API.

Для создания сегментов существует post метод, который в качестве параметров принимает имя и правила сегментации. Я написал в своем классе обертку для этого метода

/**
 * Создание сегмента
 *
 * @param string $name
 * @param string $list_id
 * @param array  $cond
 *
 * @return mixed
 */
public function createSegment($name = '', $list_id = '', $cond = array())
{
	$data = array('name' => $name, 'options' => $cond);
	
	$res = $this->request('lists/' . $list_id . '/segments', 'post', $data);
	
	return $res;
}

Для нашего случая, правило $cond должно содержать массив

$cond = array(
	'match'      => 'all',
	'conditions' => array(
		array(
			'condition_type' => 'TextMerge',
			'field'          => 'LNAME',
			'op'             => 'contains',
			'value'          => 'ios'
		),
		array(
			'condition_type' => 'TextMerge',
			'field'          => 'LNAME',
			'op'             => 'contains',
			'value'          => 'Россия-Саратов'
		)
	)
);

А если нужны те, кто готов работать удаленно, то правило примет такой вид

$cond = array(
    'match'      => 'all',
    'conditions' => array(
        array(
            'condition_type' => 'TextMerge',
            'field'          => 'LNAME',
            'op'             => 'contains',
            'value'          => 'ios'
        ),
        array(
            'condition_type' => 'TextMerge',
            'field'          => 'LNAME',
            'op'             => 'contains',
            'value'          => 'удаленно'
        )
    )
);

Правила создавать научился. Потестировал создание и отправку рассылок по сегментам несколько раз и наткнулся на багулю. Первый раз рассылка нормально отправляется по сегментам, а второй и последующие чего-то никак. Посмотрел в логи и обнаружил, что при попытке создать сегмент с уже существующим именем, MailChimp возвращает ошибку, т.к. не может пересоздать. Имена я, конечно, создавал типовые, чтобы не плодить сегменты с одинаковыми правилами, но разными заголовками. Вообще, мне было странно такое поведение, я ожидал, что при попытке создать уже существующий сегмент, API мне вернет старый id.

Ну да ладно, раз не хочешь создавать, то давай мне верни по имени сегмента его id. Только вот незадача, он так не умеет, от слова вообще. Можно получить информацию о сегменте по его id, а по имени нельзя получить инфу. Жаль, придется изобрести что-то другое.

Посмотрев немногочисленные методы для работы с сегментами придумалось следующее, забираем из mailchimpa список всех доступных сегментов, пихаем это всё в одномерный массив, где ключ равен id сегмента, а значение равно имени, а потом перед созданием нового сегмента с нашей стороны, проверяем наличие его имени в уже созданных. В общем в класс добавился еще один метод работы с АПИ

/**
 * Получить все сегменты в листе
 *
 * @param string $list_id
 *
 * @return array
 */
public function getListSegments($list_id = '')
{
	$segments = $this->request('lists/' . $list_id . '/segments', 'get');
	
	$res = array();
	
	foreach ($segments->segments as $segment_itm) {
		$res[$segment_itm->id] = $segment_itm->name;
	}
	
	return $res;
}

Вот теперь в классе AppjobsMailchimp есть все необходимые методы, чтобы наша рассылка заработала.

И так: получаю новую вакансию, собираю для этой вакансии сегменты и… Эй! А как прицепить несколько сегментов к одной компании?! Что? Нельзя чтоль? Что за бред?!

Пятиминутка театральной пантомимы окончена.

Я в очередной раз немного лажанул. Оказалось, что к компании нельзя прикрепить несколько сегментов. Я думал, что можно будет просто указать id’шники сегментов, которые мне нужны и оправить рассылку, но в глубине души я подозревал, что такой исход возможен, но надеялся на благополучный исход. Не срослось.

Было принято решение изменить логику набора ключевых слов следующим образом: теперь одним куском будет идти специализация и регион. То есть если раньше были отдельные слова, например, наш мобильный разработчик, который пишет на ios и хочет работать удаленно или в офисе в Саратове, а еще он стал писать под android, поле фамилии будет содержать такую строку: ios android удаленно Россия-Саратов. А теперь буду писать так: ios-удаленно android-удаленно ios-Россия-Саратов android-Россия-Саратов. Немного громоздко, зато одной выборкой можно собрать сегмент для ios разработчиков которые могут работать в офисе в Саратове или удаленно.

$cond = array(
    'match'      => 'any',
    'conditions' => array(
        array(
            'condition_type' => 'TextMerge',
            'field'          => 'LNAME',
            'op'             => 'contains',
            'value'          => 'ios-удаленно'
        ),
        array(
            'condition_type' => 'TextMerge',
            'field'          => 'LNAME',
            'op'             => 'contains',
            'value'          => 'ios-Россия-Саратов'
        )
    )
);

Соответственно, то что раньше требовало 2 различных сегмента, теперь выбирается одним, но с правилом match=any.

И в общем случае скрипт рассылки будет содержать такой код:

// Получим все уже созданные сегменты для листа $list_id
$access_segments = $chimp->getListSegments($list_id);

// Попробуем найти новый сегмент среди созданных, если нет, создадим его
$segment_id = array_search($new_segment_name, $access_segments);
if ($segment_id === false) {
    $new_segment = $chimp->createSegment($new_segment_name, $list_id, $new_segment_cond);
    $segment_id  = $new_segment->id;
}

// Создем компанию
$res = $chimp->createCamping($list_id, $email_subject, $from_name, $reply_email, $segment_id);

$camp_id = $res->id;

// Добавлем к ней контент
$res = $chimp->createCampingContent($plain, $html, $camp_id);

// Отправка рассылки
$res = $chimp->sendCamping($camp_id);

Итог

Наконец-то задача, которую подразумевал заказчик выполнена, теперь точно =) Созданный класс все так же лежит на гитхабе. Надеюсь он пригодится не только мне)

Всем рок!

Это я, почтальон Печкин, принес заметку… Работа с API MailChimp

Вступление

Когда-то давно мы всем офисом sawtech выехали на хакатон. По итогам этого действа с моей стороны был создан сайт размещения вакансий AppJobs. Мне было за него немного стыдно, он был немного корявый, неоптимизированный, с плохой структуруой, его по идее нужно было писать либо на frameworke, либо на голом php, но я умел только wordpress. Но во-первых, если ты вышел на рынок с продуктом, за который тебе не стыдно, значит ты поздно вышел на рынок (мопед не мой, я только объяву дал. Кому принадлежит эта цитата не помню), А во-вторых, сейчас я уже не комплексую по поводу WP, если ты хорошо умеешь работать топором, и с его помощью сможешь вырезать скульптуру, то почему бы и нет?

Хакатон был «заказной», всё что там создавалось, создавалось не совсем для себя, а для нашего хорошего знакомого и партнера. В общем, сайтик был сделан, мы его потом еще немого допиливали, а потом благополучно забили. По размещенным вакансиям видно, что он не очень живой, хотя какие-то люди что-то на нем размещали.

И вот с месяц назад, наш старый друг пишет письмо: «А давайте мы его реанимируем, и начнем с email подписки на вакансии?» Почему бы, собственно, и нет?

Постановка задачи

Забегая вперед: всегда, всегда(!) составляй письменное ТЗ до того как начал работать, даже с друзьями, даже если работаешь с мамой.  Чтобы потом не случилось «ой». В данном случае ТЗ я получил в следующем виде: нужно сделать подписку на новые вакансии, чтобы когда добавиляется вакансия, автоматически уходила рассылка. К такому заданию была приложена картинка

Что я понял из постановки задачи:

  1. Нужна форма подписки на email рассылку
  2. нужна реализация самой рассылки
    1. Подписка пользователей
    2. Возможность отписаться
    3. Собственно сами email сообщения

Вот как-то так.

Реализация

Т.к. из предпочтений заказчик высказал варианты использования MailGun и MailChimp, то я сразу пошел гуглить API и того и другого. Вариант MailGun мне не нравился, потому, на сколько я понял, сервис предоставляет шлюз рассылки большого объема сообщений, это хорошо, но при этом всю процедуру подписки, отписки, и управления подписчиками, придется делать самому — это минус.

API MailChimp мне показалось мудреным и потратив пару дней я почти было сдался, но одним прекрасным утром меня пробило и я всё понял. Проблема была не в документации, а в моем понимании предметной области и механизмов работы сервиса.

Поясню. Я думал, что в mailchimp есть листы рассылки, и есть письма. Можно зайти в лист и создать для него новый email, либо наоборот, создаем email и указываем ему по какому листу рассылаться. А со стороны API я это представлял примерно так:

  1. добавление пользователей — это один запрос (так оно и оказалось).
  2. новая email рассылка — тоже один запрос, набрал email, в запросе указал с каким листом работать и отправил на сервер mailchimpa.

Со вторым я крупно ошибся, и по этой причине долго ковырялся. В понятиях MailChimp есть такая штука как Campaigns (компания), типа рассылочной компании. У меня почему-то ассоциации с предвыборной или агитационной компанией =) Таким образом сначала я должен создать компанию, выбрать кучу параметров, в том числе шаблон письма, с каким листом оно будет работать, и только после всех настроек можно запустить компанию в работу, т.е. разослать email’ы.

Но пойдем по порядку. Чтобы работать с API нужно получить ключ доступа, сейчас это находится в разделе Profile → Extras → API keys, к тому же, из особенностей, все запросы отправляются не на единый url, а на определенные поддомены значение которых соответствует поддомену админ-панели, проще картинкой объяснить

А адрес запросов равен https://<dc>.api.mailchimp.com/3.0/, где вместо <dc> у меня будет us15.

Подписать человека на рассылку, простейший вариант:

/**
 * Добаление подписчика
 *
 * @param string $list
 * @param string $email
 * @param string $name
 */
function addSubscriber($list = '', $email = '', $fname = '', $lname = '')
{
	$data = array(
		'email_address' => $email,
		'status'        => 'pending',
		'merge_fields'  => array('FNAME' => $fname, 'LNAME' => $lname)
	);
	
	$res = $this->request('lists/' . $list . '/members', 'post', $data);
	
	$return = 'На указанную почту придет письмо с подтвержением подписки.';
	
	if ($res->status == 400) {
		switch ($res->title) {
			case 'Member Exists':
				$return = 'Вы уже подписались ранее';
				break;
			
			default:
				$return = $res->title;
				break;
		}
	}
	
	return $return;
}

Прикольный момент, если в массиве параметров в status передать значение не pending, а subscribed, то пользователь будет подписан без дополнительного подтверждения.

А теперь опишу функции, которые умеют создавать компанию, и отправлять рассылки

<?php

/**
 * Создание компании
 *
 * @param string $list_id
 * @param string $subj
 * @param string $from_name
 * @param string $reply_to
 *
 * @return mixed
 */
public function createCamping($list_id = '', $subj, $from_name, $reply_to)
{
	$data = array(
		'type'       => 'regular',
		'recipients' => array('list_id' => $list_id),
		'settings'   => array(
			'subject_line' => $subj,
			'reply_to'     => $reply_to,
			'from_name'    => $from_name
		)
	);
	
	$res = $this->request('campaigns', 'post', $data);
	
	return $res;
}

/**
 * Создание текста для компании
 *
 * @param $html
 * @param $id_camping
 *
 * @return mixed
 */
public function createCampingContent($plain, $html, $id_camping)
{
	$data = array('plain_text' => $plain, 'html' => $html);
	
	$res = $this->request('campaigns/' . $id_camping . '/content', 'put', $data);
	
	return $res;
}

/**
 * Отправка компании
 *
 * @param $id_camping id компани
 */
public function sendCamping($id_camping = '')
{
	$res = $this->request('campaigns/' . $id_camping . '/actions/send', 'post');
	
	return $res;
}

Из вышеописанных функций можно сделать вывод, что при создании компании нельзя сразу создать контент для неё, по крайней мере, я не нашел этого в документации. Поэтому нужно отдельным запросом отправлять контент. Кстати, если попробовать изменить уже разосланную компанию, то вернется ошибка, т.к. политика MailChimp этого не позволяет, что мне показалось странно. Из этого же следует, что каждая новая рассылка будет создавать новую компанию. Вот такая вот странная политика.

Работать эти функции будут примерно так

$res = createCamping($list_id, 'Новая вакансия на AppJobs.ru', 'AppJobs', 'from@email.domain');

// Получим id новой компании
$camp_id = $res->id;

// Добавлем к ней контент
$res = createCampingContent($plain, $html, $camp_id);

// Отправка рассылки
$res = $chimp->sendCamping($camp_id);

Я, будучи крутым веб-разработчиком, естественно пожелал обернуть всё это в ООП вид, в итоге у меня получился такой класс:

Класс работы с MailChimp API

<?php


/**
 * Класс для работы с АПИ
 *
 * Class AppjobsMailchimp
 */
class AppjobsMailchimp
{
	
	/**
	 * АПИ ключь
	 *
	 * @var
	 */
	protected $api_key;
	
	/**
	 * Адрес запроса к апи
	 *
	 * @var string
	 */
	protected $url = 'https://<dc>.api.mailchimp.com/3.0/';
	
	/**
	 * AppjobsMailchimp constructor.
	 *
	 * @param $key
	 * @param $dc
	 */
	public function __construct($key, $dc)
	{
		$this->api_key = $key;
		$this->url     = str_replace('<dc>', $dc, $this->url);
	}
	
	
	/**
	 * Выполенение запроса к АПИ
	 *
	 * @param string     $method метод запроса
	 * @param array|bool $data   Данные
	 *
	 * @return mixed
	 */
	protected function request($method = '', $type = 'post', $data = false)
	{
		$url = $this->url . $method;
		
		$ch = curl_init();
		curl_setopt($ch, CURLOPT_URL, $url);
		curl_setopt($ch, CURLOPT_HTTPHEADER, array(
			'Accept: application/vnd.api+json',
			'Content-Type: application/vnd.api+json',
			'Authorization: apikey ' . $this->api_key
		));
		curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
		
		switch ($type) {
			case 'post':
				curl_setopt($ch, CURLOPT_POST, true);
				if (is_array($data)) {
					curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
				}
				break;
			
			case 'get':
				$query = http_build_query($data, '', '&');
				curl_setopt($ch, CURLOPT_URL, $url . '?' . $query);
				break;
			
			case 'delete':
				curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
				break;
			
			case 'patch':
				curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PATCH');
				if (is_array($data)) {
					curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
				}
				break;
			
			case 'put':
				curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT');
				if (is_array($data)) {
					curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
				}
				break;
		}
		
		$out = curl_exec($ch);
		curl_close($ch);
		
		return json_decode($out);
	}
	
	
	/**
	 * Получить список доступных листов рассылки
	 *
	 * @return array|mixed|object
	 */
	public function getLists()
	{
		return $this->request('lists', 'get');
	}
	
	/**
	 * Добаление подписчика
	 *
	 * @param string $list
	 * @param string $email
	 * @param string $name
	 */
	public function addSubscriber($list = '', $email = '', $name = '')
	{
		$data = array(
			'email_address' => $email,
			'status'        => 'pending',
			'merge_fields'  => array('FNAME' => $name, 'LNAME' => '')
		);
		
		$res = $this->request('lists/' . $list . '/members', 'post', $data);
		
		$return = 'На указанную почту придет письмо с подтвержением подписки.';
		
		if ($res->status == 400) {
			switch ($res->title) {
				case 'Member Exists':
					$return = 'Вы уже подписались ранее';
					break;
				
				default:
					$return = $res->title;
					break;
			}
		}
		
		return $return;
	}
	
	/**
	 * Списко компаний
	 *
	 * @return mixed
	 */
	public function getCampaigns()
	{
		$res = $this->request('campaigns', 'get');
		
		return $res;
	}
	
	
	/**
	 * Создание компании
	 *
	 * @param string $list_id
	 * @param string $subj
	 * @param string $from_name
	 * @param string $reply_to
	 *
	 * @return mixed
	 */
	public function createCamping($list_id = '', $subj, $from_name, $reply_to)
	{
		$data = array(
			'type'       => 'regular',
			'recipients' => array('list_id' => $list_id),
			'settings'   => array(
				'subject_line' => $subj,
				'reply_to'     => $reply_to,
				'from_name'    => $from_name
			)
		);
		
		$res = $this->request('campaigns', 'post', $data);
		
		return $res;
	}
	
	/**
	 * Создание текста для компании
	 *
	 * @param $html
	 * @param $id_camping
	 *
	 * @return mixed
	 */
	public function createCampingContent($plain, $html, $id_camping)
	{
		$data = array('plain_text' => $plain, 'html' => $html);
		
		$res = $this->request('campaigns/' . $id_camping . '/content', 'put', $data);
		
		return $res;
	}
	
	
	/**
	 * Отправка тестового письма для компании
	 *
	 * @param string $id_camping компании
	 * @param string $email      куда отправлять тестовое сообщение
	 *
	 * @return mixed
	 */
	public function testCamping($id_camping, $email = '')
	{
		$data = array('test_emails' => array($email), 'send_type' => 'html');
		
		$res = $this->request('campaigns/' . $id_camping . '/actions/test', 'post', $data);
		
		return $res;
	}
	
	
	/**
	 * Отправка компании
	 *
	 * @param $id_camping id компани
	 */
	public function sendCamping($id_camping = '')
	{
		$res = $this->request('campaigns/' . $id_camping . '/actions/send', 'post');
		
		return $res;
	}
	
	/**
	 * Получить список доступных шаблонов
	 *
	 * @return mixed
	 */
	public function getTemplates()
	{
		$res = $this->request('templates/', 'get');
		
		return $res;
	}
}

Для WP я еще сделал обертку в виде плагина, который использует созданный класс. Пока не буду приводить код, т.к. он заточен под конкретный сайт, когда-нибудь потом я его пересоберу в более универсальный вариант и выложу.

Итог

Поставленную задачу я решил, но есть одно но. Когда я отправил заказчику результат работы, он сказал так: «Круто! Только я хотел, чтобы можно было подписываться на конкретный раздел вакансий, а не на любую. Потому что дизайнеры не хотят получать вакансии разработчика». Дальше были мои непечатные мысли. Но я уже знаю, что у MailChimp есть какая-то штука — сегменты. Вот в сторону сегментов я и буду дальше рыть. Не переключайтесь, после рекламы мы продолжим!

Всем рок!