Skip to content

Commit

Permalink
Create/update Mautic contacts
Browse files Browse the repository at this point in the history
Fixes #202
  • Loading branch information
micheljung committed Feb 27, 2018
1 parent 06def6d commit 14e44c0
Show file tree
Hide file tree
Showing 14 changed files with 341 additions and 16 deletions.
3 changes: 3 additions & 0 deletions src/inttest/resources/config/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,6 @@ faf-api:
api-key: "banana"
replay:
download-url-format: "http://localhost/faf/vault/replay_vault/replay.php?id=%s"
mautic:
client-id: test
client-secret: test
9 changes: 9 additions & 0 deletions src/main/java/com/faforever/api/config/FafApiProperties.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ public class FafApiProperties {
private Challonge challonge = new Challonge();
private User user = new User();
private Database database = new Database();
private Mautic mautic = new Mautic();

@Data
public static class OAuth2 {
Expand Down Expand Up @@ -215,4 +216,12 @@ public static class Database {
*/
private String schemaVersion;
}

@Data
public static class Mautic {
private String baseUrl;
private String clientId;
private String clientSecret;
private String accessTokenUrl;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import java.io.IOException;


public class JsonApiOauthMessageConverter extends MappingJackson2HttpMessageConverter {
public class JsonApiOAuthMessageConverter extends MappingJackson2HttpMessageConverter {

@Override
protected void writeInternal(Object object, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public JsonApiOauthExceptionRenderer() {

private List<HttpMessageConverter<?>> createMessageConverters() {
List<HttpMessageConverter<?>> result = new ArrayList<HttpMessageConverter<?>>();
result.add(new JsonApiOauthMessageConverter());
result.add(new JsonApiOAuthMessageConverter());
result.addAll(new RestTemplate().getMessageConverters());
result.add(new JaxbOAuth2ExceptionMessageConverter());
return result;
Expand Down
27 changes: 27 additions & 0 deletions src/main/java/com/faforever/api/mautic/MauticApiErrorHandler.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.faforever.api.mautic;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.client.DefaultResponseErrorHandler;

import java.io.IOException;
import java.io.InputStream;

@Component
public class MauticApiErrorHandler extends DefaultResponseErrorHandler {

private final ObjectMapper objectMapper;

public MauticApiErrorHandler(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}

@Override
public void handleError(ClientHttpResponse response) throws IOException {
try (InputStream inputStream = response.getBody()) {
MauticErrorResponse errorResponse = objectMapper.readValue(inputStream, MauticErrorResponse.class);
throw new MauticApiException(errorResponse.getErrors());
}
}
}
16 changes: 16 additions & 0 deletions src/main/java/com/faforever/api/mautic/MauticApiException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.faforever.api.mautic;

import com.faforever.api.mautic.MauticErrorResponse.Error;
import lombok.Getter;

import java.util.List;

@Getter
public class MauticApiException extends RuntimeException {
private final List<Error> errors;

public MauticApiException(List<Error> errors) {
super(errors.toString());
this.errors = errors;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package com.faforever.api.mautic;

import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.SneakyThrows;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.AbstractHttpMessageConverter;
import org.springframework.stereotype.Component;

import javax.inject.Inject;
import java.io.InputStream;
import java.util.Map;

@Component
public class MauticApiMessageConverter extends AbstractHttpMessageConverter<Object> {

private final ObjectMapper objectMapper;

@Inject
public MauticApiMessageConverter(ObjectMapper objectMapper) {
super(MediaType.APPLICATION_JSON);
this.objectMapper = objectMapper;
}

@Override
protected boolean supports(Class<?> clazz) {
return true;
}

@Override
@SneakyThrows
@SuppressWarnings("unchecked")
protected Object readInternal(Class<?> clazz, HttpInputMessage inputMessage) {
try(InputStream inputStream = inputMessage.getBody()) {
return objectMapper.readValue(inputStream, Map.class);
}
}

@Override
@SneakyThrows
protected void writeInternal(Object o, HttpOutputMessage outputMessage) {
outputMessage.getBody().write(objectMapper.writeValueAsBytes(o));
}
}
20 changes: 20 additions & 0 deletions src/main/java/com/faforever/api/mautic/MauticErrorResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.faforever.api.mautic;

import lombok.Data;
import lombok.ToString;

import java.util.List;
import java.util.Map;

@Data
public class MauticErrorResponse {
private List<Error> errors;

@Data
@ToString
public static class Error {
private int code;
private String message;
private Map<String, Object> details;
}
}
74 changes: 74 additions & 0 deletions src/main/java/com/faforever/api/mautic/MauticService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package com.faforever.api.mautic;

import com.faforever.api.config.FafApiProperties;
import com.faforever.api.config.FafApiProperties.Mautic;
import com.google.common.annotations.VisibleForTesting;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.web.client.ResponseErrorHandler;
import org.springframework.web.client.RestTemplate;

import javax.inject.Inject;
import java.time.OffsetDateTime;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;

/**
* Provides access to a Mautic (Open Source Marketing Automation) instance via REST API.
*/
@Service
public class MauticService {

private final RestTemplate restOperations;

@Inject
public MauticService(MauticApiMessageConverter mauticApiMessageConverter,
ResponseErrorHandler mauticApiErrorHandler, FafApiProperties properties) {
this(mauticApiMessageConverter, mauticApiErrorHandler, properties, new RestTemplateBuilder());
}

@VisibleForTesting
MauticService(MauticApiMessageConverter mauticApiMessageConverter,
ResponseErrorHandler mauticApiErrorHandler, FafApiProperties properties,
RestTemplateBuilder restTemplateBuilder) {

Mautic mauticProperties = properties.getMautic();

restTemplateBuilder
.additionalMessageConverters(mauticApiMessageConverter)
.errorHandler(mauticApiErrorHandler)
.rootUri(mauticProperties.getBaseUrl());

// TODO use as soon as this is solved: https://github.com/mautic/mautic/issues/5743
// ClientCredentialsResourceDetails details = new ClientCredentialsResourceDetails();
// details.setClientId(mauticProperties.getClientId());
// details.setClientSecret(mauticProperties.getClientSecret());
// details.setClientAuthenticationScheme(AuthenticationScheme.header);
// details.setAccessTokenUri(mauticProperties.getAccessTokenUrl());
// restOperations = restTemplateBuilder.configure(new OAuth2RestTemplate(details));

// TODO for new, client ID needs to be a username and client secret the user's password.
restTemplateBuilder = restTemplateBuilder.basicAuthorization(mauticProperties.getClientId(), mauticProperties.getClientSecret());

restOperations = restTemplateBuilder.build();
}

@Async
public CompletableFuture<Object> createOrUpdateContact(String email, String fafUserId, String fafUserName, String ipAddress, OffsetDateTime lastActive) {
Map<String, Object> body = new HashMap<>();

// These are Mautic default fields
Optional.ofNullable(email).ifPresent(s -> body.put("email", email));
Optional.ofNullable(ipAddress).ifPresent(s -> body.put("ipAddress", ipAddress));
Optional.ofNullable(lastActive).ifPresent(s -> body.put("lastActive", lastActive));

// These are Mautic "custom fields" that need to be created explicitly.
Optional.ofNullable(fafUserId).ifPresent(s -> body.put("faf_user_id", fafUserId));
Optional.ofNullable(fafUserName).ifPresent(s -> body.put("faf_username", fafUserName));

return CompletableFuture.completedFuture(restOperations.postForObject("/contacts/new", body, Object.class));
}
}
36 changes: 32 additions & 4 deletions src/main/java/com/faforever/api/user/UserService.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import com.faforever.api.error.ApiException;
import com.faforever.api.error.Error;
import com.faforever.api.error.ErrorCode;
import com.faforever.api.mautic.MauticService;
import com.faforever.api.player.PlayerRepository;
import com.faforever.api.security.FafPasswordEncoder;
import com.faforever.api.security.FafTokenService;
Expand All @@ -22,6 +23,7 @@

import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.time.OffsetDateTime;
import java.util.Map;
import java.util.Objects;
import java.util.regex.Pattern;
Expand All @@ -45,9 +47,12 @@ public class UserService {
private final AnopeUserRepository anopeUserRepository;
private final FafTokenService fafTokenService;
private final SteamService steamService;
private final MauticService mauticService;

public UserService(EmailService emailService, PlayerRepository playerRepository, UserRepository userRepository,
NameRecordRepository nameRecordRepository, FafApiProperties properties, AnopeUserRepository anopeUserRepository, FafTokenService fafTokenService, SteamService steamService) {
NameRecordRepository nameRecordRepository, FafApiProperties properties,
AnopeUserRepository anopeUserRepository, FafTokenService fafTokenService,
SteamService steamService, MauticService mauticService) {
this.emailService = emailService;
this.playerRepository = playerRepository;
this.userRepository = userRepository;
Expand All @@ -56,6 +61,7 @@ public UserService(EmailService emailService, PlayerRepository playerRepository,
this.anopeUserRepository = anopeUserRepository;
this.fafTokenService = fafTokenService;
this.steamService = steamService;
this.mauticService = mauticService;
this.passwordEncoder = new FafPasswordEncoder();
}

Expand Down Expand Up @@ -122,8 +128,10 @@ void activate(String token) {
user.setEmail(email);
user.setLogin(username);

user = userRepository.save(user);
log.debug("User has been activated: {}", user);
userRepository.save(user);

createOrUpdateMauticContact(user);
}

void changePassword(String currentPassword, String newPassword, User user) {
Expand Down Expand Up @@ -158,7 +166,23 @@ void changeLogin(String newLogin, User user) {
nameRecordRepository.save(nameRecord);

user.setLogin(newLogin);
userRepository.save(user);

createOrUpdateMauticContact(userRepository.save(user));
}

private void createOrUpdateMauticContact(User user) {
mauticService.createOrUpdateContact(
user.getEmail(),
String.valueOf(user.getId()),
user.getLogin(),
null,
OffsetDateTime.now()
)
.thenAccept(result -> log.debug("Updated contact in Mautic: {}", user.getEmail()))
.exceptionally(throwable -> {
log.warn("Could not update contact in Mautic: {}", user, throwable);
return null;
});
}

public void changeEmail(String currentPassword, String newEmail, User user) {
Expand All @@ -170,7 +194,11 @@ public void changeEmail(String currentPassword, String newEmail, User user) {

log.debug("Changing email for user ''{}'' to ''{}''", user, newEmail);
user.setEmail(newEmail);
userRepository.save(user);
createOrUpdateUser(user);
}

private void createOrUpdateUser(User user) {
createOrUpdateMauticContact(userRepository.save(user));
}

void resetPassword(String identifier, String newPassword) {
Expand Down
1 change: 1 addition & 0 deletions src/main/resources/config/application-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ faf-api:
steam-redirect-url-format: ${STEAM_LINK_REDIRECT_URL:http://localhost:8010/users/linkToSteam?token=%s}
success-redirect-url: ${STEAM_LINK_SUCCESS_URL:http://localhost:8020/linked_to_steam}
error-redirect-url-format: ${STEAM_LINK_ERROR_URL:http://localhost:8020/error?code=%s}

spring:
datasource:
url: jdbc:mysql://${DATABASE_ADDRESS:127.0.0.1}/${DATABASE_NAME:faf}?useSSL=false
Expand Down
5 changes: 5 additions & 0 deletions src/main/resources/config/application-prod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@ faf-api:
steam-redirect-url-format: ${STEAM_LINK_REDIRECT_URL}
success-redirect-url: ${STEAM_LINK_SUCCESS_URL}
error-redirect-url-format: ${STEAM_LINK_ERROR_URL}
mautic:
base-url: ${MAUTIC_BASE_URL}
client-id: ${MAUTIC_CLIENT_ID}
client-secret: ${MAUTIC_CLIENT_SECRET}
access-token-url: ${MAUTIC_ACCESS_TOKEN_URL}

spring:
datasource:
Expand Down
55 changes: 55 additions & 0 deletions src/test/java/com/faforever/api/mautic/MauticServiceTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package com.faforever.api.mautic;

import com.faforever.api.config.FafApiProperties;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.web.client.ResponseErrorHandler;
import org.springframework.web.client.RestTemplate;

import java.time.OffsetDateTime;
import java.util.Map;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

@RunWith(MockitoJUnitRunner.class)
public class MauticServiceTest {

private MauticService instance;

@Mock
private MauticApiMessageConverter mauticApiMessageConverter;
@Mock
private ResponseErrorHandler mauticApiErrorHandler;
@Mock
private RestTemplateBuilder restTemplateBuilder;
@Mock
private RestTemplate restTemplate;

@Before
public void setUp() throws Exception {
when(restTemplateBuilder.build()).thenReturn(restTemplate);

when(restTemplateBuilder.additionalMessageConverters(mauticApiMessageConverter)).thenReturn(restTemplateBuilder);
when(restTemplateBuilder.errorHandler(mauticApiErrorHandler)).thenReturn(restTemplateBuilder);
when(restTemplateBuilder.rootUri(any())).thenReturn(restTemplateBuilder);
when(restTemplateBuilder.basicAuthorization(any(), any())).thenReturn(restTemplateBuilder);

instance = new MauticService(mauticApiMessageConverter, mauticApiErrorHandler, new FafApiProperties(), restTemplateBuilder);
}

@Test
public void createOrUpdateContact() {
instance.createOrUpdateContact("junit@example.com", "111", "JUnit", "1.1.1.1", OffsetDateTime.now());

ArgumentCaptor<Map> captor = ArgumentCaptor.forClass(Map.class);
verify(restTemplate).postForObject(eq("/contacts/new"), captor.capture(), eq(Object.class));
}
}
Loading

0 comments on commit 14e44c0

Please sign in to comment.