twitter4jads.impl.TwitterAdsAudienceApiImpl Maven / Gradle / Ivy
The newest version!
package twitter4jads.impl;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import twitter4jads.*;
import twitter4jads.api.TwitterAdsAudienceApi;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import twitter4jads.BaseAdsListBatchPostResponse;
import twitter4jads.BaseAdsListResponse;
import twitter4jads.BaseAdsListResponseIterable;
import twitter4jads.BaseAdsResponse;
import twitter4jads.ErrorResponse;
import twitter4jads.TwitterAdsClient;
import twitter4jads.internal.http.HttpParameter;
import twitter4jads.internal.http.HttpResponse;
import twitter4jads.internal.models4j.RateLimitStatus;
import twitter4jads.internal.models4j.TwitterException;
import twitter4jads.models.ads.HttpVerb;
import twitter4jads.models.ads.audience.TailoredAudience;
import twitter4jads.models.ads.audience.AudienceApiResponse;
import twitter4jads.models.ads.audience.TailoredAudienceMatchingRules;
import twitter4jads.models.ads.audience.TailoredAudienceOperation;
import twitter4jads.models.ads.audience.TailoredAudiencePermission;
import twitter4jads.models.ads.audience.TailoredAudiencePermissionLevel;
import twitter4jads.models.ads.audience.TailoredAudienceUserDetails;
import twitter4jads.util.TwitterAdUtil;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Optional;
import java.util.PriorityQueue;
import java.util.Set;
import java.util.stream.Collectors;
import static twitter4jads.internal.http.HttpResponseCode.BAD_REQUEST;
import static twitter4jads.internal.http.HttpResponseCode.NOT_FOUND;
import static twitter4jads.internal.http.HttpResponseCode.TOO_MANY_REQUESTS;
/**
*
* Date: 4/5/16
* Time: 10:54 AM
*/
public class TwitterAdsAudienceApiImpl implements TwitterAdsAudienceApi {
private final TwitterAdsClient twitterAdsClient;
private static final Gson GSON = new Gson();
private static final long SIXTY_FOUR_MB = 64 * 1024 * 1024;
private static final Set acceptableApiErrors = Sets.newHashSet(BAD_REQUEST, NOT_FOUND, TOO_MANY_REQUESTS);
public TwitterAdsAudienceApiImpl(TwitterAdsClient twitterAdsClient) {
this.twitterAdsClient = twitterAdsClient;
}
@Override
public BaseAdsListResponseIterable getAllTailoredAudiences(String accountId, Optional count,
Optional withDeleted, Optional cursor)
throws TwitterException {
TwitterAdUtil.ensureNotNull(accountId, "AccountId");
final String baseUrl = twitterAdsClient.getBaseAdsAPIUrl() + TwitterAdsConstants.PREFIX_ACCOUNTS_URI + accountId + TwitterAdsConstants.PATH_TAILORED_AUDIENCES;
final List params = new ArrayList<>();
if (count != null && count.isPresent() && count.get() < 1000) {
params.add(new HttpParameter("count", count.get()));
}
if (withDeleted != null && withDeleted.isPresent()) {
params.add(new HttpParameter("with_deleted", withDeleted.get()));
}
if (cursor != null && cursor.isPresent()) {
params.add(new HttpParameter(TwitterAdsConstants.PARAM_CURSOR, cursor.get()));
}
final Type type = new TypeToken>() {
}.getType();
return twitterAdsClient.executeHttpListRequest(baseUrl, params, type);
}
@SuppressWarnings("Duplicates")
@Override
public BaseAdsResponse getTailoredAudienceForId(String accountId, String tailoredAudienceId) throws TwitterException {
TwitterAdUtil.ensureNotNull(accountId, "AccountId");
TwitterAdUtil.ensureNotNull(tailoredAudienceId, "tailoredAudienceId");
final String baseUrl = twitterAdsClient.getBaseAdsAPIUrl() + TwitterAdsConstants.PREFIX_ACCOUNTS_URI + accountId + TwitterAdsConstants.PATH_TAILORED_AUDIENCE + tailoredAudienceId;
final Type type = new TypeToken>() {
}.getType();
return twitterAdsClient.executeHttpRequest(baseUrl, null, type, HttpVerb.GET);
}
@Override
public BaseAdsResponse deleteTailoredAudience(String accountId, String tailoredAudienceId) throws TwitterException {
TwitterAdUtil.ensureNotNull(accountId, "AccountId");
TwitterAdUtil.ensureNotNull(tailoredAudienceId, "tailoredAudienceId");
final String baseUrl = twitterAdsClient.getBaseAdsAPIUrl() + TwitterAdsConstants.PREFIX_ACCOUNTS_URI + accountId + TwitterAdsConstants.PATH_TAILORED_AUDIENCE + tailoredAudienceId;
final Type type = new TypeToken>() {
}.getType();
return twitterAdsClient.executeHttpRequest(baseUrl, null, type, HttpVerb.DELETE);
}
@SuppressWarnings("Duplicates")
@Override
public BaseAdsResponse createTailoredAudience(String accountId, String name) throws TwitterException {
TwitterAdUtil.ensureNotNull(accountId, "AccountId");
TwitterAdUtil.ensureNotNull(name, "name");
final String baseUrl = twitterAdsClient.getBaseAdsAPIUrl() + TwitterAdsConstants.PREFIX_ACCOUNTS_URI + accountId + TwitterAdsConstants.PATH_TAILORED_AUDIENCE;
final List params = new ArrayList<>();
params.add(new HttpParameter("name", name));
final Type type = new TypeToken>() {
}.getType();
return twitterAdsClient.executeHttpRequest(baseUrl, params.toArray(new HttpParameter[params.size()]), type, HttpVerb.POST);
}
public BaseAdsResponse addMatchingRulesToAudience(TailoredAudienceMatchingRules tailoredAudienceMatchingRules,
String accountId) throws TwitterException {
TwitterAdUtil.ensureNotNull(accountId, "AccountId");
TwitterAdUtil.ensureNotNull(tailoredAudienceMatchingRules, "Matching Rules");
final String baseUrl = twitterAdsClient.getBaseAdsAPIUrl() + TwitterAdsConstants.PREFIX_ACCOUNTS_URI + accountId + TwitterAdsConstants.PATH_TAILORED_AUDIENCE_MATCHING_RULES;
final List params = new ArrayList<>();
params.add(new HttpParameter("tailored_audience_id", tailoredAudienceMatchingRules.getTailoredAudienceId()));
params.add(new HttpParameter("website_tag_id", tailoredAudienceMatchingRules.getWebsiteTagId()));
params.add(new HttpParameter("rule_type", tailoredAudienceMatchingRules.getRuleType().name()));
if (StringUtils.isNotBlank(tailoredAudienceMatchingRules.getRuleValue())) {
params.add(new HttpParameter("rule_value", tailoredAudienceMatchingRules.getRuleValue()));
}
final Type type = new TypeToken>() {
}.getType();
return twitterAdsClient.executeHttpRequest(baseUrl, params.toArray(new HttpParameter[params.size()]), type, HttpVerb.POST);
}
@Override
public BaseAdsListBatchPostResponse createFlexibleTailoredAudience(String accountId, String requestBody) throws TwitterException {
TwitterAdUtil.ensureNotNull(accountId, "AccountId");
TwitterAdUtil.ensureNotNull(requestBody, "params");
String baseUrl = twitterAdsClient.getBaseAdsAPIUrl() + TwitterAdsConstants.PREFIX_BATCH_ACCOUNTS_URI + accountId + TwitterAdsConstants.PATH_TAILORED_AUDIENCE;
Type type = new TypeToken>() {
}.getType();
final HttpResponse httpResponse = twitterAdsClient.postBatchRequest(baseUrl, requestBody);
return GSON.fromJson(httpResponse.asString(), type);
}
@Override
public List updateTailoredAudienceById(String accountId, String tailoredAudienceId,
List operations)
throws TwitterException {
TwitterAdUtil.ensureNotNull(accountId, "AccountId");
TwitterAdUtil.ensureNotNull(tailoredAudienceId, "tailoredAudienceId");
TwitterAdUtil.ensureNotEmpty(operations, "operations");
final String baseUrl =
twitterAdsClient.getBaseAdsAPIUrl() + TwitterAdsConstants.PREFIX_ACCOUNTS_URI + accountId + TwitterAdsConstants.PATH_TAILORED_AUDIENCE + tailoredAudienceId + TwitterAdsConstants.SLASH + TwitterAdsConstants.USERS;
final Gson gson = new Gson();
final List result = Lists.newArrayList();
final Iterator> batchIterator = generateBatchSequence(operations);
while (batchIterator.hasNext()) {
final List batch = batchIterator.next();
final List apiOperation =
batch.stream().map(this::generateRequestOperation).collect(Collectors.toList());
final String requestBody = gson.toJson(apiOperation);
final AudienceApiResponse apiResponse = publishAudienceWithRetry(baseUrl, requestBody);
final boolean errorFlag = handleAudienceUpdateResponse(batch, apiResponse, result);
if (errorFlag) {
break;
}
}
return result;
}
@Override
public BaseAdsListResponse getTailoredAudiencePermission(String accountId, String tailoredAudienceId) throws
TwitterException {
TwitterAdUtil.ensureNotNull(accountId, "AccountId");
TwitterAdUtil.ensureNotNull(tailoredAudienceId, "tailoredAudienceId");
final String baseUrl = twitterAdsClient.getBaseAdsAPIUrl() + TwitterAdsConstants.PREFIX_ACCOUNTS_URI + accountId + TwitterAdsConstants.PATH_TAILORED_AUDIENCE + tailoredAudienceId
+ TwitterAdsConstants.PATH_TAILORED_AUDIENCE_PERMISSIONS;
final Type type = new TypeToken>() {
}.getType();
return twitterAdsClient.executeRequest(baseUrl, null, type, HttpVerb.GET);
}
@Override
public BaseAdsResponse shareTailoredAudience(String accountId, String tailoredAudienceId, String
grantedAccountId) throws TwitterException {
TwitterAdUtil.ensureNotNull(accountId, "AccountId");
TwitterAdUtil.ensureNotNull(tailoredAudienceId, "tailoredAudienceId");
TwitterAdUtil.ensureNotNull(grantedAccountId, "grantedAccountId");
final String baseUrl = twitterAdsClient.getBaseAdsAPIUrl() + TwitterAdsConstants.PREFIX_ACCOUNTS_URI + accountId + TwitterAdsConstants.PATH_TAILORED_AUDIENCE + tailoredAudienceId
+ TwitterAdsConstants.PATH_TAILORED_AUDIENCE_PERMISSIONS;
final List params = new ArrayList<>();
params.add(new HttpParameter("granted_account_id", grantedAccountId));
params.add(new HttpParameter("permission_level", TailoredAudiencePermissionLevel.READ_WRITE.name()));
final Type type = new TypeToken>() {
}.getType();
return twitterAdsClient.executeHttpRequest(baseUrl, params.toArray(new HttpParameter[params.size()]), type, HttpVerb.POST);
}
// ----------------------------------------------------------------------- PRIVATE METHODS -----------------------------------------------------
private Iterator> generateBatchSequence(List operations) {
PriorityQueue operationPriorityQueue =
new PriorityQueue<>(Comparator.comparingInt(operation -> operation.getUsers().size()).reversed());
for (TailoredAudienceOperation operation : operations) {
operationPriorityQueue.offer(operation);
}
return new Iterator>() {
@Override
public boolean hasNext() {
return !operationPriorityQueue.isEmpty();
}
@Override
public List next() {
List toReturn = Lists.newArrayList();
int totalSize = 0;
while (totalSize < TwitterAdsConstants.TAILORED_AUDIENCE_UPDATE_BATCH_SIZE && !operationPriorityQueue.isEmpty()) {
int peekSize = operationPriorityQueue.peek().getUsers().size();
if (totalSize + peekSize < TwitterAdsConstants.TAILORED_AUDIENCE_UPDATE_BATCH_SIZE) {
totalSize += peekSize;
toReturn.add(operationPriorityQueue.poll());
} else {
int diff = TwitterAdsConstants.TAILORED_AUDIENCE_UPDATE_BATCH_SIZE - totalSize;
totalSize = TwitterAdsConstants.TAILORED_AUDIENCE_UPDATE_BATCH_SIZE;
TailoredAudienceOperation topOperation = operationPriorityQueue.poll();
TailoredAudienceOperation fractionalTopOperation = new TailoredAudienceOperation();
fractionalTopOperation.setOperationType(topOperation.getOperationType());
fractionalTopOperation.setEffectiveFrom(topOperation.getEffectiveFrom());
fractionalTopOperation.setExpireAt(topOperation.getExpireAt());
Set fractionalTopOperationUsers = Sets.newHashSet();
Iterator topOperationUsersIterator = topOperation.getUsers().iterator();
while (diff > 0 && topOperationUsersIterator.hasNext()) {
fractionalTopOperationUsers.add(topOperationUsersIterator.next());
diff--;
}
topOperation.getUsers().removeAll(fractionalTopOperationUsers);
fractionalTopOperation.setUsers(fractionalTopOperationUsers);
operationPriorityQueue.offer(topOperation);
toReturn.add(fractionalTopOperation);
break;
}
}
return toReturn;
}
};
}
private NewAdsAudienceApiOperation generateRequestOperation(TailoredAudienceOperation operation) {
NewAdsAudienceApiOperation apiOperation = new NewAdsAudienceApiOperation();
apiOperation.setOperationType(operation.getOperationType().name());
NewAdsAudienceApiParams apiParams = new NewAdsAudienceApiParams();
apiParams.setExpireAt(operation.getExpireAt());
apiParams.setEffectiveAt(operation.getEffectiveFrom());
apiParams.setTailoredAudienceUserDetails(operation.getUsers());
apiOperation.setParams(apiParams);
return apiOperation;
}
private AudienceApiResponse publishAudienceWithRetry(String baseUrl, String requestBody) throws TwitterException {
boolean retryFlag = true;
final int retryCount = 2;
int retry = 1;
while (retryFlag && retry <= retryCount) {
try {
final HttpResponse httpResponse = twitterAdsClient.postRequest(baseUrl, requestBody);
final String stringResponse = httpResponse.asString();
return TwitterAdUtil.constructAudienceApiResponse(httpResponse, stringResponse);
} catch (TwitterException eX) {
if (!acceptableApiErrors.contains(eX.getStatusCode()) || StringUtils.isBlank(eX.getActualDetailMessage())
|| eX.getResponse() == null) {
throw eX;
}
final AudienceApiResponse audienceApiResponse =
TwitterAdUtil.constructAudienceApiResponse(eX.getResponse(), eX.getActualDetailMessage());
retryFlag = shouldRetryForRateLimitError(audienceApiResponse.getRateLimitStatus());
if (!retryFlag) {
return audienceApiResponse;
} else {
TwitterAdUtil.reallySleep(audienceApiResponse.getRateLimitStatus().getSecondsUntilReset() * 1000L);
}
} catch (Exception eX) {
throw new TwitterException("Failed to update audience", eX);
}
retry++;
}
return null;
}
private boolean shouldRetryForRateLimitError(RateLimitStatus rateLimitStatus) {
if (rateLimitStatus.getRemaining() == 0 && rateLimitStatus.getSecondsUntilReset() > 0) {
return true;
}
return false;
}
private boolean handleAudienceUpdateResponse(List batch, AudienceApiResponse apiResponse,
List result) {
boolean errorFlag = false;
if (apiResponse.getData() != null) {
AudienceApiResponse.NewAudienceApiResponseData response = apiResponse.getData();
if (response.getSuccessCount().compareTo(response.getTotalCount()) != 0
|| response.getSuccessCount().compareTo(countUsersInBatch(batch)) != 0) {
fillErrorsInOperation(batch, apiResponse);
errorFlag = true;
}
} else {
fillErrorsInOperation(batch, apiResponse);
errorFlag = true;
}
result.addAll(batch);
return errorFlag;
}
private Long countUsersInBatch(List batch) {
long totalSize = 0;
if (batch == null) {
return totalSize;
}
for (TailoredAudienceOperation operation : batch) {
totalSize += operation.getUsers().size();
}
return totalSize;
}
private void fillErrorsInOperation(List batch, AudienceApiResponse apiResponse) {
if (CollectionUtils.isEmpty(batch) ||
(CollectionUtils.isEmpty(apiResponse.getErrors()) && CollectionUtils.isEmpty(apiResponse.getOperationErrors()))) {
return;
}
final int batchSize = batch.size();
for (int index = 0; index < batchSize; ++index) {
final TailoredAudienceOperation operation = batch.get(index);
// batch level errors
if (CollectionUtils.isNotEmpty(apiResponse.getErrors())) {
final List errors = apiResponse.getErrors().stream().map(ErrorResponse::getMessage).collect(Collectors.toList());
operation.getErrors().addAll(errors);
}
//operation level errors
if (CollectionUtils.isNotEmpty(apiResponse.getOperationErrors())
&& CollectionUtils.isNotEmpty(apiResponse.getOperationErrors().get(index))) {
final List operationErrors =
apiResponse.getOperationErrors().get(index).stream().map(ErrorResponse::getMessage).collect(Collectors.toList());
operation.getOperationErrors().addAll(operationErrors);
}
}
}
}