diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml
new file mode 100644
index 0000000..919ce1f
--- /dev/null
+++ b/.idea/codeStyles/Project.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml
new file mode 100644
index 0000000..a55e7a1
--- /dev/null
+++ b/.idea/codeStyles/codeStyleConfig.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/encodings.xml b/.idea/encodings.xml
new file mode 100644
index 0000000..aa00ffa
--- /dev/null
+++ b/.idea/encodings.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
new file mode 100644
index 0000000..9ea3508
--- /dev/null
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 0000000..6ddae43
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/uiDesigner.xml b/.idea/uiDesigner.xml
new file mode 100644
index 0000000..2b63946
--- /dev/null
+++ b/.idea/uiDesigner.xml
@@ -0,0 +1,124 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..94a25f7
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..7ae6270
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,47 @@
+
+
+ 4.0.0
+
+ net.nosial
+ socialclient
+ 1.0-SNAPSHOT
+
+
+ 22
+ 22
+ UTF-8
+
+
+
+
+ org.junit.jupiter
+ junit-jupiter-api
+ 5.8.1
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ 5.8.1
+ test
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+ 2.15.2
+
+
+ com.fasterxml.jackson.dataformat
+ jackson-dataformat-yaml
+ 2.15.2
+
+
+ com.squareup.okhttp
+ okhttp
+ 2.7.5
+
+
+
+
\ No newline at end of file
diff --git a/src/main/java/net/nosial/Main.java b/src/main/java/net/nosial/Main.java
new file mode 100644
index 0000000..38dfc79
--- /dev/null
+++ b/src/main/java/net/nosial/Main.java
@@ -0,0 +1,17 @@
+package net.nosial;
+
+//TIP To Run code, press or
+// click the icon in the gutter.
+public class Main {
+ public static void main(String[] args) {
+ //TIP Press with your caret at the highlighted text
+ // to see how IntelliJ IDEA suggests fixing it.
+ System.out.printf("Hello and welcome!");
+
+ for (int i = 1; i <= 5; i++) {
+ //TIP Press to start debugging your code. We have set one breakpoint
+ // for you, but you can always add more by pressing .
+ System.out.println("i = " + i);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/net/nosial/socialclient/Client.java b/src/main/java/net/nosial/socialclient/Client.java
new file mode 100644
index 0000000..d4a10e3
--- /dev/null
+++ b/src/main/java/net/nosial/socialclient/Client.java
@@ -0,0 +1,4 @@
+package net.nosial.socialclient;
+
+public class Client {
+}
diff --git a/src/main/java/net/nosial/socialclient/abstracts/RpcResult.java b/src/main/java/net/nosial/socialclient/abstracts/RpcResult.java
new file mode 100644
index 0000000..e1e6365
--- /dev/null
+++ b/src/main/java/net/nosial/socialclient/abstracts/RpcResult.java
@@ -0,0 +1,39 @@
+package net.nosial.socialclient.abstracts;
+
+import net.nosial.socialclient.objects.RpcError;
+import net.nosial.socialclient.objects.RpcResponse;
+
+import java.util.Map;
+
+public abstract class RpcResult
+{
+ private final boolean success;
+
+ protected RpcResult(boolean success)
+ {
+ this.success = success;
+ }
+
+ public boolean isSuccess()
+ {
+ return this.success;
+ }
+
+ public abstract RpcError getErrorResponse();
+ public abstract RpcResponse getResponse();
+
+ public static RpcResult fromMap(Map data)
+ {
+ if(data.containsKey("error"))
+ {
+ return new RpcError(data);
+ }
+ else if(data.containsKey("id"))
+ {
+ return new RpcResponse(data);
+ }
+
+ // TODO: Use standard RpcException for this or something
+ throw new IllegalArgumentException("Cannot recognize RPC object");
+ }
+}
diff --git a/src/main/java/net/nosial/socialclient/classes/Cryptography.java b/src/main/java/net/nosial/socialclient/classes/Cryptography.java
new file mode 100644
index 0000000..7f874a3
--- /dev/null
+++ b/src/main/java/net/nosial/socialclient/classes/Cryptography.java
@@ -0,0 +1,402 @@
+package net.nosial.socialclient.classes;
+
+
+import net.nosial.socialclient.exceptions.CryptographyException;
+
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.NoSuchPaddingException;
+import java.security.*;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.PKCS8EncodedKeySpec;
+import java.security.spec.X509EncodedKeySpec;
+import java.util.ArrayList;
+import java.util.Base64;
+import java.util.List;
+
+public final class Cryptography
+{
+ private static final int TIME_BLOCK = 60;
+ private static final int KEY_SIZE = 2048;
+ private static final String ALGORITHM = "RSA";
+ private static final String SIGNATURE_ALGORITHM = "SHA256withRSA";
+ private static final String CIPHER_TRANSFORMATION = "RSA/ECB/OAEPWithSHA-1AndMGF1Padding";
+ private static final int MAX_ENCRYPT_BLOCK_SIZE = 214;
+ private static final int MAX_DECRYPT_BLOCK_SIZE = 256;
+
+ /**
+ * Returns the KeyFactory object for the specified algorithm.
+ *
+ * @return the KeyFactory object for the specified algorithm
+ * @throws NoSuchAlgorithmException if the requested algorithm is not available
+ */
+ private static KeyFactory getKeyFactory() throws NoSuchAlgorithmException
+ {
+ return KeyFactory.getInstance(ALGORITHM);
+ }
+
+ /**
+ * Generates a key pair using the RSA algorithm.
+ *
+ * @return the generated key pair
+ * @throws CryptographyException if there is an error generating the key pair
+ */
+ public static KeyPair generateKeyPair() throws CryptographyException
+ {
+ try
+ {
+ KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(ALGORITHM);
+ keyPairGenerator.initialize(KEY_SIZE);
+ return keyPairGenerator.generateKeyPair();
+ }
+ catch (NoSuchAlgorithmException e)
+ {
+ throw new CryptographyException("Failed to generate key pair, RSA algorithm not found", e);
+ }
+ }
+
+ /**
+ * Signs the given content using the provided private key.
+ *
+ * @param content the content to be signed
+ * @param privateKey the private key used for signing
+ * @return the base64 encoded signature of the content
+ * @throws CryptographyException if an error occurs in the cryptography process
+ */
+ public static String signContent(String content, String privateKey) throws CryptographyException
+ {
+ return signContent(content, importPrivateKey(privateKey));
+ }
+
+ /**
+ * Signs the given content using the provided private key.
+ *
+ * @param content the content to be signed
+ * @param privateKey the private key used for signing
+ * @return the base64 encoded signature of the content
+ * @throws CryptographyException if an error occurs in the cryptography process
+ */
+ public static String signContent(String content, PrivateKey privateKey) throws CryptographyException
+ {
+ // Convert content to sha1 hash
+ try
+ {
+ final Signature signature = Signature.getInstance(SIGNATURE_ALGORITHM);
+
+ signature.initSign(privateKey);
+ signature.update(MessageDigest.getInstance("SHA-1").digest(content.getBytes()));
+
+ return Base64.getEncoder().encodeToString(signature.sign());
+ }
+ catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException e)
+ {
+ throw new CryptographyException("Failed to sign content", e);
+ }
+ }
+
+ /**
+ * Verifies the content using the provided signature and public key.
+ *
+ * @param content the content to be verified
+ * @param signature the signature of the content
+ * @param publicKey the public key used for verification
+ * @return true if the content is verified, false otherwise
+ * @throws CryptographyException if an error occurs in the cryptography process
+ */
+ public static boolean verifyContent(String content, String signature, String publicKey) throws CryptographyException
+ {
+ return verifyContent(content, signature, importPublicKey(publicKey));
+ }
+
+ /**
+ * Verifies the signature of the given content using the provided public key.
+ *
+ * @param content the content to be verified
+ * @param signature the signature to be verified
+ * @param publicKey the public key used for verification
+ * @return true if the signature is verified, false otherwise
+ * @throws CryptographyException if there is an error during the verification process
+ */
+ public static boolean verifyContent(String content, String signature, PublicKey publicKey) throws CryptographyException
+ {
+ try
+ {
+ final Signature sign = Signature.getInstance(SIGNATURE_ALGORITHM);
+
+ sign.initVerify(publicKey);
+ sign.update(MessageDigest.getInstance("SHA-1").digest(content.getBytes()));
+
+ return sign.verify(Base64.getDecoder().decode(signature));
+ }
+ catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException e)
+ {
+ throw new CryptographyException("Failed to verify content", e);
+ }
+ }
+
+ /**
+ * Generates a temporary signature for the given content using the provided private key.
+ *
+ * @param content the content to be signed
+ * @param privateKey the private key used for signing
+ * @return the base64 encoded signature of the content
+ * @throws CryptographyException if an error occurs in the cryptography process
+ */
+ public static String temporarySignature(String content, String privateKey) throws CryptographyException
+ {
+ return signContent(String.format("%s|%d", content, (System.currentTimeMillis() / 1000 / TIME_BLOCK)), privateKey);
+ }
+
+ /**
+ * Generates a temporary signature for the given content using the provided private key.
+ *
+ * @param content the content to be signed
+ * @param privateKey the private key used for signing
+ * @return the base64 encoded signature of the content
+ * @throws CryptographyException if an error occurs in the cryptography process
+ */
+ public static String temporarySignature(String content, PrivateKey privateKey) throws CryptographyException
+ {
+ return signContent(String.format("%s|%d", content, (System.currentTimeMillis() / 1000 / TIME_BLOCK)), privateKey);
+ }
+
+ /**
+ * Verifies the temporary signature of the given content using the specified public key and number of frames.
+ *
+ * @param content The content to verify the signature for. Must not be null.
+ * @param signature The signature to verify. Must not be null.
+ * @param publicKey The public key used for verification. Must not be null.
+ * @param frames The number of frames to use for verification. Must be a positive integer.
+ * @return true if the signature is valid, false otherwise.
+ * @throws CryptographyException if an error occurs during verification.
+ */
+ public static boolean verifyTemporarySignature(String content, String signature, String publicKey, int frames) throws CryptographyException
+ {
+ return verifyTemporarySignature(content, signature, importPublicKey(publicKey), frames);
+ }
+
+ /**
+ * Verifies the temporary signature of the given content using the specified public key and number of frames.
+ *
+ * @param content The content to verify the signature for. Must not be null.
+ * @param signature The signature to verify. Must not be null.
+ * @param publicKey The public key used for verification. Must not be null.
+ * @param frames The number of frames to use for verification. Must be a positive integer.
+ * @return true if the signature is valid, false otherwise.
+ * @throws CryptographyException if an error occurs during verification.
+ */
+ public static boolean verifyTemporarySignature(String content, String signature, PublicKey publicKey, int frames) throws CryptographyException
+ {
+ if (frames <= 0) frames = 1;
+ long currentTime = System.currentTimeMillis() / 1000 / TIME_BLOCK;
+
+ for (int i = 0; i < frames; i++)
+ {
+ if (verifyContent(String.format("%s|%d", content, currentTime - i), signature, publicKey))
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Encrypts the given content using the provided public key.
+ *
+ * @param content the content to be encrypted
+ * @param publicKey the public key used for encryption
+ * @return the encrypted content as a String
+ * @throws CryptographyException if an error occurs during the encryption process
+ */
+ public static String encrypt(String content, String publicKey) throws CryptographyException
+ {
+ return encrypt(content, importPublicKey(publicKey));
+ }
+
+ /**
+ * Encrypts the given content using the provided public key.
+ *
+ * @param content the content to be encrypted
+ * @param publicKey the public key used for encryption
+ * @return the encrypted content as a Base64 encoded string
+ * @throws CryptographyException if an error occurs during the encryption process
+ */
+ public static String encrypt(String content, PublicKey publicKey) throws CryptographyException
+ {
+ try
+ {
+ Cipher cipher = Cipher.getInstance(CIPHER_TRANSFORMATION);
+ cipher.init(Cipher.ENCRYPT_MODE, publicKey);
+
+ byte[] data = content.getBytes();
+ int inputLength = data.length;
+ List outputList = new ArrayList<>();
+
+ for (int i = 0; i < inputLength; i += MAX_ENCRYPT_BLOCK_SIZE)
+ {
+ int length = Math.min(inputLength - i, MAX_ENCRYPT_BLOCK_SIZE);
+ byte[] encryptedBlock = cipher.doFinal(data, i, length);
+ for (byte b : encryptedBlock)
+ {
+ outputList.add(b);
+ }
+ }
+
+ byte[] output = new byte[outputList.size()];
+ for (int i = 0; i < outputList.size(); i++)
+ {
+ output[i] = outputList.get(i);
+ }
+
+ return Base64.getEncoder().encodeToString(output);
+ }
+ catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | IllegalBlockSizeException | BadPaddingException e)
+ {
+ throw new CryptographyException("Failed to encrypt content", e);
+ }
+ }
+
+ /**
+ * Decrypts the given content using the provided private key.
+ *
+ * @param content the encrypted content to be decrypted
+ * @param privateKey the private key used for decryption
+ * @return the decrypted content as a String
+ * @throws CryptographyException if an error occurs during the decryption process
+ */
+ public static String decrypt(String content, String privateKey) throws CryptographyException
+ {
+ return decrypt(content, importPrivateKey(privateKey));
+ }
+
+ /**
+ * Decrypts the given content using the provided private key.
+ *
+ * @param content the encrypted content to be decrypted
+ * @param privateKey the private key used for decryption
+ * @return the decrypted content as a String
+ * @throws CryptographyException if an error occurs during the decryption process
+ */
+ public static String decrypt(String content, PrivateKey privateKey) throws CryptographyException
+ {
+ try
+ {
+ Cipher cipher = Cipher.getInstance(CIPHER_TRANSFORMATION);
+ cipher.init(Cipher.DECRYPT_MODE, privateKey);
+
+ byte[] data = Base64.getDecoder().decode(content);
+ int inputLength = data.length;
+ List outputList = new ArrayList<>();
+
+ for (int i = 0; i < inputLength; i += MAX_DECRYPT_BLOCK_SIZE)
+ {
+ int length = Math.min(inputLength - i, MAX_DECRYPT_BLOCK_SIZE);
+ byte[] decryptedBlock = cipher.doFinal(data, i, length);
+ for (byte b : decryptedBlock)
+ {
+ outputList.add(b);
+ }
+ }
+
+ byte[] output = new byte[outputList.size()];
+ for (int i = 0; i < outputList.size(); i++)
+ {
+ output[i] = outputList.get(i);
+ }
+
+ return new String(output);
+ }
+ catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | IllegalBlockSizeException | BadPaddingException e)
+ {
+ throw new CryptographyException("Failed to decrypt content", e);
+ }
+ }
+
+ /**
+ * Exports the given private key as a Base64 encoded string.
+ *
+ * @param privateKey the private key to be exported
+ * @return the Base64 encoded string representation of the private key
+ */
+ public static String exportPrivateKey(PrivateKey privateKey)
+ {
+ return Base64.getEncoder().encodeToString(privateKey.getEncoded());
+ }
+
+ /**
+ * Exports the given private key as a Base64 encoded string.
+ *
+ * @param keyPair the private key to be exported
+ * @return the Base64 encoded string representation of the private key
+ */
+ public static String exportPrivateKey(KeyPair keyPair)
+ {
+ return exportPrivateKey(keyPair.getPrivate());
+ }
+
+ /**
+ * Exports the given public key as a Base64 encoded string.
+ *
+ * @param publicKey the public key to be exported
+ * @return a Base64 encoded string representation of the public key
+ */
+ public static String exportPublicKey(PublicKey publicKey)
+ {
+ return Base64.getEncoder().encodeToString(publicKey.getEncoded());
+ }
+
+ /**
+ * Exports the given public key as a Base64 encoded string.
+ *
+ * @param keyPair the public key to be exported
+ * @return a Base64 encoded string representation of the public key
+ */
+ public static String exportPublicKey(KeyPair keyPair)
+ {
+ return exportPublicKey(keyPair.getPublic());
+ }
+
+ /**
+ * Imports a public key from a Base64 encoded string representation.
+ *
+ * @param publicKey the Base64 encoded string representation of the public key
+ * @return the imported PublicKey object
+ * @throws CryptographyException if there is an error importing the public key
+ */
+ public static PublicKey importPublicKey(String publicKey) throws CryptographyException
+ {
+ try
+ {
+ byte[] keyBytes = Base64.getDecoder().decode(publicKey);
+ X509EncodedKeySpec spec = new X509EncodedKeySpec(keyBytes);
+ return getKeyFactory().generatePublic(spec);
+ }
+ catch (InvalidKeySpecException | NoSuchAlgorithmException e)
+ {
+ throw new CryptographyException("Failed to import public key", e);
+ }
+ }
+
+ /**
+ * Imports a private key from a Base64 encoded string representation.
+ *
+ * @param privateKey the Base64 encoded string representation of the private key
+ * @return the imported PrivateKey object
+ * @throws CryptographyException if there is an error importing the private key
+ */
+ public static PrivateKey importPrivateKey(String privateKey) throws CryptographyException
+ {
+ try
+ {
+ byte[] keyBytes = Base64.getDecoder().decode(privateKey);
+ PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(keyBytes);
+ return getKeyFactory().generatePrivate(spec);
+ }
+ catch (InvalidKeySpecException | NoSuchAlgorithmException | IllegalArgumentException e)
+ {
+ throw new CryptographyException("Failed to import private key", e);
+ }
+ }
+
+}
diff --git a/src/main/java/net/nosial/socialclient/classes/Resolver.java b/src/main/java/net/nosial/socialclient/classes/Resolver.java
new file mode 100644
index 0000000..601e9fa
--- /dev/null
+++ b/src/main/java/net/nosial/socialclient/classes/Resolver.java
@@ -0,0 +1,95 @@
+package net.nosial.socialclient.classes;
+
+import net.nosial.socialclient.exceptions.ResolutionException;
+import net.nosial.socialclient.objects.ResolvedServer;
+
+import javax.naming.NamingException;
+import javax.naming.directory.*;
+import java.util.Hashtable;
+
+// Improved Resolver class
+public class Resolver
+{
+ /**
+ * Resolves the domain to obtain endpoint and public key from its DNS TXT records.
+ *
+ * @param domain The domain to be resolved.
+ * @return ResolvedServer An instance of ResolvedServer containing the endpoint and public key.
+ * @throws ResolutionException If the DNS TXT records cannot be resolved or if required information is missing.
+ */
+ public static ResolvedServer resolveDomain(String domain) throws ResolutionException
+ {
+ final String endpointPrefix = "socialbox=";
+ final String keyPrefix = "socialbox-key=";
+
+ // Disable caching
+ Hashtable env = new Hashtable<>();
+ env.put("com.sun.jndi.dns.timeout.retries", "1");
+ env.put("com.sun.jndi.dns.cache.ttl", "0");
+ env.put("com.sun.jndi.dns.cache.negative.ttl", "0");
+
+ try
+ {
+ DirContext dirContext = new InitialDirContext(env);
+ Attributes attributes = dirContext.getAttributes("dns:/" + domain, new String[]{"TXT"});
+ Attribute attributeTXT = attributes.get("TXT");
+
+ if (attributeTXT == null)
+ {
+ throw new ResolutionException("Failed to resolve DNS TXT records for " + domain);
+ }
+
+ String endpoint = null;
+ StringBuilder publicKeyBuilder = new StringBuilder();
+ boolean publicKeyFound = false;
+
+ for (int i = 0; i < attributeTXT.size(); i++)
+ {
+ String value = (String) attributeTXT.get(i);
+
+ // Remove surrounding quotes if present
+ if (value.startsWith("\"") && value.endsWith("\""))
+ {
+ value = value.substring(1, value.length() - 1);
+ }
+
+ // Split the value into fragments to find the relevant keys
+ String[] fragments = value.split("\\s+");
+ for (String fragment : fragments)
+ {
+ if (fragment.startsWith(endpointPrefix))
+ {
+ endpoint = fragment.substring(endpointPrefix.length());
+ }
+ else if (fragment.startsWith(keyPrefix))
+ {
+ publicKeyBuilder.append(fragment.substring(keyPrefix.length()));
+ publicKeyFound = true;
+ }
+ else if (publicKeyFound)
+ {
+ // If the public key has already started, append the fragment
+ publicKeyBuilder.append(fragment);
+ }
+ }
+ }
+
+ if (endpoint == null)
+ {
+ throw new ResolutionException("Failed to resolve RPC endpoint for " + domain);
+ }
+
+ if (publicKeyBuilder.isEmpty())
+ {
+ throw new ResolutionException("Failed to resolve public key for " + domain);
+ }
+
+ String publicKey = publicKeyBuilder.toString();
+ return new ResolvedServer(endpoint, publicKey);
+ }
+ catch (NamingException e)
+ {
+ throw new ResolutionException("Error resolving domain: " + e.getMessage(), e);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/net/nosial/socialclient/classes/RpcClient.java b/src/main/java/net/nosial/socialclient/classes/RpcClient.java
new file mode 100644
index 0000000..77b1596
--- /dev/null
+++ b/src/main/java/net/nosial/socialclient/classes/RpcClient.java
@@ -0,0 +1,361 @@
+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.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 String CLIENT_NAME = "SocialClient Java";
+ private final static String CLIENT_VERSION = "1.0";
+ 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 sessionUuid;
+ private 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.
+ */
+ protected RpcClient(String domain) throws ResolutionException, CryptographyException
+ {
+ 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
+ */
+ public 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
+ */
+ public void setSession(String sessionUuid, PrivateKey privateKey)
+ {
+ this.sessionUuid = sessionUuid;
+ this.privateKey = privateKey;
+ }
+
+ /**
+ * 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", CLIENT_NAME);
+ this.addHeader("Client-Version", CLIENT_VERSION);
+
+ if(sessionUuid != null)
+ {
+ this.addHeader("Session-UUID", sessionUuid);
+ }
+
+ if(privateKey != null)
+ {
+ try
+ {
+ 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())
+ {
+ if(!responseString.isEmpty())
+ {
+ throw new RuntimeException(responseString);
+ }
+
+ throw new RuntimeException("Failed to send request");
+ }
+
+ if(response.code() == 204)
+ {
+ // The response is empty
+ return new ArrayList<>();
+ }
+
+ Object decoded = decode(responseString);
+
+ // Singular object response
+ if(decoded instanceof List>)
+ {
+ List