Refactored model Room
This commit is contained in:
parent
385dfea98b
commit
dfd6dbaf17
@ -11,6 +11,9 @@ import org.springframework.lang.NonNull;
|
||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
import ru.dragonestia.picker.api.model.node.PickingMethod;
|
||||
import ru.dragonestia.picker.api.model.room.IRoom;
|
||||
import ru.dragonestia.picker.api.repository.type.NodeIdentifier;
|
||||
import ru.dragonestia.picker.api.repository.type.RoomIdentifier;
|
||||
import ru.dragonestia.picker.interceptor.DebugInterceptor;
|
||||
import ru.dragonestia.picker.model.Room;
|
||||
import ru.dragonestia.picker.model.Node;
|
||||
@ -42,9 +45,9 @@ public class TestConfig implements WebMvcConfigurer {
|
||||
|
||||
@Bean
|
||||
void createNodes() {
|
||||
createNodeWithContent(new Node("game-servers", PickingMethod.ROUND_ROBIN, false));
|
||||
createNodeWithContent(new Node("game-lobbies", PickingMethod.LEAST_PICKED, false));
|
||||
createNodeWithContent(new Node("hub", PickingMethod.SEQUENTIAL_FILLING, false));
|
||||
createNodeWithContent(new Node(NodeIdentifier.of("game-servers"), PickingMethod.ROUND_ROBIN, false));
|
||||
createNodeWithContent(new Node(NodeIdentifier.of("game-lobbies"), PickingMethod.LEAST_PICKED, false));
|
||||
createNodeWithContent(new Node(NodeIdentifier.of("hub"), PickingMethod.SEQUENTIAL_FILLING, false));
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
@ -54,7 +57,7 @@ public class TestConfig implements WebMvcConfigurer {
|
||||
|
||||
for (int i = 1; i <= 5; i++) {
|
||||
var slots = 5 * i;
|
||||
var room = Room.create("test-" + i, node, SlotLimit.of(slots), json.writeValueAsString(generatePayload()), false);
|
||||
var room = new Room(RoomIdentifier.of("test-" + i), node, slots, json.writeValueAsString(generatePayload()), false);
|
||||
roomRepository.create(room);
|
||||
|
||||
for (int j = 0, n = rand.nextInt(slots + 1); j < n; j++) {
|
||||
@ -64,7 +67,7 @@ public class TestConfig implements WebMvcConfigurer {
|
||||
}
|
||||
|
||||
for (int i = 0; i < 5; i++) {
|
||||
var room = Room.create(randomUUID().toString(), node, SlotLimit.unlimited(), json.writeValueAsString(generatePayload()), false);
|
||||
var room = new Room(RoomIdentifier.of(randomUUID().toString()), node, IRoom.UNLIMITED_SLOTS, json.writeValueAsString(generatePayload()), false);
|
||||
room.setLocked((i & 1) == 0);
|
||||
roomRepository.create(room);
|
||||
}
|
||||
|
||||
@ -11,6 +11,7 @@ import ru.dragonestia.picker.api.model.node.PickingMethod;
|
||||
import ru.dragonestia.picker.api.repository.response.NodeDetailsResponse;
|
||||
import ru.dragonestia.picker.api.repository.response.NodeListResponse;
|
||||
import ru.dragonestia.picker.api.repository.response.PickedRoomResponse;
|
||||
import ru.dragonestia.picker.api.repository.type.NodeIdentifier;
|
||||
import ru.dragonestia.picker.model.Node;
|
||||
import ru.dragonestia.picker.service.NodeService;
|
||||
import ru.dragonestia.picker.service.RoomService;
|
||||
@ -45,7 +46,7 @@ public class NodeController {
|
||||
@Parameter(description = "Picking method method") @RequestParam(name = "method") PickingMethod method,
|
||||
@Parameter(description = "Save node") @RequestParam(name = "persist", required = false, defaultValue = "false") boolean persist
|
||||
) {
|
||||
nodeService.create(new Node(nodeId, method, persist));
|
||||
nodeService.create(new Node(NodeIdentifier.of(nodeId), method, persist));
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
|
||||
@ -11,8 +11,8 @@ import ru.dragonestia.picker.api.exception.NodeNotFoundException;
|
||||
import ru.dragonestia.picker.api.exception.RoomNotFoundException;
|
||||
import ru.dragonestia.picker.api.repository.response.RoomInfoResponse;
|
||||
import ru.dragonestia.picker.api.repository.response.RoomListResponse;
|
||||
import ru.dragonestia.picker.api.repository.type.RoomIdentifier;
|
||||
import ru.dragonestia.picker.model.Room;
|
||||
import ru.dragonestia.picker.model.type.SlotLimit;
|
||||
import ru.dragonestia.picker.service.RoomService;
|
||||
import ru.dragonestia.picker.service.NodeService;
|
||||
import ru.dragonestia.picker.util.DetailsParser;
|
||||
@ -54,7 +54,7 @@ public class RoomController {
|
||||
@Parameter(description = "Save room") @RequestParam(name = "persist", required = false, defaultValue = "false") boolean persist
|
||||
) {
|
||||
var node = nodeService.find(nodeId).orElseThrow(() -> new NodeNotFoundException(nodeId));
|
||||
var room = Room.create(roomId, node, SlotLimit.of(slots), payload, persist);
|
||||
var room = new Room(RoomIdentifier.of(roomId), node, slots, payload, persist);
|
||||
room.setLocked(locked);
|
||||
roomService.create(room);
|
||||
|
||||
|
||||
@ -42,7 +42,7 @@ public class UserRoomController {
|
||||
var room = getNodeAndRoom(nodeId, roomId).room();
|
||||
var users = userService.getRoomUsersWithDetailsResponse(room, detailsParser.parseUserDetails(detailsSeq));
|
||||
|
||||
return ResponseEntity.ok(new RoomUserListResponse(room.getSlots().getSlots(), users.size(), users));
|
||||
return ResponseEntity.ok(new RoomUserListResponse(room.getMaxSlots(), users.size(), users));
|
||||
}
|
||||
|
||||
@Operation(summary = "Link users with room")
|
||||
@ -56,7 +56,7 @@ public class UserRoomController {
|
||||
var room = getNodeAndRoom(nodeId, roomId).room();
|
||||
var users = namingValidator.validateUserIds(Arrays.stream(userIds.split(",")).toList());
|
||||
var usedSlots = userService.linkUsersWithRoom(room, users, force);
|
||||
return ResponseEntity.ok(new LinkUsersWithRoomResponse(usedSlots, room.getSlots().getSlots()));
|
||||
return ResponseEntity.ok(new LinkUsersWithRoomResponse(usedSlots, room.getMaxSlots()));
|
||||
}
|
||||
|
||||
@Operation(summary = "Unlink users from room")
|
||||
|
||||
@ -6,6 +6,7 @@ import ru.dragonestia.picker.api.model.node.INode;
|
||||
import ru.dragonestia.picker.api.model.node.NodeDetails;
|
||||
import ru.dragonestia.picker.api.model.node.PickingMethod;
|
||||
import ru.dragonestia.picker.api.model.node.ResponseNode;
|
||||
import ru.dragonestia.picker.api.repository.type.NodeIdentifier;
|
||||
|
||||
public class Node implements INode {
|
||||
|
||||
@ -13,8 +14,8 @@ public class Node implements INode {
|
||||
private final PickingMethod pickingMethod;
|
||||
private final boolean persist;
|
||||
|
||||
public Node(@NotNull String identifier, @NotNull PickingMethod pickingMethod, boolean persist) {
|
||||
this.identifier = identifier;
|
||||
public Node(@NotNull NodeIdentifier identifier, @NotNull PickingMethod pickingMethod, boolean persist) {
|
||||
this.identifier = identifier.getValue();
|
||||
this.pickingMethod = pickingMethod;
|
||||
this.persist = persist;
|
||||
}
|
||||
|
||||
@ -1,42 +1,86 @@
|
||||
package ru.dragonestia.picker.model;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import ru.dragonestia.picker.api.model.room.IRoom;
|
||||
import ru.dragonestia.picker.api.model.room.ResponseRoom;
|
||||
import ru.dragonestia.picker.api.model.room.RoomDetails;
|
||||
import ru.dragonestia.picker.api.model.room.ShortResponseRoom;
|
||||
import ru.dragonestia.picker.model.type.SlotLimit;
|
||||
import ru.dragonestia.picker.api.repository.type.RoomIdentifier;
|
||||
|
||||
import java.util.HashMap;
|
||||
public class Room implements IRoom {
|
||||
|
||||
@Getter
|
||||
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public class Room {
|
||||
|
||||
private final String id;
|
||||
private final String nodeId;
|
||||
private final SlotLimit slots;
|
||||
private final String identifier;
|
||||
private final String nodeIdentifier;
|
||||
private final int slots;
|
||||
private final String payload;
|
||||
private final boolean persist;
|
||||
private boolean locked = false;
|
||||
|
||||
public static Room create(String roomId, Node node, SlotLimit limit, String payload, boolean persist) {
|
||||
return new Room(roomId, node.getIdentifier(), limit, payload, persist);
|
||||
public Room(@NotNull RoomIdentifier identifier, @NotNull Node node, int slots, @NotNull String payload, boolean persist) {
|
||||
this.identifier = identifier.getValue();
|
||||
this.nodeIdentifier = node.getIdentifier();
|
||||
this.slots = slots;
|
||||
this.payload = payload;
|
||||
this.persist = persist;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull String getIdentifier() {
|
||||
return identifier;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull String getNodeIdentifier() {
|
||||
return nodeIdentifier;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getMaxSlots() {
|
||||
return slots;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLocked() {
|
||||
return locked;
|
||||
}
|
||||
|
||||
public void setLocked(boolean value) {
|
||||
locked = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Boolean isPersist() {
|
||||
return persist;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull String getPayload() {
|
||||
return payload;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable String getDetail(@NotNull RoomDetails detail) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
public boolean isAvailable(int usedSlots, int requiredSlots) {
|
||||
if (locked) return false;
|
||||
if (slots.isUnlimited()) return true;
|
||||
return slots.getSlots() >= usedSlots + requiredSlots;
|
||||
if (hasUnlimitedSlots()) return true;
|
||||
return slots >= usedSlots + requiredSlots;
|
||||
}
|
||||
|
||||
public @NotNull ResponseRoom toResponseObject() {
|
||||
return new ResponseRoom(identifier, nodeIdentifier, slots, locked, payload);
|
||||
}
|
||||
|
||||
public @NotNull ShortResponseRoom toShortResponseObject() {
|
||||
return new ShortResponseRoom(identifier, nodeIdentifier, slots, locked);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return id.hashCode();
|
||||
return identifier.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -44,16 +88,8 @@ public class Room {
|
||||
if (object == this) return true;
|
||||
if (object == null) return false;
|
||||
if (object instanceof Room other) {
|
||||
return id.equals(other.id);
|
||||
return identifier.equals(other.identifier);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public ResponseRoom toResponseObject() {
|
||||
return new ResponseRoom(id, nodeId, slots.getSlots(), locked, payload);
|
||||
}
|
||||
|
||||
public ShortResponseRoom toShortResponseObject() {
|
||||
return new ShortResponseRoom(id, nodeId, slots.getSlots(), locked);
|
||||
}
|
||||
}
|
||||
|
||||
@ -24,11 +24,11 @@ public class RoomRepositoryImpl implements RoomRepository {
|
||||
|
||||
@Override
|
||||
public void create(Room room) throws RoomAlreadyExistException {
|
||||
var nodeId = room.getNodeId();
|
||||
var nodeId = room.getNodeIdentifier();
|
||||
|
||||
synchronized (node2roomsMap) {
|
||||
var node = node2roomsMap.keySet().stream()
|
||||
.filter(n -> room.getNodeId().equals(n.getIdentifier()))
|
||||
.filter(n -> room.getNodeIdentifier().equals(n.getIdentifier()))
|
||||
.findFirst();
|
||||
|
||||
if (node.isEmpty()) {
|
||||
@ -36,19 +36,19 @@ public class RoomRepositoryImpl implements RoomRepository {
|
||||
}
|
||||
|
||||
var rooms = node2roomsMap.get(node.get());
|
||||
if (rooms.containsKey(room.getId())) {
|
||||
throw new RoomAlreadyExistException(room.getNodeId(), room.getId());
|
||||
if (rooms.containsKey(room.getIdentifier())) {
|
||||
throw new RoomAlreadyExistException(room.getNodeIdentifier(), room.getIdentifier());
|
||||
}
|
||||
rooms.put(room.getId(), new RoomContainer(room, new AtomicInteger(0)));
|
||||
pickerRepository.find(room.getNodeId()).add(room);
|
||||
rooms.put(room.getIdentifier(), new RoomContainer(room, new AtomicInteger(0)));
|
||||
pickerRepository.find(room.getNodeIdentifier()).add(room);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove(Room room) {
|
||||
var nodeId = room.getNodeId();
|
||||
var nodeId = room.getNodeIdentifier();
|
||||
var node = node2roomsMap.keySet().stream()
|
||||
.filter(n -> room.getNodeId().equals(n.getIdentifier()))
|
||||
.filter(n -> room.getNodeIdentifier().equals(n.getIdentifier()))
|
||||
.findFirst();
|
||||
|
||||
synchronized (node2roomsMap) {
|
||||
@ -56,8 +56,8 @@ public class RoomRepositoryImpl implements RoomRepository {
|
||||
throw new NodeNotFoundException("Node '" + nodeId + "' does not exist");
|
||||
}
|
||||
|
||||
node2roomsMap.get(node.get()).remove(room.getId());
|
||||
pickerRepository.find(room.getNodeId()).remove(room);
|
||||
node2roomsMap.get(node.get()).remove(room.getIdentifier());
|
||||
pickerRepository.find(room.getNodeIdentifier()).remove(room);
|
||||
}
|
||||
|
||||
userRepository.onRemoveRoom(room);
|
||||
@ -100,7 +100,7 @@ public class RoomRepositoryImpl implements RoomRepository {
|
||||
|
||||
Optional<RoomContainer> container = room == null?
|
||||
Optional.empty() :
|
||||
Optional.of(node2roomsMap.get(node).get(room.getId()));
|
||||
Optional.of(node2roomsMap.get(node).get(room.getIdentifier()));
|
||||
|
||||
if (container.isPresent()) {
|
||||
var cont = container.get();
|
||||
|
||||
@ -26,10 +26,10 @@ public class UserRepositoryImpl implements UserRepository {
|
||||
var result = new HashMap<User, Boolean>();
|
||||
|
||||
synchronized (usersMap) {
|
||||
var path = new NodeRoomPath(room.getNodeId(), room.getId());
|
||||
var path = new NodeRoomPath(room.getNodeIdentifier(), room.getIdentifier());
|
||||
var usersSet = roomUsers.getOrDefault(path, new HashSet<>());
|
||||
|
||||
if (force || room.getSlots().isUnlimited()) {
|
||||
if (force || room.hasUnlimitedSlots()) {
|
||||
users.forEach(user -> result.put(user, true));
|
||||
} else {
|
||||
for (var user : users) {
|
||||
@ -37,8 +37,8 @@ public class UserRepositoryImpl implements UserRepository {
|
||||
result.put(user, !set.contains(room));
|
||||
}
|
||||
|
||||
if (room.getSlots().getSlots() < usersSet.size() + users.size()) {
|
||||
throw new RoomAreFullException(room.getNodeId(), room.getId());
|
||||
if (room.getMaxSlots() < usersSet.size() + users.size()) {
|
||||
throw new RoomAreFullException(room.getNodeIdentifier(), room.getIdentifier());
|
||||
}
|
||||
}
|
||||
|
||||
@ -51,7 +51,7 @@ public class UserRepositoryImpl implements UserRepository {
|
||||
usersSet.addAll(users);
|
||||
roomUsers.put(path, usersSet);
|
||||
|
||||
var picker = nodeId2PickerModeCache.get(room.getNodeId());
|
||||
var picker = nodeId2PickerModeCache.get(room.getNodeIdentifier());
|
||||
if (picker instanceof LeastPickedPicker leastPickedPicker) {
|
||||
leastPickedPicker.updateUsersAmount(room, roomUsers.get(path).size());
|
||||
}
|
||||
@ -75,7 +75,7 @@ public class UserRepositoryImpl implements UserRepository {
|
||||
}
|
||||
});
|
||||
|
||||
var path = new NodeRoomPath(room.getNodeId(), room.getId());
|
||||
var path = new NodeRoomPath(room.getNodeIdentifier(), room.getIdentifier());
|
||||
var set = roomUsers.getOrDefault(path, new HashSet<>());
|
||||
set.removeAll(users);
|
||||
if (set.isEmpty()) {
|
||||
@ -84,7 +84,7 @@ public class UserRepositoryImpl implements UserRepository {
|
||||
roomUsers.put(path, set);
|
||||
}
|
||||
|
||||
var picker = nodeId2PickerModeCache.get(room.getNodeId());
|
||||
var picker = nodeId2PickerModeCache.get(room.getNodeIdentifier());
|
||||
if (picker instanceof LeastPickedPicker leastPickedPicker) {
|
||||
leastPickedPicker.updateUsersAmount(room, set.size());
|
||||
}
|
||||
@ -114,7 +114,7 @@ public class UserRepositoryImpl implements UserRepository {
|
||||
@Override
|
||||
public List<User> usersOf(Room room) {
|
||||
synchronized (usersMap) {
|
||||
return roomUsers.getOrDefault(new NodeRoomPath(room.getNodeId(), room.getId()), new HashSet<>())
|
||||
return roomUsers.getOrDefault(new NodeRoomPath(room.getNodeIdentifier(), room.getIdentifier()), new HashSet<>())
|
||||
.stream()
|
||||
.toList();
|
||||
}
|
||||
|
||||
@ -27,7 +27,7 @@ public class LeastPickedPicker implements RoomPicker {
|
||||
@Override
|
||||
public void remove(Room room) {
|
||||
synchronized (map) {
|
||||
map.removeById(room.getId());
|
||||
map.removeById(room.getIdentifier());
|
||||
}
|
||||
}
|
||||
|
||||
@ -50,7 +50,7 @@ public class LeastPickedPicker implements RoomPicker {
|
||||
|
||||
public void updateUsersAmount(Room room, int users) {
|
||||
synchronized (map) {
|
||||
map.updateItem(room.getId(), prevValue -> users);
|
||||
map.updateItem(room.getIdentifier(), prevValue -> users);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -20,7 +20,7 @@ public class RoomWrapper implements ItemWrapper<Room>, QueuedLinkedList.Item, Dy
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return room.getId();
|
||||
return room.getIdentifier();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -30,7 +30,7 @@ public class RoomWrapper implements ItemWrapper<Room>, QueuedLinkedList.Item, Dy
|
||||
|
||||
@Override
|
||||
public int maxUnits() {
|
||||
return room.getSlots().getSlots();
|
||||
return room.getMaxSlots();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@ -29,7 +29,7 @@ public class RoundRobinPicker implements RoomPicker {
|
||||
@Override
|
||||
public void remove(Room room) {
|
||||
synchronized (list) {
|
||||
list.removeById(room.getId());
|
||||
list.removeById(room.getIdentifier());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -21,14 +21,14 @@ public class SequentialFillingPicker implements RoomPicker {
|
||||
@Override
|
||||
public void add(Room room) {
|
||||
synchronized (wrappers) {
|
||||
wrappers.put(room.getId(), new RoomWrapper(room, () -> userRepository.usersOf(room).size()));
|
||||
wrappers.put(room.getIdentifier(), new RoomWrapper(room, () -> userRepository.usersOf(room).size()));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove(Room room) {
|
||||
synchronized (wrappers) {
|
||||
wrappers.remove(room.getId());
|
||||
wrappers.remove(room.getIdentifier());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -41,11 +41,11 @@ public class RoomServiceImpl implements RoomService {
|
||||
|
||||
@Override
|
||||
public void create(Room room) throws InvalidRoomIdentifierException, RoomAlreadyExistException, NotPersistedNodeException {
|
||||
namingValidator.validateRoomId(room.getNodeId(), room.getId());
|
||||
namingValidator.validateRoomId(room.getNodeIdentifier(), room.getIdentifier());
|
||||
|
||||
var node = nodeRepository.find(room.getNodeId()).orElseThrow(() -> new NodeNotFoundException(room.getNodeId()));
|
||||
var node = nodeRepository.find(room.getNodeIdentifier()).orElseThrow(() -> new NodeNotFoundException(room.getNodeIdentifier()));
|
||||
if (!node.isPersist() && room.isPersist()) {
|
||||
throw new NotPersistedNodeException(node.getIdentifier(), room.getId());
|
||||
throw new NotPersistedNodeException(node.getIdentifier(), room.getIdentifier());
|
||||
}
|
||||
|
||||
roomRepository.create(room);
|
||||
@ -84,10 +84,10 @@ public class RoomServiceImpl implements RoomService {
|
||||
var roomUsers = userRepository.usersOf(room);
|
||||
|
||||
return new PickedRoomResponse(
|
||||
room.getNodeId(),
|
||||
room.getId(),
|
||||
room.getNodeIdentifier(),
|
||||
room.getIdentifier(),
|
||||
room.getPayload(),
|
||||
room.getSlots().getSlots(),
|
||||
room.getMaxSlots(),
|
||||
roomUsers.size(),
|
||||
room.isLocked(),
|
||||
roomUsers.stream().map(User::id).collect(Collectors.toSet())
|
||||
|
||||
@ -88,7 +88,7 @@ public class FileStorageImpl implements NodeAndRoomStorage {
|
||||
@Override
|
||||
public void saveRoom(Room room) {
|
||||
if (!room.isPersist()) return;
|
||||
var roomFile = new File(path + "/rooms/" + room.getNodeId() + "." + room.getId() + ".json");
|
||||
var roomFile = new File("%s/rooms/%s.%s.json".formatted(path, room.getNodeIdentifier(), room.getIdentifier()));
|
||||
var writer = objectMapper.writer();
|
||||
|
||||
try {
|
||||
@ -101,8 +101,8 @@ public class FileStorageImpl implements NodeAndRoomStorage {
|
||||
@Override
|
||||
public void removeRoom(Room room) {
|
||||
if (!room.isPersist()) return;
|
||||
new File(path + "/rooms/" + room.getNodeId() + "." + room.getId() + ".json").delete();
|
||||
new File("%s/rooms/%s.%s.json".formatted(path, room.getNodeIdentifier(), room.getIdentifier())).delete();
|
||||
|
||||
log.info("Removed room '%s/%s' from disk storage".formatted(room.getNodeId(), room.getId()));
|
||||
log.info("Removed room '%s/%s' from disk storage".formatted(room.getNodeIdentifier(), room.getIdentifier()));
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user