diff --git a/build.gradle.kts b/build.gradle.kts index 9d573f3..e098e27 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,8 +1,7 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { - val kotlinVersion = "2.2.21" + val kotlinVersion = "2.3.20" - id("com.android.library") version "8.13.1" apply false - id("org.jetbrains.kotlin.android") version kotlinVersion apply false + id("com.android.library") version "9.1.0" apply false id("org.jetbrains.kotlin.plugin.serialization") version kotlinVersion apply false } \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index ca025c8..c61a118 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/library/build.gradle.kts b/library/build.gradle.kts index 5cafddd..ae9b3b1 100644 --- a/library/build.gradle.kts +++ b/library/build.gradle.kts @@ -2,10 +2,9 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget plugins { id("com.android.library") - id("org.jetbrains.kotlin.android") id("org.jetbrains.kotlin.plugin.serialization") id("maven-publish") - id("com.google.protobuf") version "0.9.5" + id("com.google.protobuf") version "0.10.0" } group = "org.sessionfoundation" diff --git a/library/src/main/cpp/ed25519.cpp b/library/src/main/cpp/ed25519.cpp index e5bb3b1..7eea202 100644 --- a/library/src/main/cpp/ed25519.cpp +++ b/library/src/main/cpp/ed25519.cpp @@ -2,6 +2,7 @@ #include "util.h" #include +#include #include extern "C" @@ -60,6 +61,19 @@ Java_network_loki_messenger_libsession_1util_ED25519_generateProMasterKey(JNIEnv }); } +extern "C" +JNIEXPORT jbyteArray JNICALL +Java_network_loki_messenger_libsession_1util_ED25519_proRotatingSeed(JNIEnv *env, jobject thiz, + jbyteArray master_seed, + jlong unix_ts) { + return jni_utils::run_catching_cxx_exception_or_throws(env, [=] { + auto seed = session::ProProof::rotating_seed( + jni_utils::JavaByteArrayRef(env, master_seed).get(), + std::chrono::sys_seconds{std::chrono::seconds{unix_ts}}); + return util::bytes_from_span(env, seed).release(); + }); +} + extern "C" JNIEXPORT jbyteArray JNICALL Java_network_loki_messenger_libsession_1util_ED25519_positiveEd25519PubKeyFromCurve25519( diff --git a/library/src/main/cpp/jni_utils.h b/library/src/main/cpp/jni_utils.h index 2248688..037f14a 100644 --- a/library/src/main/cpp/jni_utils.h +++ b/library/src/main/cpp/jni_utils.h @@ -4,6 +4,8 @@ #include #include #include +#include +#include #include namespace jni_utils { @@ -246,37 +248,6 @@ namespace jni_utils { } }; - class JavaCharsRef { - JNIEnv *env; - jstring s; - std::span data; - - public: - JavaCharsRef(JNIEnv *env, jstring s) : env(env), s(s) { - const jchar *c_str = env->GetStringChars(s, nullptr); - data = std::span(const_cast(c_str), env->GetStringLength(s)); - } - - JavaCharsRef(const JavaCharsRef &) = delete; - - ~JavaCharsRef() { - env->ReleaseStringChars(s, data.data()); - } - - const jchar* chars() const { - return data.data(); - } - - size_t size() const { - return data.size(); - } - - // Get the data as a span. Only valid during the lifetime of this object. - std::span get() const { - return data; - } - }; - /** * A RAII wrapper for a Java byte array. This will automatically release the byte array when it goes out of scope. */ diff --git a/library/src/main/cpp/pro_backend.cpp b/library/src/main/cpp/pro_backend.cpp index 9b6e10a..06214be 100644 --- a/library/src/main/cpp/pro_backend.cpp +++ b/library/src/main/cpp/pro_backend.cpp @@ -77,7 +77,7 @@ std::string_view plan_unit_to_string(pb::ProPlanUnit unit) { JavaLocalRef serialize_payment_item(JNIEnv* env, const pb::ProPaymentItem& it) { static BasicJavaClassInfo cls(env, "network/loki/messenger/libsession_util/pro/ProPaymentItem", - "(Ljava/lang/String;ILjava/lang/String;Ljava/lang/String;ZJJJJJJJLjava/lang/String;)V"); + "(Ljava/lang/String;ILjava/lang/String;Ljava/lang/String;ZJJJJJLjava/lang/String;)V"); return {env, env->NewObject(cls.java_class, cls.constructor, jstring_from_optional(env, std::string_view(it.status)).get(), // opaque status code string static_cast(it.plan.count), @@ -85,12 +85,10 @@ JavaLocalRef serialize_payment_item(JNIEnv* env, const pb::ProPaymentIt jstring_from_optional(env, std::string_view(it.payment_provider)).get(), static_cast(it.auto_renewing), static_cast(it.purchased_at.time_since_epoch().count()), // ms - static_cast(it.redeemed_at.time_since_epoch().count()), // s static_cast(it.expiry_at.time_since_epoch().count()), // s static_cast(it.grace_period_duration.count()), // s static_cast(it.platform_refund_expiry_at.time_since_epoch().count()), // s static_cast(it.revoked_at.time_since_epoch().count()), // ms - static_cast(it.refund_requested_at.time_since_epoch().count()), // s jstring_from_optional(env, std::string_view(it.payment_id)).get())}; } @@ -103,15 +101,21 @@ JavaLocalRef serialize_revocation_item(JNIEnv* env, const pb::ProRevoca static_cast(it.effective_at.time_since_epoch().count()))}; } -jobject serialize_pro_proof_response(JNIEnv* env, const pb::ProProofResponse& resp) { +jobject serialize_pro_proof_response(JNIEnv* env, const pb::GenerateProProofResponse& resp) { static BasicJavaClassInfo cls(env, "network/loki/messenger/libsession_util/pro/ProProofResponse", "(Lnetwork/loki/messenger/libsession_util/pro/ProResponseHeader;" - "Lnetwork/loki/messenger/libsession_util/pro/ProProof;)V"); + "Lnetwork/loki/messenger/libsession_util/pro/ProProof;J)V"); auto header = serialize_response_header(env, resp); JavaLocalRef proof(env, nullptr); if (resp) // ResponseBase::operator bool: true iff status == Ok (proof populated on success) proof = cpp_to_java_proof(env, resp.proof); - return env->NewObject(cls.java_class, cls.constructor, header.get(), proof.get()); + // Advisory account expiry as epoch seconds (0 = absent); present on success + subscription_expired. + jlong account_expiry_s = + resp.account_expiry + ? static_cast(resp.account_expiry->time_since_epoch().count()) + : 0; + return env->NewObject( + cls.java_class, cls.constructor, header.get(), proof.get(), account_expiry_s); } jobject serialize_pro_status_response(JNIEnv* env, const pb::ProStatusResponse& resp) { @@ -119,7 +123,7 @@ jobject serialize_pro_status_response(JNIEnv* env, const pb::ProStatusResponse& // latest_payment (has-flag + nullable item) instead of an items[] list + payments_total. static BasicJavaClassInfo cls(env, "network/loki/messenger/libsession_util/pro/GetProStatusResponse", "(Lnetwork/loki/messenger/libsession_util/pro/ProResponseHeader;Ljava/lang/String;Z" - "Lnetwork/loki/messenger/libsession_util/pro/ProPaymentItem;IZJJJ)V"); + "Lnetwork/loki/messenger/libsession_util/pro/ProPaymentItem;ZJJ)V"); auto header = serialize_response_header(env, resp); JavaLocalRef latest_payment(env, nullptr); if (resp.latest_payment) @@ -129,11 +133,9 @@ jobject serialize_pro_status_response(JNIEnv* env, const pb::ProStatusResponse& jstring_from_optional(env, std::string_view(resp.user_status)).get(), // opaque status code string static_cast(resp.latest_payment.has_value()), latest_payment.get(), - static_cast(resp.error_report), static_cast(resp.auto_renewing), static_cast(resp.expiry_at.time_since_epoch().count()), - static_cast(resp.grace_period_duration.count()), - static_cast(resp.refund_requested_at.time_since_epoch().count())); + static_cast(resp.grace_period_duration.count())); } jobject serialize_revocations_response(JNIEnv* env, const pb::GetProRevocationsResponse& resp) { @@ -152,35 +154,10 @@ jobject serialize_revocations_response(JNIEnv* env, const pb::GetProRevocationsR items.get()); } -jobject serialize_refund_response(JNIEnv* env, const pb::SetPaymentRefundRequestedResponse& resp) { - static BasicJavaClassInfo cls(env, "network/loki/messenger/libsession_util/pro/SetPaymentRefundRequestedResponse", - "(Lnetwork/loki/messenger/libsession_util/pro/ProResponseHeader;Z)V"); - auto header = serialize_response_header(env, resp); - return env->NewObject(cls.java_class, cls.constructor, header.get(), - static_cast(resp.updated)); -} - } // namespace // --- Request builders (return ProRequest{endpoint, body}) --- -extern "C" -JNIEXPORT jobject JNICALL -Java_network_loki_messenger_libsession_1util_pro_BackendRequests_buildAddProPaymentRequest( - JNIEnv* env, jobject, jbyteArray master_private_key, jbyteArray rotating_private_key, - jstring provider_code, jstring payment_id) { - return run_catching_cxx_exception_or_throws(env, [=]() { - JavaStringRef provider_ref(env, provider_code); - JavaStringRef payment_ref(env, payment_id); - auto req = pb::add_payment_request( - JavaByteArrayRef(env, master_private_key).get(), - JavaByteArrayRef(env, rotating_private_key).get(), - provider_ref.view(), - string_to_span(payment_ref.view())); - return serialize_pro_request(env, req); - }); -} - extern "C" JNIEXPORT jobject JNICALL Java_network_loki_messenger_libsession_1util_pro_BackendRequests_buildGenerateProProofRequest( @@ -207,24 +184,6 @@ Java_network_loki_messenger_libsession_1util_pro_BackendRequests_buildGetProStat }); } -extern "C" -JNIEXPORT jobject JNICALL -Java_network_loki_messenger_libsession_1util_pro_BackendRequests_buildRefundRequest( - JNIEnv* env, jobject, jbyteArray master_private_key, jlong now_seconds, - jlong refund_requested_seconds, jstring provider_code, jstring payment_id) { - return run_catching_cxx_exception_or_throws(env, [=]() { - JavaStringRef provider_ref(env, provider_code); - JavaStringRef payment_ref(env, payment_id); - auto req = pb::refund_request( - JavaByteArrayRef(env, master_private_key).get(), - std::chrono::sys_seconds{std::chrono::seconds(now_seconds)}, - std::chrono::sys_seconds{std::chrono::seconds(refund_requested_seconds)}, - provider_ref.view(), - string_to_span(payment_ref.view())); - return serialize_pro_request(env, req); - }); -} - extern "C" JNIEXPORT jobject JNICALL Java_network_loki_messenger_libsession_1util_pro_BackendRequests_buildRevocationsRequest( @@ -237,16 +196,6 @@ Java_network_loki_messenger_libsession_1util_pro_BackendRequests_buildRevocation // --- Response parsers (return typed structs) --- -extern "C" -JNIEXPORT jobject JNICALL -Java_network_loki_messenger_libsession_1util_pro_BackendRequests_parseAddPaymentResponse( - JNIEnv* env, jobject, jstring json) { - return run_catching_cxx_exception_or_throws(env, [=]() { - JavaStringRef json_ref(env, json); - return serialize_pro_proof_response(env, pb::parse_add_payment(json_ref.view())); - }); -} - extern "C" JNIEXPORT jobject JNICALL Java_network_loki_messenger_libsession_1util_pro_BackendRequests_parseProProofResponse( @@ -267,16 +216,6 @@ Java_network_loki_messenger_libsession_1util_pro_BackendRequests_parseProStatusR }); } -extern "C" -JNIEXPORT jobject JNICALL -Java_network_loki_messenger_libsession_1util_pro_BackendRequests_parseRefundResponse( - JNIEnv* env, jobject, jstring json) { - return run_catching_cxx_exception_or_throws(env, [=]() { - JavaStringRef json_ref(env, json); - return serialize_refund_response(env, pb::parse_refund(json_ref.view())); - }); -} - extern "C" JNIEXPORT jobject JNICALL Java_network_loki_messenger_libsession_1util_pro_BackendRequests_parseRevocationsResponse( diff --git a/library/src/main/cpp/pro_proof_util.cpp b/library/src/main/cpp/pro_proof_util.cpp index e640168..3102ff4 100644 --- a/library/src/main/cpp/pro_proof_util.cpp +++ b/library/src/main/cpp/pro_proof_util.cpp @@ -75,20 +75,22 @@ Java_network_loki_messenger_libsession_1util_pro_ProProof_nativeStatus(JNIEnv *e jbyteArray signed_message_data, jbyteArray signed_message_signature) { return run_catching_cxx_exception_or_throws(env, [=]() { - std::optional signed_msg; JavaByteArrayRef signed_message_data_ref(env, signed_message_data); JavaByteArrayRef signed_message_signature_ref(env, signed_message_signature); + // §4: ProProof::status now takes an optional 64-byte user signature span + the message span + // directly (ProSignedMessage was removed). + std::optional> user_sig; + std::span signed_msg; if (signed_message_data && signed_message_signature) { - signed_msg.emplace(session::ProSignedMessage { - .sig = signed_message_signature_ref.get(), - .msg = signed_message_data_ref.get(), - }); + user_sig = signed_message_signature_ref.get(); + signed_msg = signed_message_data_ref.get(); } return static_cast(java_to_cpp_proof(env, thiz).status( JavaByteArrayRef(env, verify_pub_key).get(), std::chrono::sys_seconds{std::chrono::seconds(now_unix_ts)}, + user_sig, signed_msg )); }); diff --git a/library/src/main/cpp/protocol.cpp b/library/src/main/cpp/protocol.cpp index b762f32..465dedd 100644 --- a/library/src/main/cpp/protocol.cpp +++ b/library/src/main/cpp/protocol.cpp @@ -90,7 +90,8 @@ extern "C" JNIEXPORT jbyteArray JNICALL Java_network_loki_messenger_libsession_1util_protocol_SessionProtocol_encodeForCommunityInbox( JNIEnv *env, jobject thiz, jbyteArray plaintext, jbyteArray my_ed25519_priv_key, - jlong timestamp_ms, jbyteArray recipient_pub_key, jbyteArray community_server_pub_key, + jlong /* timestamp_ms: removed from libsession — community-inbox carries no envelope ts */, + jbyteArray recipient_pub_key, jbyteArray community_server_pub_key, jbyteArray rotating_key) { return run_catching_cxx_exception_or_throws(env, [=] { return util::bytes_from_vector( @@ -98,7 +99,6 @@ Java_network_loki_messenger_libsession_1util_protocol_SessionProtocol_encodeForC session::encode_for_community_inbox( JavaByteArrayRef(env, plaintext).get(), JavaByteArrayRef(env, my_ed25519_priv_key).get(), - std::chrono::milliseconds{timestamp_ms}, *java_to_cpp_array<33>(env, recipient_pub_key), *java_to_cpp_array<32>(env, community_server_pub_key), rotating_key ? std::optional(JavaByteArrayRef(env, rotating_key).get()) @@ -246,3 +246,27 @@ Java_network_loki_messenger_libsession_1util_protocol_SessionProtocol_decodeForG )).release(); }); } + +extern "C" +JNIEXPORT jlong JNICALL +Java_network_loki_messenger_libsession_1util_protocol_SessionProtocol_proFeaturesForMessage( + JNIEnv *env, jobject thiz, jint codepoint_count) { + return run_catching_cxx_exception_or_throws(env, [=]() { + return static_cast( + session::pro_features_for_message(static_cast(codepoint_count)).bitset.data); + }); +} + +extern "C" +JNIEXPORT jint JNICALL +Java_network_loki_messenger_libsession_1util_protocol_SessionProtocol_standardCharacterLimit( + JNIEnv *env, jobject thiz) { + return static_cast(session::STANDARD_CHARACTER_LIMIT); +} + +extern "C" +JNIEXPORT jint JNICALL +Java_network_loki_messenger_libsession_1util_protocol_SessionProtocol_proHigherCharacterLimit( + JNIEnv *env, jobject thiz) { + return static_cast(session::PRO_HIGHER_CHARACTER_LIMIT); +} diff --git a/library/src/main/cpp/user_profile.cpp b/library/src/main/cpp/user_profile.cpp index 3ff0dab..593bfdc 100644 --- a/library/src/main/cpp/user_profile.cpp +++ b/library/src/main/cpp/user_profile.cpp @@ -214,4 +214,54 @@ Java_network_loki_messenger_libsession_1util_UserProfile_getProAccessExpiryOrZer jobject thiz) { auto expiry = ptrToProfile(env, thiz)->get_pro_access_expiry(); return expiry ? expiry->time_since_epoch().count() : 0; +} + +extern "C" +JNIEXPORT jlong JNICALL +Java_network_loki_messenger_libsession_1util_UserProfile_getRefundRequestedOrZero(JNIEnv *env, + jobject thiz) { + auto when = ptrToProfile(env, thiz)->get_refund_requested(); + return when ? when->time_since_epoch().count() : 0; +} + +extern "C" +JNIEXPORT void JNICALL +Java_network_loki_messenger_libsession_1util_UserProfile_nativeSetRefundRequested(JNIEnv *env, + jobject thiz, + jlong epoch_seconds) { + if (epoch_seconds <= 0) + ptrToProfile(env, thiz)->set_refund_requested(std::nullopt); + else + ptrToProfile(env, thiz)->set_refund_requested( + std::chrono::sys_seconds{std::chrono::seconds{epoch_seconds}}); +} + +extern "C" +JNIEXPORT jlong JNICALL +Java_network_loki_messenger_libsession_1util_UserProfile_getProPrepaidOrZero(JNIEnv *env, + jobject thiz) { + auto when = ptrToProfile(env, thiz)->get_pro_prepaid(); + return when ? when->time_since_epoch().count() : 0; +} + +extern "C" +JNIEXPORT void JNICALL +Java_network_loki_messenger_libsession_1util_UserProfile_nativeSetProPrepaid(JNIEnv *env, + jobject thiz, + jlong epoch_seconds) { + if (epoch_seconds <= 0) + ptrToProfile(env, thiz)->set_pro_prepaid(std::nullopt); + else + ptrToProfile(env, thiz)->set_pro_prepaid( + std::chrono::sys_seconds{std::chrono::seconds{epoch_seconds}}); +} + +extern "C" +JNIEXPORT jlong JNICALL +Java_network_loki_messenger_libsession_1util_UserProfile_getProRenewalTargetOrZero(JNIEnv *env, + jobject thiz, + jlong now_seconds) { + auto target = ptrToProfile(env, thiz)->pro_renewal_target( + std::chrono::sys_seconds{std::chrono::seconds{now_seconds}}); + return target ? target->time_since_epoch().count() : 0; } \ No newline at end of file diff --git a/library/src/main/cpp/util.cpp b/library/src/main/cpp/util.cpp index 9eef555..f7f5c81 100644 --- a/library/src/main/cpp/util.cpp +++ b/library/src/main/cpp/util.cpp @@ -306,26 +306,3 @@ Java_network_loki_messenger_libsession_1util_Config_free(JNIEnv *env, jobject th delete config; } } - -extern "C" -JNIEXPORT jint JNICALL -Java_network_loki_messenger_libsession_1util_util_Util_lengthForCodepoints(JNIEnv *env, - jobject thiz, - jstring str, - jint max_codepoints) { - return run_catching_cxx_exception_or_throws(env, [=]() { - JavaCharsRef str_ref(env, str); - return session::utf16_count_truncated_to_codepoints( - {reinterpret_cast(str_ref.chars()), str_ref.size()}, - max_codepoints - ); - }); -} - -extern "C" -JNIEXPORT jint JNICALL -Java_network_loki_messenger_libsession_1util_util_Util_countCodepoints(JNIEnv *env, jobject thiz, - jstring str) { - JavaCharsRef str_ref(env, str); - return session::utf16_count({reinterpret_cast(str_ref.chars()), str_ref.size()}); -} \ No newline at end of file diff --git a/library/src/main/java/network/loki/messenger/libsession_util/Config.kt b/library/src/main/java/network/loki/messenger/libsession_util/Config.kt index 3b7d553..98385bd 100644 --- a/library/src/main/java/network/loki/messenger/libsession_util/Config.kt +++ b/library/src/main/java/network/loki/messenger/libsession_util/Config.kt @@ -86,6 +86,18 @@ interface ReadableUserProfile: ReadableConfig { fun getProFeatures(): ProProfileFeatures fun getProConfig(): ProConfig? fun getProAccessExpiry(): Long? + + /** When a refund was requested (unix seconds), or null if none (values >1 week old read as null). */ + fun getRefundRequested(): Long? + + /** The "purchase in flight" marker timestamp (unix seconds), or null (values >1 week old read as null). */ + fun getProPrepaid(): Long? + + /** + * When to (re)request a Pro proof given [nowSeconds]: a value <= now means renew now, a future value + * can be scheduled, null means no renewal needed. Not gated on autoRenewing (entitlement gates it). + */ + fun getProRenewalTarget(nowSeconds: Long): Long? } interface MutableUserProfile : ReadableUserProfile, MutableConfig { @@ -110,6 +122,12 @@ interface MutableUserProfile : ReadableUserProfile, MutableConfig { fun setAnimatedAvatar(animatedAvatar: Boolean) fun setProAccessExpiry(epochSeconds: Long) fun removeProAccessExpiry() + + /** Record (epochSeconds) or clear (null) the "refund requested" flag; synced across devices. */ + fun setRefundRequested(epochSeconds: Long?) + + /** Record (epochSeconds) or clear (null) the "purchase in flight" marker; no-op if already Pro. */ + fun setProPrepaid(epochSeconds: Long?) } interface ReadableConversationVolatileConfig: ReadableConfig { diff --git a/library/src/main/java/network/loki/messenger/libsession_util/ED25519.kt b/library/src/main/java/network/loki/messenger/libsession_util/ED25519.kt index 172f392..02fed76 100644 --- a/library/src/main/java/network/loki/messenger/libsession_util/ED25519.kt +++ b/library/src/main/java/network/loki/messenger/libsession_util/ED25519.kt @@ -39,6 +39,15 @@ object ED25519 : LibSessionUtilCApi() { */ external fun generateProMasterKey(ed25519Seed: ByteArray): ByteArray + /** + * Deterministically derive the current rotating Session Pro seed (32 bytes) for [unixTs], from + * the account's Pro master seed (see [generateProMasterKey]). libsession owns the rotation + * schedule; every device deriving from the same master seed + time converges on the same seed, + * so concurrent proof regenerations don't race. Expand to a full keypair with [generate] when a + * libsodium key is needed. + */ + external fun proRotatingSeed(masterSeed: ByteArray, unixTs: Long): ByteArray + private external fun positiveEd25519PubKeyFromCurve25519(curve25519PubKey: ByteArray): ByteArray fun ed25519PubKeysFromCurve25519(curve25519PubKey: ByteArray): List { diff --git a/library/src/main/java/network/loki/messenger/libsession_util/UserProfile.kt b/library/src/main/java/network/loki/messenger/libsession_util/UserProfile.kt index c864b9c..92aa47a 100644 --- a/library/src/main/java/network/loki/messenger/libsession_util/UserProfile.kt +++ b/library/src/main/java/network/loki/messenger/libsession_util/UserProfile.kt @@ -53,4 +53,18 @@ class UserProfile private constructor(pointer: Long) : ConfigBase(pointer), Muta private external fun getProAccessExpiryOrZero(): Long override fun getProAccessExpiry(): Long? = getProAccessExpiryOrZero().takeIf { it != 0L } + + private external fun getRefundRequestedOrZero(): Long + override fun getRefundRequested(): Long? = getRefundRequestedOrZero().takeIf { it != 0L } + private external fun nativeSetRefundRequested(epochSeconds: Long) + override fun setRefundRequested(epochSeconds: Long?) = nativeSetRefundRequested(epochSeconds ?: 0L) + + private external fun getProPrepaidOrZero(): Long + override fun getProPrepaid(): Long? = getProPrepaidOrZero().takeIf { it != 0L } + private external fun nativeSetProPrepaid(epochSeconds: Long) + override fun setProPrepaid(epochSeconds: Long?) = nativeSetProPrepaid(epochSeconds ?: 0L) + + private external fun getProRenewalTargetOrZero(nowSeconds: Long): Long + override fun getProRenewalTarget(nowSeconds: Long): Long? = + getProRenewalTargetOrZero(nowSeconds).takeIf { it != 0L } } \ No newline at end of file diff --git a/library/src/main/java/network/loki/messenger/libsession_util/pro/BackendRequests.kt b/library/src/main/java/network/loki/messenger/libsession_util/pro/BackendRequests.kt index 38703e0..2d029b1 100644 --- a/library/src/main/java/network/loki/messenger/libsession_util/pro/BackendRequests.kt +++ b/library/src/main/java/network/loki/messenger/libsession_util/pro/BackendRequests.kt @@ -13,17 +13,10 @@ object BackendRequests : LibSessionUtilCApi() { // SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_* constants (opaque wire codes). const val PAYMENT_PROVIDER_GOOGLE_PLAY: String = "google_play" const val PAYMENT_PROVIDER_APP_STORE: String = "app_store" - const val PAYMENT_PROVIDER_RANGEPROOF: String = "rangeproof" + const val PAYMENT_PROVIDER_STF: String = "stf" // --- Request builders (sign internally; return endpoint + JSON body) --- - external fun buildAddProPaymentRequest( - masterPrivateKey: ByteArray, - rotatingPrivateKey: ByteArray, - providerCode: String, - paymentId: String, - ): ProRequest - external fun buildGenerateProProofRequest( masterPrivateKey: ByteArray, rotatingPrivateKey: ByteArray, @@ -35,31 +28,21 @@ object BackendRequests : LibSessionUtilCApi() { nowSeconds: Long, ): ProRequest - external fun buildRefundRequest( - masterPrivateKey: ByteArray, - nowSeconds: Long, - refundRequestedSeconds: Long, - providerCode: String, - paymentId: String, - ): ProRequest - external fun buildRevocationsRequest(ticket: Long): ProRequest // --- Response parsers (typed structs; check header.errors first) --- - external fun parseAddPaymentResponse(json: String): ProProofResponse external fun parseProProofResponse(json: String): ProProofResponse external fun parseProStatusResponse(json: String): GetProStatusResponse - external fun parseRefundResponse(json: String): SetPaymentRefundRequestedResponse external fun parseRevocationsResponse(json: String): GetProRevocationsResponse - // --- Provider URLs (libsession is the source of truth; null if none, e.g. rangeproof/unknown) --- + // --- Provider URLs (libsession is the source of truth; null if none, e.g. stf/unknown) --- external fun providerUrls(providerCode: String): ProviderUrls? /** * The purchasable payment-provider slugs to surface to users (single source of truth in libsession; - * excludes non-purchasable providers like `rangeproof`). Order is not significant — the client + * excludes non-purchasable providers like `stf`). Order is not significant — the client * applies its own ordering and skips slugs it has no display translation for. */ external fun visiblePlatforms(): Array diff --git a/library/src/main/java/network/loki/messenger/libsession_util/pro/ProBackendResponses.kt b/library/src/main/java/network/loki/messenger/libsession_util/pro/ProBackendResponses.kt index 0ee9508..d2a7445 100644 --- a/library/src/main/java/network/loki/messenger/libsession_util/pro/ProBackendResponses.kt +++ b/library/src/main/java/network/loki/messenger/libsession_util/pro/ProBackendResponses.kt @@ -102,18 +102,33 @@ interface ProResponse { val header: ProResponseHeader } -/** Response to add-payment / generate-proof: carries the freshly-issued proof (null on error). */ +/** Response to generate-proof: carries the freshly-issued proof (null on error). */ @Keep data class ProProofResponse( override val header: ProResponseHeader, val proof: ProProof?, -) : ProResponse + /** + * Advisory account (subscription) expiry — grace-inclusive true entitlement end; set on a + * successful proof and on a `subscription_expired` failure (a past value), null otherwise. + * Distinct from [proof]'s own clamped expiry; use for the "Pro until X" display and to refresh + * the cached access expiry, never for entitlement gating. + */ + val accountExpiry: Instant?, +) : ProResponse { + /** Raw-epoch constructor used by the JNI layer (see the file header). */ + @Keep + constructor( + header: ProResponseHeader, + proof: ProProof?, + accountExpirySeconds: Long, + ) : this(header, proof, accountExpirySeconds.secondsToInstantOrNull()) +} /** One payment/subscription record from get-pro-status. */ @Serializable @Keep data class ProPaymentItem( - val status: String, // opaque per-payment status slug: unredeemed/redeemed/expired/revoked + val status: String, // opaque per-payment status slug: redeemed/expired/revoked val planCount: Int, // parsed billing-period count; >= 1 for periodic units, 0 for "lifetime" val planUnit: String, // parsed unit name: second/day/week/month/year/lifetime (never canonicalized) val paymentProvider: String, // provider slug, e.g. "google_play" @@ -121,8 +136,6 @@ data class ProPaymentItem( @Serializable(with = InstantAsEpochMillisSerializer::class) val purchased: Instant, // provider purchase instant; always set @Serializable(with = InstantAsEpochMillisSerializer::class) - val redeemed: Instant?, // when activated; null if not activated - @Serializable(with = InstantAsEpochMillisSerializer::class) val expiry: Instant?, // access expiry for this payment; null if not activated @Serializable(with = DurationAsSecondsSerializer::class) val gracePeriod: Duration, // grace beyond [expiry] before access is really lost @@ -130,8 +143,6 @@ data class ProPaymentItem( val platformRefundExpiry: Instant?, // deadline for a platform ("quick") refund; null if n/a @Serializable(with = InstantAsEpochMillisSerializer::class) val revoked: Instant?, // when revoked; null if not revoked - @Serializable(with = InstantAsEpochMillisSerializer::class) - val refundRequested: Instant?, // when a refund was requested; null if none val paymentId: String, // opaque; confidential ) { /** Raw-epoch constructor used by the JNI layer (see the file header); converts to the typed fields. */ @@ -143,12 +154,10 @@ data class ProPaymentItem( paymentProvider: String, autoRenewing: Boolean, purchasedUnixTsMs: Long, - redeemedUnixTs: Long, expiryUnixTs: Long, gracePeriodDurationSeconds: Long, platformRefundExpiryUnixTs: Long, revokedUnixTsMs: Long, - refundRequestedUnixTs: Long, paymentId: String, ) : this( status = status, @@ -157,12 +166,10 @@ data class ProPaymentItem( paymentProvider = paymentProvider, autoRenewing = autoRenewing, purchased = Instant.ofEpochMilli(purchasedUnixTsMs), - redeemed = redeemedUnixTs.secondsToInstantOrNull(), expiry = expiryUnixTs.secondsToInstantOrNull(), gracePeriod = Duration.ofSeconds(gracePeriodDurationSeconds), platformRefundExpiry = platformRefundExpiryUnixTs.secondsToInstantOrNull(), revoked = revokedUnixTsMs.msToInstantOrNull(), - refundRequested = refundRequestedUnixTs.secondsToInstantOrNull(), paymentId = paymentId, ) } @@ -178,14 +185,11 @@ data class GetProStatusResponse( override val header: ProResponseHeader, val userStatus: String, // opaque account-status slug: never/active/expired val latestPayment: ProPaymentItem?, // the single most-recent payment, or null when none - val errorReport: Int, // SESSION_PRO_BACKEND_GET_PRO_STATUS_ERROR_REPORT val autoRenewing: Boolean, @Serializable(with = InstantAsEpochMillisSerializer::class) val expiry: Instant?, // account access expiry (incl. grace); null if never subscribed @Serializable(with = DurationAsSecondsSerializer::class) val gracePeriod: Duration, // grace included in [expiry] - @Serializable(with = InstantAsEpochMillisSerializer::class) - val refundRequested: Instant?, // when a refund was requested; null if none ) : ProResponse { /** Raw-epoch constructor used by the JNI layer (see the file header); converts to the typed fields. */ @Keep @@ -194,20 +198,16 @@ data class GetProStatusResponse( userStatus: String, hasLatestPayment: Boolean, latestPayment: ProPaymentItem?, - errorReport: Int, autoRenewing: Boolean, expiryUnixTs: Long, gracePeriodDurationSeconds: Long, - refundRequestedUnixTs: Long, ) : this( header = header, userStatus = userStatus, latestPayment = if (hasLatestPayment) latestPayment else null, - errorReport = errorReport, autoRenewing = autoRenewing, expiry = expiryUnixTs.secondsToInstantOrNull(), gracePeriod = Duration.ofSeconds(gracePeriodDurationSeconds), - refundRequested = refundRequestedUnixTs.secondsToInstantOrNull(), ) } @@ -231,9 +231,3 @@ data class GetProRevocationsResponse( val items: List, ) : ProResponse -/** Response to set-refund-requested. */ -@Keep -data class SetPaymentRefundRequestedResponse( - override val header: ProResponseHeader, - val updated: Boolean, -) : ProResponse diff --git a/library/src/main/java/network/loki/messenger/libsession_util/protocol/SessionProtocol.kt b/library/src/main/java/network/loki/messenger/libsession_util/protocol/SessionProtocol.kt index d329749..016a7ea 100644 --- a/library/src/main/java/network/loki/messenger/libsession_util/protocol/SessionProtocol.kt +++ b/library/src/main/java/network/loki/messenger/libsession_util/protocol/SessionProtocol.kt @@ -53,4 +53,20 @@ object SessionProtocol : LibSessionUtilCApi() { groupEd25519PrivateKeys: Array, // all available group private keys proBackendPubKey: ByteArray, // 32 bytes backend key ): DecodedEnvelope + + /** + * Given a message's codepoint count (counted natively), applies libsession's message-feature + * policy and returns the raw [ProMessageFeatures] bitset (e.g. the higher-character-limit bit + * when the message exceeds the standard limit). Convert with [Long.toProMessageFeatures]. + */ + external fun proFeaturesForMessage(codepointCount: Int): Long + + private external fun standardCharacterLimit(): Int + private external fun proHigherCharacterLimit(): Int + + /** Maximum message codepoints for a non-Pro message (libsession-owned constant). */ + val STANDARD_CHARACTER_LIMIT: Int by lazy { standardCharacterLimit() } + + /** Maximum message codepoints for a Pro message (libsession-owned constant). */ + val PRO_HIGHER_CHARACTER_LIMIT: Int by lazy { proHigherCharacterLimit() } } \ No newline at end of file diff --git a/library/src/main/java/network/loki/messenger/libsession_util/util/Util.kt b/library/src/main/java/network/loki/messenger/libsession_util/util/Util.kt deleted file mode 100644 index 51485de..0000000 --- a/library/src/main/java/network/loki/messenger/libsession_util/util/Util.kt +++ /dev/null @@ -1,12 +0,0 @@ -package network.loki.messenger.libsession_util.util - -import network.loki.messenger.libsession_util.LibSessionUtilCApi - -object Util : LibSessionUtilCApi() { - private external fun lengthForCodepoints(str: String, maxCodepoints: Int): Int - - fun truncateCodepoints(str: String, maxCodepoints: Int): String = - str.take(lengthForCodepoints(str, maxCodepoints)) - - external fun countCodepoints(str: String): Int -} \ No newline at end of file diff --git a/libsession-util b/libsession-util index 7e4d764..947d72a 160000 --- a/libsession-util +++ b/libsession-util @@ -1 +1 @@ -Subproject commit 7e4d7641a8243baa9ca6ec3a749f251fdd18488b +Subproject commit 947d72afae455d059f154e8ba564fc486852ea7a