Skip to content
This repository was archived by the owner on Apr 5, 2024. It is now read-only.

hotsapi.net upload support #162

Open
wants to merge 3 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -198,5 +198,15 @@
<version>1.10.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>fluent-hc</artifactId>
<version>4.5.3</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpmime</artifactId>
<version>4.5.3</version>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
@@ -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;
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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.<br>
Expand Down Expand Up @@ -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()) {
Expand All @@ -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();
Expand Down Expand Up @@ -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<String> 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;
}
Expand Down
Loading