Skip to content

Commit eebc6e2

Browse files
AlexProgrammerDEStackAnvil Patch Bot
authored andcommitted
feat(bedrock): add social, party, and Realm Hub support
Import the Bedrock social and Realms integration from viafabricplus-bedrock PR #7. Keep it as one feature patch so it can be reviewed and promoted independently.
1 parent d9b4509 commit eebc6e2

67 files changed

Lines changed: 5487 additions & 278 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎build.gradle.kts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ dependencies {
3939
exclude(group = "at.yawk.lz4", module = "lz4-java")
4040
exclude(group = "io.netty")
4141
}
42-
shade("net.raphimc:MinecraftAuth:5.0.2") {
42+
shade("net.raphimc:MinecraftAuth:5.0.3-SNAPSHOT") {
4343
exclude(group = "com.google.code.gson", module = "gson")
4444
}
4545
shade("dev.kastle.netty:netty-transport-raknet:1.7.0") {
@@ -51,6 +51,7 @@ dependencies {
5151
exclude(group = "dev.opencollab", module = "libdatachannel-java")
5252
}
5353
shade("dev.opencollab:libdatachannel-java-arch-detect:0.24.5.0-20260921.140330-12")
54+
shade("com.microsoft.signalr.messagepack:signalr-messagepack:9.0.4")
5455
}
5556

5657
includeTransitiveJijDependencies()

‎src/main/java/com/viaversion/viafabricplus/bedrock/account/BedrockAccount.java‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import com.viaversion.viafabricplus.bedrock.ViaFabricPlusBedrock;
2626
import com.viaversion.viafabricplus.bedrock.injection.access.IConfirmScreen;
2727
import com.viaversion.viafabricplus.bedrock.friends.BedrockFriendsService;
28+
import com.viaversion.viafabricplus.bedrock.party.BedrockPartyService;
2829
import com.viaversion.viafabricplus.bedrock.screen.BedrockRealmsScreen;
2930
import com.viaversion.viafabricplus.screen.base.VFPScreen;
3031
import com.viaversion.viafabricplus.util.JsonSave;
@@ -102,6 +103,7 @@ public <T> void onChange(final T oldValue, final T newValue) {
102103
account.getMinecraftCertificateChain().refreshIfExpired();
103104
this.account = account;
104105
BedrockFriendsService.leaveCurrent();
106+
BedrockPartyService.reset();
105107
BedrockRealmsScreen.invalidate(); // The realms of the previous account no longer apply
106108

107109
VFPScreen.setScreen(prevScreen);

‎src/main/java/com/viaversion/viafabricplus/bedrock/friends/BedrockFriendsService.java‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,8 @@ private BedrockFriendsService() {
6060
public static CompletableFuture<List<FriendWorld>> worlds(final BedrockAuthManager account) {
6161
return CompletableFuture.supplyAsync(() -> {
6262
try {
63-
final XblXstsToken token = account.getXboxLiveXstsToken().refresh();
64-
final String xuid = account.getXboxUserProfile().refresh().getId();
63+
final XblXstsToken token = account.getXboxLiveXstsToken().getUpToDate();
64+
final String xuid = account.getXboxUserProfile().getUpToDate().getId();
6565
final JsonObject owners = new JsonObject();
6666
final JsonObject people = new JsonObject();
6767
people.addProperty("moniker", "people");
@@ -105,8 +105,8 @@ public static CompletableFuture<JoinedWorld> join(final BedrockAuthManager accou
105105
RtaSubscription subscription = null;
106106
JoinedWorld joined = null;
107107
try {
108-
final XblXstsToken token = account.getXboxLiveXstsToken().refresh();
109-
final String xuid = account.getXboxUserProfile().refresh().getId();
108+
final XblXstsToken token = account.getXboxLiveXstsToken().getUpToDate();
109+
final String xuid = account.getXboxUserProfile().getUpToDate().getId();
110110
subscription = RtaSubscription.open(token);
111111

112112
final JsonObject systemConstants = new JsonObject();
@@ -313,7 +313,7 @@ public void close() {
313313
members.add("me", null);
314314
final JsonObject body = new JsonObject();
315315
body.add("members", members);
316-
updateSession(this.sessionUri, body, this.account.getXboxLiveXstsToken().refresh());
316+
updateSession(this.sessionUri, body, this.account.getXboxLiveXstsToken().getUpToDate());
317317
} catch (Exception ignored) {
318318
// The session directory expires abandoned memberships when offline.
319319
} finally {

‎src/main/java/com/viaversion/viafabricplus/bedrock/friends/BedrockSocialService.java‎

Lines changed: 132 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@
2121

2222
package com.viaversion.viafabricplus.bedrock.friends;
2323

24+
import com.viaversion.viafabricplus.bedrock.visual.BedrockPlayerImages;
2425
import com.google.gson.JsonElement;
26+
import com.google.gson.JsonArray;
2527
import com.google.gson.JsonObject;
2628
import com.google.gson.JsonParser;
2729
import java.io.IOException;
@@ -34,6 +36,8 @@
3436
import java.time.Duration;
3537
import java.util.ArrayList;
3638
import java.util.List;
39+
import java.util.Map;
40+
import java.util.HashMap;
3741
import java.util.concurrent.CompletableFuture;
3842
import net.raphimc.minecraftauth.bedrock.BedrockAuthManager;
3943
import net.raphimc.minecraftauth.xbl.model.XblXstsToken;
@@ -43,6 +47,8 @@ public final class BedrockSocialService {
4347

4448
private static final URI PEOPLE = URI.create("https://peoplehub.xboxlive.com/users/me/people/");
4549
private static final URI SOCIAL = URI.create("https://social.xboxlive.com/users/me/people/friends/v2/");
50+
private static final URI FAVORITES = URI.create("https://social.xboxlive.com/users/me/people/favorites/xuids");
51+
private static final URI PROFILES = URI.create("https://profile.xboxlive.com/users/batch/profile/settings");
4652
private static final String DECORATIONS = "/decoration/bio,detail,multiplayerSummary,preferredColor,presenceDetail";
4753
private static final HttpClient HTTP = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).build();
4854

@@ -52,7 +58,7 @@ private BedrockSocialService() {
5258
public static CompletableFuture<List<SocialUser>> friends(final BedrockAuthManager account) {
5359
return CompletableFuture.supplyAsync(() -> {
5460
try {
55-
return users(PEOPLE.resolve("friends" + DECORATIONS), account.getXboxLiveXstsToken().refresh());
61+
return users(PEOPLE.resolve("friends" + DECORATIONS), account.getXboxLiveXstsToken().getUpToDate());
5662
} catch (Exception exception) {
5763
throw new IllegalStateException("Could not load Xbox friends", exception);
5864
}
@@ -62,7 +68,7 @@ public static CompletableFuture<List<SocialUser>> friends(final BedrockAuthManag
6268
public static CompletableFuture<FriendRequests> requests(final BedrockAuthManager account) {
6369
return CompletableFuture.supplyAsync(() -> {
6470
try {
65-
final XblXstsToken token = account.getXboxLiveXstsToken().refresh();
71+
final XblXstsToken token = account.getXboxLiveXstsToken().getUpToDate();
6672
return new FriendRequests(
6773
users(PEOPLE.resolve("friendRequests(received)" + DECORATIONS), token),
6874
users(PEOPLE.resolve("friendRequests(sent)" + DECORATIONS), token)
@@ -78,13 +84,130 @@ public static CompletableFuture<List<SocialUser>> search(final BedrockAuthManage
7884
try {
7985
final String encoded = URLEncoder.encode(query, StandardCharsets.UTF_8);
8086
final URI uri = PEOPLE.resolve("search/decoration/detail,preferredColor?q=" + encoded);
81-
return users(uri, account.getXboxLiveXstsToken().refresh());
87+
return users(uri, account.getXboxLiveXstsToken().getUpToDate());
8288
} catch (Exception exception) {
8389
throw new IllegalStateException("Could not search Xbox players", exception);
8490
}
8591
});
8692
}
8793

94+
public static CompletableFuture<List<SocialUser>> recommendations(final BedrockAuthManager account) {
95+
return list(account, "recommendations", "Could not load recommended players");
96+
}
97+
98+
public static CompletableFuture<List<SocialUser>> recentPlayers(final BedrockAuthManager account) {
99+
return list(account, "recentplayers", "Could not load recent players");
100+
}
101+
102+
public static CompletableFuture<List<SocialUser>> followers(final BedrockAuthManager account) {
103+
return list(account, "followers", "Could not load Xbox followers");
104+
}
105+
106+
private static CompletableFuture<List<SocialUser>> list(final BedrockAuthManager account, final String path, final String error) {
107+
return CompletableFuture.supplyAsync(() -> {
108+
try {
109+
return users(PEOPLE.resolve(path + DECORATIONS), account.getXboxLiveXstsToken().getUpToDate());
110+
} catch (Exception exception) {
111+
throw new IllegalStateException(error, exception);
112+
}
113+
});
114+
}
115+
116+
public static CompletableFuture<Void> updateFavorite(final BedrockAuthManager account, final String xuid, final boolean add) {
117+
if (!xuid.matches("[0-9]+")) {
118+
return CompletableFuture.failedFuture(new IllegalArgumentException("Invalid Xbox user ID"));
119+
}
120+
return CompletableFuture.runAsync(() -> {
121+
try {
122+
final JsonObject body = new JsonObject();
123+
final com.google.gson.JsonArray xuids = new com.google.gson.JsonArray();
124+
xuids.add(xuid);
125+
body.add("xuids", xuids);
126+
final HttpRequest request = HttpRequest.newBuilder(URI.create(FAVORITES + "?method=" + (add ? "add" : "remove")))
127+
.version(HttpClient.Version.HTTP_1_1)
128+
.timeout(Duration.ofSeconds(15))
129+
.header("Authorization", account.getXboxLiveXstsToken().getUpToDate().getAuthorizationHeader())
130+
.header("X-Xbl-Contract-Version", "1")
131+
.header("Content-Type", "application/json")
132+
.header("Accept", "application/json")
133+
.POST(HttpRequest.BodyPublishers.ofString(body.toString()))
134+
.build();
135+
final HttpResponse<String> response = HTTP.send(request, HttpResponse.BodyHandlers.ofString());
136+
if (response.statusCode() / 100 != 2) {
137+
throw BedrockXboxError.response("Xbox favorite update", response);
138+
}
139+
} catch (Exception exception) {
140+
throw new IllegalStateException("Could not update Xbox favorite", exception);
141+
}
142+
});
143+
}
144+
145+
/** Resolve party members who are not on the current user's friends list. */
146+
public static CompletableFuture<Map<String, String>> profileNames(final BedrockAuthManager account,
147+
final List<String> xuids) {
148+
if (xuids.isEmpty()) {
149+
return CompletableFuture.completedFuture(Map.of());
150+
}
151+
if (xuids.size() > 100 || xuids.stream().anyMatch(xuid -> !xuid.matches("[0-9]+"))) {
152+
return CompletableFuture.failedFuture(new IllegalArgumentException("Invalid Xbox profile IDs"));
153+
}
154+
return CompletableFuture.supplyAsync(() -> {
155+
try {
156+
final JsonObject body = new JsonObject();
157+
final JsonArray userIds = new JsonArray();
158+
xuids.forEach(userIds::add);
159+
body.add("userIds", userIds);
160+
final JsonArray settings = new JsonArray();
161+
settings.add("GameDisplayName");
162+
settings.add("Gamertag");
163+
settings.add("GameDisplayPicRaw");
164+
settings.add("AppDisplayPicRaw");
165+
body.add("settings", settings);
166+
final HttpRequest request = HttpRequest.newBuilder(PROFILES)
167+
.timeout(Duration.ofSeconds(15))
168+
.header("Authorization", account.getXboxLiveXstsToken().getUpToDate().getAuthorizationHeader())
169+
.header("X-Xbl-Contract-Version", "2")
170+
.header("Content-Type", "application/json")
171+
.header("Accept", "application/json")
172+
.POST(HttpRequest.BodyPublishers.ofString(body.toString())).build();
173+
final HttpResponse<String> response = HTTP.send(request, HttpResponse.BodyHandlers.ofString());
174+
if (response.statusCode() / 100 != 2) {
175+
throw BedrockXboxError.response("Xbox profile lookup", response);
176+
}
177+
final JsonObject data = JsonParser.parseString(response.body()).getAsJsonObject();
178+
final Map<String, String> names = new HashMap<>();
179+
for (final JsonElement element : data.getAsJsonArray("profileUsers")) {
180+
final JsonObject profile = element.getAsJsonObject();
181+
String displayName = "";
182+
String gamertag = "";
183+
String picture = "";
184+
String appPicture = "";
185+
for (final JsonElement settingElement : profile.getAsJsonArray("settings")) {
186+
final JsonObject setting = settingElement.getAsJsonObject();
187+
if ("GameDisplayName".equals(string(setting, "id"))) {
188+
displayName = string(setting, "value");
189+
} else if ("Gamertag".equals(string(setting, "id"))) {
190+
gamertag = string(setting, "value");
191+
} else if ("GameDisplayPicRaw".equals(string(setting, "id"))) {
192+
picture = string(setting, "value");
193+
} else if ("AppDisplayPicRaw".equals(string(setting, "id"))) {
194+
appPicture = string(setting, "value");
195+
}
196+
}
197+
BedrockPlayerImages.remember(string(profile, "id"), picture.isBlank() ? appPicture : picture);
198+
final String name = displayName.isBlank() ? gamertag : displayName;
199+
if (!name.isBlank()) {
200+
BedrockPlayerImages.rememberName(string(profile, "id"), gamertag.isBlank() ? name : gamertag);
201+
names.put(string(profile, "id"), name);
202+
}
203+
}
204+
return Map.copyOf(names);
205+
} catch (Exception exception) {
206+
throw new IllegalStateException("Could not load Xbox party profiles", exception);
207+
}
208+
});
209+
}
210+
88211
public static CompletableFuture<Void> updateFriend(final BedrockAuthManager account, final String xuid, final boolean add) {
89212
if (!xuid.matches("[0-9]+")) {
90213
return CompletableFuture.failedFuture(new IllegalArgumentException("Invalid Xbox user ID"));
@@ -96,7 +219,7 @@ public static CompletableFuture<Void> updateFriend(final BedrockAuthManager acco
96219
// Xbox requires Content-Length: 0 here; Java's HTTP/2 request omits it and receives 411.
97220
.version(HttpClient.Version.HTTP_1_1)
98221
.timeout(Duration.ofSeconds(15))
99-
.header("Authorization", account.getXboxLiveXstsToken().refresh().getAuthorizationHeader())
222+
.header("Authorization", account.getXboxLiveXstsToken().getUpToDate().getAuthorizationHeader())
100223
.header("X-Xbl-Contract-Version", "3")
101224
.header("Accept", "application/json")
102225
.header("Cache-Control", "no-cache")
@@ -136,15 +259,17 @@ private static List<SocialUser> users(final URI uri, final XblXstsToken token) t
136259
if (xuid.isBlank()) {
137260
continue;
138261
}
262+
BedrockPlayerImages.remember(xuid, string(user, "displayPicRaw"));
139263
final String gamertag = string(user, "uniqueModernGamertag").isBlank()
140264
? string(user, "gamertag") : string(user, "uniqueModernGamertag");
265+
BedrockPlayerImages.rememberName(xuid, gamertag);
141266
final JsonObject detail = user.has("detail") && user.get("detail").isJsonObject()
142267
? user.getAsJsonObject("detail") : new JsonObject();
143268
result.add(new SocialUser(xuid, gamertag, string(user, "displayName"),
144269
"Online".equalsIgnoreCase(string(user, "presenceState")), string(user, "presenceText"),
145270
string(user, "gamerScore"), number(detail, "friendCount"), bool(user, "isFriend") || bool(detail, "friend"),
146271
bool(user, "isFriendRequestReceived") || bool(detail, "isFriendRequestReceived"),
147-
bool(user, "isFriendRequestSent") || bool(detail, "isFriendRequestSent")));
272+
bool(user, "isFriendRequestSent") || bool(detail, "isFriendRequestSent"), bool(user, "isFavorite")));
148273
}
149274
return List.copyOf(result);
150275
}
@@ -166,7 +291,8 @@ public record FriendRequests(List<SocialUser> incoming, List<SocialUser> outgoin
166291
}
167292

168293
public record SocialUser(String xuid, String gamertag, String displayName, boolean online, String presence,
169-
String gamerScore, int friendCount, boolean friend, boolean incoming, boolean outgoing) {
294+
String gamerScore, int friendCount, boolean friend, boolean incoming, boolean outgoing,
295+
boolean favorite) {
170296

171297
public String name() {
172298
return !this.displayName.isBlank() ? this.displayName : !this.gamertag.isBlank() ? this.gamertag : this.xuid;

‎src/main/java/com/viaversion/viafabricplus/bedrock/friends/BedrockXboxError.java‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@
2727
import java.net.SocketTimeoutException;
2828
import java.net.http.HttpResponse;
2929
import java.net.http.HttpTimeoutException;
30+
import net.lenni0451.commons.httpclient.exceptions.HttpRequestException;
3031
import net.minecraft.network.chat.Component;
32+
import net.raphimc.minecraftauth.bedrock.exception.MinecraftServicesRequestException;
3133
import org.jetbrains.annotations.Nullable;
3234

3335
/** Formats Xbox PeopleHub, Social, and multiplayer session errors for the Friends screen. */
@@ -77,6 +79,18 @@ public static Component describe(final Throwable error) {
7779
default -> Component.translatable("bedrock_friends.viafabricplus.error.http", request.status);
7880
};
7981
}
82+
if (cause instanceof HttpRequestException request && request.getResponse() != null) {
83+
final int status = request.getResponse().getStatusCode();
84+
return switch (status) {
85+
case 401 -> Component.translatable("bedrock_friends.viafabricplus.error.auth");
86+
case 429 -> Component.translatable(request instanceof MinecraftServicesRequestException
87+
? "bedrock_friends.viafabricplus.error.token_rate_limit"
88+
: "bedrock_friends.viafabricplus.error.rate_limit");
89+
default -> status >= 500
90+
? Component.translatable("bedrock_friends.viafabricplus.error.unavailable", status)
91+
: Component.translatable("bedrock_friends.viafabricplus.error.http", status);
92+
};
93+
}
8094
if (cause instanceof HttpTimeoutException || cause instanceof SocketTimeoutException) {
8195
return Component.translatable("bedrock_friends.viafabricplus.error.timeout");
8296
}
@@ -90,6 +104,19 @@ public static Component describe(final Throwable error) {
90104
: Component.translatable("bedrock_friends.viafabricplus.error.other", message);
91105
}
92106

107+
public static boolean isRateLimited(final Throwable error) {
108+
for (Throwable cause = error; cause != null; cause = cause.getCause()) {
109+
if (cause instanceof XboxRequestException request && request.status == 429) {
110+
return true;
111+
}
112+
if (cause instanceof HttpRequestException request && request.getResponse() != null
113+
&& request.getResponse().getStatusCode() == 429) {
114+
return true;
115+
}
116+
}
117+
return false;
118+
}
119+
93120
private static String first(final JsonObject object, final String... keys) {
94121
for (final String key : keys) {
95122
if (object.has(key) && object.get(key).isJsonPrimitive()) {

0 commit comments

Comments
 (0)