From 69b68ed1e496abb90326497b41b6d6ca0e9fd2f2 Mon Sep 17 00:00:00 2001 From: netkas Date: Mon, 9 Dec 2024 19:03:33 -0500 Subject: [PATCH] Add user and captcha handling methods --- .../java/net/nosial/socialclient/Client.java | 93 +- .../nosial/socialclient/classes/Resolver.java | 32 +- .../socialclient/classes/RpcClient.java | 815 +++++++++--------- .../enums/StandardErrorCodes.java | 11 +- .../socialclient/enums/flags/PeerFlags.java | 39 + .../objects/standard/ImageCaptcha.java | 46 + .../objects/standard/SelfUser.java | 106 +++ .../CreateSessionTest.java} | 21 +- .../socialclient/methods/RegisterTest.java | 127 +++ 9 files changed, 854 insertions(+), 436 deletions(-) create mode 100644 src/main/java/net/nosial/socialclient/enums/flags/PeerFlags.java create mode 100644 src/main/java/net/nosial/socialclient/objects/standard/ImageCaptcha.java create mode 100644 src/main/java/net/nosial/socialclient/objects/standard/SelfUser.java rename src/test/java/net/nosial/socialclient/{SessionTest.java => methods/CreateSessionTest.java} (88%) create mode 100644 src/test/java/net/nosial/socialclient/methods/RegisterTest.java diff --git a/src/main/java/net/nosial/socialclient/Client.java b/src/main/java/net/nosial/socialclient/Client.java index fa0a1d7..f1af53e 100644 --- a/src/main/java/net/nosial/socialclient/Client.java +++ b/src/main/java/net/nosial/socialclient/Client.java @@ -8,6 +8,8 @@ import net.nosial.socialclient.exceptions.CryptographyException; import net.nosial.socialclient.exceptions.ResolutionException; import net.nosial.socialclient.exceptions.RpcException; import net.nosial.socialclient.objects.RpcRequest; +import net.nosial.socialclient.objects.standard.ImageCaptcha; +import net.nosial.socialclient.objects.standard.SelfUser; import java.security.KeyPair; import java.security.PrivateKey; @@ -17,8 +19,6 @@ import java.util.Map; public class Client extends RpcClient { - - public Client(String domain) throws ResolutionException, CryptographyException { super(domain); @@ -31,7 +31,8 @@ public class Client extends RpcClient public String createSession(KeyPair keyPair) throws RpcException { - RpcResult response = this.sendRequest(new RpcRequest("create_session", Utilities.randomCrc32(), new HashMap<>(){{ + RpcResult response = this.sendRequest(new RpcRequest("createSession", Utilities.randomCrc32(), new HashMap<>() + {{ put("public_key", Cryptography.exportPublicKey(keyPair)); }})); @@ -78,4 +79,90 @@ public class Client extends RpcClient return true; } + + public boolean register(String username) throws RpcException + { + RpcResult response = this.sendRequest(new RpcRequest("register", Utilities.randomCrc32(), new HashMap<>() + {{ + put("username", username); + }})); + + if(response == null) + { + throw new RpcException("Response is null"); + } + + if(!response.isSuccess()) + { + throw new RpcException(response); + } + + return (boolean)response.getResponse().getResult(); + } + + @SuppressWarnings("unchecked") + public SelfUser getMe() throws RpcException + { + RpcResult response = this.sendRequest(new RpcRequest("getMe", Utilities.randomCrc32())); + + if(response == null) + { + throw new RpcException("Response is null"); + } + + if(!response.isSuccess()) + { + throw new RpcException(response); + } + + if(response.getResponse().getResult() instanceof Map) + { + return new SelfUser((Map) response.getResponse().getResult()); + } + + throw new RpcException("Unexpected result type, expected Map"); + } + + @SuppressWarnings("unchecked") + public ImageCaptcha verificationGetImageCaptcha() throws RpcException + { + RpcResult response = this.sendRequest(new RpcRequest("verificationGetImageCaptcha", Utilities.randomCrc32())); + + if(response == null) + { + throw new RpcException("Response is null"); + } + + if(!response.isSuccess()) + { + throw new RpcException(response); + } + + if(response.getResponse().getResult() instanceof Map) + { + return new ImageCaptcha((Map) response.getResponse().getResult()); + } + + throw new RpcException("Unexpected result type, expected Map"); + } + + public boolean verificationAnswerImageCaptcha(String answer) throws RpcException + { + RpcResult response = this.sendRequest(new RpcRequest("verificationAnswerImageCaptcha", Utilities.randomCrc32(), new HashMap<>() + {{ + put("answer", answer); + }})); + + if(response == null) + { + throw new RpcException("Response is null"); + } + + if(!response.isSuccess()) + { + throw new RpcException(response); + } + + return (boolean)response.getResponse().getResult(); + } } diff --git a/src/main/java/net/nosial/socialclient/classes/Resolver.java b/src/main/java/net/nosial/socialclient/classes/Resolver.java index 2381daa..d8dcd9a 100644 --- a/src/main/java/net/nosial/socialclient/classes/Resolver.java +++ b/src/main/java/net/nosial/socialclient/classes/Resolver.java @@ -9,8 +9,10 @@ import java.util.Hashtable; import java.util.regex.Matcher; import java.util.regex.Pattern; -public class Resolver { - public static ResolvedServer resolveDomain(String domain) throws ResolutionException { +public class Resolver +{ + public static ResolvedServer resolveDomain(String domain) throws ResolutionException + { final Pattern fullPattern = Pattern.compile("v=socialbox;sb-rpc=(https?://[^;]+);sb-key=([^;]+)"); Hashtable env = new Hashtable<>(); @@ -18,18 +20,21 @@ public class Resolver { env.put("com.sun.jndi.dns.cache.ttl", "1"); env.put("com.sun.jndi.dns.cache.negative.ttl", "0"); - try { + try + { DirContext dirContext = new InitialDirContext(env); Attributes attributes = dirContext.getAttributes("dns:/" + domain, new String[]{"TXT"}); Attribute attributeTXT = attributes.get("TXT"); - if (attributeTXT == null) { + if (attributeTXT == null) + { throw new ResolutionException("Failed to resolve DNS TXT records for " + domain); } StringBuilder fullRecordBuilder = new StringBuilder(); - for (int i = 0; i < attributeTXT.size(); i++) { + for (int i = 0; i < attributeTXT.size(); i++) + { String value = (String) attributeTXT.get(i); fullRecordBuilder.append(value.replaceAll("^\"|\"$", "").trim()); } @@ -37,23 +42,30 @@ public class Resolver { String fullRecord = fullRecordBuilder.toString(); Matcher matcher = fullPattern.matcher(fullRecord); - if (matcher.find()) { + if (matcher.find()) + { String endpoint = matcher.group(1); String publicKey = matcher.group(2).replaceAll("\\s+", ""); - if (endpoint == null || endpoint.isEmpty()) { + if (endpoint == null || endpoint.isEmpty()) + { throw new ResolutionException("Failed to resolve RPC endpoint for " + domain); } - if (publicKey == null || publicKey.isEmpty()) { + if (publicKey == null || publicKey.isEmpty()) + { throw new ResolutionException("Failed to resolve public key for " + domain); } return new ResolvedServer(endpoint, publicKey); - } else { + } + else + { throw new ResolutionException("Failed to find valid SocialBox record for " + domain); } - } catch (NamingException e) { + } + catch (NamingException e) + { throw new ResolutionException("Error resolving domain: " + e.getMessage(), e); } } diff --git a/src/main/java/net/nosial/socialclient/classes/RpcClient.java b/src/main/java/net/nosial/socialclient/classes/RpcClient.java index 9292008..3fe07cd 100644 --- a/src/main/java/net/nosial/socialclient/classes/RpcClient.java +++ b/src/main/java/net/nosial/socialclient/classes/RpcClient.java @@ -1,423 +1,426 @@ -package net.nosial.socialclient.classes; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.squareup.okhttp.*; -import net.nosial.socialclient.abstracts.RpcResult; -import net.nosial.socialclient.exceptions.CryptographyException; -import net.nosial.socialclient.exceptions.ResolutionException; -import net.nosial.socialclient.objects.ResolvedServer; -import net.nosial.socialclient.objects.RpcRequest; - -import java.io.IOException; -import java.security.NoSuchAlgorithmException; -import java.security.PrivateKey; -import java.security.PublicKey; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -public class RpcClient -{ - private final static ObjectMapper OBJECT_MAPPER = new ObjectMapper(); - private final static MediaType MEDIA_TYPE_JSON = MediaType.parse("application/json; charset=utf-8"); - - private final String domain; - private final String endpoint; - private final PublicKey serverPublicKey; - private final OkHttpClient httpClient; - - private String clientName = "SocialClient Java"; - private String clientVersion = "1.0.0"; - - protected String sessionUuid; - protected PrivateKey privateKey; - - /** - * Initializes a new instance of the RpcClient class using the specified domain. - * This constructor resolves the domain to obtain the RPC endpoint and public key. - * - * @param domain The domain to be resolved for the RPC endpoint and public key. - * @throws ResolutionException If the domain cannot be resolved or if required information is missing. - * @throws CryptographyException If an error occurs while importing the public key. - */ - public RpcClient(String domain) throws ResolutionException, CryptographyException + package net.nosial.socialclient.classes; + + import com.fasterxml.jackson.core.JsonProcessingException; + import com.fasterxml.jackson.core.type.TypeReference; + import com.fasterxml.jackson.databind.JsonNode; + import com.fasterxml.jackson.databind.ObjectMapper; + import com.squareup.okhttp.*; + import net.nosial.socialclient.abstracts.RpcResult; + import net.nosial.socialclient.exceptions.CryptographyException; + import net.nosial.socialclient.exceptions.ResolutionException; + import net.nosial.socialclient.objects.ResolvedServer; + import net.nosial.socialclient.objects.RpcRequest; + + import java.io.IOException; + import java.security.NoSuchAlgorithmException; + import java.security.PrivateKey; + import java.security.PublicKey; + import java.util.ArrayList; + import java.util.List; + import java.util.Map; + + public class RpcClient { - this.domain = domain; - this.httpClient = new OkHttpClient(); - - // Resolve the domain to get the endpoint and public key - ResolvedServer resolvedServer = Resolver.resolveDomain(domain); - - this.endpoint = resolvedServer.getEndpoint(); - this.serverPublicKey = Cryptography.importPublicKey(resolvedServer.getPublicKey()); - this.sessionUuid = null; - this.privateKey = null; - } - - /** - * Constructs an RpcClient instance with the specified endpoint and public key. - * - * @param endpoint The endpoint to which RPC requests will be sent. - * @param serverPublicKey The public key used for cryptographic operations with the RPC server. - */ - public RpcClient(String endpoint, PublicKey serverPublicKey) - { - this.domain = null; - this.endpoint = endpoint; - this.serverPublicKey = serverPublicKey; - this.httpClient = new OkHttpClient(); - this.sessionUuid = null; - this.privateKey = null; - } - - /** - * Retrieves the domain associated with this RpcClient instance. - * - * @return the domain as a string. - */ - public String getDomain() - { - return this.domain; - } - - /** - * Returns the RPC endpoint for the client. - * - * @return the endpoint as a String. - */ - public String getEndpoint() - { - return this.endpoint; - } - - /** - * Returns the public key associated with the RpcClient instance. - * - * @return the public key as a PublicKey object. - */ - public PublicKey getServerPublicKey() - { - return this.serverPublicKey; - } - - /** - * Retrieves the session UUID associated with the RpcClient instance. - * - * @return the session UUID as a string. - */ - public String getSessionUuid() - { - return this.sessionUuid; - } - - /** - * Sets the session UUID and imports the provided private key. - * - * @param sessionUuid the unique identifier for this session - * @param privateKey the private key to be imported - * @throws CryptographyException if an error occurs while importing the private key - */ - protected void setSession(String sessionUuid, String privateKey) throws CryptographyException - { - this.sessionUuid = sessionUuid; - this.privateKey = Cryptography.importPrivateKey(privateKey); - } - - /** - * Sets the session details for the RPC client. - * - * @param sessionUuid the UUID of the session - * @param privateKey the private key associated with the session - */ - protected void setSession(String sessionUuid, PrivateKey privateKey) - { - this.sessionUuid = sessionUuid; - this.privateKey = privateKey; - } - - /** - * Retrieves the client name associated with this RpcClient instance. - * - * @return the client name as a string. - */ - public String getClientName() - { - return clientName; - } - - /** - * Sets the name of the client. - * - * @param clientName The name to be set for the client. - * @throws NullPointerException if the provided*/ - public void setClientName(String clientName) - { - if(clientName == null) + private final static ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + private final static MediaType MEDIA_TYPE_JSON = MediaType.parse("application/json; charset=utf-8"); + + private final String domain; + private final String endpoint; + private final PublicKey serverPublicKey; + private final OkHttpClient httpClient; + + private String clientName = "SocialClient Java"; + private String clientVersion = "1.0"; + + protected String sessionUuid; + protected PrivateKey privateKey; + + /** + * Initializes a new instance of the RpcClient class using the specified domain. + * This constructor resolves the domain to obtain the RPC endpoint and public key. + * + * @param domain The domain to be resolved for the RPC endpoint and public key. + * @throws ResolutionException If the domain cannot be resolved or if required information is missing. + * @throws CryptographyException If an error occurs while importing the public key. + */ + public RpcClient(String domain) throws ResolutionException, CryptographyException { - throw new NullPointerException("Client name cannot be null"); + this.domain = domain; + this.httpClient = new OkHttpClient(); + + // Resolve the domain to get the endpoint and public key + ResolvedServer resolvedServer = Resolver.resolveDomain(domain); + + this.endpoint = resolvedServer.getEndpoint(); + this.serverPublicKey = Cryptography.importPublicKey(resolvedServer.getPublicKey()); + this.sessionUuid = null; + this.privateKey = null; } - - this.clientName = clientName; - } - - /** - * Returns the client's version. - * - * @return the client version as a string. - */ - public String getClientVersion() - { - return clientVersion; - } - - /** - * Sets the client version for the RpcClient instance. - * - * @param clientVersion The version of the client software to be set. - * @throws NullPointerException if the provided clientVersion*/ - public void setClientVersion(String clientVersion) - { - if(clientVersion == null) + + /** + * Constructs an RpcClient instance with the specified endpoint and public key. + * + * @param endpoint The endpoint to which RPC requests will be sent. + * @param serverPublicKey The public key used for cryptographic operations with the RPC server. + */ + public RpcClient(String endpoint, PublicKey serverPublicKey) { - throw new NullPointerException("Client version cannot be null"); + this.domain = null; + this.endpoint = endpoint; + this.serverPublicKey = serverPublicKey; + this.httpClient = new OkHttpClient(); + this.sessionUuid = null; + this.privateKey = null; } - - this.clientVersion = clientVersion; - } - - /** - * Clears the current session by setting the sessionUuid and privateKey fields to null. - * This method effectively logs out the user from the current session. - */ - public void clearSession() - { - this.sessionUuid = null; - this.privateKey = null; - } - - /** - * Sends an RPC request with the provided JSON data and returns a list of RpcResult objects. - * - * @param jsonData the JSON-formatted string representing the RPC request data. - * @return a list of RpcResult objects representing the response(s) from the RPC server. - * @throws RuntimeException if there is an error during the request or response processing. - */ - @SuppressWarnings("unchecked") - public List sendRequest(String jsonData) - { - final Request request = new Request.Builder() - {{ - this.url(endpoint); - this.post(RequestBody.create(MEDIA_TYPE_JSON, jsonData)); - this.addHeader("Client-Name", clientName); - this.addHeader("Client-Version", clientVersion); - - if(sessionUuid != null) - { - // We are seeing the session UUID correctly. - this.addHeader("Session-UUID", sessionUuid); - } - - if(privateKey != null) - { - try - { - // NOTE: Signature looks okay, it's base64 encoded. - System.out.println("Hash: " + Cryptography.sha1(jsonData)); - System.out.println("Signature: " + Cryptography.signContent(jsonData, privateKey)); - this.addHeader("Signature", Cryptography.signContent(jsonData, privateKey)); - } - catch(CryptographyException | NoSuchAlgorithmException e) - { - throw new RuntimeException(e); - } - } - }}.build(); - - try + + /** + * Retrieves the domain associated with this RpcClient instance. + * + * @return the domain as a string. + */ + public String getDomain() { - final Response response = this.httpClient.newCall(request).execute(); - final String responseString = response.body().string(); - - if (!response.isSuccessful()) + return this.domain; + } + + /** + * Returns the RPC endpoint for the client. + * + * @return the endpoint as a String. + */ + public String getEndpoint() + { + return this.endpoint; + } + + /** + * Returns the public key associated with the RpcClient instance. + * + * @return the public key as a PublicKey object. + */ + public PublicKey getServerPublicKey() + { + return this.serverPublicKey; + } + + /** + * Retrieves the session UUID associated with the RpcClient instance. + * + * @return the session UUID as a string. + */ + public String getSessionUuid() + { + return this.sessionUuid; + } + + /** + * Sets the session UUID and imports the provided private key. + * + * @param sessionUuid the unique identifier for this session + * @param privateKey the private key to be imported + * @throws CryptographyException if an error occurs while importing the private key + */ + protected void setSession(String sessionUuid, String privateKey) throws CryptographyException + { + this.sessionUuid = sessionUuid; + this.privateKey = Cryptography.importPrivateKey(privateKey); + } + + /** + * Sets the session details for the RPC client. + * + * @param sessionUuid the UUID of the session + * @param privateKey the private key associated with the session + */ + protected void setSession(String sessionUuid, PrivateKey privateKey) + { + this.sessionUuid = sessionUuid; + this.privateKey = privateKey; + } + + /** + * Retrieves the client name associated with this RpcClient instance. + * + * @return the client name as a string. + */ + public String getClientName() + { + return clientName; + } + + /** + * Sets the name of the client. + * + * @param clientName The name to be set for the client. + * @throws NullPointerException if the provided*/ + public void setClientName(String clientName) + { + if(clientName == null) { - if(!responseString.isEmpty()) + throw new NullPointerException("Client name cannot be null"); + } + + this.clientName = clientName; + } + + /** + * Returns the client's version. + * + * @return the client version as a string. + */ + public String getClientVersion() + { + return clientVersion; + } + + /** + * Sets the client version for the RpcClient instance. + * + * @param clientVersion The version of the client software to be set. + * @throws NullPointerException if the provided clientVersion*/ + public void setClientVersion(String clientVersion) + { + if(clientVersion == null) + { + throw new NullPointerException("Client version cannot be null"); + } + + this.clientVersion = clientVersion; + } + + /** + * Clears the current session by setting the sessionUuid and privateKey fields to null. + * This method effectively logs out the user from the current session. + */ + public void clearSession() + { + this.sessionUuid = null; + this.privateKey = null; + } + + /** + * Sends an RPC request with the provided JSON data and returns a list of RpcResult objects. + * + * @param jsonData the JSON-formatted string representing the RPC request data. + * @return a list of RpcResult objects representing the response(s) from the RPC server. + * @throws RuntimeException if there is an error during the request or response processing. + */ + @SuppressWarnings("unchecked") + public List sendRequest(String jsonData) + { + final Request request = new Request.Builder() + {{ + this.url(endpoint); + this.post(RequestBody.create(MEDIA_TYPE_JSON, jsonData)); + this.addHeader("Client-Name", clientName); + this.addHeader("Client-Version", clientVersion); + + if(sessionUuid != null) + { + // We are seeing the session UUID correctly. + this.addHeader("Session-UUID", sessionUuid); + } + + if(privateKey != null) + { + try + { + // NOTE: Signature looks okay, it's base64 encoded. + this.addHeader("Signature", Cryptography.signContent(jsonData, privateKey)); + } + catch(CryptographyException e) + { + throw new RuntimeException(e); + } + } + }}.build(); + + try + { + final Response response = this.httpClient.newCall(request).execute(); + final String responseString = response.body().string(); + + if (!response.isSuccessful()) { - throw new RuntimeException(responseString); + if(!responseString.isEmpty()) + { + throw new RuntimeException(responseString); + } + + throw new RuntimeException("Failed to send request: " + response.code()); } - - throw new RuntimeException("Failed to send request: " + response.code()); - } - - if(response.code() == 204) - { - // The response is empty - return new ArrayList<>(); - } - - Object decoded = decode(responseString); - - // Singular object response - if(decoded instanceof List) - { - List> responseList = (List>) decoded; - List results = new ArrayList<>(responseList.size()); - for(Map responseMap : responseList) + + if(response.code() == 204) { - results.add(RpcResult.fromMap(responseMap)); + // The response is empty + return new ArrayList<>(); } - - return results; - } - - if(decoded instanceof Map) - { + + if(responseString.isEmpty()) + { + throw new RuntimeException("The request was successful but the server did not indicate a empty response"); + } + + Object decoded = decode(responseString); + // Singular object response - List results = new ArrayList<>(1); - results.add(RpcResult.fromMap((Map) decoded)); - return results; + if(decoded instanceof List) + { + List> responseList = (List>) decoded; + List results = new ArrayList<>(responseList.size()); + for(Map responseMap : responseList) + { + results.add(RpcResult.fromMap(responseMap)); + } + + return results; + } + + if(decoded instanceof Map) + { + // Singular object response + List results = new ArrayList<>(1); + results.add(RpcResult.fromMap((Map) decoded)); + return results; + } + + + throw new RuntimeException("Failed to decode response"); + } + catch (IOException e) + { + throw new RuntimeException(e); } - - - throw new RuntimeException("Failed to decode response"); } - catch (IOException e) + + /** + * Sends an RPC request to the configured endpoint and returns the first result. + * + * @param request the RPC request to be sent + * @return the first RPC result received from the response + */ + public RpcResult sendRequest(RpcRequest request) { - throw new RuntimeException(e); + List results = sendRequest(encode(request.toMap())); + + if(results.isEmpty()) + { + return null; + } + + return results.getFirst(); + } + + /** + * Sends a list of RPC requests to the server and returns their results as a list of RpcResult objects. + * + * @param requests the list of RPC requests to be sent + * @return a list of RpcResult objects containing the responses from the server + */ + public List sendRequests(List requests) + { + return sendRequest(encode(requests)); + } + + /** + * Decodes a JSON string into a corresponding Java object. If the JSON string + * represents an object, it decodes it into a Map. If the JSON string represents + * an array, it decodes it into a List. + * + * @param json the JSON string to be decoded + * @return a decoded Java object which can be a Map or a List depending on the JSON structure + * @throws RuntimeException if the JSON string is neither an object nor an array, or if decoding fails + */ + private static Object decode(String json) + { + try + { + JsonNode jsonNode = OBJECT_MAPPER.readTree(json); + if (jsonNode.isObject()) + { + return decodeJson(OBJECT_MAPPER.readValue(json, new TypeReference<>() {})); + } + else if (jsonNode.isArray()) + { + return decodeList(OBJECT_MAPPER.readValue(json, new TypeReference<>() {})); + } + else + { + throw new RuntimeException("JSON is neither an object nor an array"); + } + } + catch (JsonProcessingException e) + { + throw new RuntimeException("Failed to decode input", e); + } + } + + /** + * Recursively decodes a JSON structure represented as a Map. If the value + * of an entry is another Map, it recursively decodes that Map. If the value + * is a List, it passes the List to the decodeList method. + * + * @param map the JSON structure to be decoded represented as a Map + * @return the decoded JSON structure as a Map + */ + @SuppressWarnings("unchecked") + private static Map decodeJson(Map map) + { + for (Map.Entry entry : map.entrySet()) + { + if (entry.getValue() instanceof Map) + { + entry.setValue(decodeJson((Map) entry.getValue())); + } + else if (entry.getValue() instanceof List) + { + entry.setValue(decodeList((List) entry.getValue())); + } + } + + return map; + } + + /** + * Recursively decodes a list of objects, transforming any nested maps or lists within it. + * + * @param list the list of objects to be decoded + * @return the decoded list of objects, with all nested maps and lists transformed + */ + @SuppressWarnings("unchecked") + private static List decodeList(List list) + { + for (int i = 0; i < list.size(); i++) + { + final Object item = list.get(i); + if (item instanceof Map) + { + list.set(i, decodeJson((Map) item)); + } + else if (item instanceof List) + { + list.set(i, decodeList((List) item)); + } + } + + return list; + } + + /** + * Encodes the given input object to its JSON string representation. + * + * @param input the object to encode + * @return the JSON string representation of the input object + */ + private static String encode(Object input) + { + try + { + return OBJECT_MAPPER.writeValueAsString(input); + } + catch(JsonProcessingException e) + { + throw new RuntimeException("Failed to encode input to JSON Data", e); + } } } - - /** - * Sends an RPC request to the configured endpoint and returns the first result. - * - * @param request the RPC request to be sent - * @return the first RPC result received from the response - */ - public RpcResult sendRequest(RpcRequest request) - { - List results = sendRequest(encode(request.toMap())); - - if(results.isEmpty()) - { - return null; - } - - return results.getFirst(); - } - - /** - * Sends a list of RPC requests to the server and returns their results as a list of RpcResult objects. - * - * @param requests the list of RPC requests to be sent - * @return a list of RpcResult objects containing the responses from the server - */ - public List sendRequests(List requests) - { - return sendRequest(encode(requests)); - } - - /** - * Decodes a JSON string into a corresponding Java object. If the JSON string - * represents an object, it decodes it into a Map. If the JSON string represents - * an array, it decodes it into a List. - * - * @param json the JSON string to be decoded - * @return a decoded Java object which can be a Map or a List depending on the JSON structure - * @throws RuntimeException if the JSON string is neither an object nor an array, or if decoding fails - */ - private static Object decode(String json) - { - try - { - JsonNode jsonNode = OBJECT_MAPPER.readTree(json); - if (jsonNode.isObject()) - { - return decodeJson(OBJECT_MAPPER.readValue(json, new TypeReference<>() {})); - } - else if (jsonNode.isArray()) - { - return decodeList(OBJECT_MAPPER.readValue(json, new TypeReference<>() {})); - } - else - { - throw new RuntimeException("JSON is neither an object nor an array"); - } - } - catch (JsonProcessingException e) - { - throw new RuntimeException("Failed to decode input", e); - } - } - - /** - * Recursively decodes a JSON structure represented as a Map. If the value - * of an entry is another Map, it recursively decodes that Map. If the value - * is a List, it passes the List to the decodeList method. - * - * @param map the JSON structure to be decoded represented as a Map - * @return the decoded JSON structure as a Map - */ - @SuppressWarnings("unchecked") - private static Map decodeJson(Map map) - { - for (Map.Entry entry : map.entrySet()) - { - if (entry.getValue() instanceof Map) - { - entry.setValue(decodeJson((Map) entry.getValue())); - } - else if (entry.getValue() instanceof List) - { - entry.setValue(decodeList((List) entry.getValue())); - } - } - - return map; - } - - /** - * Recursively decodes a list of objects, transforming any nested maps or lists within it. - * - * @param list the list of objects to be decoded - * @return the decoded list of objects, with all nested maps and lists transformed - */ - @SuppressWarnings("unchecked") - private static List decodeList(List list) - { - for (int i = 0; i < list.size(); i++) - { - final Object item = list.get(i); - if (item instanceof Map) - { - list.set(i, decodeJson((Map) item)); - } - else if (item instanceof List) - { - list.set(i, decodeList((List) item)); - } - } - - return list; - } - - /** - * Encodes the given input object to its JSON string representation. - * - * @param input the object to encode - * @return the JSON string representation of the input object - */ - private static String encode(Object input) - { - try - { - return OBJECT_MAPPER.writeValueAsString(input); - } - catch(JsonProcessingException e) - { - throw new RuntimeException("Failed to encode input to JSON Data", e); - } - } -} diff --git a/src/main/java/net/nosial/socialclient/enums/StandardErrorCodes.java b/src/main/java/net/nosial/socialclient/enums/StandardErrorCodes.java index 08d552d..ad0fe8f 100644 --- a/src/main/java/net/nosial/socialclient/enums/StandardErrorCodes.java +++ b/src/main/java/net/nosial/socialclient/enums/StandardErrorCodes.java @@ -11,8 +11,15 @@ public enum StandardErrorCodes SERVER_UNAVAILABLE(-2001), INVALID_PUBLIC_KEY(-3000), - SESSION_NOT_FOUND(-3001), - UNSUPPORTED_AUTHENTICATION_TYPE(-3002); + UNSUPPORTED_AUTHENTICATION_TYPE(-3001), + ALREADY_AUTHENTICATED(-3002), + AUTHENTICATION_REQUIRED(-3003), + SESSION_NOT_FOUND(-3004), + SESSION_REQUIRED(-3005), + + PEER_NOT_FOUND(-4000), + INVALID_USERNAME(-4001), + USERNAME_ALREADY_EXISTS(-4002); private final int code; diff --git a/src/main/java/net/nosial/socialclient/enums/flags/PeerFlags.java b/src/main/java/net/nosial/socialclient/enums/flags/PeerFlags.java new file mode 100644 index 0000000..b26b706 --- /dev/null +++ b/src/main/java/net/nosial/socialclient/enums/flags/PeerFlags.java @@ -0,0 +1,39 @@ +package net.nosial.socialclient.enums.flags; + +public enum PeerFlags +{ + // Administrative Flags + ADMIN(true, "ADMIN"), + MODERATOR(true, "MODERATOR"), + + // General Flags + VERIFIED(true, "VERIFIED"), + + // Verification Flags + VER_SET_PASSWORD(false, "VER_SET_PASSWORD"), + VER_SET_OTP(false, "VER_SET_OTP"), + VER_SET_DISPLAY_NAME(false, "VER_SET_DISPLAY_NAME"), + VER_EMAIL(false, "VER_EMAIL"), + VER_SMS(false, "VER_SMS"), + VER_PHONE_CALL(false, "VER_PHONE_CALL"), + VER_SOLVE_IMAGE_CAPTCHA(false, "VER_SOLVE_IMAGE_CAPTCHA"); + + private final boolean isPublic; + private final String flag; + + PeerFlags(boolean isPublic, String flag) + { + this.isPublic = isPublic; + this.flag = flag; + } + + public String getFlag() + { + return this.flag; + } + + public boolean isPublic() + { + return this.isPublic; + } +} diff --git a/src/main/java/net/nosial/socialclient/objects/standard/ImageCaptcha.java b/src/main/java/net/nosial/socialclient/objects/standard/ImageCaptcha.java new file mode 100644 index 0000000..3153210 --- /dev/null +++ b/src/main/java/net/nosial/socialclient/objects/standard/ImageCaptcha.java @@ -0,0 +1,46 @@ +package net.nosial.socialclient.objects.standard; + +import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.Base64; +import java.util.Map; + +public class ImageCaptcha +{ + private final int expires; + private final String image; + + public ImageCaptcha(Map data) + { + this.expires = (int) data.get("expires"); + this.image = (String) data.get("image"); + } + + public int getExpires() + { + return expires; + } + + public String getImage() + { + return image; + } + + public BufferedImage getImageBuffer() throws IOException + { + String base64Image = this.image; + if(this.image.contains(",")) + { + base64Image = this.image.split(",")[1]; + } + + byte[] imageBytes = Base64.getDecoder().decode(base64Image); + + ByteArrayInputStream bis = new ByteArrayInputStream(imageBytes); + BufferedImage image = javax.imageio.ImageIO.read(bis); + bis.close(); + + return image; + } +} diff --git a/src/main/java/net/nosial/socialclient/objects/standard/SelfUser.java b/src/main/java/net/nosial/socialclient/objects/standard/SelfUser.java new file mode 100644 index 0000000..f6a12fc --- /dev/null +++ b/src/main/java/net/nosial/socialclient/objects/standard/SelfUser.java @@ -0,0 +1,106 @@ +package net.nosial.socialclient.objects.standard; + +import net.nosial.socialclient.enums.flags.PeerFlags; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public class SelfUser +{ + private final String uuid; + private final boolean enabled; + private final String address; + private final String username; + private final String displayName; + private final List flags; + private final int created; + + /** + * Constructs a SelfUser instance by populating fields from a provided data map. + * + * @param data a map containing user details such as uuid, username, display name, flags, + * enabled status, and creation time. + */ + @SuppressWarnings("unchecked") + public SelfUser(Map data) + { + this.uuid = (String) data.get("uuid"); + this.enabled = (boolean) data.get("enabled"); + this.username = (String) data.get("username"); + this.address = (String) data.get("address"); + this.displayName = (String) data.get("display_name"); + this.flags = ((List) data.get("flags")).stream().map(PeerFlags::valueOf).collect(Collectors.toList()); + this.created = (int) data.get("created"); + } + + /** + * Retrieves the universally unique identifier (UUID) associated with the user. + * + * @return the UUID as a String + */ + public String getUuid() + { + return uuid; + } + + /** + * Checks if the user account is enabled. + * + * @return true if the user account is enabled; false otherwise. + */ + public boolean isEnabled() + { + return enabled; + } + + /** + * Retrieves the username associated with this SelfUser instance. + * + * @return the username of the user + */ + public String getUsername() + { + return username; + } + + /** + * Retrieves the address associated with this SelfUser instance. + * + * @return the address of the user + */ + public String getAddress() + { + return address; + } + + /** + * Retrieves the display name of the user. + * + * @return the display name of the user + */ + public String getDisplayName() + { + return displayName; + } + + /** + * Retrieves the list of flags associated with the user. + * + * @return a list of PeerFlags indicating various user permissions and statuses. + */ + public List getFlags() + { + return flags; + } + + /** + * Returns the creation timestamp of this user. + * + * @return the creation timestamp as an integer + */ + public int getCreated() + { + return created; + } +} diff --git a/src/test/java/net/nosial/socialclient/SessionTest.java b/src/test/java/net/nosial/socialclient/methods/CreateSessionTest.java similarity index 88% rename from src/test/java/net/nosial/socialclient/SessionTest.java rename to src/test/java/net/nosial/socialclient/methods/CreateSessionTest.java index 34b0aca..c1e3842 100644 --- a/src/test/java/net/nosial/socialclient/SessionTest.java +++ b/src/test/java/net/nosial/socialclient/methods/CreateSessionTest.java @@ -1,5 +1,6 @@ -package net.nosial.socialclient; +package net.nosial.socialclient.methods; +import net.nosial.socialclient.Client; import net.nosial.socialclient.classes.Cryptography; import net.nosial.socialclient.exceptions.CryptographyException; import net.nosial.socialclient.exceptions.ResolutionException; @@ -10,11 +11,10 @@ import java.security.KeyPair; import static org.junit.jupiter.api.Assertions.*; -class SessionTest +public class CreateSessionTest { - @Test - void createSessionTest() + void testSessionCreation() { Client socialClient; KeyPair keyPair; @@ -23,18 +23,9 @@ class SessionTest try { socialClient = new Client("n64.cc"); - } - catch (ResolutionException | CryptographyException e) - { - fail(e.getMessage(), e); - return; - } - - try - { keyPair = Cryptography.generateKeyPair(); } - catch (CryptographyException e) + catch (ResolutionException | CryptographyException e) { fail(e.getMessage(), e); return; @@ -86,4 +77,4 @@ class SessionTest fail(e.getMessage(), e); } } -} \ No newline at end of file +} diff --git a/src/test/java/net/nosial/socialclient/methods/RegisterTest.java b/src/test/java/net/nosial/socialclient/methods/RegisterTest.java new file mode 100644 index 0000000..49479c8 --- /dev/null +++ b/src/test/java/net/nosial/socialclient/methods/RegisterTest.java @@ -0,0 +1,127 @@ +package net.nosial.socialclient.methods; + +import net.nosial.socialclient.Client; +import net.nosial.socialclient.classes.Cryptography; +import net.nosial.socialclient.exceptions.CryptographyException; +import net.nosial.socialclient.exceptions.ResolutionException; +import net.nosial.socialclient.exceptions.RpcException; +import net.nosial.socialclient.objects.standard.ImageCaptcha; +import net.nosial.socialclient.objects.standard.SelfUser; +import org.junit.jupiter.api.Test; + +import javax.swing.*; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.security.KeyPair; +import java.util.Random; + +import static org.junit.jupiter.api.Assertions.*; + +public class RegisterTest +{ + @Test + public void registerTest() + { + // Step 1. Connect and establish a session to the server + final Client socialClient; + final KeyPair keyPair; + final String sessionUuid; + + try + { + socialClient = new Client("n64.cc"); + keyPair = Cryptography.generateKeyPair(); + sessionUuid = socialClient.createAndSetSession(keyPair); + + } + catch (ResolutionException | CryptographyException | RpcException e) + { + fail(e.getMessage(), e); + return; + } + + System.out.println("Session UUID: " + sessionUuid); + assertNotNull(sessionUuid); + + final SelfUser selfUser; + Random random = new Random(); + String username = "user" + random.nextInt(10000); + + // Step 2. Register the username and getSelf + try + { + assertTrue(socialClient.register(username)); + selfUser = socialClient.getMe(); + } + catch (RpcException e) + { + fail(e.getMessage(), e); + return; + } + + assertNotNull(selfUser); + + // Step 3. Get the captcha + ImageCaptcha captcha; + + try + { + captcha = socialClient.verificationGetImageCaptcha(); + } + catch(RpcException e) + { + fail(e.getMessage(), e); + return; + } + + SwingUtilities.invokeLater(() -> { + try + { + ImageFrame frame = new ImageFrame(captcha.getImageBuffer()); + frame.setVisible(true); + } + catch(IOException e) + { + e.printStackTrace(); + } + }); + + assertNotNull(captcha); + + // Step 4. Verify the captcha, get input from the user + String captchaInput = JOptionPane.showInputDialog("Enter the captcha: "); + assertNotNull(captchaInput); + + // Step 5. Verify the captcha + try + { + assertTrue(socialClient.verificationAnswerImageCaptcha(captchaInput)); + } + catch(RpcException e) + { + fail(e.getMessage(), e); + return; + } + } + + private static class ImageFrame extends JFrame { + public ImageFrame(BufferedImage image) { + setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + setResizable(false); + + JPanel panel = new JPanel() { + @Override + protected void paintComponent(Graphics g) { + super.paintComponent(g); + g.drawImage(image, 0, 0, null); + } + }; + + panel.setPreferredSize(new Dimension(image.getWidth(), image.getHeight())); + add(panel); + pack(); + setLocationRelativeTo(null); + } + } +}