Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode VERSIONCODE as Integer
versionName "4.67.0"
versionName "4.67.1"
vectorDrawables.useSupportLibrary = true
manifestPlaceholders = [BugsnagAPIKey: BugsnagAPIKey as String]
missingDimensionStrategy "RNNotifications.reactNativeVersion", "reactNative60" // See note below!
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@

import java.math.BigInteger;

import chat.rocket.reactnative.BuildConfig;

class RNCallback implements Callback {
public void invoke(Object... args) {

Expand Down Expand Up @@ -64,7 +66,7 @@ public Ejson() {
* needs access to React-specific keystore resources. This means MMKV cannot be initialized
* before React Native starts.
*/
private void ensureMMKVInitialized() {
private synchronized void ensureMMKVInitialized() {
if (initializationAttempted) {
return;
}
Expand Down Expand Up @@ -105,28 +107,112 @@ private void ensureMMKVInitialized() {
}

public String getAvatarUri() {
if (type == null) {
if (sender == null || sender.username == null || sender.username.isEmpty()) {
Log.w(TAG, "Cannot generate avatar URI: sender or username is null");
return null;
}

String server = serverURL();
if (server == null || server.isEmpty()) {
Log.w(TAG, "Cannot generate avatar URI: serverURL is null");
return null;
}

String userToken = token();
String uid = userId();

if (userToken.isEmpty() || uid.isEmpty()) {
Log.w(TAG, "Cannot generate avatar URI: missing auth credentials (token=" + !userToken.isEmpty() + ", uid=" + !uid.isEmpty() + ")");
return null;
}
return serverURL() + "/avatar/" + this.sender.username + "?rc_token=" + token() + "&rc_uid=" + userId();

String uri = server + "/avatar/" + sender.username + "?format=png&size=100&rc_token=" + userToken + "&rc_uid=" + uid;

if (BuildConfig.DEBUG) {
Log.d(TAG, "Generated avatar URI for user: " + sender.username);
}

return uri;
}

public String token() {
ensureMMKVInitialized();
String userId = userId();
if (mmkv != null && userId != null) {
return mmkv.decodeString(TOKEN_KEY.concat(userId));

if (mmkv == null) {
Log.e(TAG, "token() called but MMKV is null");
return "";
}

if (userId == null || userId.isEmpty()) {
Log.w(TAG, "token() called but userId is null or empty");
return "";
}
return "";

String key = TOKEN_KEY.concat(userId);
if (BuildConfig.DEBUG) {
Log.d(TAG, "Looking up token with key: " + key);
}

String token = mmkv.decodeString(key);

if (token == null || token.isEmpty()) {
Log.w(TAG, "No token found in MMKV for userId");
} else if (BuildConfig.DEBUG) {
Log.d(TAG, "Successfully retrieved token from MMKV");
}

return token != null ? token : "";
}

public String userId() {
ensureMMKVInitialized();
String serverURL = serverURL();
if (mmkv != null && serverURL != null) {
return mmkv.decodeString(TOKEN_KEY.concat(serverURL));
String key = TOKEN_KEY.concat(serverURL);

if (mmkv == null) {
Log.e(TAG, "userId() called but MMKV is null");
return "";
}
return "";

if (serverURL == null) {
Log.e(TAG, "userId() called but serverURL is null");
return "";
}

if (BuildConfig.DEBUG) {
Log.d(TAG, "Looking up userId with key: " + key);
}

String userId = mmkv.decodeString(key);

if (userId == null || userId.isEmpty()) {
Log.w(TAG, "No userId found in MMKV for server: " + NotificationHelper.sanitizeUrl(serverURL));

// Only list keys in debug builds for diagnostics
if (BuildConfig.DEBUG) {
try {
String[] allKeys = mmkv.allKeys();
if (allKeys != null && allKeys.length > 0) {
Log.d(TAG, "Available MMKV keys count: " + allKeys.length);
// Log only keys that match the TOKEN_KEY pattern for security
for (String k : allKeys) {
if (k != null && k.startsWith("reactnativemeteor_usertoken")) {
Log.d(TAG, "Found auth key: " + k);
}
}
} else {
Log.w(TAG, "MMKV has no keys stored");
}
} catch (Exception e) {
Log.e(TAG, "Error listing MMKV keys", e);
}
}
} else if (BuildConfig.DEBUG) {
Log.d(TAG, "Successfully retrieved userId from MMKV");
}

return userId != null ? userId : "";
}

public String privateKey() {
Expand Down Expand Up @@ -158,4 +244,4 @@ static class Content {
String kid;
String iv;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
package chat.rocket.reactnative.notification;

import android.os.Bundle;
import android.util.Log;

import com.facebook.react.bridge.ReactApplicationContext;
import com.google.gson.Gson;
import com.google.gson.JsonSyntaxException;

import java.io.IOException;
import java.util.concurrent.TimeUnit;

import okhttp3.HttpUrl;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;

class JsonResponse {
Data data;
Expand All @@ -31,54 +37,163 @@ class Payload {
String notificationType;
String name;
String messageType;
String senderName;
String msg;
String tmid;
Content content;

class Sender {
String _id;
String username;
String name;
}

class Content {
String algorithm;
String ciphertext;
String kid;
String iv;
}
}
}
}
}

public class LoadNotification {
private static final String TAG = "RocketChat.LoadNotif";
private int RETRY_COUNT = 0;
private int[] TIMEOUT = new int[]{0, 1, 3, 5, 10};
private String TOKEN_KEY = "reactnativemeteor_usertoken-";

public void load(ReactApplicationContext reactApplicationContext, final Ejson ejson, Callback callback) {
final OkHttpClient client = new OkHttpClient();
HttpUrl.Builder url = HttpUrl.parse(ejson.serverURL().concat("/api/v1/push.get")).newBuilder();
Log.i(TAG, "Starting notification load for message-id-only notification");

// Validate ejson object
if (ejson == null) {
Log.e(TAG, "Failed to load notification: ejson is null");
callback.call(null);
return;
}

final String serverURL = ejson.serverURL();
final String messageId = ejson.messageId;

Log.d(TAG, "Notification payload - serverURL: " + NotificationHelper.sanitizeUrl(serverURL) + ", messageId: " + (messageId != null ? "[present]" : "[null]"));

// Validate required fields
if (serverURL == null || serverURL.isEmpty()) {
Log.e(TAG, "Failed to load notification: serverURL is null or empty");
callback.call(null);
return;
}

if (messageId == null || messageId.isEmpty()) {
Log.e(TAG, "Failed to load notification: messageId is null or empty");
callback.call(null);
return;
}

final String userId = ejson.userId();
final String userToken = ejson.token();

if (userId == null || userToken == null) {
if (userId == null || userId.isEmpty()) {
Log.w(TAG, "Failed to load notification: userId is null or empty (user may not be logged in)");
callback.call(null);
return;
}

if (userToken == null || userToken.isEmpty()) {
Log.w(TAG, "Failed to load notification: userToken is null or empty (user may not be logged in)");
callback.call(null);
return;
}

// Configure OkHttpClient with proper timeouts
final OkHttpClient client = new OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.build();

HttpUrl.Builder urlBuilder;
try {
urlBuilder = HttpUrl.parse(serverURL.concat("/api/v1/push.get")).newBuilder();
} catch (Exception e) {
Log.e(TAG, "Failed to parse server URL: " + NotificationHelper.sanitizeUrl(serverURL), e);
callback.call(null);
return;
}

Request request = new Request.Builder()
.header("x-user-id", userId)
.header("x-auth-token", userToken)
.url(url.addQueryParameter("id", ejson.messageId).build())
.url(urlBuilder.addQueryParameter("id", messageId).build())
.build();

String sanitizedEndpoint = NotificationHelper.sanitizeUrl(serverURL) + "/api/v1/push.get";
Log.d(TAG, "Built request to endpoint: " + sanitizedEndpoint);

runRequest(client, request, callback);
runRequest(client, request, callback, sanitizedEndpoint);
}

private void runRequest(OkHttpClient client, Request request, Callback callback) {
private void runRequest(OkHttpClient client, Request request, Callback callback, String sanitizedEndpoint) {
try {
Thread.sleep(TIMEOUT[RETRY_COUNT] * 1000);
int delay = TIMEOUT[RETRY_COUNT];
if (delay > 0) {
Log.d(TAG, "Retry attempt " + RETRY_COUNT + ", waiting " + delay + " seconds before request");
} else {
Log.d(TAG, "Attempt " + (RETRY_COUNT + 1) + ", executing request to " + sanitizedEndpoint);
}

Thread.sleep(delay * 1000);

Response response = client.newCall(request).execute();
String body = response.body().string();
int statusCode = response.code();

ResponseBody responseBody = response.body();
if (responseBody == null) {
Log.e(TAG, "Request failed: response body is null (status: " + statusCode + ")");
throw new IOException("Response body is null");
}

String body = responseBody.string();

if (!response.isSuccessful()) {
throw new Exception("Error");
if (statusCode == 401 || statusCode == 403) {
Log.w(TAG, "Authentication failed: HTTP " + statusCode + " - user may need to re-login");
} else if (statusCode >= 500) {
Log.e(TAG, "Server error: HTTP " + statusCode + " - server may be experiencing issues");
} else {
Log.w(TAG, "Request failed with HTTP " + statusCode);
}
throw new IOException("HTTP " + statusCode);
}

Log.i(TAG, "Successfully received response (HTTP " + statusCode + "), parsing notification data");

Gson gson = new Gson();
JsonResponse json = gson.fromJson(body, JsonResponse.class);
JsonResponse json;
try {
json = gson.fromJson(body, JsonResponse.class);
} catch (JsonSyntaxException e) {
Log.e(TAG, "Failed to parse JSON response", e);
throw e;
}

// Validate parsed response structure
if (json == null || json.data == null || json.data.notification == null) {
Log.e(TAG, "Invalid response structure: missing required fields");
throw new IllegalStateException("Invalid response structure");
}

// Log encryption fields if present
if (json.data.notification.payload != null) {
boolean hasEncryption = json.data.notification.payload.msg != null || json.data.notification.payload.content != null;
if (hasEncryption) {
Log.d(TAG, "Notification contains encrypted content: msg=" + (json.data.notification.payload.msg != null) +
", content=" + (json.data.notification.payload.content != null));
}
}

Bundle bundle = new Bundle();
bundle.putString("notId", json.data.notification.notId);
Expand All @@ -87,15 +202,33 @@ private void runRequest(OkHttpClient client, Request request, Callback callback)
bundle.putString("ejson", gson.toJson(json.data.notification.payload));
bundle.putBoolean("notificationLoaded", true);

Log.i(TAG, "Successfully loaded and parsed notification data");
callback.call(bundle);

} catch (IOException e) {
Log.e(TAG, "Network error on attempt " + (RETRY_COUNT + 1) + ": " + e.getClass().getSimpleName() + " - " + e.getMessage());
handleRetryOrFailure(client, request, callback, sanitizedEndpoint);
} catch (JsonSyntaxException e) {
Log.e(TAG, "JSON parsing error: " + e.getMessage());
handleRetryOrFailure(client, request, callback, sanitizedEndpoint);
} catch (InterruptedException e) {
Log.e(TAG, "Request interrupted: " + e.getMessage());
Thread.currentThread().interrupt(); // Restore interrupt status
callback.call(null);
} catch (Exception e) {
if (RETRY_COUNT <= TIMEOUT.length) {
RETRY_COUNT++;
runRequest(client, request, callback);
} else {
callback.call(null);
}
Log.e(TAG, "Unexpected error on attempt " + (RETRY_COUNT + 1) + ": " + e.getClass().getSimpleName() + " - " + e.getMessage());
handleRetryOrFailure(client, request, callback, sanitizedEndpoint);
}
}

private void handleRetryOrFailure(OkHttpClient client, Request request, Callback callback, String sanitizedEndpoint) {
if (RETRY_COUNT < TIMEOUT.length - 1) {
RETRY_COUNT++;
Log.d(TAG, "Will retry request (attempt " + (RETRY_COUNT + 1) + " of " + TIMEOUT.length + ")");
runRequest(client, request, callback, sanitizedEndpoint);
} else {
Log.e(TAG, "All retry attempts exhausted (" + TIMEOUT.length + " attempts). Notification load failed.");
callback.call(null);
}
}
}
Loading
Loading