forked from Qortal/qortal
Compare commits
16 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
e48529704c | ||
|
53508f9298 | ||
|
33aeec7e87 | ||
|
16dc23ddc7 | ||
|
e80494b784 | ||
|
111ec3b483 | ||
|
db4a9ee880 | ||
|
b1ebe1864b | ||
|
3c251c35ea | ||
|
4954a1744b | ||
|
a7bbad17d7 | ||
|
8ca9423c52 | ||
|
32b9b7e578 | ||
|
f045e10ada | ||
|
560282dc1d | ||
|
9cd6372161 |
26
Q-Apps.md
26
Q-Apps.md
@@ -46,6 +46,8 @@ IMAGE,
|
||||
THUMBNAIL,
|
||||
VIDEO,
|
||||
AUDIO,
|
||||
PODCAST,
|
||||
VOICE,
|
||||
ARBITRARY_DATA,
|
||||
JSON,
|
||||
DOCUMENT,
|
||||
@@ -55,7 +57,25 @@ METADATA,
|
||||
BLOG,
|
||||
BLOG_POST,
|
||||
BLOG_COMMENT,
|
||||
GIF_REPOSITORY
|
||||
GIF_REPOSITORY,
|
||||
ATTACHMENT,
|
||||
FILE,
|
||||
FILES,
|
||||
CHAIN_DATA,
|
||||
STORE,
|
||||
PRODUCT,
|
||||
OFFER,
|
||||
COUPON,
|
||||
CODE,
|
||||
PLUGIN,
|
||||
EXTENSION,
|
||||
GAME,
|
||||
ITEM,
|
||||
NFT,
|
||||
DATABASE,
|
||||
SNAPSHOT,
|
||||
COMMENT,
|
||||
CHAIN_COMMENT,
|
||||
WEBSITE,
|
||||
APP,
|
||||
QCHAT_ATTACHMENT,
|
||||
@@ -320,6 +340,7 @@ let res = await qortalRequest({
|
||||
identifier: "search query goes here", // Optional - searches only the "identifier" field
|
||||
name: "search query goes here", // Optional - searches only the "name" field
|
||||
prefix: false, // Optional - if true, only the beginning of fields are matched in all of the above filters
|
||||
exactMatchNames: true, // Optional - if true, partial name matches are excluded
|
||||
default: false, // Optional - if true, only resources without identifiers are returned
|
||||
includeStatus: false, // Optional - will take time to respond, so only request if necessary
|
||||
includeMetadata: false, // Optional - will take time to respond, so only request if necessary
|
||||
@@ -652,6 +673,7 @@ let res = await qortalRequest({
|
||||
```
|
||||
|
||||
### Get URL to load a QDN resource
|
||||
Note: this returns a "Resource does not exist" error if a non-existent resource is requested.
|
||||
```
|
||||
let url = await qortalRequest({
|
||||
action: "GET_QDN_RESOURCE_URL",
|
||||
@@ -663,6 +685,7 @@ let url = await qortalRequest({
|
||||
```
|
||||
|
||||
### Get URL to load a QDN website
|
||||
Note: this returns a "Resource does not exist" error if a non-existent resource is requested.
|
||||
```
|
||||
let url = await qortalRequest({
|
||||
action: "GET_QDN_RESOURCE_URL",
|
||||
@@ -672,6 +695,7 @@ let url = await qortalRequest({
|
||||
```
|
||||
|
||||
### Get URL to load a specific file from a QDN website
|
||||
Note: this returns a "Resource does not exist" error if a non-existent resource is requested.
|
||||
```
|
||||
let url = await qortalRequest({
|
||||
action: "GET_QDN_RESOURCE_URL",
|
||||
|
2
pom.xml
2
pom.xml
@@ -3,7 +3,7 @@
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>org.qortal</groupId>
|
||||
<artifactId>qortal</artifactId>
|
||||
<version>3.9.1</version>
|
||||
<version>4.0.0</version>
|
||||
<packaging>jar</packaging>
|
||||
<properties>
|
||||
<skipTests>true</skipTests>
|
||||
|
@@ -175,6 +175,7 @@ public class ArbitraryResource {
|
||||
@Parameter(description = "Identifier (searches identifier field only)") @QueryParam("identifier") String identifier,
|
||||
@Parameter(description = "Name (searches name field only)") @QueryParam("name") List<String> names,
|
||||
@Parameter(description = "Prefix only (if true, only the beginning of fields are matched)") @QueryParam("prefix") Boolean prefixOnly,
|
||||
@Parameter(description = "Exact match names only (if true, partial name matches are excluded)") @QueryParam("exactmatchnames") Boolean exactMatchNamesOnly,
|
||||
@Parameter(description = "Default resources (without identifiers) only") @QueryParam("default") Boolean defaultResource,
|
||||
@Parameter(description = "Filter names by list (exact matches only)") @QueryParam("namefilter") String nameListFilter,
|
||||
@Parameter(description = "Include followed names only") @QueryParam("followedonly") Boolean followedOnly,
|
||||
@@ -202,6 +203,12 @@ public class ArbitraryResource {
|
||||
}
|
||||
}
|
||||
|
||||
// Move names to exact match list, if requested
|
||||
if (exactMatchNamesOnly != null && exactMatchNamesOnly && names != null) {
|
||||
exactMatchNames.addAll(names);
|
||||
names = null;
|
||||
}
|
||||
|
||||
List<ArbitraryResourceInfo> resources = repository.getArbitraryRepository()
|
||||
.searchArbitraryResources(service, query, identifier, names, usePrefixOnly, exactMatchNames, defaultRes, followedOnly, excludeBlocked, limit, offset, reverse);
|
||||
|
||||
|
@@ -184,7 +184,7 @@ public class AdminResource {
|
||||
)
|
||||
}
|
||||
)
|
||||
public Object setting(@PathParam("setting") String setting) {
|
||||
public String setting(@PathParam("setting") String setting) {
|
||||
try {
|
||||
Object settingValue = FieldUtils.readField(Settings.getInstance(), setting, true);
|
||||
if (settingValue == null) {
|
||||
@@ -198,8 +198,8 @@ public class AdminResource {
|
||||
JSONArray array = new JSONArray((List<Object>) settingValue);
|
||||
return array.toString(4);
|
||||
}
|
||||
return settingValue;
|
||||
|
||||
return settingValue.toString();
|
||||
} catch (IllegalAccessException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA, e);
|
||||
}
|
||||
|
@@ -79,7 +79,7 @@ public class ArbitraryDataFile {
|
||||
this.signature = signature;
|
||||
}
|
||||
|
||||
public ArbitraryDataFile(byte[] fileContent, byte[] signature) throws DataException {
|
||||
public ArbitraryDataFile(byte[] fileContent, byte[] signature, boolean useTemporaryFile) throws DataException {
|
||||
if (fileContent == null) {
|
||||
LOGGER.error("fileContent is null");
|
||||
return;
|
||||
@@ -90,7 +90,20 @@ public class ArbitraryDataFile {
|
||||
this.signature = signature;
|
||||
LOGGER.trace(String.format("File digest: %s, size: %d bytes", this.hash58, fileContent.length));
|
||||
|
||||
Path outputFilePath = getOutputFilePath(this.hash58, signature, true);
|
||||
Path outputFilePath;
|
||||
if (useTemporaryFile) {
|
||||
try {
|
||||
outputFilePath = Files.createTempFile("qortalRawData", null);
|
||||
outputFilePath.toFile().deleteOnExit();
|
||||
}
|
||||
catch (IOException e) {
|
||||
throw new DataException(String.format("Unable to write data with hash %s to temporary file: %s", this.hash58, e.getMessage()));
|
||||
}
|
||||
}
|
||||
else {
|
||||
outputFilePath = getOutputFilePath(this.hash58, signature, true);
|
||||
}
|
||||
|
||||
File outputFile = outputFilePath.toFile();
|
||||
try (FileOutputStream outputStream = new FileOutputStream(outputFile)) {
|
||||
outputStream.write(fileContent);
|
||||
@@ -116,7 +129,7 @@ public class ArbitraryDataFile {
|
||||
if (data == null) {
|
||||
return null;
|
||||
}
|
||||
return new ArbitraryDataFile(data, signature);
|
||||
return new ArbitraryDataFile(data, signature, true);
|
||||
}
|
||||
|
||||
public static ArbitraryDataFile fromTransactionData(ArbitraryTransactionData transactionData) throws DataException {
|
||||
|
@@ -18,7 +18,7 @@ public class ArbitraryDataFileChunk extends ArbitraryDataFile {
|
||||
}
|
||||
|
||||
public ArbitraryDataFileChunk(byte[] fileContent, byte[] signature) throws DataException {
|
||||
super(fileContent, signature);
|
||||
super(fileContent, signature, false);
|
||||
}
|
||||
|
||||
public static ArbitraryDataFileChunk fromHash58(String hash58, byte[] signature) throws DataException {
|
||||
|
@@ -47,6 +47,10 @@ public enum Service {
|
||||
return ValidationResult.OK;
|
||||
}
|
||||
},
|
||||
ATTACHMENT(130, false, null, true, null),
|
||||
FILE(140, false, null, true, null),
|
||||
FILES(150, false, null, false, null),
|
||||
CHAIN_DATA(160, true, 239L, true, null),
|
||||
WEBSITE(200, true, null, false, null) {
|
||||
@Override
|
||||
public ValidationResult validate(Path path) throws IOException {
|
||||
@@ -77,9 +81,11 @@ public enum Service {
|
||||
AUDIO(600, false, null, true, null),
|
||||
QCHAT_AUDIO(610, true, 10*1024*1024L, true, null),
|
||||
QCHAT_VOICE(620, true, 10*1024*1024L, true, null),
|
||||
VOICE(630, true, 10*1024*1024L, true, null),
|
||||
PODCAST(640, false, null, true, null),
|
||||
BLOG(700, false, null, false, null),
|
||||
BLOG_POST(777, false, null, true, null),
|
||||
BLOG_COMMENT(778, false, null, true, null),
|
||||
BLOG_COMMENT(778, true, 500*1024L, true, null),
|
||||
DOCUMENT(800, false, null, true, null),
|
||||
LIST(900, true, null, true, null),
|
||||
PLAYLIST(910, true, null, true, null),
|
||||
@@ -139,7 +145,21 @@ public enum Service {
|
||||
}
|
||||
return ValidationResult.OK;
|
||||
}
|
||||
};
|
||||
},
|
||||
STORE(1300, false, null, true, null),
|
||||
PRODUCT(1310, false, null, true, null),
|
||||
OFFER(1330, false, null, true, null),
|
||||
COUPON(1340, false, null, true, null),
|
||||
CODE(1400, false, null, true, null),
|
||||
PLUGIN(1410, false, null, true, null),
|
||||
EXTENSION(1420, false, null, true, null),
|
||||
GAME(1500, false, null, false, null),
|
||||
ITEM(1510, false, null, true, null),
|
||||
NFT(1600, false, null, true, null),
|
||||
DATABASE(1700, false, null, false, null),
|
||||
SNAPSHOT(1710, false, null, false, null),
|
||||
COMMENT(1800, true, 500*1024L, true, null),
|
||||
CHAIN_COMMENT(1810, true, 239L, true, null);
|
||||
|
||||
public final int value;
|
||||
private final boolean requiresValidation;
|
||||
|
@@ -46,7 +46,7 @@ public class ArbitraryResourceStatus {
|
||||
this.description = status.description;
|
||||
this.localChunkCount = localChunkCount;
|
||||
this.totalChunkCount = totalChunkCount;
|
||||
this.percentLoaded = (this.localChunkCount != null && this.totalChunkCount != null) ? this.localChunkCount / (float)this.totalChunkCount * 100.0f : null;
|
||||
this.percentLoaded = (this.localChunkCount != null && this.totalChunkCount != null && this.totalChunkCount > 0) ? this.localChunkCount / (float)this.totalChunkCount * 100.0f : null;
|
||||
}
|
||||
|
||||
public ArbitraryResourceStatus(Status status) {
|
||||
|
@@ -68,7 +68,7 @@ public class ArbitraryDataFileMessage extends Message {
|
||||
byteBuffer.get(data);
|
||||
|
||||
try {
|
||||
ArbitraryDataFile arbitraryDataFile = new ArbitraryDataFile(data, signature);
|
||||
ArbitraryDataFile arbitraryDataFile = new ArbitraryDataFile(data, signature, false);
|
||||
return new ArbitraryDataFileMessage(id, signature, arbitraryDataFile);
|
||||
} catch (DataException e) {
|
||||
LOGGER.info("Unable to process received file: {}", e.getMessage());
|
||||
|
@@ -64,7 +64,7 @@ public class ArbitraryMetadataMessage extends Message {
|
||||
byteBuffer.get(data);
|
||||
|
||||
try {
|
||||
ArbitraryDataFile arbitraryMetadataFile = new ArbitraryDataFile(data, signature);
|
||||
ArbitraryDataFile arbitraryMetadataFile = new ArbitraryDataFile(data, signature, false);
|
||||
return new ArbitraryMetadataMessage(id, signature, arbitraryMetadataFile);
|
||||
} catch (DataException e) {
|
||||
throw new MessageException("Unable to process arbitrary metadata message: " + e.getMessage(), e);
|
||||
|
@@ -452,12 +452,12 @@ public class HSQLDBArbitraryRepository implements ArbitraryRepository {
|
||||
|
||||
// Handle name exact matches
|
||||
if (exactMatchNames != null && !exactMatchNames.isEmpty()) {
|
||||
sql.append(" AND name IN (?");
|
||||
bindParams.add(exactMatchNames.get(0));
|
||||
sql.append(" AND LCASE(name) IN (?");
|
||||
bindParams.add(exactMatchNames.get(0).toLowerCase());
|
||||
|
||||
for (int i = 1; i < exactMatchNames.size(); ++i) {
|
||||
sql.append(", ?");
|
||||
bindParams.add(exactMatchNames.get(i));
|
||||
bindParams.add(exactMatchNames.get(i).toLowerCase());
|
||||
}
|
||||
sql.append(")");
|
||||
}
|
||||
@@ -466,12 +466,12 @@ public class HSQLDBArbitraryRepository implements ArbitraryRepository {
|
||||
if (followedOnly != null && followedOnly) {
|
||||
List<String> followedNames = ListUtils.followedNames();
|
||||
if (followedNames != null && !followedNames.isEmpty()) {
|
||||
sql.append(" AND name IN (?");
|
||||
bindParams.add(followedNames.get(0));
|
||||
sql.append(" AND LCASE(name) IN (?");
|
||||
bindParams.add(followedNames.get(0).toLowerCase());
|
||||
|
||||
for (int i = 1; i < followedNames.size(); ++i) {
|
||||
sql.append(", ?");
|
||||
bindParams.add(followedNames.get(i));
|
||||
bindParams.add(followedNames.get(i).toLowerCase());
|
||||
}
|
||||
sql.append(")");
|
||||
}
|
||||
@@ -481,12 +481,12 @@ public class HSQLDBArbitraryRepository implements ArbitraryRepository {
|
||||
if (excludeBlocked != null && excludeBlocked) {
|
||||
List<String> blockedNames = ListUtils.blockedNames();
|
||||
if (blockedNames != null && !blockedNames.isEmpty()) {
|
||||
sql.append(" AND name NOT IN (?");
|
||||
bindParams.add(blockedNames.get(0));
|
||||
sql.append(" AND LCASE(name) NOT IN (?");
|
||||
bindParams.add(blockedNames.get(0).toLowerCase());
|
||||
|
||||
for (int i = 1; i < blockedNames.size(); ++i) {
|
||||
sql.append(", ?");
|
||||
bindParams.add(blockedNames.get(i));
|
||||
bindParams.add(blockedNames.get(i).toLowerCase());
|
||||
}
|
||||
sql.append(")");
|
||||
}
|
||||
|
@@ -50,12 +50,16 @@ window.addEventListener("message", (event) => {
|
||||
switch (data.action) {
|
||||
case "GET_USER_ACCOUNT":
|
||||
case "PUBLISH_QDN_RESOURCE":
|
||||
case "PUBLISH_MULTIPLE_QDN_RESOURCES":
|
||||
case "SEND_CHAT_MESSAGE":
|
||||
case "JOIN_GROUP":
|
||||
case "DEPLOY_AT":
|
||||
case "GET_WALLET_BALANCE":
|
||||
case "SEND_COIN":
|
||||
const errorString = "Authentication was requested, but this is not yet supported when viewing via a gateway. To use interactive features, please access using the Qortal UI desktop app. More info at: https://qortal.org";
|
||||
case "GET_LIST_ITEMS":
|
||||
case "ADD_LIST_ITEMS":
|
||||
case "DELETE_LIST_ITEM":
|
||||
const errorString = "Interactive features were requested, but these are not yet supported when viewing via a gateway. To use interactive features, please access using the Qortal UI desktop app. More info at: https://qortal.org";
|
||||
response = "{\"error\": \"" + errorString + "\"}"
|
||||
|
||||
const modalText = "This app is powered by the Qortal blockchain. You are viewing in read-only mode. To use interactive features, please access using the Qortal UI desktop app.";
|
||||
|
@@ -23,7 +23,7 @@ function httpGetAsyncWithEvent(event, url) {
|
||||
.catch((error) => {
|
||||
let res = {};
|
||||
res.error = error;
|
||||
handleResponse(JSON.stringify(res), responseText);
|
||||
handleResponse(event, JSON.stringify(res));
|
||||
})
|
||||
}
|
||||
|
||||
@@ -46,6 +46,18 @@ function handleResponse(event, response) {
|
||||
responseObj = response;
|
||||
}
|
||||
|
||||
// GET_QDN_RESOURCE_URL has custom handling
|
||||
const data = event.data;
|
||||
if (data.action == "GET_QDN_RESOURCE_URL") {
|
||||
if (responseObj == null || responseObj.status == null || responseObj.status == "NOT_PUBLISHED") {
|
||||
responseObj = {};
|
||||
responseObj.error = "Resource does not exist";
|
||||
}
|
||||
else {
|
||||
responseObj = buildResourceUrl(data.service, data.name, data.identifier, data.path, false);
|
||||
}
|
||||
}
|
||||
|
||||
// Respond to app
|
||||
if (responseObj.error != null) {
|
||||
event.ports[0].postMessage({
|
||||
@@ -173,9 +185,10 @@ window.addEventListener("message", (event) => {
|
||||
return httpGetAsyncWithEvent(event, "/names/" + data.name);
|
||||
|
||||
case "GET_QDN_RESOURCE_URL":
|
||||
const response = buildResourceUrl(data.service, data.name, data.identifier, data.path, false);
|
||||
handleResponse(event, response);
|
||||
return;
|
||||
// Check status first; URL is built and returned automatically after status check
|
||||
url = "/arbitrary/resource/status/" + data.service + "/" + data.name;
|
||||
if (data.identifier != null) url = url.concat("/" + data.identifier);
|
||||
return httpGetAsyncWithEvent(event, url);
|
||||
|
||||
case "LINK_TO_QDN_RESOURCE":
|
||||
if (data.service == null) data.service = "WEBSITE"; // Default to WEBSITE
|
||||
@@ -206,6 +219,7 @@ window.addEventListener("message", (event) => {
|
||||
if (data.name != null) url = url.concat("&name=" + data.name);
|
||||
if (data.names != null) data.names.forEach((x, i) => url = url.concat("&name=" + x));
|
||||
if (data.prefix != null) url = url.concat("&prefix=" + new Boolean(data.prefix).toString());
|
||||
if (data.exactMatchNames != null) url = url.concat("&exactmatchnames=" + new Boolean(data.exactMatchNames).toString());
|
||||
if (data.default != null) url = url.concat("&default=" + new Boolean(data.default).toString());
|
||||
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());
|
||||
|
@@ -20,7 +20,7 @@ public class ArbitraryDataFileTests extends Common {
|
||||
@Test
|
||||
public void testSplitAndJoin() throws DataException {
|
||||
String dummyDataString = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890";
|
||||
ArbitraryDataFile arbitraryDataFile = new ArbitraryDataFile(dummyDataString.getBytes(), null);
|
||||
ArbitraryDataFile arbitraryDataFile = new ArbitraryDataFile(dummyDataString.getBytes(), null, false);
|
||||
assertTrue(arbitraryDataFile.exists());
|
||||
assertEquals(62, arbitraryDataFile.size());
|
||||
assertEquals("3eyjYjturyVe61grRX42bprGr3Cvw6ehTy4iknVnosDj", arbitraryDataFile.digest58());
|
||||
@@ -50,7 +50,7 @@ public class ArbitraryDataFileTests extends Common {
|
||||
byte[] randomData = new byte[fileSize];
|
||||
new Random().nextBytes(randomData); // No need for SecureRandom here
|
||||
|
||||
ArbitraryDataFile arbitraryDataFile = new ArbitraryDataFile(randomData, null);
|
||||
ArbitraryDataFile arbitraryDataFile = new ArbitraryDataFile(randomData, null, false);
|
||||
assertTrue(arbitraryDataFile.exists());
|
||||
assertEquals(fileSize, arbitraryDataFile.size());
|
||||
String originalFileDigest = arbitraryDataFile.digest58();
|
||||
|
@@ -33,7 +33,7 @@ public class ArbitraryTestTransaction extends TestTransaction {
|
||||
final byte[] metadataHash = new byte[32];
|
||||
random.nextBytes(metadataHash);
|
||||
|
||||
byte[] data = new byte[1024];
|
||||
byte[] data = new byte[256];
|
||||
random.nextBytes(data);
|
||||
|
||||
DataType dataType = DataType.RAW_DATA;
|
||||
|
Reference in New Issue
Block a user