/**
* This file is part of alf.io.
*
* alf.io is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* alf.io is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with alf.io. If not, see <http://www.gnu.org/licenses/>.
*/
package alfio.plugin.mailchimp;
import alfio.model.*;
import alfio.model.plugin.PluginConfigOption;
import alfio.model.system.ComponentType;
import alfio.plugin.PluginDataStorageProvider;
import alfio.plugin.PluginDataStorageProvider.PluginDataStorage;
import alfio.plugin.ReservationConfirmationPlugin;
import alfio.plugin.TicketAssignmentPlugin;
import alfio.plugin.WaitingQueueSubscriptionPlugin;
import alfio.util.Json;
import com.squareup.okhttp.*;
import lombok.extern.log4j.Log4j2;
import org.apache.commons.codec.binary.Hex;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.*;
@Log4j2
public class MailChimpPlugin implements ReservationConfirmationPlugin, TicketAssignmentPlugin, WaitingQueueSubscriptionPlugin {
private static final String DATA_CENTER = "dataCenter";
private static final String API_KEY = "apiKey";
private static final String LIST_ID = "listId";
private static final String LIST_ADDRESS = "https://%s.api.mailchimp.com/3.0/lists/%s/";
private static final String LIST_MEMBERS = "members/";
private static final String MERGE_FIELDS = "merge-fields/";
private static final String FAILURE_MSG = "cannot add user {email: %s, name:%s, language: %s} to the list (%s)";
private static final String ALFIO_EVENT_KEY = "ALFIO_EKEY";
private static final String APPLICATION_JSON = "application/json";
private final String id = "alfio.mailchimp";
private final PluginDataStorage pluginDataStorage;
private final OkHttpClient httpClient = new OkHttpClient();
public MailChimpPlugin(PluginDataStorageProvider pluginDataStorageProvider) {
this.pluginDataStorage = pluginDataStorageProvider.getDataStorage(id);
}
@Override
public void onTicketAssignment(Ticket ticket) {
Event event = pluginDataStorage.getEventById(ticket.getEventId());
CustomerName customerName = new CustomerName(ticket.getFullName(), ticket.getFirstName(), ticket.getLastName(), event);
subscribeUser(ticket.getEmail(), customerName, ticket.getUserLanguage(), ticket.getEventId(), event.getShortName());
}
@Override
public void onReservationConfirmation(TicketReservation ticketReservation, int eventId) {
Event event = pluginDataStorage.getEventById(eventId);
CustomerName customerName = new CustomerName(ticketReservation.getFullName(), ticketReservation.getFirstName(), ticketReservation.getLastName(), event);
subscribeUser(ticketReservation.getEmail(), customerName, ticketReservation.getUserLanguage(), eventId, event.getShortName());
}
@Override
public void onWaitingQueueSubscription(WaitingQueueSubscription waitingQueueSubscription) {
Event event = pluginDataStorage.getEventById(waitingQueueSubscription.getEventId());
CustomerName customerName = new CustomerName(waitingQueueSubscription.getFullName(), waitingQueueSubscription.getFirstName(), waitingQueueSubscription.getLastName(), event);
subscribeUser(waitingQueueSubscription.getEmailAddress(), customerName, waitingQueueSubscription.getUserLanguage(), waitingQueueSubscription.getEventId(), event.getShortName());
}
@Override
public String getId() {
return id;
}
@Override
public String getName() {
return "Mailchimp newsletter subscriber";
}
@Override
public boolean isEnabled(int eventId) {
return pluginDataStorage.getConfigValue(ENABLED_CONF_NAME, eventId).map(Boolean::parseBoolean).orElse(false);
}
@Override
public Collection<PluginConfigOption> getConfigOptions(int eventId) {
return Arrays.asList(new PluginConfigOption(getId(), eventId, DATA_CENTER, "", "The MailChimp data center used by your account (e.g. us6)", ComponentType.TEXT),
new PluginConfigOption(getId(), eventId, API_KEY, "", "the Mailchimp API Key", ComponentType.TEXT),
new PluginConfigOption(getId(), eventId, LIST_ID, "", "the list ID", ComponentType.TEXT));
}
@Override
public void install(int eventId) {
getConfigOptions(eventId).forEach(o -> pluginDataStorage.insertConfigValue(eventId, o.getOptionName(), o.getOptionValue(), o.getDescription(), o.getComponentType()));
}
private Optional<String> getListAddress(int eventId, String email, CustomerName name, String language) {
Optional<String> dataCenter = pluginDataStorage.getConfigValue(DATA_CENTER, eventId);
Optional<String> listId = pluginDataStorage.getConfigValue(LIST_ID, eventId);
if(dataCenter.isPresent() && listId.isPresent()) {
return Optional.of(String.format(LIST_ADDRESS, dataCenter.get(), listId.get()));
} else {
pluginDataStorage.registerFailure(String.format(FAILURE_MSG, email, name, language, "check listId and dataCenter"), eventId);
}
return Optional.empty();
}
private Optional<String> getApiKey(int eventId, String email, CustomerName name, String language) {
Optional<String> apiKey = pluginDataStorage.getConfigValue(API_KEY, eventId);
if(!apiKey.isPresent()) {
pluginDataStorage.registerFailure(String.format(FAILURE_MSG, email, name, language, "missing API Key"), eventId);
}
return apiKey;
}
private void subscribeUser(String email, CustomerName name, String language, int eventId, String eventKey) {
Optional<String> listAddress = getListAddress(eventId, email, name, language);
Optional<String> apiKey = getApiKey(eventId, email, name, language);
if(listAddress.isPresent() && apiKey.isPresent()) {
createMergeFieldIfNotPresent(listAddress.get(), apiKey.get(), eventId, eventKey);
send(eventId, listAddress.get() + LIST_MEMBERS + getMd5Email(email), apiKey.get(), email, name, language, eventKey);
}
}
static String getMd5Email(String email) {
try {
return Hex.encodeHexString(MessageDigest.getInstance("MD5").digest(email.trim().getBytes("UTF-8")));
} catch (NoSuchAlgorithmException | UnsupportedEncodingException e) {
return email;//should never happen...
}
}
private boolean send(int eventId, String address, String apiKey, String email, CustomerName name, String language, String eventKey) {
Map<String, Object> content = new HashMap<>();
content.put("email_address", email);
content.put("status", "subscribed");
Map<String, String> mergeFields = new HashMap<>();
mergeFields.put("FNAME", name.isHasFirstAndLastName() ? name.getFirstName() : name.getFullName());
mergeFields.put(ALFIO_EVENT_KEY, eventKey);
content.put("merge_fields", mergeFields);
content.put("language", language);
Request request = new Request.Builder()
.url(address)
.header("Authorization", Credentials.basic("alfio", apiKey))
.put(RequestBody.create(MediaType.parse(APPLICATION_JSON), Json.GSON.toJson(content, Map.class)))
.build();
try {
Response response = httpClient.newCall(request).execute();
if(response.isSuccessful()) {
pluginDataStorage.registerSuccess(String.format("user %s has been subscribed to list", email), eventId);
return true;
}
String responseBody = response.body().string();
if(response.code() != 400 || responseBody.contains("\"errors\"")) {
pluginDataStorage.registerFailure(String.format(FAILURE_MSG, email, name, language, responseBody), eventId);
return false;
} else {
pluginDataStorage.registerWarning(String.format(FAILURE_MSG, email, name, language, responseBody), eventId);
}
return true;
} catch (IOException e) {
pluginDataStorage.registerFailure(String.format(FAILURE_MSG, email, name, language, e.toString()), eventId);
return false;
}
}
private void createMergeFieldIfNotPresent(String listAddress, String apiKey, int eventId, String eventKey) {
Request request = new Request.Builder()
.url(listAddress + MERGE_FIELDS)
.header("Authorization", Credentials.basic("alfio", apiKey))
.get()
.build();
try {
Response response = httpClient.newCall(request).execute();
String responseBody = response.body().string();
if(!responseBody.contains(ALFIO_EVENT_KEY)) {
log.debug("can't find ALFIO_EKEY for event "+eventKey);
createMergeField(listAddress, apiKey, eventKey, eventId);
}
} catch (IOException e) {
pluginDataStorage.registerFailure(String.format("Cannot get merge fields for %s, got: %s", eventKey, e.getMessage()), eventId);
log.warn("exception while reading merge fields for event id "+eventId, e);
}
}
private void createMergeField(String listAddress, String apiKey, String eventKey, int eventId) {
Map<String, Object> mergeField = new HashMap<>();
mergeField.put("tag", ALFIO_EVENT_KEY);
mergeField.put("name", "Alfio's event key");
mergeField.put("type", "text");
mergeField.put("required", false);
mergeField.put("public", false);
Request request = new Request.Builder()
.url(listAddress + MERGE_FIELDS)
.header("Authorization", Credentials.basic("alfio", apiKey))
.post(RequestBody.create(MediaType.parse(APPLICATION_JSON), Json.GSON.toJson(mergeField, Map.class)))
.build();
try {
Response response = httpClient.newCall(request).execute();
if(!response.isSuccessful()) {
log.debug("can't create {} merge field. Got: {}", ALFIO_EVENT_KEY, response.body().string());
}
} catch (IOException e) {
pluginDataStorage.registerFailure(String.format("Cannot create merge field for %s, got: %s", eventKey, e.getMessage()), eventId);
log.warn("exception while creating ALFIO_EKEY for event id "+eventId, e);
}
}
}