Browse Source

Initial implementation of resource lists

The ResourceList class 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.

This first implementation allows access to an address blacklist only, but has been written in such a way that other lists can be easily added. This might be needed in the future, e.g. to blacklist a group, a poll, or some hosted data. It could also be used by community members to curate lists of favourite or problematic content, which could then be shared or even subscribed to on the chain by other users.
lists
CalDescent 3 years ago
parent
commit
24f1fb566d
  1. 1
      .gitignore
  2. 124
      src/main/java/org/qortal/api/resource/ListsResource.java
  3. 124
      src/main/java/org/qortal/list/ResourceList.java
  4. 63
      src/main/java/org/qortal/list/ResourceListManager.java
  5. 7
      src/main/java/org/qortal/settings/Settings.java

1
.gitignore vendored

@ -1,4 +1,5 @@
/db*
/lists/
/bin/
/target/
/qortal-backup/

124
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);
}
}
}

124
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<String> 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<String> list) {
JSONArray items = new JSONArray();
for (String item : list) {
items.put(item);
}
return items.toString(4);
}
private static List<String> listFromJSONString(String jsonString) {
JSONArray jsonList = new JSONArray(jsonString);
List<String> resourceList = new ArrayList<>();
for (int i=0; i<jsonList.length(); i++) {
String item = (String)jsonList.get(i);
resourceList.add(item);
}
return resourceList;
}
}

63
src/main/java/org/qortal/list/ResourceListManager.java

@ -0,0 +1,63 @@
package org.qortal.list;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.eclipse.jetty.util.IO;
import java.io.IOException;
public class ResourceListManager {
private static final Logger LOGGER = LogManager.getLogger(ResourceListManager.class);
private static ResourceListManager instance;
private ResourceList addressBlacklist;
public ResourceListManager() {
try {
this.addressBlacklist = new ResourceList("blacklist", "address");
} catch (IOException e) {
LOGGER.info("Error while loading address blacklist. Blocking is currently unavailable.");
}
}
public static synchronized ResourceListManager getInstance() {
if (instance == null) {
instance = new ResourceListManager();
}
return instance;
}
public boolean addAddressToBlacklist(String address) {
try {
this.addressBlacklist.add(address);
this.addressBlacklist.save();
return true;
} catch (IllegalStateException | IOException e) {
LOGGER.info("Unable to add address to blacklist", e);
return false;
}
}
public boolean removeAddressFromBlacklist(String address) {
try {
this.addressBlacklist.remove(address);
this.addressBlacklist.save();
return true;
} catch (IllegalStateException | IOException e) {
LOGGER.info("Unable to remove address from blacklist", e);
return false;
}
}
public boolean isAddressInBlacklist(String address) {
if (this.addressBlacklist == null) {
return false;
}
return this.addressBlacklist.contains(address);
}
}

7
src/main/java/org/qortal/settings/Settings.java

@ -161,6 +161,9 @@ public class Settings {
"https://[email protected]/Qortal/qortal/%s/qortal.update"
};
// Lists
private String listsPath = "lists";
/** Array of NTP server hostnames. */
private String[] ntpServers = new String[] {
"pool.ntp.org",
@ -474,6 +477,10 @@ public class Settings {
return this.autoUpdateRepos;
}
public String getListsPath() {
return this.listsPath;
}
public String[] getNtpServers() {
return this.ntpServers;
}

Loading…
Cancel
Save