mirror of
https://github.com/Qortal/qortal.git
synced 2025-07-22 20:26:50 +00:00
Initial APIs and core support for Q-Apps
This commit is contained in:
@@ -53,7 +53,7 @@ public class ApiService {
|
||||
|
||||
private ApiService() {
|
||||
this.config = new ResourceConfig();
|
||||
this.config.packages("org.qortal.api.resource");
|
||||
this.config.packages("org.qortal.api.resource", "org.qortal.api.apps.resource");
|
||||
this.config.register(OpenApiResource.class);
|
||||
this.config.register(ApiDefinition.class);
|
||||
this.config.register(AnnotationPostProcessor.class);
|
||||
|
@@ -37,7 +37,7 @@ public class GatewayService {
|
||||
|
||||
private GatewayService() {
|
||||
this.config = new ResourceConfig();
|
||||
this.config.packages("org.qortal.api.gateway.resource");
|
||||
this.config.packages("org.qortal.api.gateway.resource", "org.qortal.api.apps.resource");
|
||||
this.config.register(OpenApiResource.class);
|
||||
this.config.register(ApiDefinition.class);
|
||||
this.config.register(AnnotationPostProcessor.class);
|
||||
|
@@ -25,6 +25,10 @@ public class HTMLParser {
|
||||
String baseUrl = this.linkPrefix + "/";
|
||||
Elements head = document.getElementsByTag("head");
|
||||
if (!head.isEmpty()) {
|
||||
// Add q-apps script tag
|
||||
String qAppsScriptElement = String.format("<script src=\"/apps/q-apps.js?time=%d\">", System.currentTimeMillis());
|
||||
head.get(0).prepend(qAppsScriptElement);
|
||||
|
||||
// Add base href tag
|
||||
String baseElement = String.format("<base href=\"%s\">", baseUrl);
|
||||
head.get(0).prepend(baseElement);
|
||||
|
210
src/main/java/org/qortal/api/apps/resource/AppsResource.java
Normal file
210
src/main/java/org/qortal/api/apps/resource/AppsResource.java
Normal file
@@ -0,0 +1,210 @@
|
||||
package org.qortal.api.apps.resource;
|
||||
|
||||
import com.google.common.io.Resources;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
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.Hidden;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.qortal.api.ApiError;
|
||||
import org.qortal.api.ApiExceptionFactory;
|
||||
import org.qortal.arbitrary.apps.QApp;
|
||||
import org.qortal.arbitrary.misc.Service;
|
||||
import org.qortal.data.account.AccountData;
|
||||
import org.qortal.data.arbitrary.ArbitraryResourceInfo;
|
||||
import org.qortal.data.arbitrary.ArbitraryResourceStatus;
|
||||
import org.qortal.data.at.ATData;
|
||||
import org.qortal.data.chat.ChatMessage;
|
||||
import org.qortal.data.group.GroupData;
|
||||
import org.qortal.data.naming.NameData;
|
||||
import org.qortal.repository.DataException;
|
||||
|
||||
import javax.servlet.ServletContext;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.ws.rs.*;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
@Path("/apps")
|
||||
@Tag(name = "Apps")
|
||||
public class AppsResource {
|
||||
|
||||
@Context HttpServletRequest request;
|
||||
@Context HttpServletResponse response;
|
||||
@Context ServletContext context;
|
||||
|
||||
@GET
|
||||
@Path("/q-apps.js")
|
||||
@Hidden // For internal Q-App API use only
|
||||
@Operation(
|
||||
summary = "Javascript interface for Q-Apps",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "javascript",
|
||||
content = @Content(
|
||||
mediaType = MediaType.TEXT_PLAIN,
|
||||
schema = @Schema(
|
||||
type = "string"
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
public String getQAppsJs() {
|
||||
URL url = Resources.getResource("q-apps/q-apps.js");
|
||||
try {
|
||||
return Resources.toString(url, StandardCharsets.UTF_8);
|
||||
} catch (IOException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FILE_NOT_FOUND);
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/q-apps-helper.js")
|
||||
@Hidden // For testing only
|
||||
public String getQAppsHelperJs() {
|
||||
URL url = Resources.getResource("q-apps/q-apps-helper.js");
|
||||
try {
|
||||
return Resources.toString(url, StandardCharsets.UTF_8);
|
||||
} catch (IOException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FILE_NOT_FOUND);
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/account")
|
||||
@Hidden // For internal Q-App API use only
|
||||
public AccountData getAccount(@QueryParam("address") String address) {
|
||||
try {
|
||||
return QApp.getAccountData(address);
|
||||
} catch (DataException | IllegalArgumentException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_DATA, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/account/names")
|
||||
@Hidden // For internal Q-App API use only
|
||||
public List<NameData> getAccountNames(@QueryParam("address") String address) {
|
||||
try {
|
||||
return QApp.getAccountNames(address);
|
||||
} catch (DataException | IllegalArgumentException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_DATA, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/name")
|
||||
@Hidden // For internal Q-App API use only
|
||||
public NameData getName(@QueryParam("name") String name) {
|
||||
try {
|
||||
return QApp.getNameData(name);
|
||||
} catch (DataException | IllegalArgumentException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_DATA, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/chatmessages")
|
||||
@Hidden // For internal Q-App API use only
|
||||
public List<ChatMessage> searchChatMessages(@QueryParam("before") Long before, @QueryParam("after") Long after, @QueryParam("txGroupId") Integer txGroupId, @QueryParam("involving") List<String> involvingAddresses, @QueryParam("reference") String reference, @QueryParam("chatReference") String chatReference, @QueryParam("hasChatReference") Boolean hasChatReference, @QueryParam("limit") Integer limit, @QueryParam("offset") Integer offset, @QueryParam("reverse") Boolean reverse) {
|
||||
try {
|
||||
return QApp.searchChatMessages(before, after, txGroupId, involvingAddresses, reference, chatReference, hasChatReference, limit, offset, reverse);
|
||||
} catch (DataException | IllegalArgumentException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_DATA, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/resources")
|
||||
@Hidden // For internal Q-App API use only
|
||||
public List<ArbitraryResourceInfo> getResources(@QueryParam("service") Service service, @QueryParam("identifier") String identifier, @Parameter(description = "Default resources (without identifiers) only") @QueryParam("default") Boolean defaultResource, @Parameter(description = "Filter names by list") @QueryParam("nameListFilter") String nameListFilter, @Parameter(description = "Include status") @QueryParam("includeStatus") Boolean includeStatus, @Parameter(description = "Include metadata") @QueryParam("includeMetadata") Boolean includeMetadata, @Parameter(ref = "limit") @QueryParam("limit") Integer limit, @Parameter(ref = "offset") @QueryParam("offset") Integer offset, @Parameter(ref = "reverse") @QueryParam("reverse") Boolean reverse) {
|
||||
try {
|
||||
return QApp.searchQdnResources(service, identifier, defaultResource, nameListFilter, includeStatus, includeMetadata, limit, offset, reverse);
|
||||
} catch (DataException | IllegalArgumentException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_DATA, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/resourcestatus")
|
||||
@Hidden // For internal Q-App API use only
|
||||
public ArbitraryResourceStatus getResourceStatus(@QueryParam("service") Service service, @QueryParam("name") String name, @QueryParam("identifier") String identifier) {
|
||||
return QApp.getQdnResourceStatus(service, name, identifier);
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/resource")
|
||||
@Hidden // For internal Q-App API use only
|
||||
public String getResource(@QueryParam("service") Service service, @QueryParam("name") String name, @QueryParam("identifier") String identifier, @QueryParam("filepath") String filepath, @QueryParam("rebuild") boolean rebuild) {
|
||||
try {
|
||||
return QApp.fetchQdnResource64(service, name, identifier, filepath, rebuild);
|
||||
} catch (DataException | IllegalArgumentException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_DATA, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/groups")
|
||||
@Hidden // For internal Q-App API use only
|
||||
public List<GroupData> listGroups(@Parameter(ref = "limit") @QueryParam("limit") Integer limit, @Parameter(ref = "offset") @QueryParam("offset") Integer offset, @Parameter(ref = "reverse") @QueryParam("reverse") Boolean reverse) {
|
||||
try {
|
||||
return QApp.listGroups(limit, offset, reverse);
|
||||
} catch (DataException | IllegalArgumentException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_DATA, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/balance")
|
||||
@Hidden // For internal Q-App API use only
|
||||
public Long getBalance(@QueryParam("assetId") long assetId, @QueryParam("address") String address) {
|
||||
try {
|
||||
return QApp.getBalance(assetId, address);
|
||||
} catch (DataException | IllegalArgumentException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_DATA, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/at")
|
||||
@Hidden // For internal Q-App API use only
|
||||
public ATData getAT(@QueryParam("atAddress") String atAddress) {
|
||||
try {
|
||||
return QApp.getAtInfo(atAddress);
|
||||
} catch (DataException | IllegalArgumentException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_DATA, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/atdata")
|
||||
@Hidden // For internal Q-App API use only
|
||||
public String getATData(@QueryParam("atAddress") String atAddress) {
|
||||
try {
|
||||
return QApp.getAtData58(atAddress);
|
||||
} catch (DataException | IllegalArgumentException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_DATA, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/ats")
|
||||
@Hidden // For internal Q-App API use only
|
||||
public List<ATData> listATs(@QueryParam("codeHash58") String codeHash58, @QueryParam("isExecutable") Boolean isExecutable, @Parameter(ref = "limit") @QueryParam("limit") Integer limit, @Parameter(ref = "offset") @QueryParam("offset") Integer offset, @Parameter(ref = "reverse") @QueryParam("reverse") Boolean reverse) {
|
||||
try {
|
||||
return QApp.listATs(codeHash58, isExecutable, limit, offset, reverse);
|
||||
} catch (DataException | IllegalArgumentException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_DATA, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -128,10 +128,10 @@ public class ArbitraryResource {
|
||||
}
|
||||
|
||||
if (includeStatus != null && includeStatus) {
|
||||
resources = this.addStatusToResources(resources);
|
||||
resources = ArbitraryTransactionUtils.addStatusToResources(resources);
|
||||
}
|
||||
if (includeMetadata != null && includeMetadata) {
|
||||
resources = this.addMetadataToResources(resources);
|
||||
resources = ArbitraryTransactionUtils.addMetadataToResources(resources);
|
||||
}
|
||||
|
||||
return resources;
|
||||
@@ -175,10 +175,10 @@ public class ArbitraryResource {
|
||||
}
|
||||
|
||||
if (includeStatus != null && includeStatus) {
|
||||
resources = this.addStatusToResources(resources);
|
||||
resources = ArbitraryTransactionUtils.addStatusToResources(resources);
|
||||
}
|
||||
if (includeMetadata != null && includeMetadata) {
|
||||
resources = this.addMetadataToResources(resources);
|
||||
resources = ArbitraryTransactionUtils.addMetadataToResources(resources);
|
||||
}
|
||||
|
||||
return resources;
|
||||
@@ -232,10 +232,10 @@ public class ArbitraryResource {
|
||||
.getArbitraryResources(service, identifier, Arrays.asList(name), defaultRes, null, null, reverse);
|
||||
|
||||
if (includeStatus != null && includeStatus) {
|
||||
resources = this.addStatusToResources(resources);
|
||||
resources = ArbitraryTransactionUtils.addStatusToResources(resources);
|
||||
}
|
||||
if (includeMetadata != null && includeMetadata) {
|
||||
resources = this.addMetadataToResources(resources);
|
||||
resources = ArbitraryTransactionUtils.addMetadataToResources(resources);
|
||||
}
|
||||
|
||||
creatorName.resources = resources;
|
||||
@@ -511,10 +511,10 @@ public class ArbitraryResource {
|
||||
}
|
||||
|
||||
if (includeStatus != null && includeStatus) {
|
||||
resources = this.addStatusToResources(resources);
|
||||
resources = ArbitraryTransactionUtils.addStatusToResources(resources);
|
||||
}
|
||||
if (includeMetadata != null && includeMetadata) {
|
||||
resources = this.addMetadataToResources(resources);
|
||||
resources = ArbitraryTransactionUtils.addMetadataToResources(resources);
|
||||
}
|
||||
|
||||
return resources;
|
||||
@@ -1258,42 +1258,4 @@ public class ArbitraryResource {
|
||||
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.FILE_NOT_FOUND, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private List<ArbitraryResourceInfo> addStatusToResources(List<ArbitraryResourceInfo> resources) {
|
||||
// Determine and add the status of each resource
|
||||
List<ArbitraryResourceInfo> updatedResources = new ArrayList<>();
|
||||
for (ArbitraryResourceInfo resourceInfo : resources) {
|
||||
try {
|
||||
ArbitraryDataResource resource = new ArbitraryDataResource(resourceInfo.name, ResourceIdType.NAME,
|
||||
resourceInfo.service, resourceInfo.identifier);
|
||||
ArbitraryResourceStatus status = resource.getStatus(true);
|
||||
if (status != null) {
|
||||
resourceInfo.status = status;
|
||||
}
|
||||
updatedResources.add(resourceInfo);
|
||||
|
||||
} catch (Exception e) {
|
||||
// Catch and log all exceptions, since some systems are experiencing 500 errors when including statuses
|
||||
LOGGER.info("Caught exception when adding status to resource %s: %s", resourceInfo, e.toString());
|
||||
}
|
||||
}
|
||||
return updatedResources;
|
||||
}
|
||||
|
||||
private List<ArbitraryResourceInfo> addMetadataToResources(List<ArbitraryResourceInfo> resources) {
|
||||
// Add metadata fields to each resource if they exist
|
||||
List<ArbitraryResourceInfo> updatedResources = new ArrayList<>();
|
||||
for (ArbitraryResourceInfo resourceInfo : resources) {
|
||||
ArbitraryDataResource resource = new ArbitraryDataResource(resourceInfo.name, ResourceIdType.NAME,
|
||||
resourceInfo.service, resourceInfo.identifier);
|
||||
ArbitraryDataTransactionMetadata transactionMetadata = resource.getLatestTransactionMetadata();
|
||||
ArbitraryResourceMetadata resourceMetadata = ArbitraryResourceMetadata.fromTransactionMetadata(transactionMetadata);
|
||||
if (resourceMetadata != null) {
|
||||
resourceInfo.metadata = resourceMetadata;
|
||||
}
|
||||
updatedResources.add(resourceInfo);
|
||||
}
|
||||
return updatedResources;
|
||||
}
|
||||
}
|
||||
|
276
src/main/java/org/qortal/arbitrary/apps/QApp.java
Normal file
276
src/main/java/org/qortal/arbitrary/apps/QApp.java
Normal file
@@ -0,0 +1,276 @@
|
||||
package org.qortal.arbitrary.apps;
|
||||
|
||||
import org.apache.commons.lang3.ArrayUtils;
|
||||
import org.bouncycastle.util.encoders.Base64;
|
||||
import org.ciyam.at.MachineState;
|
||||
import org.qortal.account.Account;
|
||||
import org.qortal.arbitrary.ArbitraryDataFile;
|
||||
import org.qortal.arbitrary.ArbitraryDataReader;
|
||||
import org.qortal.arbitrary.exception.MissingDataException;
|
||||
import org.qortal.arbitrary.misc.Service;
|
||||
import org.qortal.asset.Asset;
|
||||
import org.qortal.controller.Controller;
|
||||
import org.qortal.controller.LiteNode;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.data.account.AccountData;
|
||||
import org.qortal.data.arbitrary.ArbitraryResourceInfo;
|
||||
import org.qortal.data.arbitrary.ArbitraryResourceStatus;
|
||||
import org.qortal.data.at.ATData;
|
||||
import org.qortal.data.at.ATStateData;
|
||||
import org.qortal.data.chat.ChatMessage;
|
||||
import org.qortal.data.group.GroupData;
|
||||
import org.qortal.data.naming.NameData;
|
||||
import org.qortal.list.ResourceListManager;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
import org.qortal.settings.Settings;
|
||||
import org.qortal.utils.ArbitraryTransactionUtils;
|
||||
import org.qortal.utils.Base58;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class QApp {
|
||||
|
||||
public static AccountData getAccountData(String address) throws DataException {
|
||||
if (!Crypto.isValidAddress(address))
|
||||
throw new IllegalArgumentException("Invalid address");
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
return repository.getAccountRepository().getAccount(address);
|
||||
}
|
||||
}
|
||||
|
||||
public static List<NameData> getAccountNames(String address) throws DataException {
|
||||
if (!Crypto.isValidAddress(address))
|
||||
throw new IllegalArgumentException("Invalid address");
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
return repository.getNameRepository().getNamesByOwner(address);
|
||||
}
|
||||
}
|
||||
|
||||
public static NameData getNameData(String name) throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
if (Settings.getInstance().isLite()) {
|
||||
return LiteNode.getInstance().fetchNameData(name);
|
||||
} else {
|
||||
return repository.getNameRepository().fromName(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static List<ChatMessage> searchChatMessages(Long before, Long after, Integer txGroupId, List<String> involvingAddresses,
|
||||
String reference, String chatReference, Boolean hasChatReference,
|
||||
Integer limit, Integer offset, Boolean reverse) throws DataException {
|
||||
// Check args meet expectations
|
||||
if ((txGroupId == null && involvingAddresses.size() != 2)
|
||||
|| (txGroupId != null && !involvingAddresses.isEmpty()))
|
||||
throw new IllegalArgumentException("Invalid txGroupId or involvingAddresses");
|
||||
|
||||
// Check any provided addresses are valid
|
||||
if (involvingAddresses.stream().anyMatch(address -> !Crypto.isValidAddress(address)))
|
||||
throw new IllegalArgumentException("Invalid address");
|
||||
|
||||
if (before != null && before < 1500000000000L)
|
||||
throw new IllegalArgumentException("Invalid timestamp");
|
||||
|
||||
byte[] referenceBytes = null;
|
||||
if (reference != null)
|
||||
referenceBytes = Base58.decode(reference);
|
||||
|
||||
byte[] chatReferenceBytes = null;
|
||||
if (chatReference != null)
|
||||
chatReferenceBytes = Base58.decode(chatReference);
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
return repository.getChatRepository().getMessagesMatchingCriteria(
|
||||
before,
|
||||
after,
|
||||
txGroupId,
|
||||
referenceBytes,
|
||||
chatReferenceBytes,
|
||||
hasChatReference,
|
||||
involvingAddresses,
|
||||
limit, offset, reverse);
|
||||
}
|
||||
}
|
||||
|
||||
public static List<ArbitraryResourceInfo> searchQdnResources(Service service, String identifier, Boolean defaultResource,
|
||||
String nameListFilter, Boolean includeStatus, Boolean includeMetadata,
|
||||
Integer limit, Integer offset, Boolean reverse) throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
|
||||
// Treat empty identifier as null
|
||||
if (identifier != null && identifier.isEmpty()) {
|
||||
identifier = null;
|
||||
}
|
||||
|
||||
// Ensure that "default" and "identifier" parameters cannot coexist
|
||||
boolean defaultRes = Boolean.TRUE.equals(defaultResource);
|
||||
if (defaultRes == true && identifier != null) {
|
||||
throw new IllegalArgumentException("identifier cannot be specified when requesting a default resource");
|
||||
}
|
||||
|
||||
// Load filter from list if needed
|
||||
List<String> names = null;
|
||||
if (nameListFilter != null) {
|
||||
names = ResourceListManager.getInstance().getStringsInList(nameListFilter);
|
||||
if (names.isEmpty()) {
|
||||
// List doesn't exist or is empty - so there will be no matches
|
||||
return new ArrayList<>();
|
||||
}
|
||||
}
|
||||
|
||||
List<ArbitraryResourceInfo> resources = repository.getArbitraryRepository()
|
||||
.getArbitraryResources(service, identifier, names, defaultRes, limit, offset, reverse);
|
||||
|
||||
if (resources == null) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
if (includeStatus != null && includeStatus) {
|
||||
resources = ArbitraryTransactionUtils.addStatusToResources(resources);
|
||||
}
|
||||
if (includeMetadata != null && includeMetadata) {
|
||||
resources = ArbitraryTransactionUtils.addMetadataToResources(resources);
|
||||
}
|
||||
|
||||
return resources;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public static ArbitraryResourceStatus getQdnResourceStatus(Service service, String name, String identifier) {
|
||||
return ArbitraryTransactionUtils.getStatus(service, name, identifier, false);
|
||||
}
|
||||
|
||||
public static String fetchQdnResource64(Service service, String name, String identifier, String filepath, boolean rebuild) throws DataException {
|
||||
ArbitraryDataReader arbitraryDataReader = new ArbitraryDataReader(name, ArbitraryDataFile.ResourceIdType.NAME, service, identifier);
|
||||
try {
|
||||
|
||||
int attempts = 0;
|
||||
int maxAttempts = 5;
|
||||
|
||||
// Loop until we have data
|
||||
while (!Controller.isStopping()) {
|
||||
attempts++;
|
||||
if (!arbitraryDataReader.isBuilding()) {
|
||||
try {
|
||||
arbitraryDataReader.loadSynchronously(rebuild);
|
||||
break;
|
||||
} catch (MissingDataException e) {
|
||||
if (attempts > maxAttempts) {
|
||||
// Give up after 5 attempts
|
||||
throw new DataException("Data unavailable. Please try again later.");
|
||||
}
|
||||
}
|
||||
}
|
||||
Thread.sleep(3000L);
|
||||
}
|
||||
|
||||
java.nio.file.Path outputPath = arbitraryDataReader.getFilePath();
|
||||
if (outputPath == null) {
|
||||
// Assume the resource doesn't exist
|
||||
throw new DataException("File not found");
|
||||
}
|
||||
|
||||
if (filepath == null || filepath.isEmpty()) {
|
||||
// No file path supplied - so check if this is a single file resource
|
||||
String[] files = ArrayUtils.removeElement(outputPath.toFile().list(), ".qortal");
|
||||
if (files.length == 1) {
|
||||
// This is a single file resource
|
||||
filepath = files[0];
|
||||
}
|
||||
else {
|
||||
throw new IllegalArgumentException("filepath is required for resources containing more than one file");
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: limit file size that can be read into memory
|
||||
java.nio.file.Path path = Paths.get(outputPath.toString(), filepath);
|
||||
if (!Files.exists(path)) {
|
||||
return null;
|
||||
}
|
||||
byte[] bytes = Files.readAllBytes(path);
|
||||
if (bytes != null) {
|
||||
return Base64.toBase64String(bytes);
|
||||
}
|
||||
throw new DataException("File contents could not be read");
|
||||
|
||||
} catch (Exception e) {
|
||||
throw new DataException(String.format("Unable to fetch resource: %s", e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
public static List<GroupData> listGroups(Integer limit, Integer offset, Boolean reverse) throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
List<GroupData> allGroupData = repository.getGroupRepository().getAllGroups(limit, offset, reverse);
|
||||
allGroupData.forEach(groupData -> {
|
||||
try {
|
||||
groupData.memberCount = repository.getGroupRepository().countGroupMembers(groupData.getGroupId());
|
||||
} catch (DataException e) {
|
||||
// Exclude memberCount for this group
|
||||
}
|
||||
});
|
||||
return allGroupData;
|
||||
}
|
||||
}
|
||||
|
||||
public static Long getBalance(Long assetId, String address) throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
if (assetId == null)
|
||||
assetId = Asset.QORT;
|
||||
|
||||
Account account = new Account(repository, address);
|
||||
return account.getConfirmedBalance(assetId);
|
||||
}
|
||||
}
|
||||
|
||||
public static ATData getAtInfo(String atAddress) throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
if (atData == null) {
|
||||
throw new IllegalArgumentException("AT not found");
|
||||
}
|
||||
return atData;
|
||||
}
|
||||
}
|
||||
|
||||
public static String getAtData58(String atAddress) throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
ATStateData atStateData = repository.getATRepository().getLatestATState(atAddress);
|
||||
if (atStateData == null) {
|
||||
throw new IllegalArgumentException("AT not found");
|
||||
}
|
||||
byte[] stateData = atStateData.getStateData();
|
||||
byte[] dataBytes = MachineState.extractDataBytes(stateData);
|
||||
return Base58.encode(dataBytes);
|
||||
}
|
||||
}
|
||||
|
||||
public static List<ATData> listATs(String codeHash58, Boolean isExecutable, Integer limit, Integer offset, Boolean reverse) throws DataException {
|
||||
// Decode codeHash
|
||||
byte[] codeHash;
|
||||
try {
|
||||
codeHash = Base58.decode(codeHash58);
|
||||
} catch (NumberFormatException e) {
|
||||
throw new IllegalArgumentException(e);
|
||||
}
|
||||
|
||||
// codeHash must be present and have correct length
|
||||
if (codeHash == null || codeHash.length != 32)
|
||||
throw new IllegalArgumentException("Invalid code hash");
|
||||
|
||||
// Impose a limit on 'limit'
|
||||
if (limit != null && limit > 100)
|
||||
throw new IllegalArgumentException("Limit is too high");
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
return repository.getATRepository().getATsByFunctionality(codeHash, isExecutable, limit, offset, reverse);
|
||||
}
|
||||
}
|
||||
}
|
@@ -3,11 +3,11 @@ package org.qortal.utils;
|
||||
import org.apache.commons.lang3.ArrayUtils;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.qortal.arbitrary.ArbitraryDataFile;
|
||||
import org.qortal.arbitrary.ArbitraryDataFileChunk;
|
||||
import org.qortal.arbitrary.ArbitraryDataReader;
|
||||
import org.qortal.arbitrary.ArbitraryDataResource;
|
||||
import org.qortal.arbitrary.*;
|
||||
import org.qortal.arbitrary.metadata.ArbitraryDataTransactionMetadata;
|
||||
import org.qortal.arbitrary.misc.Service;
|
||||
import org.qortal.data.arbitrary.ArbitraryResourceInfo;
|
||||
import org.qortal.data.arbitrary.ArbitraryResourceMetadata;
|
||||
import org.qortal.data.arbitrary.ArbitraryResourceStatus;
|
||||
import org.qortal.data.transaction.ArbitraryTransactionData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
@@ -440,4 +440,41 @@ public class ArbitraryTransactionUtils {
|
||||
return resource.getStatus(false);
|
||||
}
|
||||
|
||||
public static List<ArbitraryResourceInfo> addStatusToResources(List<ArbitraryResourceInfo> resources) {
|
||||
// Determine and add the status of each resource
|
||||
List<ArbitraryResourceInfo> updatedResources = new ArrayList<>();
|
||||
for (ArbitraryResourceInfo resourceInfo : resources) {
|
||||
try {
|
||||
ArbitraryDataResource resource = new ArbitraryDataResource(resourceInfo.name, ArbitraryDataFile.ResourceIdType.NAME,
|
||||
resourceInfo.service, resourceInfo.identifier);
|
||||
ArbitraryResourceStatus status = resource.getStatus(true);
|
||||
if (status != null) {
|
||||
resourceInfo.status = status;
|
||||
}
|
||||
updatedResources.add(resourceInfo);
|
||||
|
||||
} catch (Exception e) {
|
||||
// Catch and log all exceptions, since some systems are experiencing 500 errors when including statuses
|
||||
LOGGER.info("Caught exception when adding status to resource %s: %s", resourceInfo, e.toString());
|
||||
}
|
||||
}
|
||||
return updatedResources;
|
||||
}
|
||||
|
||||
public static List<ArbitraryResourceInfo> addMetadataToResources(List<ArbitraryResourceInfo> resources) {
|
||||
// Add metadata fields to each resource if they exist
|
||||
List<ArbitraryResourceInfo> updatedResources = new ArrayList<>();
|
||||
for (ArbitraryResourceInfo resourceInfo : resources) {
|
||||
ArbitraryDataResource resource = new ArbitraryDataResource(resourceInfo.name, ArbitraryDataFile.ResourceIdType.NAME,
|
||||
resourceInfo.service, resourceInfo.identifier);
|
||||
ArbitraryDataTransactionMetadata transactionMetadata = resource.getLatestTransactionMetadata();
|
||||
ArbitraryResourceMetadata resourceMetadata = ArbitraryResourceMetadata.fromTransactionMetadata(transactionMetadata);
|
||||
if (resourceMetadata != null) {
|
||||
resourceInfo.metadata = resourceMetadata;
|
||||
}
|
||||
updatedResources.add(resourceInfo);
|
||||
}
|
||||
return updatedResources;
|
||||
}
|
||||
|
||||
}
|
||||
|
206
src/main/resources/q-apps/q-apps.js
Normal file
206
src/main/resources/q-apps/q-apps.js
Normal file
@@ -0,0 +1,206 @@
|
||||
function httpGet(url) {
|
||||
var request = new XMLHttpRequest();
|
||||
request.open("GET", url, false);
|
||||
request.send(null);
|
||||
return request.responseText;
|
||||
}
|
||||
|
||||
function handleResponse(event, response) {
|
||||
if (event == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle emmpty or missing responses
|
||||
if (response == null || response.length == 0) {
|
||||
response = "{\"error\": \"Empty response\"}"
|
||||
}
|
||||
|
||||
// Parse response
|
||||
let responseObj;
|
||||
try {
|
||||
responseObj = JSON.parse(response);
|
||||
} catch (e) {
|
||||
// Not all responses will be JSON
|
||||
responseObj = response;
|
||||
}
|
||||
|
||||
// Respond to app
|
||||
if (responseObj.error != null) {
|
||||
event.ports[0].postMessage({
|
||||
result: null,
|
||||
error: responseObj
|
||||
});
|
||||
}
|
||||
else {
|
||||
event.ports[0].postMessage({
|
||||
result: responseObj,
|
||||
error: null
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("message", (event) => {
|
||||
if (event == null || event.data == null || event.data.length == 0) {
|
||||
return;
|
||||
}
|
||||
if (event.data.action == null) {
|
||||
// This could be a response from the UI
|
||||
handleResponse(event, event.data);
|
||||
}
|
||||
if (event.data.requestedHandler != null && event.data.requestedHandler === "UI") {
|
||||
// This request was destined for the UI, so ignore it
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("Core received event: " + JSON.stringify(event.data));
|
||||
|
||||
let url;
|
||||
let response;
|
||||
let data = event.data;
|
||||
|
||||
switch (data.action) {
|
||||
case "GET_ACCOUNT_DATA":
|
||||
response = httpGet("/apps/account?address=" + data.address);
|
||||
break;
|
||||
|
||||
case "GET_ACCOUNT_NAMES":
|
||||
response = httpGet("/apps/account/names?address=" + data.address);
|
||||
break;
|
||||
|
||||
case "GET_NAME_DATA":
|
||||
response = httpGet("/apps/name?name=" + data.name);
|
||||
break;
|
||||
|
||||
case "SEARCH_QDN_RESOURCES":
|
||||
url = "/apps/resources?";
|
||||
if (data.service != null) url = url.concat("&service=" + data.service);
|
||||
if (data.identifier != null) url = url.concat("&identifier=" + data.identifier);
|
||||
if (data.default != null) url = url.concat("&default=" + data.default);
|
||||
if (data.nameListFilter != null) url = url.concat("&nameListFilter=" + data.nameListFilter);
|
||||
if (data.includeStatus != null) url = url.concat("&includeStatus=" + new Boolean(data.includeStatus).toString());
|
||||
if (data.includeMetadata != null) url = url.concat("&includeMetadata=" + new Boolean(data.includeMetadata).toString());
|
||||
if (data.limit != null) url = url.concat("&limit=" + data.limit);
|
||||
if (data.offset != null) url = url.concat("&offset=" + data.offset);
|
||||
if (data.reverse != null) url = url.concat("&reverse=" + new Boolean(data.reverse).toString());
|
||||
response = httpGet(url);
|
||||
break;
|
||||
|
||||
case "FETCH_QDN_RESOURCE":
|
||||
url = "/apps/resource?";
|
||||
if (data.service != null) url = url.concat("&service=" + data.service);
|
||||
if (data.name != null) url = url.concat("&name=" + data.name);
|
||||
if (data.identifier != null) url = url.concat("&identifier=" + data.identifier);
|
||||
if (data.filepath != null) url = url.concat("&filepath=" + data.filepath);
|
||||
if (data.rebuild != null) url = url.concat("&rebuild=" + new Boolean(data.rebuild).toString())
|
||||
response = httpGet(url);
|
||||
break;
|
||||
|
||||
case "GET_QDN_RESOURCE_STATUS":
|
||||
url = "/apps/resourcestatus?";
|
||||
if (data.service != null) url = url.concat("&service=" + data.service);
|
||||
if (data.name != null) url = url.concat("&name=" + data.name);
|
||||
if (data.identifier != null) url = url.concat("&identifier=" + data.identifier);
|
||||
response = httpGet(url);
|
||||
break;
|
||||
|
||||
case "SEARCH_CHAT_MESSAGES":
|
||||
url = "/apps/chatmessages?";
|
||||
if (data.before != null) url = url.concat("&before=" + data.before);
|
||||
if (data.after != null) url = url.concat("&after=" + data.after);
|
||||
if (data.txGroupId != null) url = url.concat("&txGroupId=" + data.txGroupId);
|
||||
if (data.involving != null) data.involving.forEach((x, i) => url = url.concat("&involving=" + x));
|
||||
if (data.reference != null) url = url.concat("&reference=" + data.reference);
|
||||
if (data.chatReference != null) url = url.concat("&chatReference=" + data.chatReference);
|
||||
if (data.hasChatReference != null) url = url.concat("&hasChatReference=" + new Boolean(data.hasChatReference).toString());
|
||||
if (data.limit != null) url = url.concat("&limit=" + data.limit);
|
||||
if (data.offset != null) url = url.concat("&offset=" + data.offset);
|
||||
if (data.reverse != null) url = url.concat("&reverse=" + new Boolean(data.reverse).toString());
|
||||
response = httpGet(url);
|
||||
break;
|
||||
|
||||
case "LIST_GROUPS":
|
||||
url = "/apps/groups?";
|
||||
if (data.limit != null) url = url.concat("&limit=" + data.limit);
|
||||
if (data.offset != null) url = url.concat("&offset=" + data.offset);
|
||||
if (data.reverse != null) url = url.concat("&reverse=" + new Boolean(data.reverse).toString());
|
||||
response = httpGet(url);
|
||||
break;
|
||||
|
||||
case "GET_BALANCE":
|
||||
url = "/apps/balance?";
|
||||
if (data.assetId != null) url = url.concat("&assetId=" + data.assetId);
|
||||
if (data.address != null) url = url.concat("&address=" + data.address);
|
||||
response = httpGet(url);
|
||||
break;
|
||||
|
||||
case "GET_AT":
|
||||
url = "/apps/at?";
|
||||
if (data.atAddress != null) url = url.concat("&atAddress=" + data.atAddress);
|
||||
response = httpGet(url);
|
||||
break;
|
||||
|
||||
case "GET_AT_DATA":
|
||||
url = "/apps/atdata?";
|
||||
if (data.atAddress != null) url = url.concat("&atAddress=" + data.atAddress);
|
||||
response = httpGet(url);
|
||||
break;
|
||||
|
||||
case "LIST_ATS":
|
||||
url = "/apps/ats?";
|
||||
if (data.codeHash58 != null) url = url.concat("&codeHash58=" + data.codeHash58);
|
||||
if (data.isExecutable != null) url = url.concat("&isExecutable=" + data.isExecutable);
|
||||
if (data.limit != null) url = url.concat("&limit=" + data.limit);
|
||||
if (data.offset != null) url = url.concat("&offset=" + data.offset);
|
||||
if (data.reverse != null) url = url.concat("&reverse=" + new Boolean(data.reverse).toString());
|
||||
response = httpGet(url);
|
||||
break;
|
||||
|
||||
default:
|
||||
// Pass to parent (UI), in case they can fulfil this request
|
||||
event.data.requestedHandler = "UI";
|
||||
parent.postMessage(event.data, '*', [event.ports[0]]);
|
||||
return;
|
||||
}
|
||||
|
||||
handleResponse(event, response);
|
||||
|
||||
}, false);
|
||||
|
||||
const awaitTimeout = (timeout, reason) =>
|
||||
new Promise((resolve, reject) =>
|
||||
setTimeout(
|
||||
() => (reason === undefined ? resolve() : reject(reason)),
|
||||
timeout
|
||||
)
|
||||
);
|
||||
|
||||
/**
|
||||
* Make a Qortal (Q-Apps) request with no timeout
|
||||
*/
|
||||
const qortalRequestWithNoTimeout = (request) => new Promise((res, rej) => {
|
||||
const channel = new MessageChannel();
|
||||
|
||||
channel.port1.onmessage = ({data}) => {
|
||||
channel.port1.close();
|
||||
|
||||
if (data.error) {
|
||||
rej(data.error);
|
||||
} else {
|
||||
res(data.result);
|
||||
}
|
||||
};
|
||||
|
||||
window.postMessage(request, '*', [channel.port2]);
|
||||
});
|
||||
|
||||
/**
|
||||
* Make a Qortal (Q-Apps) request with the default timeout (10 seconds)
|
||||
*/
|
||||
const qortalRequest = (request) =>
|
||||
Promise.race([qortalRequestWithNoTimeout(request), awaitTimeout(10000, "The request timed out")]);
|
||||
|
||||
/**
|
||||
* Make a Qortal (Q-Apps) request with a custom timeout, specified in milliseconds
|
||||
*/
|
||||
const qortalRequestWithTimeout = (request, timeout) =>
|
||||
Promise.race([qortalRequestWithNoTimeout(request), awaitTimeout(timeout, "The request timed out")]);
|
Reference in New Issue
Block a user