Skip to content

Commit 70a8567

Browse files
committed
Buffer friend adding and handle errors better
1 parent 4bc0769 commit 70a8567

2 files changed

Lines changed: 173 additions & 38 deletions

File tree

core/src/main/java/com/rtm516/mcxboxbroadcast/core/FriendManager.java

Lines changed: 164 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import com.rtm516.mcxboxbroadcast.core.configs.FriendSyncConfig;
44
import com.rtm516.mcxboxbroadcast.core.exceptions.XboxFriendsException;
5+
import com.rtm516.mcxboxbroadcast.core.models.FriendModifyResponse;
56
import com.rtm516.mcxboxbroadcast.core.models.session.FollowerResponse;
67

78
import java.io.IOException;
@@ -10,18 +11,29 @@
1011
import java.net.http.HttpRequest;
1112
import java.net.http.HttpResponse;
1213
import java.util.ArrayList;
14+
import java.util.HashMap;
1315
import java.util.List;
16+
import java.util.Map;
17+
import java.util.Optional;
18+
import java.util.concurrent.Future;
1419
import java.util.concurrent.TimeUnit;
1520

1621
public 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
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package com.rtm516.mcxboxbroadcast.core.models;
2+
3+
public record FriendModifyResponse(
4+
int code,
5+
String description,
6+
String source,
7+
Object traceInformation
8+
) {
9+
}

0 commit comments

Comments
 (0)