diff --git a/src/main/java/com/example/tampopotest/config/JerseyConfig.java b/src/main/java/com/example/tampopotest/config/JerseyConfig.java new file mode 100644 index 0000000..f7d9bf8 --- /dev/null +++ b/src/main/java/com/example/tampopotest/config/JerseyConfig.java @@ -0,0 +1,20 @@ +package com.example.tampopotest.config; + +import com.example.tampopotest.web.*; +import org.glassfish.jersey.server.ResourceConfig; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class JerseyConfig extends ResourceConfig { + public JerseyConfig() { + // Register all resource classes here + register(UsersResource.class); + register(ActivitiesResource.class); + register(NotificationsResource.class); + register(UserFriendsResource.class); + register(FriendsResource.class); + register(FriendRequestsResource.class); + register(ChatRequestsResource.class); + register(ChatRoomsResource.class); + } +} diff --git a/src/main/java/com/example/tampopotest/service/InMemoryStore.java b/src/main/java/com/example/tampopotest/service/InMemoryStore.java new file mode 100644 index 0000000..9e47596 --- /dev/null +++ b/src/main/java/com/example/tampopotest/service/InMemoryStore.java @@ -0,0 +1,315 @@ +package com.example.tampopotest.service; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * 超簡易なインメモリデータストア。 + * 本実装はデモ目的であり、永続化・排他・セキュリティ等は考慮していません。 + */ +public class InMemoryStore { + private static final InMemoryStore INSTANCE = new InMemoryStore(); + + public static InMemoryStore get() {return INSTANCE;} + + private InMemoryStore() {} + + public static class User { + public final String userId; + public String password; + public String name = ""; + public String email = ""; + public String iconUrl = "https://example.com/images/default.png"; + public String iconHash = ""; // icon更新時に返す値の例 + public String token; // 簡易トークン + + public User(String userId, String password) { + this.userId = userId; + this.password = password; + } + } + + public static class Activity { + public final String id; + public final String userId; + public String text; + public String updatedTime; // 仕様例のフォーマットに合わせて文字列で格納 + + public Activity(String id, String userId, String text) { + this.id = id; + this.userId = userId; + this.text = text; + this.updatedTime = nowString(); + } + } + + public static class FriendPair { + public final int pairId; + public String user0Id; + public String user1Id; + + public FriendPair(int pairId, String user0Id, String user1Id) { + this.pairId = pairId; + this.user0Id = user0Id; + this.user1Id = user1Id; + } + } + + public static class FriendRequest { + public final int id; + public final String senderId; + public final String receiverId; + + public FriendRequest(int id, String senderId, String receiverId) { + this.id = id; + this.senderId = senderId; + this.receiverId = receiverId; + } + } + + public static class ChatRequest { + public final int id; + public final String senderId; + public final String receiverId; + + public ChatRequest(int id, String senderId, String receiverId) { + this.id = id; + this.senderId = senderId; + this.receiverId = receiverId; + } + } + + public static class ChatRoom { + public final int roomId; + public final Set members = new HashSet<>(); + // 各ユーザーの最新メッセージ + public final Map latestMessageByUser = new HashMap<>(); + + public ChatRoom(int roomId) {this.roomId = roomId;} + } + + private final Map users = new ConcurrentHashMap<>(); + private final Map> activitiesByUser = new ConcurrentHashMap<>(); + private final Map friendPairs = new ConcurrentHashMap<>(); + private final Map friendRequests = new ConcurrentHashMap<>(); + private final Map chatRequests = new ConcurrentHashMap<>(); + private final Map chatRooms = new ConcurrentHashMap<>(); + + private final AtomicInteger friendRequestSeq = new AtomicInteger(1); + private final AtomicInteger friendPairSeq = new AtomicInteger(1); + private final AtomicInteger chatRequestSeq = new AtomicInteger(1); + private final AtomicInteger chatRoomSeq = new AtomicInteger(1); + + public static String nowString() { + return LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm")); + } + + public static String newToken() { + return UUID.randomUUID().toString().replace("-", ""); + } + + public String findUserIdByToken(String token) { + if (token == null) return null; + for (User u : users.values()) { + if (token.equals(u.token)) return u.userId; + } + return null; + } + + // Users + public synchronized User createUser(String userId, String password) { + if (users.containsKey(userId)) return null; + User u = new User(userId, password); + u.token = newToken(); + users.put(userId, u); + return u; + } + + public User getUser(String userId) {return users.get(userId);} + + public List listUserIds() {return new ArrayList<>(users.keySet());} + + public synchronized boolean deleteUser(String userId) { + User u = users.remove(userId); + activitiesByUser.remove(userId); + // フレンドペアからも除去 + friendPairs.values().removeIf(fp -> fp.user0Id.equals(userId) || fp.user1Id.equals(userId)); + // チャットルームからも除去 + chatRooms.values().forEach(r -> r.members.remove(userId)); + return u != null; + } + + public boolean verifyToken(String userId, String token) { + User u = users.get(userId); + return u != null && Objects.equals(u.token, token); + } + + public String login(String userId, String password) { + User u = users.get(userId); + if (u == null) return null; + if (!Objects.equals(u.password, password)) return ""; // 空文字でパスワード誤りを表現 + u.token = newToken(); + return u.token; + } + + // Activities + public synchronized String addActivity(String userId, String text) { + String id = UUID.randomUUID().toString().substring(0, 6); + Activity a = new Activity(id, userId, text); + activitiesByUser.computeIfAbsent(userId, k -> new ArrayList<>()).add(a); + return id; + } + + public List listActivities(String userId) { + return new ArrayList<>(activitiesByUser.getOrDefault(userId, Collections.emptyList())); + } + + public Activity getActivity(String userId, String activityId) { + return activitiesByUser.getOrDefault(userId, Collections.emptyList()) + .stream().filter(a -> a.id.equals(activityId)).findFirst().orElse(null); + } + + public boolean deleteActivity(String userId, String activityId) { + List list = activitiesByUser.get(userId); + if (list == null) return false; + return list.removeIf(a -> a.id.equals(activityId)); + } + + public String getLatestActivityTime(String userId) { + return activitiesByUser.getOrDefault(userId, Collections.emptyList()) + .stream().max(Comparator.comparing(a -> a.updatedTime)) + .map(a -> a.updatedTime).orElse(null); + } + + // Friend Requests + public int createFriendRequest(String senderId, String receiverId) { + int id = friendRequestSeq.getAndIncrement(); + friendRequests.put(id, new FriendRequest(id, senderId, receiverId)); + return id; + } + + public List listFriendRequestsFor(String userId) { + List list = new ArrayList<>(); + for (FriendRequest fr : friendRequests.values()) { + if (fr.senderId.equals(userId) || fr.receiverId.equals(userId)) list.add(fr); + } + return list; + } + + public boolean deleteFriendRequest(int id) { + return friendRequests.remove(id) != null; + } + + public boolean hasFriendRequest(int id) {return friendRequests.containsKey(id);} + + public FriendRequest getFriendRequest(int id) {return friendRequests.get(id);} + + // Friends + public int createFriendPair(String user0Id, String user1Id) { + int id = friendPairSeq.getAndIncrement(); + friendPairs.put(id, new FriendPair(id, user0Id, user1Id)); + return id; + } + + public boolean existsFriendPair(String a, String b) { + for (FriendPair p : friendPairs.values()) { + if ((p.user0Id.equals(a) && p.user1Id.equals(b)) || (p.user0Id.equals(b) && p.user1Id.equals(a))) { + return true; + } + } + return false; + } + + public FriendPair getFriendPair(int pairId) {return friendPairs.get(pairId);} + + public boolean deleteFriendPair(int pairId) {return friendPairs.remove(pairId) != null;} + + public Map> listFriendPairsOf(String userId) { + Map> res = new LinkedHashMap<>(); + for (FriendPair p : friendPairs.values()) { + if (p.user0Id.equals(userId) || p.user1Id.equals(userId)) { + Map map = new LinkedHashMap<>(); + map.put("user0-id", p.user0Id); + map.put("user1-id", p.user1Id); + res.put(p.pairId, map); + } + } + return res; + } + + public String friendOpponent(String userId, int pairId) { + FriendPair p = friendPairs.get(pairId); + if (p == null) return null; + if (Objects.equals(p.user0Id, userId)) return p.user1Id; + if (Objects.equals(p.user1Id, userId)) return p.user0Id; + return null; + } + + public List listFriends(String userId) { + List res = new ArrayList<>(); + for (FriendPair p : friendPairs.values()) { + if (p.user0Id.equals(userId)) res.add(p.user1Id); + else if (p.user1Id.equals(userId)) res.add(p.user0Id); + } + return res; + } + + // Chat Requests + public int createChatRequest(String senderId, String receiverId) { + int id = chatRequestSeq.getAndIncrement(); + chatRequests.put(id, new ChatRequest(id, senderId, receiverId)); + return id; + } + + public boolean deleteChatRequest(int id) {return chatRequests.remove(id) != null;} + + public ChatRequest getChatRequest(int id) {return chatRequests.get(id);} + + // Chat Rooms + public int createChatRoom(String user0Id, String user1Id) { + int id = chatRoomSeq.getAndIncrement(); + ChatRoom room = new ChatRoom(id); + room.members.add(user0Id); + room.members.add(user1Id); + chatRooms.put(id, room); + return id; + } + + public ChatRoom getChatRoom(int roomId) {return chatRooms.get(roomId);} + + public Integer findRoomIdByMember(String userId) { + for (ChatRoom room : chatRooms.values()) { + if (room.members.contains(userId)) return room.roomId; + } + return null; + } + + public boolean addMemberToRoom(int roomId, String userId) { + ChatRoom room = chatRooms.get(roomId); + if (room == null) return false; + room.members.add(userId); + return true; + } + + public boolean removeMemberFromRoom(int roomId, String userId) { + ChatRoom room = chatRooms.get(roomId); + if (room == null) return false; + return room.members.remove(userId); + } + + public void updateMessage(int roomId, String userId, String message) { + ChatRoom room = chatRooms.get(roomId); + if (room != null) { + room.latestMessageByUser.put(userId, message); + } + } + + public String getMessageFrom(int roomId, String senderId) { + ChatRoom room = chatRooms.get(roomId); + if (room == null) return null; + return room.latestMessageByUser.getOrDefault(senderId, ""); + } +} diff --git a/src/main/java/com/example/tampopotest/web/ActivitiesResource.java b/src/main/java/com/example/tampopotest/web/ActivitiesResource.java new file mode 100644 index 0000000..9d4c562 --- /dev/null +++ b/src/main/java/com/example/tampopotest/web/ActivitiesResource.java @@ -0,0 +1,105 @@ +package com.example.tampopotest.web; + +import com.example.tampopotest.service.InMemoryStore; +import com.example.tampopotest.service.InMemoryStore.Activity; +import com.example.tampopotest.service.InMemoryStore.User; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import java.util.*; + +@Path("/users/{user-id}/activities") +@Produces(MediaType.APPLICATION_JSON) +public class ActivitiesResource { + private final InMemoryStore store = InMemoryStore.get(); + + @GET + public Response list(@PathParam("user-id") String userId, + @QueryParam("filter") String filter) { + User u = store.getUser(userId); + if (u == null) return Response.status(Response.Status.NOT_FOUND).build(); + List list = store.listActivities(userId); + Map map = new LinkedHashMap<>(); + if ("LATEST".equalsIgnoreCase(filter)) { + list.stream().max(Comparator.comparing(a -> a.updatedTime)).ifPresent(a -> { + Map m = new LinkedHashMap<>(); + m.put("text", a.text); + m.put("updated-time", a.updatedTime); + map.put(a.id, m); + }); + } else { + for (Activity a : list) { + Map m = new LinkedHashMap<>(); + m.put("text", a.text); + m.put("updated-time", a.updatedTime); + map.put(a.id, m); + } + } + return Response.ok(map).build(); + } + + @POST + @Consumes(MediaType.APPLICATION_FORM_URLENCODED) + public Response create(@PathParam("user-id") String userId, + @FormParam("token") String token, + @FormParam("new-activity") String text) { + if (text == null || token == null) return Response.status(Response.Status.BAD_REQUEST).build(); + User u = store.getUser(userId); + if (u == null) return Response.status(Response.Status.NOT_FOUND).build(); + if (!store.verifyToken(userId, token)) return Response.status(403).build(); + String id = store.addActivity(userId, text); + Map res = Map.of("activity-id", id); + return Response.ok(res).build(); + } + + @Path("/{activity-id}") + @GET + public Response get(@PathParam("user-id") String userId, + @PathParam("activity-id") String activityId) { + Activity a = store.getActivity(userId, activityId); + if (a == null) return Response.status(Response.Status.NOT_FOUND).build(); + Map res = new LinkedHashMap<>(); + res.put("activity-id", a.id); + res.put("text", a.text); + res.put("updated-time", a.updatedTime); + return Response.ok(res).build(); + } + + @Path("/{activity-id}") + @DELETE + public Response delete(@PathParam("user-id") String userId, + @PathParam("activity-id") String activityId, + @QueryParam("token") String token) { + if (token == null) return Response.status(Response.Status.BAD_REQUEST).build(); + if (!store.verifyToken(userId, token)) return Response.status(403).build(); + boolean ok = store.deleteActivity(userId, activityId); + if (!ok) return Response.status(Response.Status.NOT_FOUND).build(); + return Response.ok().build(); + } + + @Path("/{activity-id}/text") + @GET + public Response getText(@PathParam("user-id") String userId, + @PathParam("activity-id") String activityId) { + Activity a = store.getActivity(userId, activityId); + if (a == null) return Response.status(Response.Status.NOT_FOUND).build(); + return Response.ok(a.text).build(); + } + + @Path("/{activity-id}/updated-time") + @GET + public Response getUpdatedTime(@PathParam("user-id") String userId, + @PathParam("activity-id") String activityId) { + Activity a = store.getActivity(userId, activityId); + if (a == null) return Response.status(Response.Status.NOT_FOUND).build(); + return Response.ok(a.updatedTime).build(); + } + + @Path("/last-updated-time") + @GET + public Response lastUpdated(@PathParam("user-id") String userId) { + String t = store.getLatestActivityTime(userId); + if (t == null) return Response.status(Response.Status.NOT_FOUND).build(); + return Response.ok(t).build(); + } +} diff --git a/src/main/java/com/example/tampopotest/web/ChatRequestsResource.java b/src/main/java/com/example/tampopotest/web/ChatRequestsResource.java new file mode 100644 index 0000000..95aeda1 --- /dev/null +++ b/src/main/java/com/example/tampopotest/web/ChatRequestsResource.java @@ -0,0 +1,48 @@ +package com.example.tampopotest.web; + +import com.example.tampopotest.service.InMemoryStore; +import com.example.tampopotest.service.InMemoryStore.ChatRequest; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import java.util.Map; + +@Path("/chat-requests") +@Produces(MediaType.APPLICATION_JSON) +public class ChatRequestsResource { + private final InMemoryStore store = InMemoryStore.get(); + + // POST /chat-requests/ + @POST + @Consumes(MediaType.APPLICATION_FORM_URLENCODED) + public Response create(@FormParam("token") String token, + @FormParam("sender-id") String senderId, + @FormParam("receiver-id") String receiverId) { + if (token == null || senderId == null || receiverId == null) + return Response.status(Response.Status.BAD_REQUEST).build(); + String u = store.findUserIdByToken(token); + if (u == null) return Response.status(Response.Status.UNAUTHORIZED).build(); + // 成りすまし禁止: tokenのユーザとsenderが一致 + if (!u.equals(senderId)) return Response.status(Response.Status.FORBIDDEN).build(); + if (store.getUser(senderId) == null || store.getUser(receiverId) == null) + return Response.status(Response.Status.NOT_FOUND).build(); + int id = store.createChatRequest(senderId, receiverId); + // 仕様は204だが本文を返す例があるため、204 + body を返す + return Response.status(204).entity(Map.of("chat-request-id", id)).build(); + } + + // DELETE /chat-requests/{chat-request-id}/ + @DELETE + @Path("/{chat-request-id}/") + public Response delete(@PathParam("chat-request-id") int id, + @QueryParam("token") String token) { + if (token == null) return Response.status(Response.Status.BAD_REQUEST).build(); + String u = store.findUserIdByToken(token); + if (u == null) return Response.status(Response.Status.UNAUTHORIZED).build(); + ChatRequest cr = store.getChatRequest(id); + if (cr == null) return Response.status(Response.Status.NOT_FOUND).build(); + if (!u.equals(cr.senderId) && !u.equals(cr.receiverId)) return Response.status(Response.Status.FORBIDDEN).build(); + store.deleteChatRequest(id); + return Response.status(204).build(); + } +} diff --git a/src/main/java/com/example/tampopotest/web/ChatRoomsResource.java b/src/main/java/com/example/tampopotest/web/ChatRoomsResource.java new file mode 100644 index 0000000..1a2ee71 --- /dev/null +++ b/src/main/java/com/example/tampopotest/web/ChatRoomsResource.java @@ -0,0 +1,112 @@ +package com.example.tampopotest.web; + +import com.example.tampopotest.service.InMemoryStore; +import com.example.tampopotest.service.InMemoryStore.ChatRoom; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import java.util.Map; +import java.util.Objects; + +@Path("/chat-rooms") +@Produces(MediaType.APPLICATION_JSON) +public class ChatRoomsResource { + private final InMemoryStore store = InMemoryStore.get(); + + // POST /chat-rooms/ + @POST + public Response create(@QueryParam("user0-id") String user0, + @QueryParam("user1-id") String user1, + @QueryParam("token") String token) { + if (user0 == null || user1 == null || token == null) + return Response.status(Response.Status.BAD_REQUEST).build(); + String tokenUser = store.findUserIdByToken(token); + if (tokenUser == null) return Response.status(Response.Status.UNAUTHORIZED).build(); + if (!Objects.equals(tokenUser, user0)) return Response.status(Response.Status.UNAUTHORIZED).build(); + if (store.getUser(user0) == null || store.getUser(user1) == null) + return Response.status(Response.Status.NOT_FOUND).build(); + int id = store.createChatRoom(user0, user1); + return Response.ok(Map.of("room-id", id)).build(); + } + + // GET /chat-rooms/?user0-id=...&token=... + @GET + public Response find(@QueryParam("user0-id") String user, + @QueryParam("token") String token) { + if (user == null || token == null) return Response.status(Response.Status.BAD_REQUEST).build(); + if (!token.equals(store.getUser(user) != null ? store.getUser(user).token : null)) + return Response.status(Response.Status.UNAUTHORIZED).build(); + Integer roomId = store.findRoomIdByMember(user); + if (roomId == null) return Response.status(Response.Status.NOT_FOUND).build(); + return Response.ok(Map.of("room-id", roomId)).build(); + } + + // GET /chat-rooms/{chatroom-id}/{user-id}/?token=... + @GET + @Path("/{chatroom-id}/{user-id}/") + public Response getMessage(@PathParam("chatroom-id") int roomId, + @PathParam("user-id") String targetUserId, + @QueryParam("token") String token) { + if (token == null) return Response.status(Response.Status.BAD_REQUEST).build(); + String caller = store.findUserIdByToken(token); + if (caller == null) return Response.status(Response.Status.UNAUTHORIZED).build(); + ChatRoom room = store.getChatRoom(roomId); + if (room == null) return Response.status(Response.Status.NOT_FOUND).build(); + if (!room.members.contains(caller)) return Response.status(Response.Status.FORBIDDEN).build(); + String msg = store.getMessageFrom(roomId, targetUserId); + return Response.ok(Map.of("message", msg == null ? "" : msg)).build(); + + } + + // DELETE /chat-rooms/{chatroom-id}/{user-id}/?token=... + @DELETE + @Path("/{chatroom-id}/{user-id}/") + public Response leaveOrDelete(@PathParam("chatroom-id") int roomId, + @PathParam("user-id") String userId, + @QueryParam("token") String token) { + if (token == null) return Response.status(Response.Status.BAD_REQUEST).build(); + String caller = store.findUserIdByToken(token); + if (caller == null) return Response.status(Response.Status.UNAUTHORIZED).build(); + if (!Objects.equals(caller, userId)) return Response.status(Response.Status.FORBIDDEN).build(); + ChatRoom room = store.getChatRoom(roomId); + if (room == null) return Response.status(204).build(); + boolean removed = store.removeMemberFromRoom(roomId, userId); + if (!removed) return Response.status(Response.Status.NOT_FOUND).build(); + return Response.ok(Map.of("message", "ユーザをルームから退出しました")).build(); + } + + // PUT /chat-rooms/{chatroom-id}/{user-id}/?token=... + @PUT + @Path("/{chatroom-id}/{user-id}/") + public Response join(@PathParam("chatroom-id") int roomId, + @PathParam("user-id") String userId, + @QueryParam("token") String token) { + if (token == null) return Response.status(Response.Status.BAD_REQUEST).build(); + String caller = store.findUserIdByToken(token); + if (caller == null) return Response.status(Response.Status.UNAUTHORIZED).build(); + if (!Objects.equals(caller, userId)) return Response.status(Response.Status.FORBIDDEN).build(); + ChatRoom room = store.getChatRoom(roomId); + if (room == null) return Response.status(Response.Status.NOT_FOUND).build(); + store.addMemberToRoom(roomId, userId); + return Response.ok().build(); + } + + // PUT /chat-rooms/{chatroom-id}/{user-id}/message + @PUT + @Path("/{chatroom-id}/{user-id}/message") + @Consumes(MediaType.APPLICATION_FORM_URLENCODED) + public Response updateMessage(@PathParam("chatroom-id") int roomId, + @PathParam("user-id") String userId, + @FormParam("token") String token, + @FormParam("message") String message) { + if (token == null || message == null) return Response.status(Response.Status.BAD_REQUEST).build(); + String caller = store.findUserIdByToken(token); + if (caller == null) return Response.status(Response.Status.UNAUTHORIZED).build(); + if (!Objects.equals(caller, userId)) return Response.status(Response.Status.FORBIDDEN).build(); + ChatRoom room = store.getChatRoom(roomId); + if (room == null) return Response.status(Response.Status.NOT_FOUND).build(); + if (!room.members.contains(userId)) return Response.status(Response.Status.FORBIDDEN).build(); + store.updateMessage(roomId, userId, message); + return Response.ok().build(); + } +} diff --git a/src/main/java/com/example/tampopotest/web/FriendRequestsResource.java b/src/main/java/com/example/tampopotest/web/FriendRequestsResource.java new file mode 100644 index 0000000..33f9b39 --- /dev/null +++ b/src/main/java/com/example/tampopotest/web/FriendRequestsResource.java @@ -0,0 +1,63 @@ +package com.example.tampopotest.web; + +import com.example.tampopotest.service.InMemoryStore; +import com.example.tampopotest.service.InMemoryStore.FriendRequest; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import java.util.*; + +@Path("/friend-requests") +@Produces(MediaType.APPLICATION_JSON) +public class FriendRequestsResource { + private final InMemoryStore store = InMemoryStore.get(); + + @GET + public Response list(@QueryParam("token") String token) { + if (token == null) return Response.status(Response.Status.BAD_REQUEST).build(); + String userId = store.findUserIdByToken(token); + if (userId == null) return Response.status(Response.Status.UNAUTHORIZED).build(); + List list = store.listFriendRequestsFor(userId); + List> res = new ArrayList<>(); + for (FriendRequest fr : list) { + Map m = new LinkedHashMap<>(); + m.put("id", fr.id); + m.put("senderId", fr.senderId); + m.put("receiverId", fr.receiverId); + res.add(m); + } + return Response.ok(res).build(); + } + + @POST + @Consumes(MediaType.APPLICATION_FORM_URLENCODED) + public Response create(@FormParam("token") String token, + @FormParam("sender-id") String senderId, + @FormParam("receiver-id") String receiverId) { + if (token == null || senderId == null || receiverId == null) { + return Response.status(Response.Status.BAD_REQUEST).build(); + } + String tokenUser = store.findUserIdByToken(token); + if (tokenUser == null) return Response.status(Response.Status.UNAUTHORIZED).build(); + if (!senderId.equals(tokenUser)) return Response.status(Response.Status.FORBIDDEN).build(); + if (store.getUser(senderId) == null || store.getUser(receiverId) == null) { + return Response.status(Response.Status.NOT_FOUND).build(); + } + int id = store.createFriendRequest(senderId, receiverId); + return Response.status(201).entity(String.valueOf(id)).build(); + } + + @Path("/{friend-request-id}") + @DELETE + public Response delete(@PathParam("friend-request-id") int requestId, + @QueryParam("receiver-token") String receiverToken) { + if (receiverToken == null) return Response.status(Response.Status.BAD_REQUEST).build(); + String userId = store.findUserIdByToken(receiverToken); + if (userId == null) return Response.status(Response.Status.UNAUTHORIZED).build(); + FriendRequest fr = store.getFriendRequest(requestId); + if (fr == null) return Response.status(Response.Status.NOT_FOUND).build(); + if (!userId.equals(fr.receiverId)) return Response.status(Response.Status.FORBIDDEN).build(); + store.deleteFriendRequest(requestId); + return Response.ok().build(); + } +} diff --git a/src/main/java/com/example/tampopotest/web/FriendsResource.java b/src/main/java/com/example/tampopotest/web/FriendsResource.java new file mode 100644 index 0000000..2e4bf85 --- /dev/null +++ b/src/main/java/com/example/tampopotest/web/FriendsResource.java @@ -0,0 +1,78 @@ +package com.example.tampopotest.web; + +import com.example.tampopotest.service.InMemoryStore; +import com.example.tampopotest.service.InMemoryStore.FriendPair; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import java.util.*; + +@Path("/friends") +@Produces(MediaType.APPLICATION_JSON) +public class FriendsResource { + private final InMemoryStore store = InMemoryStore.get(); + + // POST /friends/ + @POST + @Consumes(MediaType.APPLICATION_FORM_URLENCODED) + public Response createPair(@FormParam("token") String token, + @FormParam("user0-id") String user0, + @FormParam("user1-id") String user1) { + if (token == null || user0 == null || user1 == null) return Response.status(Response.Status.BAD_REQUEST).build(); + String tokenUser = store.findUserIdByToken(token); + if (tokenUser == null) return Response.status(Response.Status.UNAUTHORIZED).build(); + if (!tokenUser.equals(user0) && !tokenUser.equals(user1)) return Response.status(Response.Status.FORBIDDEN).build(); + if (store.getUser(user0) == null || store.getUser(user1) == null) return Response.status(Response.Status.NOT_FOUND).build(); + if (store.existsFriendPair(user0, user1)) return Response.status(409).build(); + int pid = store.createFriendPair(user0, user1); + Map res = Map.of("pair-id", pid); + return Response.ok(res).build(); + } + + // GET /friends/pairs/{pair-id}/ + @GET + @Path("/pairs/{pair-id}/") + public Response getPair(@PathParam("pair-id") int pairId, + @QueryParam("token") String token) { + if (token == null) return Response.status(Response.Status.BAD_REQUEST).build(); + String user = store.findUserIdByToken(token); + if (user == null) return Response.status(Response.Status.UNAUTHORIZED).build(); + FriendPair p = store.getFriendPair(pairId); + if (p == null) return Response.status(Response.Status.NOT_FOUND).build(); + if (!p.user0Id.equals(user) && !p.user1Id.equals(user)) return Response.status(Response.Status.FORBIDDEN).build(); + Map res = new LinkedHashMap<>(); + res.put("id", p.pairId); + res.put("user0id", p.user0Id); + res.put("user1Id", p.user1Id); + return Response.ok(res).build(); + } + + // DELETE /friends/pairs/{pair-id}/ + @DELETE + @Path("/pairs/{pair-id}/") + public Response deletePair(@PathParam("pair-id") int pairId, + @QueryParam("token") String token) { + if (token == null) return Response.status(Response.Status.BAD_REQUEST).build(); + String user = store.findUserIdByToken(token); + if (user == null) return Response.status(Response.Status.UNAUTHORIZED).build(); + FriendPair p = store.getFriendPair(pairId); + if (p == null) return Response.status(Response.Status.NOT_FOUND).build(); + if (!p.user0Id.equals(user) && !p.user1Id.equals(user)) return Response.status(Response.Status.FORBIDDEN).build(); + store.deleteFriendPair(pairId); + return Response.ok().build(); + } + + // GET /friends/users/{user-id} + @GET + @Path("/users/{user-id}") + public Response listFriends(@PathParam("user-id") String userId, + @QueryParam("token") String token) { + if (token == null) return Response.status(Response.Status.BAD_REQUEST).build(); + if (!token.equals(Optional.ofNullable(store.getUser(userId)).map(u -> u.token).orElse(null))) { + return Response.status(Response.Status.FORBIDDEN).build(); + } + if (store.getUser(userId) == null) return Response.status(Response.Status.NOT_FOUND).build(); + List list = store.listFriends(userId); + return Response.ok(list).build(); + } +} diff --git a/src/main/java/com/example/tampopotest/web/NotificationsResource.java b/src/main/java/com/example/tampopotest/web/NotificationsResource.java new file mode 100644 index 0000000..5a60920 --- /dev/null +++ b/src/main/java/com/example/tampopotest/web/NotificationsResource.java @@ -0,0 +1,17 @@ +package com.example.tampopotest.web; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; + +@Path("/notifications") +@Produces(MediaType.APPLICATION_JSON) +public class NotificationsResource { + // 本API仕様には通知エンドポイントの定義はないため、スタブのみ用意 + @GET + public Response ping() { + return Response.ok("[]").build(); + } +} diff --git a/src/main/java/com/example/tampopotest/web/UserFriendsResource.java b/src/main/java/com/example/tampopotest/web/UserFriendsResource.java new file mode 100644 index 0000000..158005e --- /dev/null +++ b/src/main/java/com/example/tampopotest/web/UserFriendsResource.java @@ -0,0 +1,58 @@ +package com.example.tampopotest.web; + +import com.example.tampopotest.service.InMemoryStore; +import com.example.tampopotest.service.InMemoryStore.User; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import java.util.LinkedHashMap; +import java.util.Map; + +@Path("/users/{user-id}/friends") +@Produces(MediaType.APPLICATION_JSON) +public class UserFriendsResource { + private final InMemoryStore store = InMemoryStore.get(); + + // GET /users/{user-id}/friends + @GET + public Response listPairs(@PathParam("user-id") String userId, + @QueryParam("token") String token) { + if (token == null) return Response.status(Response.Status.BAD_REQUEST).build(); + User u = store.getUser(userId); + if (u == null) return Response.status(Response.Status.NOT_FOUND).build(); + if (!store.verifyToken(userId, token)) return Response.status(403).build(); + Map> map = store.listFriendPairsOf(userId); + // OpenAPIではpid1: {...} のような形を要求。キーをそのまま文字列にする + Map res = new LinkedHashMap<>(); + for (Map.Entry> e : map.entrySet()) { + res.put("pid" + e.getKey(), e.getValue()); + } + return Response.ok(res).build(); + } + + // GET /users/{user-id}/friends/{pair-id} + @GET + @Path("/{pair-id}") + public Response getOpponent(@PathParam("user-id") String userId, + @PathParam("pair-id") int pairId, + @QueryParam("token") String token) { + if (token == null) return Response.status(Response.Status.BAD_REQUEST).build(); + if (!store.verifyToken(userId, token)) return Response.status(403).build(); + String other = store.friendOpponent(userId, pairId); + if (other == null) return Response.status(Response.Status.NOT_FOUND).build(); + return Response.ok(Map.of("user-id", other)).build(); + } + + // DELETE /users/{user-id}/friends/{pair-id} + @DELETE + @Path("/{pair-id}") + public Response deletePair(@PathParam("user-id") String userId, + @PathParam("pair-id") int pairId, + @QueryParam("token") String token) { + if (token == null) return Response.status(Response.Status.BAD_REQUEST).build(); + if (!store.verifyToken(userId, token)) return Response.status(403).build(); + boolean ok = store.deleteFriendPair(pairId); + if (!ok) return Response.status(Response.Status.NOT_FOUND).build(); + return Response.ok().build(); + } +} diff --git a/src/main/java/com/example/tampopotest/web/UsersResource.java b/src/main/java/com/example/tampopotest/web/UsersResource.java new file mode 100644 index 0000000..062a252 --- /dev/null +++ b/src/main/java/com/example/tampopotest/web/UsersResource.java @@ -0,0 +1,198 @@ +package com.example.tampopotest.web; + +import com.example.tampopotest.service.InMemoryStore; +import com.example.tampopotest.service.InMemoryStore.User; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import java.net.URI; +import java.util.*; + +@Path("/users") +@Produces(MediaType.APPLICATION_JSON) +public class UsersResource { + + private final InMemoryStore store = InMemoryStore.get(); + + // GET /users + @GET + public Response listUsers() { + List ids = store.listUserIds(); + return Response.ok(ids).build(); + } + + // POST /users (create) + @POST + @Consumes(MediaType.APPLICATION_FORM_URLENCODED) + public Response createUser(@FormParam("user-id") String userId, + @FormParam("password") String password) { + if (isBlank(userId) || isBlank(password)) { + return Response.status(Response.Status.BAD_REQUEST).entity(msg("不正なリクエスト")).build(); + } + User u = store.createUser(userId, password); + if (u == null) { + return Response.status(409).entity(msg("ユーザーIDの重複")).build(); + } + Map res = new LinkedHashMap<>(); + res.put("user-id", u.userId); + res.put("token", u.token); + return Response.ok(res).location(URI.create("/users/" + u.userId)).build(); + } + + // GET /users/{user-id} + @GET + @Path("/{user-id}") + public Response getUser(@PathParam("user-id") String userId) { + User u = store.getUser(userId); + if (u == null) return Response.status(Response.Status.NOT_FOUND).build(); + Map res = new LinkedHashMap<>(); + res.put("name", u.name == null ? "" : u.name); + res.put("icon", u.iconUrl); + return Response.ok(res).build(); + } + + // DELETE /users/{user-id}?token=... + @DELETE + @Path("/{user-id}") + public Response deleteUser(@PathParam("user-id") String userId, + @QueryParam("token") String token) { + if (isBlank(token)) return Response.status(Response.Status.BAD_REQUEST).build(); + User u = store.getUser(userId); + if (u == null) return Response.status(Response.Status.NOT_FOUND).build(); + if (!store.verifyToken(userId, token)) return Response.status(403).build(); + store.deleteUser(userId); + return Response.ok().build(); + } + + // POST /users/{user-id}/login + @POST + @Path("/{user-id}/login") + @Consumes(MediaType.APPLICATION_FORM_URLENCODED) + public Response login(@PathParam("user-id") String userId, + @FormParam("password") String password) { + if (isBlank(password)) return Response.status(Response.Status.BAD_REQUEST).build(); + String token = store.login(userId, password); + if (token == null) return Response.status(Response.Status.NOT_FOUND).build(); + if (token.isEmpty()) return Response.status(Response.Status.UNAUTHORIZED).build(); + Map res = Map.of("token", token); + return Response.ok(res).build(); + } + + // GET /users/{user-id}/name + @GET + @Path("/{user-id}/name") + public Response getName(@PathParam("user-id") String userId) { + User u = store.getUser(userId); + if (u == null) return Response.status(Response.Status.NOT_FOUND).build(); + return Response.ok(u.name == null ? "" : u.name).build(); + } + + // PUT /users/{user-id}/name + @PUT + @Path("/{user-id}/name") + @Consumes(MediaType.APPLICATION_FORM_URLENCODED) + public Response updateName(@PathParam("user-id") String userId, + @FormParam("token") String token, + @FormParam("new-name") String newName) { + if (isBlank(token) || newName == null) return Response.status(Response.Status.BAD_REQUEST).build(); + User u = store.getUser(userId); + if (u == null) return Response.status(Response.Status.NOT_FOUND).build(); + if (!store.verifyToken(userId, token)) return Response.status(403).build(); + u.name = newName; + return Response.ok(newName).build(); + } + + // GET /users/{user-id}/password + @GET + @Path("/{user-id}/password") + public Response getPassword(@PathParam("user-id") String userId, + @QueryParam("token") String token) { + if (isBlank(token)) return Response.status(Response.Status.BAD_REQUEST).build(); + User u = store.getUser(userId); + if (u == null) return Response.status(Response.Status.NOT_FOUND).build(); + if (!store.verifyToken(userId, token)) return Response.status(403).build(); + Map res = Map.of("password", u.password); + return Response.ok(res).build(); + } + + // PUT /users/{user-id}/password + @PUT + @Path("/{user-id}/password") + @Consumes(MediaType.APPLICATION_FORM_URLENCODED) + public Response updatePassword(@PathParam("user-id") String userId, + @FormParam("token") String token, + @FormParam("new-password") String newPassword) { + if (isBlank(token) || isBlank(newPassword)) return Response.status(Response.Status.BAD_REQUEST).build(); + User u = store.getUser(userId); + if (u == null) return Response.status(Response.Status.NOT_FOUND).build(); + if (!store.verifyToken(userId, token)) return Response.status(403).build(); + u.password = newPassword; + return Response.ok().build(); + } + + // GET /users/{user-id}/email + @GET + @Path("/{user-id}/email") + public Response getEmail(@PathParam("user-id") String userId, + @QueryParam("token") String token) { + if (isBlank(token)) return Response.status(Response.Status.BAD_REQUEST).build(); + User u = store.getUser(userId); + if (u == null) return Response.status(Response.Status.NOT_FOUND).build(); + if (!store.verifyToken(userId, token)) return Response.status(403).build(); + Map res = Map.of("email", u.email == null ? "" : u.email); + return Response.ok(res).build(); + } + + // PUT /users/{user-id}/email + @PUT + @Path("/{user-id}/email") + @Consumes(MediaType.APPLICATION_FORM_URLENCODED) + public Response updateEmail(@PathParam("user-id") String userId, + @FormParam("token") String token, + @FormParam("new-email") String newEmail) { + if (isBlank(token) || isBlank(newEmail)) return Response.status(Response.Status.BAD_REQUEST).build(); + User u = store.getUser(userId); + if (u == null) return Response.status(Response.Status.NOT_FOUND).build(); + if (!store.verifyToken(userId, token)) return Response.status(403).build(); + // 重複チェック(同じメールのユーザーが居るか) + for (String id : store.listUserIds()) { + User ou = store.getUser(id); + if (ou != null && newEmail.equalsIgnoreCase(ou.email) && !id.equals(userId)) { + return Response.status(409).build(); + } + } + u.email = newEmail; + return Response.ok().build(); + } + + // GET /users/{user-id}/icon + @GET + @Path("/{user-id}/icon") + public Response getIcon(@PathParam("user-id") String userId) { + User u = store.getUser(userId); + if (u == null) return Response.status(Response.Status.NOT_FOUND).build(); + return Response.ok(u.iconUrl).build(); + } + + // PUT /users/{user-id}/icon + @PUT + @Path("/{user-id}/icon") + @Consumes(MediaType.APPLICATION_FORM_URLENCODED) + public Response updateIcon(@PathParam("user-id") String userId, + @FormParam("token") String token, + @FormParam("new-icon") String newIconBase64) { + if (isBlank(token) || newIconBase64 == null) return Response.status(Response.Status.BAD_REQUEST).build(); + User u = store.getUser(userId); + if (u == null) return Response.status(Response.Status.NOT_FOUND).build(); + if (!store.verifyToken(userId, token)) return Response.status(403).build(); + // 本来は保存しURLを返す。ここではハッシュのようなIDを返す。 + String hash = Integer.toHexString(Objects.hash(newIconBase64)).toUpperCase(Locale.ROOT); + u.iconHash = hash; + Map res = Map.of("icon", hash); + return Response.ok(res).build(); + } + + private static boolean isBlank(String s) {return s == null || s.isBlank();} + + private static Map msg(String m) {return Map.of("message", m);} +}