22
33import com .rtm516 .mcxboxbroadcast .core .configs .FriendSyncConfig ;
44import com .rtm516 .mcxboxbroadcast .core .exceptions .XboxFriendsException ;
5+ import com .rtm516 .mcxboxbroadcast .core .models .FriendModifyResponse ;
56import com .rtm516 .mcxboxbroadcast .core .models .session .FollowerResponse ;
67
78import java .io .IOException ;
1011import java .net .http .HttpRequest ;
1112import java .net .http .HttpResponse ;
1213import java .util .ArrayList ;
14+ import java .util .HashMap ;
1315import java .util .List ;
16+ import java .util .Map ;
17+ import java .util .Optional ;
18+ import java .util .concurrent .Future ;
1419import java .util .concurrent .TimeUnit ;
1520
1621public class FriendManager {
1722 private final HttpClient httpClient ;
1823 private final Logger logger ;
1924 private final SessionManagerCore sessionManager ;
25+ private final Map <String , String > toAdd ;
26+ private final Map <String , String > toRemove ;
27+
28+ private Future internalScheduledFuture ;
2029
2130 public FriendManager (HttpClient httpClient , Logger logger , SessionManagerCore sessionManager ) {
2231 this .httpClient = httpClient ;
2332 this .logger = logger ;
2433 this .sessionManager = sessionManager ;
34+
35+ this .toAdd = new HashMap <>();
36+ this .toRemove = new HashMap <>();
2537 }
2638
2739 /**
@@ -106,44 +118,32 @@ public List<FollowerResponse.Person> get() throws XboxFriendsException {
106118 * Add a friend from xbox live
107119 *
108120 * @param xuid The XUID of the friend to add
109- * @return If the request was successful, this will be true even if the user is already your friend, false if something goes wrong
110121 */
111- public boolean add (String xuid ) {
112- HttpRequest xboxFriendRequest = HttpRequest .newBuilder ()
113- .uri (URI .create (Constants .PEOPLE .formatted (xuid )))
114- .header ("Authorization" , sessionManager .getTokenHeader ())
115- .PUT (HttpRequest .BodyPublishers .noBody ())
116- .build ();
122+ public void add (String xuid , String gamertag ) {
123+ // Remove the user from the remove list (if they are on it)
124+ toRemove .remove (xuid );
117125
118- try {
119- HttpResponse <String > response = httpClient .send (xboxFriendRequest , HttpResponse .BodyHandlers .ofString ());
120- return response .statusCode () == 204 ;
121- } catch (IOException | InterruptedException e ) {
122- logger .debug ("Failed to add friend: " + e .getMessage ());
123- return false ;
124- }
126+ // Add the user to the add list
127+ toAdd .put (xuid , gamertag );
128+
129+ // Process the add/remove requests
130+ internalProcess ();
125131 }
126132
127133 /**
128134 * Remove a friend from xbox live
129135 *
130136 * @param xuid The XUID of the friend to remove
131- * @return If the request was successful, this will be true even if the user isn't your friend, false if something goes wrong
132137 */
133- public boolean remove (String xuid ) {
134- HttpRequest xboxFriendRequest = HttpRequest .newBuilder ()
135- .uri (URI .create (Constants .PEOPLE .formatted (xuid )))
136- .header ("Authorization" , sessionManager .getTokenHeader ())
137- .DELETE ()
138- .build ();
138+ public void remove (String xuid , String gamertag ) {
139+ // Remove the user from the add list (if they are on it)
140+ toAdd .remove (xuid );
139141
140- try {
141- HttpResponse <String > response = httpClient .send (xboxFriendRequest , HttpResponse .BodyHandlers .ofString ());
142- return response .statusCode () == 204 ;
143- } catch (IOException | InterruptedException e ) {
144- logger .debug ("Failed to remove friend: " + e .getMessage ());
145- return false ;
146- }
142+ // Add the user to the remove list
143+ toRemove .put (xuid , gamertag );
144+
145+ // Process the add/remove requests
146+ internalProcess ();
147147 }
148148
149149 /**
@@ -159,20 +159,12 @@ public void initAutoFriend(FriendSyncConfig friendSyncConfig) {
159159 for (FollowerResponse .Person person : get (friendSyncConfig .autoFollow (), friendSyncConfig .autoUnfollow ())) {
160160 // Follow the person back
161161 if (friendSyncConfig .autoFollow () && person .isFollowingCaller && !person .isFollowedByCaller ) {
162- if (add (person .xuid )) {
163- logger .info ("Added " + person .displayName + " (" + person .xuid + ") as a friend" );
164- } else {
165- logger .warning ("Failed to add " + person .displayName + " (" + person .xuid + ") as a friend" );
166- }
162+ add (person .xuid , person .displayName );
167163 }
168164
169165 // Unfollow the person
170166 if (friendSyncConfig .autoUnfollow () && !person .isFollowingCaller && person .isFollowedByCaller ) {
171- if (remove (person .xuid )) {
172- logger .info ("Removed " + person .displayName + " (" + person .xuid + ") as a friend" );
173- } else {
174- logger .warning ("Failed to remove " + person .displayName + " (" + person .xuid + ") as a friend" );
175- }
167+ remove (person .xuid , person .displayName );
176168 }
177169 }
178170 } catch (XboxFriendsException e ) {
@@ -181,4 +173,138 @@ public void initAutoFriend(FriendSyncConfig friendSyncConfig) {
181173 }, friendSyncConfig .updateInterval (), friendSyncConfig .updateInterval (), TimeUnit .SECONDS );
182174 }
183175 }
176+
177+ /**
178+ * Internal function to process the add/remove requests
179+ * This will also handle retrying requests if they fail due to rate limits or other errors
180+ */
181+ private void internalProcess () {
182+ // If we are already running then don't run again
183+ if (internalScheduledFuture != null && !internalScheduledFuture .isDone ()) {
184+ return ;
185+ }
186+
187+ internalScheduledFuture = sessionManager .scheduledThread ().submit (() -> {
188+ int retryAfter = 0 ;
189+
190+ // If we have friends to add then add them
191+ if (!toAdd .isEmpty ()) {
192+ // Create a copy of the list to iterate over, so we don't get a concurrent modification exception
193+ Map <String , String > toProcess = new HashMap <>(toAdd );
194+ for (Map .Entry <String , String > entry : toProcess .entrySet ()) {
195+ // Create the request for adding the friend
196+ HttpRequest xboxFriendRequest = HttpRequest .newBuilder ()
197+ .uri (URI .create (Constants .PEOPLE .formatted (entry .getKey ())))
198+ .header ("Authorization" , sessionManager .getTokenHeader ())
199+ .PUT (HttpRequest .BodyPublishers .noBody ())
200+ .build ();
201+
202+ try {
203+ HttpResponse <String > response = httpClient .send (xboxFriendRequest , HttpResponse .BodyHandlers .ofString ());
204+ if (response .statusCode () == 204 ) {
205+ // The friend was added successfully so remove them from the list
206+ toAdd .remove (entry .getKey ());
207+
208+ // Let the user know we added a friend
209+ logger .info ("Added " + entry .getValue () + " (" + entry .getKey () + ") as a friend" );
210+ } else if (response .statusCode () == 429 ) {
211+ // The friend wasn't added successfully so get the retry after header
212+ Optional <String > header = response .headers ().firstValue ("Retry-After" );
213+ if (header .isPresent ()) {
214+ retryAfter = Integer .parseInt (header .get ());
215+ }
216+
217+ // Log the error
218+ logger .debug ("Failed to add " + entry .getValue () + " (" + entry .getKey () + ") as a friend: (" + response .statusCode () + ") " + response .body ());
219+
220+ // Break out of the loop, so we don't try to add more friends
221+ break ;
222+ } else if (response .statusCode () == 400 ) {
223+ FriendModifyResponse modifyResponse = Constants .OBJECT_MAPPER .readValue (response .body (), FriendModifyResponse .class );
224+ if (modifyResponse .code () == 1028 ) {
225+ logger .error ("Friend list full, unable to add " + entry .getValue () + " (" + entry .getKey () + ") as a friend" );
226+ break ;
227+ }
228+
229+ logger .warning ("Failed to add " + entry .getValue () + " (" + entry .getKey () + ") as a friend: (" + response .statusCode () + ") " + response .body ());
230+ } else {
231+ try {
232+ FriendModifyResponse modifyResponse = Constants .OBJECT_MAPPER .readValue (response .body (), FriendModifyResponse .class );
233+
234+ // 1011 - The requested friend operation was forbidden.
235+ // 1015 - An invalid request was attempted.
236+ // 1028 - The attempted People request was rejected because it would exceed the People list limit.
237+ // 1039 - Request could not be completed due to another request taking precedence.
238+
239+ if (modifyResponse .code () == 1028 ) {
240+ logger .error ("Friend list full, unable to add " + entry .getValue () + " (" + entry .getKey () + ") as a friend" );
241+ break ;
242+ } else if (modifyResponse .code () == 1011 ) {
243+ // The friend wasn't added successfully so remove them from the list
244+ // This seems to happen in some cases, I assume from the user blocking us or having account restrictions
245+ toAdd .remove (entry .getKey ());
246+ // TODO Remove these people from following us (block and unblock)
247+ }
248+ } catch (IOException e ) {
249+ // Ignore this error as it is just a fallback
250+ }
251+
252+ logger .warning ("Failed to add " + entry .getValue () + " (" + entry .getKey () + ") as a friend: (" + response .statusCode () + ") " + response .body ());
253+ }
254+ } catch (IOException | InterruptedException e ) {
255+ logger .error ("Failed to add " + entry .getValue () + " (" + entry .getKey () + ") as a friend: " + e .getMessage ());
256+ break ;
257+ }
258+ }
259+ }
260+
261+ // If we have friends to remove then remove them
262+ // Note: This can be run even if add hits the rate limit as it seems to be separate
263+ if (!toRemove .isEmpty ()) {
264+ // Create a copy of the list to iterate over, so we don't get a concurrent modification exception
265+ Map <String , String > toProcess = new HashMap <>(toRemove );
266+ for (Map .Entry <String , String > entry : toProcess .entrySet ()) {
267+ // Create the request for removing the friend
268+ HttpRequest xboxFriendRequest = HttpRequest .newBuilder ()
269+ .uri (URI .create (Constants .PEOPLE .formatted (entry .getKey ())))
270+ .header ("Authorization" , sessionManager .getTokenHeader ())
271+ .DELETE ()
272+ .build ();
273+
274+ try {
275+ HttpResponse <String > response = httpClient .send (xboxFriendRequest , HttpResponse .BodyHandlers .ofString ());
276+ if (response .statusCode () == 204 ) {
277+ // The friend was removed successfully so remove them from the list
278+ toRemove .remove (entry .getKey ());
279+
280+ // Let the user know we added a friend
281+ logger .info ("Removed " + entry .getValue () + " (" + entry .getKey () + ") as a friend" );
282+ } else if (response .statusCode () == 429 ) {
283+ // The friend wasn't removed successfully so get the retry after header
284+ Optional <String > header = response .headers ().firstValue ("Retry-After" );
285+ if (header .isPresent ()) {
286+ retryAfter = Integer .parseInt (header .get ());
287+ }
288+
289+ // Log the error
290+ logger .debug ("Failed to remove " + entry .getValue () + " (" + entry .getKey () + ") as a friend: (" + response .statusCode () + ") " + response .body ());
291+
292+ // Break out of the loop, so we don't try to remove more friends
293+ break ;
294+ } else {
295+ logger .warning ("Failed to remove " + entry .getValue () + " (" + entry .getKey () + ") as a friend: (" + response .statusCode () + ") " + response .body ());
296+ }
297+ } catch (IOException | InterruptedException e ) {
298+ logger .error ("Failed to remove " + entry .getValue () + " (" + entry .getKey () + ") as a friend: " + e .getMessage ());
299+ break ;
300+ }
301+ }
302+ }
303+
304+ // If we still have friends to add or remove then schedule another run after the retry after time
305+ if (!toAdd .isEmpty () || !toRemove .isEmpty ()) {
306+ internalScheduledFuture = sessionManager .scheduledThread ().schedule (this ::internalProcess , retryAfter , TimeUnit .SECONDS );
307+ }
308+ });
309+ }
184310}
0 commit comments