diff --git a/pom.xml b/pom.xml index 612c5174..60c681cb 100644 --- a/pom.xml +++ b/pom.xml @@ -198,5 +198,15 @@ 1.10.1 test + + org.apache.httpcomponents + fluent-hc + 4.5.3 + + + org.apache.httpcomponents + httpmime + 4.5.3 + diff --git a/src/main/java/ninja/eivind/hotsreplayuploader/providers/hotsapi/HotsApiProvider.java b/src/main/java/ninja/eivind/hotsreplayuploader/providers/hotsapi/HotsApiProvider.java new file mode 100644 index 00000000..46435ad9 --- /dev/null +++ b/src/main/java/ninja/eivind/hotsreplayuploader/providers/hotsapi/HotsApiProvider.java @@ -0,0 +1,166 @@ +// Copyright 2015-2016 Eivind Vegsundvåg +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ninja.eivind.hotsreplayuploader.providers.hotsapi; + +import ninja.eivind.hotsreplayuploader.models.ReplayFile; +import ninja.eivind.hotsreplayuploader.models.Status; +import ninja.eivind.hotsreplayuploader.providers.Provider; +import ninja.eivind.stormparser.models.Replay; +import ninja.eivind.hotsreplayuploader.utils.ReplayUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; +import org.apache.http.entity.mime.MultipartEntityBuilder; +import org.apache.http.entity.ContentType; +import org.apache.http.HttpEntity; +import org.apache.http.client.fluent.Request; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.io.File; +import java.io.IOException; +import java.security.NoSuchAlgorithmException; +import java.util.UUID; + +/** + * JSON response from hotsapi.net call to check if a replay exists + */ +@JsonIgnoreProperties(ignoreUnknown = true) +class DuplicateResponse { + @JsonProperty() + private boolean exists; + + public boolean isDuplicate() { + return exists; + } +} + +/** + * JSON response from hotsapi.net upload call + */ +@JsonIgnoreProperties(ignoreUnknown = true) +class UploadResponse { + @JsonProperty() + private boolean success; + + @JsonProperty() + private String status; + + public boolean isSuccess() { + return success; + } + + /** + * Upload result (Success, Duplicate, AiDetected, CustomGame, PtrRegion, TooOld, Incomplete) + */ + public String getStatus() { + return status; + } +} + + +/** + * Implements a {@link Provider} to upload replays to hotsapi.net. + */ +@Component +public class HotsApiProvider extends Provider { + + private static final Logger LOG = LoggerFactory.getLogger(HotsApiProvider.class); + + private static final String UPLOAD_URL = "http://hotsapi.net/api/v1/replays/"; + private static final String DUPLICATE_URL = "http://hotsapi.net/api/v1/replays/fingerprints/v3/"; + + public HotsApiProvider() { + super("HotsApi"); + } + + @Override + public Status upload(final ReplayFile replayFile) { + final File file = replayFile.getFile(); + if (!(file.exists() && file.canRead())) { + return Status.EXCEPTION; + } + + final String fileName = UUID.randomUUID() + ".StormReplay"; + LOG.info("Assigning remote file name " + fileName + " to " + replayFile); + + return uploadFile(file, fileName); + } + + @Override + public Status getPreStatus(final Replay replay) { + + // Temporary fix for computer players found until the parser supports this + if (ReplayUtils.replayHasComputerPlayers(replay)) { + LOG.info("Computer players found for replay, tagging as uploaded."); + return Status.UNSUPPORTED_GAME_MODE; + } + try { + final String matchId = ReplayUtils.getMatchId(replay); + LOG.info("Calculated matchId to be" + matchId); + final String uri = DUPLICATE_URL + matchId; + final String response = Request.Get(uri) + .execute() + .returnContent() + .asString(); + final DuplicateResponse result = new ObjectMapper().readValue(response, DuplicateResponse.class); + if (result.isDuplicate()) { + return Status.UPLOADED; + } + } catch (NoSuchAlgorithmException e) { + LOG.warn("Platform does not support MD5; cannot proceed with parsing", e); + } catch (IOException e) { + return Status.EXCEPTION; + } + return Status.NEW; + } + + private Status uploadFile(File file, String fileName) { + try { + final HttpEntity entity = MultipartEntityBuilder.create() + .addBinaryBody("file", file, ContentType.APPLICATION_OCTET_STREAM, fileName) + .build(); + final String response = Request.Post(UPLOAD_URL) + .body(entity) + .execute() + .returnContent() + .asString(); + final UploadResponse result = new ObjectMapper().readValue(response, UploadResponse.class); + LOG.info("File " + fileName + " uploaded to remote storage."); + switch (result.getStatus()) { + case "Duplicate": + case "Success": + LOG.info("File registered with hotsapi.net"); + return Status.UPLOADED; + case "AiDetected": + case "CustomGame": + case "PtrRegion": + case "TooOld": + case "Incomplete": + LOG.warn("File not supported by hotsapi.net"); + return Status.UNSUPPORTED_GAME_MODE; + default: + LOG.error("Could not upload file. Unknown status \"" + result + "\" received."); + return Status.EXCEPTION; + } + + } catch (Exception e) { + LOG.error("Could not upload file.", e); + return Status.EXCEPTION; + } + } + +} diff --git a/src/main/java/ninja/eivind/hotsreplayuploader/providers/hotslogs/HotsLogsProvider.java b/src/main/java/ninja/eivind/hotsreplayuploader/providers/hotslogs/HotsLogsProvider.java index b394b01b..310616e0 100644 --- a/src/main/java/ninja/eivind/hotsreplayuploader/providers/hotslogs/HotsLogsProvider.java +++ b/src/main/java/ninja/eivind/hotsreplayuploader/providers/hotslogs/HotsLogsProvider.java @@ -19,8 +19,7 @@ import ninja.eivind.hotsreplayuploader.models.Status; import ninja.eivind.hotsreplayuploader.providers.Provider; import ninja.eivind.hotsreplayuploader.utils.SimpleHttpClient; -import ninja.eivind.stormparser.models.Player; -import ninja.eivind.stormparser.models.PlayerType; +import ninja.eivind.hotsreplayuploader.utils.ReplayUtils; import ninja.eivind.stormparser.models.Replay; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -30,11 +29,8 @@ import java.io.File; import java.io.IOException; -import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; -import java.util.List; import java.util.UUID; -import java.util.stream.Collectors; /** * Implements a {@link Provider} to upload replays to hotslogs.com.
@@ -65,48 +61,6 @@ public static boolean isMaintenance() { return maintenance + 600000L > System.currentTimeMillis(); } - private static UUID getUUIDForString(String concatenatedString) throws NoSuchAlgorithmException { - final byte[] hashed = MessageDigest.getInstance("MD5").digest(concatenatedString.getBytes()); - final byte[] reArranged = reArrangeForUUID(hashed); - return getUUID(reArranged); - } - - private static byte[] reArrangeForUUID(byte[] hashed) { - return new byte[]{ - hashed[3], - hashed[2], - hashed[1], - hashed[0], - - hashed[5], - hashed[4], - hashed[7], - hashed[6], - hashed[8], - hashed[9], - hashed[10], - hashed[11], - hashed[12], - hashed[13], - hashed[14], - hashed[15], - }; - } - - private static UUID getUUID(byte[] bytes) { - long msb = 0; - long lsb = 0; - assert bytes.length == 16 : "data must be 16 bytes in length"; - for (int i = 0; i < 8; i++) { - msb = (msb << 8) | (bytes[i] & 0xff); - } - for (int i = 8; i < 16; i++) { - lsb = (lsb << 8) | (bytes[i] & 0xff); - } - - return new UUID(msb, lsb); - } - @Override public Status upload(final ReplayFile replayFile) { if (isMaintenance()) { @@ -130,12 +84,12 @@ public Status upload(final ReplayFile replayFile) { public Status getPreStatus(final Replay replay) { // Temporary fix for computer players found until the parser supports this - if (replayHasComputerPlayers(replay)) { + if (ReplayUtils.replayHasComputerPlayers(replay)) { LOG.info("Computer players found for replay, tagging as uploaded."); return Status.UNSUPPORTED_GAME_MODE; } try { - final String matchId = getMatchId(replay); + final String matchId = ReplayUtils.getMatchId(replay); LOG.info("Calculated matchId to be" + matchId); final String uri = BASE_URL + "&ReplayHash=" + matchId; final String result = getHttpClient().simpleRequest(uri).toLowerCase(); @@ -180,37 +134,6 @@ private Status uploadFileToHotSLogs(File file, String fileName, String uri) { } } - private boolean replayHasComputerPlayers(Replay replay) { - return replay.getReplayDetails() - .getPlayers() - .stream() - .map(Player::getPlayerType) - .anyMatch(playerType -> playerType == PlayerType.COMPUTER); - } - - protected String getMatchId(Replay replay) throws NoSuchAlgorithmException { - final String concatenatedString = getConcatenatedString(replay); - - return getUUIDForString(concatenatedString).toString(); - - } - - private String getConcatenatedString(Replay replay) { - final String randomValue = String.valueOf(replay.getInitData().getRandomValue()); - final List battleNetIdsSorted = replay.getReplayDetails() - .getPlayers() - .stream() - .map(Player::getBNetId) - .map(Long::parseLong) - .sorted() - .map(String::valueOf) - .collect(Collectors.toList()); - final StringBuilder builder = new StringBuilder(); - battleNetIdsSorted.forEach(builder::append); - builder.append(randomValue); - return builder.toString(); - } - public SimpleHttpClient getHttpClient() { return httpClient; } diff --git a/src/main/java/ninja/eivind/hotsreplayuploader/utils/ReplayUtils.java b/src/main/java/ninja/eivind/hotsreplayuploader/utils/ReplayUtils.java new file mode 100644 index 00000000..a32322fb --- /dev/null +++ b/src/main/java/ninja/eivind/hotsreplayuploader/utils/ReplayUtils.java @@ -0,0 +1,100 @@ +// Copyright 2015-2016 Eivind Vegsundvåg +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ninja.eivind.hotsreplayuploader.utils; + +import ninja.eivind.stormparser.models.Replay; +import ninja.eivind.stormparser.models.Player; +import ninja.eivind.stormparser.models.PlayerType; + +import java.security.NoSuchAlgorithmException; +import java.security.MessageDigest; +import java.util.UUID; +import java.util.List; +import java.util.stream.Collectors; + +public class ReplayUtils { + private static UUID getUUIDForString(String concatenatedString) throws NoSuchAlgorithmException { + final byte[] hashed = MessageDigest.getInstance("MD5").digest(concatenatedString.getBytes()); + final byte[] reArranged = reArrangeForUUID(hashed); + return getUUID(reArranged); + } + + private static byte[] reArrangeForUUID(byte[] hashed) { + return new byte[]{ + hashed[3], + hashed[2], + hashed[1], + hashed[0], + + hashed[5], + hashed[4], + hashed[7], + hashed[6], + hashed[8], + hashed[9], + hashed[10], + hashed[11], + hashed[12], + hashed[13], + hashed[14], + hashed[15], + }; + } + + private static UUID getUUID(byte[] bytes) { + long msb = 0; + long lsb = 0; + assert bytes.length == 16 : "data must be 16 bytes in length"; + for (int i = 0; i < 8; i++) { + msb = (msb << 8) | (bytes[i] & 0xff); + } + for (int i = 8; i < 16; i++) { + lsb = (lsb << 8) | (bytes[i] & 0xff); + } + + return new UUID(msb, lsb); + } + + private static String getConcatenatedString(Replay replay) { + final String randomValue = String.valueOf(replay.getInitData().getRandomValue()); + final List battleNetIdsSorted = replay.getReplayDetails() + .getPlayers() + .stream() + .map(Player::getBNetId) + .map(Long::parseLong) + .sorted() + .map(String::valueOf) + .collect(Collectors.toList()); + final StringBuilder builder = new StringBuilder(); + battleNetIdsSorted.forEach(builder::append); + builder.append(randomValue); + return builder.toString(); + } + + /** + * Compute the replay ID, in the format shared by hotslogs and hotsapi + */ + public static String getMatchId(Replay replay) throws NoSuchAlgorithmException { + return getUUIDForString(getConcatenatedString(replay)).toString(); + } + + public static boolean replayHasComputerPlayers(Replay replay) { + return replay.getReplayDetails() + .getPlayers() + .stream() + .map(Player::getPlayerType) + .anyMatch(playerType -> playerType == PlayerType.COMPUTER); + } +} diff --git a/src/test/java/ninja/eivind/hotsreplayuploader/providers/hotslogs/HotSLogsProviderTest.java b/src/test/java/ninja/eivind/hotsreplayuploader/providers/hotslogs/HotSLogsProviderTest.java index c4994a54..93e0f33b 100644 --- a/src/test/java/ninja/eivind/hotsreplayuploader/providers/hotslogs/HotSLogsProviderTest.java +++ b/src/test/java/ninja/eivind/hotsreplayuploader/providers/hotslogs/HotSLogsProviderTest.java @@ -19,6 +19,7 @@ import ninja.eivind.hotsreplayuploader.concurrent.tasks.UploadTask; import ninja.eivind.hotsreplayuploader.models.ReplayFile; import ninja.eivind.hotsreplayuploader.utils.SimpleHttpClient; +import ninja.eivind.hotsreplayuploader.utils.ReplayUtils; import ninja.eivind.stormparser.StormParser; import ninja.eivind.stormparser.models.Replay; import org.junit.Before; @@ -67,7 +68,7 @@ public void setUp() throws IOException { @Test public void testGetMatchId() throws NoSuchAlgorithmException { String expected = "5543abb9-af35-3ce6-a026-e9d5517f2964"; - String actual = provider.getMatchId(parsedReplay); + String actual = ReplayUtils.getMatchId(parsedReplay); assertEquals("Match ID is calculated as expected", expected, actual); }