diff --git a/.gitignore b/.gitignore index 005ab005..69dd6906 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /db* +/lists/ /bin/ /target/ /qortal-backup/ diff --git a/src/main/java/org/qortal/api/resource/ListsResource.java b/src/main/java/org/qortal/api/resource/ListsResource.java new file mode 100644 index 00000000..0f243b5a --- /dev/null +++ b/src/main/java/org/qortal/api/resource/ListsResource.java @@ -0,0 +1,124 @@ +package org.qortal.api.resource; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; + +import org.qortal.api.*; +import org.qortal.crypto.Crypto; +import org.qortal.data.account.AccountData; +import org.qortal.list.ResourceListManager; +import org.qortal.repository.DataException; +import org.qortal.repository.Repository; +import org.qortal.repository.RepositoryManager; + +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.*; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; + + +@Path("/lists") +@Tag(name = "Lists") +public class ListsResource { + + @Context + HttpServletRequest request; + + @POST + @Path("/blacklist/address/{address}") + @Operation( + summary = "Add a QORT address to the local blacklist", + responses = { + @ApiResponse( + description = "Returns true on success, or an exception on failure", + content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "boolean")) + ) + } + ) + @ApiErrors({ApiError.INVALID_ADDRESS, ApiError.ADDRESS_UNKNOWN, ApiError.REPOSITORY_ISSUE}) + public String addAddressToBlacklist(@PathParam("address") String address) { + if (!Crypto.isValidAddress(address)) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS); + + try (final Repository repository = RepositoryManager.getRepository()) { + AccountData accountData = repository.getAccountRepository().getAccount(address); + // Not found? + if (accountData == null) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN); + + // Valid address, so go ahead and blacklist it + boolean success = ResourceListManager.getInstance().addAddressToBlacklist(address); + + return success ? "true" : "false"; + } catch (DataException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e); + } + } + + + @DELETE + @Path("/blacklist/address/{address}") + @Operation( + summary = "Remove a QORT address from the local blacklist", + responses = { + @ApiResponse( + description = "Returns true on success, or an exception on failure", + content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "boolean")) + ) + } + ) + @ApiErrors({ApiError.INVALID_ADDRESS, ApiError.ADDRESS_UNKNOWN, ApiError.REPOSITORY_ISSUE}) + public String removeAddressFromBlacklist(@PathParam("address") String address) { + if (!Crypto.isValidAddress(address)) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS); + + try (final Repository repository = RepositoryManager.getRepository()) { + AccountData accountData = repository.getAccountRepository().getAccount(address); + // Not found? + if (accountData == null) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN); + + // Valid address, so go ahead and blacklist it + boolean success = ResourceListManager.getInstance().removeAddressFromBlacklist(address); + + return success ? "true" : "false"; + } catch (DataException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e); + } + } + + @GET + @Path("/blacklist/address/{address}") + @Operation( + summary = "Checks if an address is present in the local blacklist", + responses = { + @ApiResponse( + description = "Returns true or false if the list was queried, or an exception on failure", + content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "boolean")) + ) + } + ) + @ApiErrors({ApiError.INVALID_ADDRESS, ApiError.ADDRESS_UNKNOWN, ApiError.REPOSITORY_ISSUE}) + public String checkAddressInBlacklist(@PathParam("address") String address) { + if (!Crypto.isValidAddress(address)) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS); + + try (final Repository repository = RepositoryManager.getRepository()) { + AccountData accountData = repository.getAccountRepository().getAccount(address); + // Not found? + if (accountData == null) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN); + + // Valid address, so go ahead and blacklist it + boolean blacklisted = ResourceListManager.getInstance().isAddressInBlacklist(address); + + return blacklisted ? "true" : "false"; + } catch (DataException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e); + } + } + +} diff --git a/src/main/java/org/qortal/list/ResourceList.java b/src/main/java/org/qortal/list/ResourceList.java new file mode 100644 index 00000000..740b23d6 --- /dev/null +++ b/src/main/java/org/qortal/list/ResourceList.java @@ -0,0 +1,124 @@ +package org.qortal.list; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.json.JSONArray; +import org.qortal.settings.Settings; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; + +public class ResourceList { + + private String category; + private String resourceName; + private List list; + + /** + * ResourceList + * Creates or updates a list for the purpose of tracking resources on the Qortal network + * This can be used for local blocking, or even for curating and sharing content lists + * Lists are backed off to JSON files (in the lists folder) to ease sharing between nodes and users + * + * @param category - for instance "blacklist", "whitelist", or "userlist" + * @param resourceName - for instance "address", "poll", or "group" + * @throws IOException + */ + public ResourceList(String category, String resourceName) throws IOException { + this.category = category; + this.resourceName = resourceName; + this.load(); + } + + + /* Filesystem */ + + private Path getFilePath() { + String pathString = String.format("%s%s%s_%s.json", Settings.getInstance().getListsPath(), + File.separator, this.resourceName, this.category); + Path outputFilePath = Paths.get(pathString); + try { + Files.createDirectories(outputFilePath.getParent()); + } catch (IOException e) { + throw new IllegalStateException("Unable to create lists directory"); + } + return outputFilePath; + } + + public void save() throws IOException { + if (this.resourceName == null) { + throw new IllegalStateException("Can't save list with missing resource name"); + } + if (this.category == null) { + throw new IllegalStateException("Can't save list with missing category"); + } + String jsonString = ResourceList.listToJSONString(this.list); + + Path filePath = this.getFilePath(); + BufferedWriter writer = new BufferedWriter(new FileWriter(filePath.toString())); + writer.write(jsonString); + writer.close(); + } + + private boolean load() throws IOException { + Path path = this.getFilePath(); + File resourceListFile = new File(path.toString()); + if (!resourceListFile.exists()) { + return false; + } + + try { + String jsonString = new String(Files.readAllBytes(path)); + this.list = ResourceList.listFromJSONString(jsonString); + } catch (IOException e) { + throw new IOException(String.format("Couldn't read contents from file %s", path.toString())); + } + + return true; + } + + + /* List management */ + + public void add(String resource) { + this.list.add(resource); + } + + public void remove(String resource) { + this.list.remove(resource); + } + + public boolean contains(String resource) { + return this.list.contains(resource); + } + + + + /* Utils */ + + public static String listToJSONString(List list) { + JSONArray items = new JSONArray(); + for (String item : list) { + items.put(item); + } + return items.toString(4); + } + + private static List listFromJSONString(String jsonString) { + JSONArray jsonList = new JSONArray(jsonString); + List resourceList = new ArrayList<>(); + for (int i=0; i