LongPoll rewritten

This commit is contained in:
Michael Wain 2024-02-25 03:53:58 +03:00
parent 9b6c3d525d
commit c19bba5930
23 changed files with 548 additions and 245 deletions

View File

@ -1,23 +1,24 @@
package com.alterdekim.game.component;
import com.alterdekim.game.component.processors.ChatProcessor;
import com.alterdekim.game.component.processors.FriendProcessor;
import com.alterdekim.game.component.processors.Processor;
import com.alterdekim.game.component.processors.RoomProcessor;
import com.alterdekim.game.component.result.LongPollResult;
import com.alterdekim.game.component.result.LongPollResultSingle;
import com.alterdekim.game.component.result.LongPollResultType;
import com.alterdekim.game.dto.*;
import com.alterdekim.game.entities.Chat;
import com.alterdekim.game.entities.Room;
import com.alterdekim.game.entities.RoomPlayer;
import com.alterdekim.game.entities.User;
import com.alterdekim.game.service.*;
import com.alterdekim.game.util.Hash;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ConcurrentHashMap;
@ -33,6 +34,8 @@ public class LongPoll {
private final BlockingQueue<LongPollingSession> longPollingQueue = new ArrayBlockingQueue<>(100);
private final ConcurrentHashMap<Long, LongPollConfig> map = new ConcurrentHashMap<>();
private final List<Processor> processors = new ArrayList<>();
@Autowired
private UserServiceImpl userService;
@ -48,28 +51,32 @@ public class LongPoll {
@Autowired
private FriendServiceImpl friendService;
public LongPoll() {
processors.addAll(Arrays.asList(new ChatProcessor(this), new FriendProcessor(this), new RoomProcessor(this)));
}
@Scheduled(fixedRate = 3000)
private void longPoll() {
getLongPollingQueue().removeIf(e -> e.getDeferredResult().isSetOrExpired());
roomService.clearEmptyRooms();
getMap().keySet().forEach(k -> {
if( (System.currentTimeMillis() - getMap().get(k).getLastRequest()) >= 60000 ) getMap().remove(k);
if( (System.currentTimeMillis() - getMap().get(k).getLastRequest()) >= 55000 ) getMap().remove(k);
});
roomService.getAll().forEach(r -> {
roomPlayerService.findByRoomId(r.getId()).stream()
.filter(p -> !map.containsKey(p.getUserId()))
.forEach(p -> roomPlayerService.leaveByUserId(p.getUserId()));
});
getLongPollingQueue().forEach(longPollingSession -> {
try {
if( !map.containsKey(longPollingSession.getUserId())) map.put(longPollingSession.getUserId(), new LongPollConfig(0L,new ArrayList<>(), 0, Hash.rnd(), new ArrayList<>(), System.currentTimeMillis(), new ArrayList<>()));
LongPollConfig config = map.get(longPollingSession.getUserId());
LongPollResult result = process(longPollingSession.getUserId(), config);
if( !result.getRooms().isEmpty() )
config.setRooms(result.getRooms().stream().filter(r -> r.getAction() == RoomResultState.ADD_CHANGE).map(r -> new RoomResult(r.getId(), r.getPlayerCount(), r.getPlayers())).collect(Collectors.toList()));
if( !result.getFriends().isEmpty() )
config.setFriends_online(result.getFriends().stream().filter(r -> r.getAction() == FriendState.ADD).map(r -> new UserResult(r.getId(), r.getUsername())).collect(Collectors.toList()));
config.setSession_pass(config.getSession_pass()+1);
if( !result.getInvites().isEmpty() || !result.getFriends().isEmpty() || !result.getRooms().isEmpty() || !result.getMessages().isEmpty() || config.getSession_pass() >= iterations) {
if( !result.getResultWithType(LongPollResultType.InviteResult, InviteResult.class).isEmpty() || !result.getResultWithType(LongPollResultType.FriendResult, FriendResult.class).isEmpty() || !result.getResultWithType(LongPollResultType.RoomResult, RoomResultV2.class).isEmpty() || !result.getResultWithType(LongPollResultType.ChatResult, ChatResult.class).isEmpty() || config.getSession_pass() >= iterations) {
longPollingSession.getDeferredResult().setResult(result);
config.setSession_pass(0);
config.setInvites(new ArrayList<>());
}
map.put(longPollingSession.getUserId(), config);
} catch (Exception e) {
@ -80,156 +87,10 @@ public class LongPoll {
}
private LongPollResult process(Long userId, LongPollConfig config) {
Integer onlineCount = map.size();
List<Chat> results;
List<UserResult> users = new ArrayList<>();
List<RoomResult> clientRooms = config.getRooms();
List<RoomResultV2> roomResults = new ArrayList<>();
List<UserResult> clientFriends = config.getFriends_online();
List<FriendResult> friendsResult = new ArrayList<>();
results = chatService.getAfterLastChatId(config.getLast_chat_id());
// Chat part
if( !results.isEmpty() ) {
users = results.stream()
.map(Chat::getUserId)
.distinct()
.map(l -> userService.findById(l))
.map(u -> new UserResult(u.getId(), u.getUsername()))
.collect(Collectors.toList());
results = results.stream().peek(c -> {
String message = c.getMessage();
for (int i = 0; i < message.length(); i++) {
if (message.charAt(i) == '@') {
int u = message.substring(i).indexOf(' ') + i;
String username = message.substring(i + 1, u);
User user = userService.findByUsername(username);
if (user != null) {
Long uid = user.getId();
message = message.substring(0, i) + "<a href=\"/profile/" + uid + "\" class=\"chat-history-user\"><span class=\"_nick\">" + username + "</span></a>" + message.substring(u + 1);
} else {
message = message.substring(0, i) + username + message.substring(u + 1);
}
i = 0;
}
}
c.setMessage(message);
}).collect(Collectors.toList());
}
// Rooms part
if( !clientRooms.isEmpty() ) {
List<RoomResult> rooms = roomService.getAllActive().stream()
.map( r -> new RoomResult(r.getId(), r.getPlayerCount(), roomPlayerService.findByRoomId(r.getId()).stream()
.map(p -> userService.findById(p.getUserId()))
.map(p -> new UserResult(p.getId(), p.getUsername()))
.collect(Collectors.toList())))
.collect(Collectors.toList());
if( !isEqualListRoom(rooms, clientRooms) ) {
List<RoomResult> rr = new ArrayList<>(rooms);
List<RoomResultV2> resultV2List = new ArrayList<>();
for( RoomResult r : clientRooms ) {
if( rooms.stream().noneMatch(t -> t.getId().longValue() == r.getId().longValue()) ) {
resultV2List.add(new RoomResultV2(RoomResultState.REMOVE, r.getId(), r.getPlayerCount(), r.getPlayers()));
} else {
RoomResult r1 = rooms.stream().filter( t -> t.getId().longValue() == r.getId().longValue()).findFirst().get();
if( !isEqual(r, r1) ) {
resultV2List.add(new RoomResultV2(RoomResultState.ADD_CHANGE, r1.getId(), r1.getPlayerCount(), r1.getPlayers()));
}
rr.remove(r1);
}
}
for( RoomResult r : rr ) {
resultV2List.add(new RoomResultV2(RoomResultState.ADD_CHANGE, r.getId(), r.getPlayerCount(), r.getPlayers()));
}
roomResults = resultV2List.stream().sorted(Comparator.comparingLong(RoomResultV2::getId)).collect(Collectors.toList());
} else {
roomResults = new ArrayList<>();
}
} else {
List<Room> rooms = roomService.getAllActive();
roomResults = rooms.stream()
.map( r -> new RoomResultV2(RoomResultState.ADD_CHANGE, r.getId(), r.getPlayerCount(), roomPlayerService.findByRoomId(r.getId()).stream()
.map(p -> userService.findById(p.getUserId()))
.map(p -> new UserResult(p.getId(), p.getUsername()))
.collect(Collectors.toList())))
.collect(Collectors.toList());
}
// Friends part
if( !clientFriends.isEmpty() ) {
List<UserResult> userResults = friendService.getFriendsOfUserId(userId)
.stream()
.map(id -> userService.findById(id))
.map(u -> new UserResult(u.getId(), u.getUsername()))
.filter(f -> getMap().keySet().stream().anyMatch(l -> l.longValue() == f.getId().longValue()))
.collect(Collectors.toList());
if( !isEqualListUser(userResults, clientFriends) ) {
List<UserResult> urr = new ArrayList<>(userResults);
List<FriendResult> fr = new ArrayList<>();
for( UserResult r : clientFriends ) {
if( userResults.stream().noneMatch(t -> t.getId().longValue() == r.getId().longValue()) ) {
fr.add(new FriendResult(FriendState.REMOVE, r.getId(), r.getUsername()));
} else {
UserResult r1 = userResults.stream().filter( t -> t.getId().longValue() == r.getId().longValue()).findFirst().get();
if( !isEqualUser(r, r1) ) {
fr.add(new FriendResult(FriendState.ADD, r1.getId(), r1.getUsername()));
}
urr.remove(r1);
}
}
for( UserResult r : urr ) {
fr.add(new FriendResult(FriendState.ADD, r.getId(), r.getUsername()));
}
friendsResult = fr.stream().sorted(Comparator.comparingLong(FriendResult::getId)).collect(Collectors.toList());
} else {
friendsResult = new ArrayList<>();
}
} else {
friendsResult = friendService.getFriendsOfUserId(userId)
.stream()
.map(id -> userService.findById(id))
.filter(f -> getMap().keySet().stream().anyMatch(l -> l.longValue() == f.getId().longValue()))
.map(f -> new FriendResult(FriendState.ADD, f.getId(), f.getUsername()))
.collect(Collectors.toList());
}
return new LongPollResult(onlineCount, results, users, roomResults, friendsResult, config.getInvites().stream().map(i -> new InviteResult(i.getRoomId(), i.getUserId(), i.getUsername())).collect(Collectors.toList()));
}
private Boolean isEqual(RoomResult r1, RoomResult r2) {
return r1.getId().longValue() == r2.getId().longValue() &&
r1.getPlayerCount().intValue() == r2.getPlayerCount().intValue() &&
isEqualListUser(r1.getPlayers(), r2.getPlayers());
}
private Boolean isEqualUser(UserResult r1, UserResult r2) {
return r1.getId().longValue() == r2.getId().longValue() &&
r1.getUsername().equals(r2.getUsername());
}
private Boolean isEqualListRoom(List<RoomResult> r1, List<RoomResult> r2) {
r1 = r1.stream().sorted(Comparator.comparingLong(RoomResult::getId)).collect(Collectors.toList());
r2 = r2.stream().sorted(Comparator.comparingLong(RoomResult::getId)).collect(Collectors.toList());
if( r1.size() != r2.size() ) return false;
for( int i = 0; i < r1.size(); i++ ) {
if( !isEqual(r1.get(i), r2.get(i)) ) return false;
}
return true;
}
private Boolean isEqualListUser(List<UserResult> r1, List<UserResult> r2) {
r1 = r1.stream().sorted(Comparator.comparingLong(UserResult::getId)).collect(Collectors.toList());
r2 = r2.stream().sorted(Comparator.comparingLong(UserResult::getId)).collect(Collectors.toList());
if( r1.size() != r2.size() ) return false;
for( int i = 0; i < r1.size(); i++ ) {
if( r1.get(i).getId().longValue() != r2.get(i).getId().longValue() ||
!r1.get(i).getUsername().equals(r2.get(i).getUsername()) ) {
return false;
}
}
return true;
List<LongPollResultSingle> result = new ArrayList<>();
result.add(new LongPollResultSingle<>(LongPollResultType.OnlineUsers, Arrays.asList(map.size())));
processors.forEach(p -> result.add(p.process(config, userId)));
result.add(new LongPollResultSingle<>(LongPollResultType.InviteResult, config.getInvites().stream().map(i -> new InviteResult(i.getRoomId(), i.getUserId(), i.getUsername())).collect(Collectors.toList())));
return new LongPollResult(result);
}
}

View File

@ -1,6 +1,6 @@
package com.alterdekim.game.component;
import com.alterdekim.game.dto.LongPollResult;
import com.alterdekim.game.component.result.LongPollResult;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.web.context.request.async.DeferredResult;

View File

@ -0,0 +1,36 @@
package com.alterdekim.game.component.processors;
import com.alterdekim.game.component.LongPoll;
import com.alterdekim.game.component.LongPollConfig;
import com.alterdekim.game.component.result.LongPollResultSingle;
import com.alterdekim.game.component.result.LongPollResultType;
import com.alterdekim.game.dto.ChatResult;
import com.alterdekim.game.dto.UserResult;
import com.alterdekim.game.entities.Chat;
import com.alterdekim.game.service.ChatServiceImpl;
import com.alterdekim.game.service.UserServiceImpl;
import com.alterdekim.game.util.StringUtil;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
public class ChatProcessor extends Processor<ChatResult> {
public ChatProcessor(LongPoll longPoll) {
super(LongPollResultType.ChatResult, longPoll);
}
@Override
public LongPollResultSingle<ChatResult> process(LongPollConfig config, Long userId) {
List<Chat> results = getParent().getChatService().getAfterLastChatId(config.getLast_chat_id());
if( results.isEmpty() ) return new LongPollResultSingle<>(getType(), new ArrayList<>());
return new LongPollResultSingle<>(getType(), results.stream()
.peek(c -> c.setMessage(StringUtil.escapeTags(getParent().getUserService(), c.getMessage())))
.map(c -> new ChatResult(c, new UserResult( c.getUserId(), getParent().getUserService().findById(c.getUserId()).getUsername())))
.collect(Collectors.toList()));
}
}

View File

@ -0,0 +1,68 @@
package com.alterdekim.game.component.processors;
import com.alterdekim.game.component.LongPoll;
import com.alterdekim.game.component.LongPollConfig;
import com.alterdekim.game.component.result.LongPollResultSingle;
import com.alterdekim.game.component.result.LongPollResultType;
import com.alterdekim.game.dto.FriendResult;
import com.alterdekim.game.dto.FriendState;
import com.alterdekim.game.dto.UserResult;
import com.alterdekim.game.service.FriendServiceImpl;
import com.alterdekim.game.service.UserServiceImpl;
import com.alterdekim.game.util.ListUtil;
import lombok.Setter;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
public class FriendProcessor extends Processor<FriendResult> {
public FriendProcessor(LongPoll parent) {
super(LongPollResultType.FriendResult, parent);
}
@Override
public LongPollResultSingle<FriendResult> process(LongPollConfig config, Long userId) {
List<UserResult> clientFriends = config.getFriends_online();
if( !clientFriends.isEmpty() ) return new LongPollResultSingle<>(getType(), rearrangeFriends(clientFriends, userId));
return new LongPollResultSingle<>(getType(), friends2Users(getParent().getFriendService().getFriendsOfUserId(userId))
.stream()
.map(f -> new FriendResult(FriendState.ADD, f.getId(), f.getUsername()))
.collect(Collectors.toList()));
}
private List<FriendResult> rearrangeFriends(List<UserResult> clientFriends, Long userId) {
List<UserResult> userResults = friends2Users(getParent().getFriendService().getFriendsOfUserId(userId));
if(ListUtil.isEqualListUser(userResults, clientFriends) ) return new ArrayList<>();
List<UserResult> urr = new ArrayList<>(userResults);
List<FriendResult> fr = new ArrayList<>();
for( UserResult r : clientFriends ) {
if( userResults.stream().noneMatch(t -> t.getId().longValue() == r.getId().longValue()) ) {
fr.add(new FriendResult(FriendState.REMOVE, r.getId(), r.getUsername()));
continue;
}
UserResult r1 = userResults.stream().filter( t -> t.getId().longValue() == r.getId().longValue()).findFirst().get();
if( !r.equals(r1) ) {
fr.add(new FriendResult(FriendState.ADD, r1.getId(), r1.getUsername()));
}
urr.remove(r1);
}
urr.forEach(r -> fr.add(new FriendResult(FriendState.ADD, r.getId(), r.getUsername())));
return fr.stream().sorted(Comparator.comparingLong(FriendResult::getId)).collect(Collectors.toList());
}
private List<UserResult> friends2Users(List<Long> friendIds) {
return friendIds.stream()
.map(id -> getParent().getUserService().findById(id))
.map(u -> new UserResult(u.getId(), u.getUsername()))
.filter(f -> getParent().getMap().keySet().stream().anyMatch(l -> l.longValue() == f.getId().longValue()))
.collect(Collectors.toList());
}
}

View File

@ -0,0 +1,19 @@
package com.alterdekim.game.component.processors;
import com.alterdekim.game.component.LongPoll;
import com.alterdekim.game.component.LongPollConfig;
import com.alterdekim.game.component.result.LongPollResultSingle;
import com.alterdekim.game.component.result.LongPollResultType;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.List;
@Getter
@AllArgsConstructor
public abstract class Processor<T> {
private LongPollResultType type;
private LongPoll parent;
public abstract LongPollResultSingle<T> process(LongPollConfig config, Long userId);
}

View File

@ -0,0 +1,78 @@
package com.alterdekim.game.component.processors;
import com.alterdekim.game.component.LongPoll;
import com.alterdekim.game.component.LongPollConfig;
import com.alterdekim.game.component.result.LongPollResultSingle;
import com.alterdekim.game.component.result.LongPollResultType;
import com.alterdekim.game.dto.RoomResult;
import com.alterdekim.game.dto.RoomResultState;
import com.alterdekim.game.dto.RoomResultV2;
import com.alterdekim.game.dto.UserResult;
import com.alterdekim.game.entities.Room;
import com.alterdekim.game.service.RoomPlayerService;
import com.alterdekim.game.service.RoomPlayerServiceImpl;
import com.alterdekim.game.service.RoomServiceImpl;
import com.alterdekim.game.service.UserServiceImpl;
import com.alterdekim.game.util.ListUtil;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
public class RoomProcessor extends Processor<RoomResultV2> {
public RoomProcessor(LongPoll longPoll) {
super(LongPollResultType.RoomResult, longPoll);
}
@Override
public LongPollResultSingle<RoomResultV2> process(LongPollConfig config, Long userId) {
List<RoomResult> clientRooms = config.getRooms();
if( clientRooms.isEmpty() ) return new LongPollResultSingle<>(getType(), roomToRoomResult2AddChange(getParent().getRoomService().getAllActive()));
List<RoomResult> rooms = roomToRoomResult(getParent().getRoomService().getAllActive());
if( ListUtil.isEqualListRoom(rooms, clientRooms) ) return new LongPollResultSingle<>(getType(), new ArrayList<>());
List<RoomResult> rr = new ArrayList<>(rooms);
List<RoomResultV2> resultV2List = new ArrayList<>();
for( RoomResult r : clientRooms ) {
if( rooms.stream().noneMatch(t -> t.getId().longValue() == r.getId().longValue()) ) {
resultV2List.add(new RoomResultV2(RoomResultState.REMOVE, r.getId(), r.getPlayerCount(), r.getPlayers()));
continue;
}
RoomResult r1 = rooms.stream().filter( t -> t.getId().longValue() == r.getId().longValue()).findFirst().get();
if( !r.equals(r1) ) {
resultV2List.add(new RoomResultV2(RoomResultState.ADD_CHANGE, r1.getId(), r1.getPlayerCount(), r1.getPlayers()));
}
rr.remove(r1);
}
rr.forEach(r -> resultV2List.add(new RoomResultV2(RoomResultState.ADD_CHANGE, r.getId(), r.getPlayerCount(), r.getPlayers())));
return new LongPollResultSingle<>(getType(), resultV2List.stream().sorted(Comparator.comparingLong(RoomResultV2::getId)).collect(Collectors.toList()));
}
private List<RoomResult> roomToRoomResult(List<Room> rooms) {
return rooms.stream()
.map( r -> new RoomResult(r.getId(), r.getPlayerCount(), getParent().getRoomPlayerService().findByRoomId(r.getId()).stream()
.map(p -> getParent().getUserService().findById(p.getUserId()))
.map(p -> new UserResult(p.getId(), p.getUsername()))
.collect(Collectors.toList())))
.collect(Collectors.toList());
}
private List<RoomResultV2> roomToRoomResult2AddChange(List<Room> rooms) {
return rooms.stream()
.map( r -> new RoomResultV2(RoomResultState.ADD_CHANGE, r.getId(), r.getPlayerCount(), getParent().getRoomPlayerService().findByRoomId(r.getId()).stream()
.map(p -> getParent().getUserService().findById(p.getUserId()))
.map(p -> new UserResult(p.getId(), p.getUsername()))
.collect(Collectors.toList())))
.collect(Collectors.toList());
}
}

View File

@ -0,0 +1,18 @@
package com.alterdekim.game.component.result;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import java.util.List;
@AllArgsConstructor
@NoArgsConstructor
@Getter
public class LongPollResult {
private List<LongPollResultSingle> result;
public <T> List<T> getResultWithType(LongPollResultType type, T elementClass) {
return (List<T>) result.stream().filter(r -> r.getType() == type).findFirst().get().getArray();
}
}

View File

@ -0,0 +1,13 @@
package com.alterdekim.game.component.result;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.List;
@AllArgsConstructor
@Getter
public class LongPollResultSingle<T> {
private LongPollResultType type;
private List<T> array;
}

View File

@ -0,0 +1,9 @@
package com.alterdekim.game.component.result;
public enum LongPollResultType {
OnlineUsers,
ChatResult,
RoomResult,
FriendResult,
InviteResult
}

View File

@ -4,12 +4,16 @@ package com.alterdekim.game.controller;
import com.alterdekim.game.component.LongPoll;
import com.alterdekim.game.component.LongPollConfig;
import com.alterdekim.game.component.LongPollingSession;
import com.alterdekim.game.component.result.LongPollResult;
import com.alterdekim.game.dto.*;
import com.alterdekim.game.entities.Chat;
import com.alterdekim.game.entities.Room;
import com.alterdekim.game.entities.User;
import com.alterdekim.game.service.*;
import com.alterdekim.game.util.Hash;
import com.alterdekim.game.util.StringUtil;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
@ -55,34 +59,12 @@ public class APIController {
private FriendServiceImpl friendService;
@GetMapping("/api/v1/chat/history/{count}/")
public ResponseEntity<ChatResult> chatList(@PathVariable Integer count ) {
public ResponseEntity<List<ChatResult>> chatList(@PathVariable Integer count ) {
List<Chat> results = chatService.getLastChats(count);
results = results.stream().map( c -> {
String message = c.getMessage();
for( int i = 0; i < message.length(); i++ ) {
if( message.charAt(i) == '@' ) {
int u = message.substring(i).indexOf(' ') + i;
String username = message.substring(i+1, u);
User user = userService.findByUsername(username);
if( user != null ) {
Long uid = user.getId();
message = message.substring(0, i) + "<a href=\"/profile/" + uid + "\" class=\"chat-history-user\"><span class=\"_nick\">" + username + "</span></a>" + message.substring(u + 1);
} else {
message = message.substring(0, i) + username + message.substring(u + 1);
}
i = 0;
}
}
c.setMessage(message);
return c;
}).collect(Collectors.toList());
List<UserResult> users = results.stream()
.map(Chat::getUserId)
.distinct()
.map( l -> userService.findById(l) )
.map( u -> new UserResult(u.getId(), u.getUsername()) )
.collect(Collectors.toList());
return ResponseEntity.ok(new ChatResult(results, users));
return ResponseEntity.ok(results.stream()
.peek(c -> c.setMessage(StringUtil.escapeTags(userService, c.getMessage())))
.map(c -> new ChatResult(c, new UserResult( c.getUserId(), userService.findById(c.getUserId()).getUsername())))
.collect(Collectors.toList()));
}
@GetMapping("/api/v1/market/banners/{lang}/")
@ -126,7 +108,7 @@ public class APIController {
@PostMapping("/api/v1/rooms/create/")
public ResponseEntity<String> createRoom( @RequestParam("is_private") Boolean is_private,
@RequestParam("players_count") Integer players_count) {
@RequestParam("players_count") Integer players_count ) {
if( !(players_count >= 2 && players_count <= 5) ) return ResponseEntity.badRequest().build();
Long id = roomService.createRoom(new Room(players_count, is_private, "0"));
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
@ -136,6 +118,16 @@ public class APIController {
return ResponseEntity.accepted().build();
}
@PostMapping("/api/v1/rooms/join/")
public ResponseEntity<String> joinRoom( @RequestParam("room_id") Long roomId ) {
if( !roomService.findById(roomId).isPresent() ) return ResponseEntity.badRequest().build();
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
Long userId = userService.findByUsername(((org.springframework.security.core.userdetails.User) auth.getPrincipal()).getUsername()).getId();
roomPlayerService.leaveByUserId(userId);
roomPlayerService.joinRoom(roomId, userId);
return ResponseEntity.accepted().build();
}
@PostMapping("/api/v1/rooms/invite/")
public ResponseEntity<String> inviteToRoom( @RequestParam("friend_id") Long friend_id ) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
@ -160,7 +152,9 @@ public class APIController {
public DeferredResult<LongPollResult> getNotify(@RequestParam("last_chat_id") Long last_chat_id,
@RequestParam("accessToken") String accessToken,
@RequestParam("uid") Long userId,
@RequestParam("poll_token") String poll_token) {
@RequestParam("poll_token") String poll_token,
@RequestParam("rids") String rooms_str,
@RequestParam("fids") String friends_str) {
try {
User u = userService.findById(userId);
if (u == null) return null;
@ -171,18 +165,28 @@ public class APIController {
}
if( poll_token.length() != 32 ) return null;
final DeferredResult<LongPollResult> deferredResult = new DeferredResult<>();
List<RoomResult> rooms = new ArrayList<>();
List<UserResult> friends = new ArrayList<>();
try {
rooms = new ObjectMapper().readValue(rooms_str, new TypeReference<List<RoomResult>>() {});
friends = new ObjectMapper().readValue(friends_str, new TypeReference<List<UserResult>>() {});
} catch (Exception e) {
log.error(e.getMessage(), e);
}
if( longPoll.getMap().containsKey(userId) ){
LongPollConfig c = longPoll.getMap().get(userId);
if( !c.getPoll_token().equals(poll_token) ) {
c = new LongPollConfig(last_chat_id, new ArrayList<>(), 0, poll_token, new ArrayList<>(), System.currentTimeMillis(), new ArrayList<>());
c = new LongPollConfig(last_chat_id, rooms, 0, poll_token, friends, System.currentTimeMillis(), new ArrayList<>());
longPoll.getLongPollingQueue().removeIf(q -> q.getUserId().longValue() == userId.longValue());
}
c.setRooms(rooms);
c.setFriends_online(friends);
c.setLast_chat_id(last_chat_id);
c.setSession_pass(0);
c.setLastRequest(System.currentTimeMillis());
longPoll.getMap().put(userId, c);
} else {
longPoll.getMap().put(userId, new LongPollConfig(last_chat_id, new ArrayList<>(), 0, poll_token, new ArrayList<>(), System.currentTimeMillis(), new ArrayList<>()));
longPoll.getMap().put(userId, new LongPollConfig(last_chat_id, rooms, 0, poll_token, friends, System.currentTimeMillis(), new ArrayList<>()));
}
longPoll.getLongPollingQueue().add(new LongPollingSession(userId, deferredResult));
return deferredResult;

View File

@ -0,0 +1,36 @@
package com.alterdekim.game.controller;
import com.alterdekim.game.entities.Image;
import com.alterdekim.game.repository.ImageRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.server.ResponseStatusException;
@RestController
class ImageController {
@Autowired
ImageRepository imageRepository;
@PostMapping(value = "/image/upload/")
private Long uploadImage(@RequestParam MultipartFile multipartImage) throws Exception {
Image dbImage = new Image();
dbImage.setContent(multipartImage.getBytes());
return imageRepository.save(dbImage)
.getId();
}
@GetMapping(value = "/image/store/{imageId}", produces = MediaType.IMAGE_JPEG_VALUE)
Resource downloadImage(@PathVariable Long imageId) {
byte[] image = imageRepository.findById(imageId)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND))
.getContent();
return new ByteArrayResource(image);
}
}

View File

@ -11,6 +11,6 @@ import java.util.List;
@NoArgsConstructor
@Getter
public class ChatResult {
private List<Chat> messages;
private List<UserResult> users;
private Chat message;
private UserResult user;
}

View File

@ -1,20 +0,0 @@
package com.alterdekim.game.dto;
import com.alterdekim.game.entities.Chat;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import java.util.List;
@AllArgsConstructor
@NoArgsConstructor
@Getter
public class LongPollResult {
private Integer onlineCount;
private List<Chat> messages;
private List<UserResult> users;
private List<RoomResultV2> rooms;
private List<FriendResult> friends;
private List<InviteResult> invites;
}

View File

@ -3,6 +3,7 @@ package com.alterdekim.game.dto;
import com.alterdekim.game.entities.Room;
import com.alterdekim.game.entities.RoomPlayer;
import com.alterdekim.game.entities.User;
import com.alterdekim.game.util.ListUtil;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
@ -16,4 +17,10 @@ public class RoomResult {
private Long id;
private Integer playerCount;
private List<UserResult> players;
public boolean equals(RoomResult r2) {
return this.getId().longValue() == r2.getId().longValue() &&
this.getPlayerCount().intValue() == r2.getPlayerCount().intValue() &&
ListUtil.isEqualListUser(this.getPlayers(), r2.getPlayers());
}
}

View File

@ -10,4 +10,9 @@ import lombok.NoArgsConstructor;
public class UserResult {
private Long id;
private String username;
public boolean equals(UserResult r2) {
return this.getId().longValue() == r2.getId().longValue() &&
this.getUsername().equals(r2.getUsername());
}
}

View File

@ -0,0 +1,25 @@
package com.alterdekim.game.entities;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.Lob;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
@Entity
public class Image {
@Id
@GeneratedValue
Long id;
@Lob
byte[] content;
}

View File

@ -28,6 +28,9 @@ public class User {
@Column(nullable=false)
private String password;
@Column(nullable = false)
private Long avatarId;
@ManyToMany(fetch = FetchType.EAGER, cascade=CascadeType.ALL)
@JoinTable(
name="users_roles",

View File

@ -0,0 +1,8 @@
package com.alterdekim.game.repository;
import com.alterdekim.game.entities.Image;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface ImageRepository extends JpaRepository<Image, Long> {}

View File

@ -34,6 +34,7 @@ public class SpringSecurity {
.authorizeHttpRequests((authorize) ->
authorize
.requestMatchers("/async/**").permitAll()
.requestMatchers("/image/**").hasAnyAuthority("ROLE_ADMIN")
.requestMatchers("/game").hasAnyAuthority("ROLE_ADMIN")
.requestMatchers("/games").hasAnyAuthority("ROLE_ADMIN")
.requestMatchers("/profile/**").hasAnyAuthority("ROLE_ADMIN")

View File

@ -0,0 +1,33 @@
package com.alterdekim.game.util;
import com.alterdekim.game.dto.RoomResult;
import com.alterdekim.game.dto.UserResult;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
public class ListUtil {
public static Boolean isEqualListRoom(List<RoomResult> r1, List<RoomResult> r2) {
r1 = r1.stream().sorted(Comparator.comparingLong(RoomResult::getId)).collect(Collectors.toList());
r2 = r2.stream().sorted(Comparator.comparingLong(RoomResult::getId)).collect(Collectors.toList());
if( r1.size() != r2.size() ) return false;
for( int i = 0; i < r1.size(); i++ ) {
if( !r1.get(i).equals(r2.get(i)) ) return false;
}
return true;
}
public static Boolean isEqualListUser(List<UserResult> r1, List<UserResult> r2) {
r1 = r1.stream().sorted(Comparator.comparingLong(UserResult::getId)).collect(Collectors.toList());
r2 = r2.stream().sorted(Comparator.comparingLong(UserResult::getId)).collect(Collectors.toList());
if( r1.size() != r2.size() ) return false;
for( int i = 0; i < r1.size(); i++ ) {
if( r1.get(i).getId().longValue() != r2.get(i).getId().longValue() ||
!r1.get(i).getUsername().equals(r2.get(i).getUsername()) ) {
return false;
}
}
return true;
}
}

View File

@ -0,0 +1,24 @@
package com.alterdekim.game.util;
import com.alterdekim.game.entities.User;
import com.alterdekim.game.service.UserServiceImpl;
public class StringUtil {
public static String escapeTags(UserServiceImpl userService, String message) {
for (int i = 0; i < message.length(); i++) {
if (message.charAt(i) == '@') {
int u = message.substring(i).indexOf(' ') + i;
String username = message.substring(i + 1, u);
User user = userService.findByUsername(username);
if (user != null) {
Long uid = user.getId();
message = message.substring(0, i) + "<a href=\"/profile/" + uid + "\" class=\"chat-history-user\"><span class=\"_nick\">" + username + "</span></a>" + message.substring(u + 1);
} else {
message = message.substring(0, i) + username + message.substring(u + 1);
}
i = 0;
}
}
return message;
}
}

View File

@ -1,5 +1,9 @@
var isPollingActive = true;
var last_chat_id = 0;
var rids = [];
var fids = [];
function createRoom() {
let isprivate = $("#flexSwitchCheckDefault").is(':checked');
let playerscnt = $("#players-count-range").val();
@ -21,27 +25,74 @@ function sendInviteMessage(uid) {
method: "POST",
data: {
friend_id: uid
},
statusCode: {
400: function() {
$(".toast-container").append('<div class="toast" role="alert" aria-live="assertive" aria-atomic="true"><div class="toast-header"><img src="/static/images/favicon.ico" width="16" height="16" class="rounded me-2" alt="Nosedive"><strong class="me-auto">Nosedive</strong><small class="text-body-secondary">just now</small><button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button></div><div class="toast-body">Can\'t send invite.</div></div>');
bootstrap.Toast.getOrCreateInstance($(".toast").last()).show();
},
200: function() {
$(".toast-container").append('<div class="toast" role="alert" aria-live="assertive" aria-atomic="true"><div class="toast-header"><img src="/static/images/favicon.ico" width="16" height="16" class="rounded me-2" alt="Nosedive"><strong class="me-auto">Nosedive</strong><small class="text-body-secondary">just now</small><button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button></div><div class="toast-body">Successfully sent invite.</div></div>');
bootstrap.Toast.getOrCreateInstance($(".toast").last()).show();
}
}
}).done(function(data) {
console.log(data);
});
}
function joinRoom(obj) {
let roomId = $(obj).attr("data-room-id");
$.ajax({
url: "/api/v1/rooms/join/",
data: {
room_id: roomId
},
method: "POST"
});
}
function takeInviteMessage(obj) {
// attr data-roomId
}
function successPolling(data) {
console.log(data);
let onlineCount = data.onlineCount;
data = data.result;
let onlineCount = 0;
let messages = [];
let friends = [];
let rooms = [];
let invites = [];
for(let i = 0; i < data.length; i++ ) {
let res = data[i];
switch(res.type) {
case "OnlineUsers":
onlineCount = res.array[0];
break;
case "ChatResult":
messages = res.array;
break;
case "FriendResult":
friends = res.array;
break;
case "RoomResult":
rooms = res.array;
break;
case "InviteResult":
invites = res.array;
break;
}
}
$(".chat-title").find("span").html(onlineCount + " online");
let messages = data.messages;
let users = data.users;
let rooms = data.rooms;
let friends = data.friends;
if( messages.length > 0 ) {
last_chat_id = messages[0].id;
}
for( let i = 0; i < messages.length; i++ ) {
let obj = messages[i];
let obj = messages[i].message;
let time = parseTime(obj.createdAt);
let username = findUser(users, obj.userId).username;
let username = messages[i].user.username;
let userid = obj.userId;
let msgtext = obj.message;
let html = '<div class="chat-history-one"><div class="chat-history-info"><span class="time">'+time+'</span><ion-icon name="arrow-undo-outline" class="reply-button" data-userid="'+userid+'" data-username="'+username+'" onClick="replyButtonClicked(this)"></ion-icon></div><span><span class="chat-history-text chat-history-content-message"><span class="formatter"><a href="/profile/'+userid+'" class="chat-history-user"><span class="_nick">'+username+'</span></a><ion-icon name="remove-outline"></ion-icon><span>'+msgtext+'</span></span></span></span></div>';
@ -50,13 +101,20 @@ function successPolling(data) {
for( let i = 0; i < rooms.length; i++ ) {
let room = rooms[i];
if( room.action == 'ADD_CHANGE' ) {
for( let u = 0; u < rids.length; u++ ) {
if( rids[u].id == room.id ) {
rids.splice(u, 1);
break;
}
}
rids.push({id: room.id, playerCount: room.playerCount, players: room.players});
let room_p_html = '';
for( let u = 0; u < room.players.length; u++ ) {
let room_player = room.players[u];
room_p_html += '<div class="games-room-one-body-members-one"><div class="games-room-one-body-members-one-avatar" style="background-image: url(&quot;https://i.dogecdn.wtf/7lurfckMFrYXm4gf&quot;);"><a href="/profile/'+room_player.id+'"></a><div class="_online"></div></div><div class="games-room-one-body-members-one-nick"><a href="/profile/'+room_player.id+'">'+room_player.username+'</a></div></div>';
}
for( let u = 0; u < (room.playerCount - room.players.length); u++ ) {
room_p_html += '<div class="games-room-one-body-members-one _slot_join"><div class="games-room-one-body-members-one-avatar"><ion-icon name="add-outline" style="color: #656d78;"></ion-icon></div><div class="games-room-one-body-members-one-nick"><span>Join</span></div></div>';
room_p_html += '<div data-room-id="'+room.id+'" onclick="joinRoom(this)" class="games-room-one-body-members-one _slot_join"><div class="games-room-one-body-members-one-avatar"><ion-icon name="add-outline" style="color: #656d78;"></ion-icon></div><div class="games-room-one-body-members-one-nick"><span>Join</span></div></div>';
}
let room_html = '<div class="games-room-one" data-room-id="'+room.id+'"><div class="games-room-one-body"><div class="games-room-one-body-head"><div class="games-room-one-body-head-info"><div class="_type"><div>Game</div></div></div><div class="games-room-one-body-head-actions"></div></div><div class="games-room-one-body-members">'+room_p_html+'</div></div></div>';
let has_element = false;
@ -70,6 +128,12 @@ function successPolling(data) {
$(".rooms-list").append(room_html);
}
} else if( room.action == 'REMOVE' ) {
for( let u = 0; u < rids.length; u++ ) {
if( rids[u].id == room.id ) {
rids.splice(u, 1);
break;
}
}
$(".games-room-one").each(function() {
if( $(this).attr("data-room-id") == room.id ) {
$(this).remove();
@ -80,9 +144,22 @@ function successPolling(data) {
for( let i = 0; i < friends.length; i++ ) {
let friend = friends[i];
if( friend.action == 'ADD' ) {
for( let u = 0; u < fids.length; u++ ) {
if( fids[u].id == friend.id ) {
fids.splice(u, 1);
break;
}
}
fids.push({id: friend.id, username: friend.username});
let fr_html = '<div class="friend-one" data-friend-id="'+friend.id+'"><a href="/profile/'+friend.id+'" class="navbar-btn"><img class="navbar-profile-img" src="https://avatars.githubusercontent.com/u/102559365?v=4"></a><span>'+friend.username+'</span><ion-icon onClick="sendInviteMessage('+friend.id+')" name="person-add-outline" role="img" class="md hydrated"></ion-icon></div>';
$(".friends-online-list").append(fr_html);
} else if( friend.action == 'REMOVE' ) {
for( let u = 0; u < fids.length; u++ ) {
if( fids[u].id == friend.id ) {
fids.splice(u, 1);
break;
}
}
$(".friend-one").each(function() {
if( $(this).attr("data-friend-id") == friend.id ) {
$(this).remove();
@ -91,6 +168,12 @@ function successPolling(data) {
}
}
for( let i = 0; i < invites.length; i++ ) {
let _i = invites[i];
$(".toast-container").append('<div class="toast" role="alert" aria-live="assertive" aria-atomic="true"><div class="toast-header"><img src="/static/images/favicon.ico" width="16" height="16" class="rounded me-2" alt="Nosedive"><strong class="me-auto">Nosedive</strong><small class="text-body-secondary">just now</small><button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button></div><div class="toast-body">'+_i.username+' invites you into Nosedive room.<div class="mt-2 pt-2 border-top"><button type="button" class="btn btn-primary btn-sm" data-roomId="'+_i.roomId+'" onclick="takeInviteMessage(this)">Take invite</button><button style="margin-left: 5px;" type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="toast">Close</button></div></div></div>');
bootstrap.Toast.getOrCreateInstance($(".toast").last()).show();
}
if( friends.length > 0 ) {
$(".friends-online-list").css("display", "");
$("#friends_list").find(".block-content").css("display", "none");
@ -118,6 +201,8 @@ function pollServer() {
last_chat_id: last_chat_id,
accessToken: accessToken,
poll_token: poll_token,
rids: JSON.stringify(rids),
fids: JSON.stringify(fids),
uid: uid
},
method: "POST"
@ -141,16 +226,6 @@ $.fn.pressEnter = function(fn) {
});
};
var last_chat_id = 0;
function findUser(users, id) {
for( let i = 0; i < users.length; i++ ) {
if( users[i].id == id ) {
return users[i];
}
}
}
function parseTime(unix_timestamp) {
var date = new Date(unix_timestamp * 1000);
var hours = "0" + date.getHours();
@ -188,15 +263,14 @@ $(document).ready(function() {
method: "GET"
}).done(function(data) {
console.log(data);
let messages = data.messages;
let users = data.users;
let messages = data;
if( messages.length > 0 ) {
last_chat_id = messages[0].id;
last_chat_id = messages[0].message.id;
}
for( let i = messages.length-1; i >= 0; i-- ) {
let obj = messages[i];
let obj = messages[i].message;
let time = parseTime(obj.createdAt);
let username = findUser(users, obj.userId).username;
let username = messages[i].user.username;
let userid = obj.userId;
let msgtext = obj.message;
let html = '<div class="chat-history-one"><div class="chat-history-info"><span class="time">'+time+'</span><ion-icon name="arrow-undo-outline" class="reply-button" data-userid="'+userid+'" data-username="'+username+'" onClick="replyButtonClicked(this)"></ion-icon></div><span><span class="chat-history-text chat-history-content-message"><span class="formatter"><a href="/profile/'+userid+'" class="chat-history-user"><span class="_nick">'+username+'</span></a><ion-icon name="remove-outline"></ion-icon><span>'+msgtext+'</span></span></span></span></div>';

View File

@ -37,6 +37,7 @@
</div>
</div>
<div class="container">
<div class="toast-container position-fixed bottom-0 end-0 p-3"></div>
<div class="grid mt-5">
<div class="g-col-4">
<div class="block" id="missions_list">