2121
2222package com .viaversion .viafabricplus .bedrock .friends ;
2323
24+ import com .viaversion .viafabricplus .bedrock .visual .BedrockPlayerImages ;
2425import com .google .gson .JsonElement ;
26+ import com .google .gson .JsonArray ;
2527import com .google .gson .JsonObject ;
2628import com .google .gson .JsonParser ;
2729import java .io .IOException ;
3436import java .time .Duration ;
3537import java .util .ArrayList ;
3638import java .util .List ;
39+ import java .util .Map ;
40+ import java .util .HashMap ;
3741import java .util .concurrent .CompletableFuture ;
3842import net .raphimc .minecraftauth .bedrock .BedrockAuthManager ;
3943import 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 ;
0 commit comments