From ca760947b284ccb58ae41485bacb7082eb1d27e2 Mon Sep 17 00:00:00 2001 From: Dongmin Kim Date: Fri, 22 Dec 2023 10:58:45 +0900 Subject: [PATCH 1/3] =?UTF-8?q?Fix:=20=EC=86=8C=EC=85=9C=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 이미 있는 회원인 경우 회원 객체에 데이터가 안담기는 버그 발견 - 로직 변경 후 회원 데이터 제대로 담기는 것 확인 --- .../security/oauth/OAuthSuccessHandler.java | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/main/java/com/api/trip/common/security/oauth/OAuthSuccessHandler.java b/src/main/java/com/api/trip/common/security/oauth/OAuthSuccessHandler.java index 88ff55c..c7e7f92 100644 --- a/src/main/java/com/api/trip/common/security/oauth/OAuthSuccessHandler.java +++ b/src/main/java/com/api/trip/common/security/oauth/OAuthSuccessHandler.java @@ -1,5 +1,7 @@ package com.api.trip.common.security.oauth; +import com.api.trip.common.exception.ErrorCode; +import com.api.trip.common.exception.custom_exception.NotFoundException; import com.api.trip.common.security.jwt.JwtToken; import com.api.trip.common.security.jwt.JwtTokenProvider; import com.api.trip.domain.member.controller.dto.LoginResponse; @@ -43,29 +45,25 @@ public void onAuthenticationSuccess(HttpServletRequest request, HttpServletRespo Optional findMember = memberRepository.findByEmail(email); // 회원이 아닌 경우에 회원 가입 진행 - - Long memberId = 0L; - String role = ""; - + Member member = null; if (findMember.isEmpty()) { String name = oAuth2User.getAttribute("name"); String picture = oAuth2User.getAttribute("picture"); - Member member = Member.of(email, "", name, picture); + member = Member.of(email, "", name, picture); memberRepository.save(member); - - memberId = member.getId(); - role = member.getRole().getValue(); + } else { + member = findMember.orElseThrow(() -> new NotFoundException(ErrorCode.NOT_FOUND_MEMBER)); } // OAuth2User 객체에서 권한 가져옴 - JwtToken jwtToken = jwtTokenProvider.createJwtToken(email, role); + JwtToken jwtToken = jwtTokenProvider.createJwtToken(member.getEmail(), member.getRole().getValue()); - // 쿠키 세팅 - response.addHeader(HttpHeaders.SET_COOKIE, createCookie("tokenType", "Bearer")); response.addHeader(HttpHeaders.SET_COOKIE, createCookie("accessToken", jwtToken.getAccessToken())); response.addHeader(HttpHeaders.SET_COOKIE, createCookie("refreshToken", jwtToken.getRefreshToken())); - response.addHeader(HttpHeaders.SET_COOKIE, createCookie("memberId", String.valueOf(memberId))); + response.addHeader(HttpHeaders.SET_COOKIE, createCookie("memberId", String.valueOf(member.getId()))); + + // TODO: 프론트 배포 주소로 변경 예정 response.sendRedirect("http://localhost:5173/home"); } @@ -73,7 +71,9 @@ private static String createCookie(String name, String value) { return ResponseCookie.from(name, value) .path("/") .httpOnly(true) - .sameSite("None") + .maxAge(60 * 60 * 6) + // .sameSite("None") https 시 활성화 + //.secure(true) https 시 활성화 .build() .toString(); } From 801c9b53533a7cd87cb6e3955f1a3b2f534a1c5c Mon Sep 17 00:00:00 2001 From: Dongmin Kim Date: Fri, 22 Dec 2023 11:08:16 +0900 Subject: [PATCH 2/3] =?UTF-8?q?Fix:=20=EC=86=8C=EC=85=9C=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=20=EC=8B=9C=20=EC=A0=80=EC=9E=A5=EB=90=98?= =?UTF-8?q?=EB=8A=94=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=ED=98=95=EC=8B=9D=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 다른 플랫폼인 경우에도 이메일이 같은 경우가 있기 때문에 이메일 앞에 플랫폼 이름을 붙여서 구분시킴. - 닉네임에도 마찬가지로 플랫폼 이름을 추가해서 저장 --- .../security/oauth/OAuth2Attribute.java | 19 ++++++++++++------- .../security/oauth/OAuthSuccessHandler.java | 8 ++++++-- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/api/trip/common/security/oauth/OAuth2Attribute.java b/src/main/java/com/api/trip/common/security/oauth/OAuth2Attribute.java index db10b7a..948c6cf 100644 --- a/src/main/java/com/api/trip/common/security/oauth/OAuth2Attribute.java +++ b/src/main/java/com/api/trip/common/security/oauth/OAuth2Attribute.java @@ -21,18 +21,19 @@ public class OAuth2Attribute { private String email; // 이메일 private String name; // 이름 private String picture; // 프로필 사진 + private String provider; // 플랫폼 static OAuth2Attribute of(String provider, String attributeKey, Map attributes) { // 각 플랫폼 별로 제공해주는 데이터가 조금씩 다르기 때문에 분기 처리함. return switch (provider) { - case "google" -> google(attributeKey, attributes); - case "kakao" -> kakao(attributeKey, attributes); - case "naver" -> naver(attributeKey, attributes); + case "google" -> google(provider, attributeKey, attributes); + case "kakao" -> kakao(provider, attributeKey, attributes); + case "naver" -> naver(provider, attributeKey, attributes); default -> throw new NotFoundException(ErrorCode.NOT_FOUND_PROVIDER); }; } - private static OAuth2Attribute google(String attributeKey, Map attributes) { + private static OAuth2Attribute google(String provider, String attributeKey, Map attributes) { log.debug("google: {}", attributes); return OAuth2Attribute.builder() .email((String) attributes.get("email")) @@ -40,23 +41,25 @@ private static OAuth2Attribute google(String attributeKey, Map a .picture((String)attributes.get("picture")) .attributes(attributes) .attributeKey(attributeKey) + .provider(provider) .build(); } - private static OAuth2Attribute kakao(String attributeKey, Map attributes) { + private static OAuth2Attribute kakao(String provider, String attributeKey, Map attributes) { Map kakaoAccount = (Map) attributes.get("kakao_account"); Map kakaoProfile = (Map) kakaoAccount.get("profile"); return OAuth2Attribute.builder() - .email("KAKAO_" + (String) kakaoAccount.get("email")) + .email((String) kakaoAccount.get("email")) .name((String) kakaoProfile.get("nickname")) .picture((String) kakaoProfile.get("profile_image_url")) .attributes(kakaoAccount) .attributeKey(attributeKey) + .provider(provider) .build(); } - private static OAuth2Attribute naver(String attributeKey, Map attributes) { + private static OAuth2Attribute naver(String provider, String attributeKey, Map attributes) { Map response = (Map) attributes.get("response"); return OAuth2Attribute.builder() @@ -65,6 +68,7 @@ private static OAuth2Attribute naver(String attributeKey, Map at .picture((String) response.get("profile_image")) .attributes(response) .attributeKey(attributeKey) + .provider(provider) .build(); } @@ -77,6 +81,7 @@ public Map convertToMap() { map.put("email", email); map.put("name", name); map.put("picture", picture); + map.put("provider", provider); return map; } diff --git a/src/main/java/com/api/trip/common/security/oauth/OAuthSuccessHandler.java b/src/main/java/com/api/trip/common/security/oauth/OAuthSuccessHandler.java index c7e7f92..e33213d 100644 --- a/src/main/java/com/api/trip/common/security/oauth/OAuthSuccessHandler.java +++ b/src/main/java/com/api/trip/common/security/oauth/OAuthSuccessHandler.java @@ -41,13 +41,17 @@ public void onAuthenticationSuccess(HttpServletRequest request, HttpServletRespo OAuth2User oAuth2User = (OAuth2User) authentication.getPrincipal(); - String email = oAuth2User.getAttribute("email"); + String provider = oAuth2User.getAttribute("provider"); + + // KAKAO_user123@naver.com + String email = provider + "_" + oAuth2User.getAttribute("email"); Optional findMember = memberRepository.findByEmail(email); // 회원이 아닌 경우에 회원 가입 진행 Member member = null; if (findMember.isEmpty()) { - String name = oAuth2User.getAttribute("name"); + // KAKAO_user123 + String name = provider + "_" + oAuth2User.getAttribute("name"); String picture = oAuth2User.getAttribute("picture"); member = Member.of(email, "", name, picture); From a70f5a52e648ed9f8623a260fa6ad4ed3092098f Mon Sep 17 00:00:00 2001 From: kwondongwook Date: Fri, 22 Dec 2023 16:04:40 +0900 Subject: [PATCH 3/3] =?UTF-8?q?Feat:=20=EC=95=8C=EB=A6=BC=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/trip/common/exception/ErrorCode.java | 4 +- .../controller/NotificationController.java | 72 ++ .../dto/GetMyNotificationsResponse.java | 36 + .../controller/dto/NotificationResponse.java | 33 + .../notification/domain/Notification.java | 41 + .../notification/emitter/SseEmitterMap.java | 55 + .../repository/NotificationRepository.java | 12 + .../NotificationRepositoryCustom.java | 15 + .../NotificationRepositoryCustomImpl.java | 42 + .../service/NotificationService.java | 77 ++ src/main/resources/static/index.html | 42 + src/main/resources/static/src/eventsource.js | 1048 +++++++++++++++++ 12 files changed, 1476 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/api/trip/domain/notification/controller/NotificationController.java create mode 100644 src/main/java/com/api/trip/domain/notification/controller/dto/GetMyNotificationsResponse.java create mode 100644 src/main/java/com/api/trip/domain/notification/controller/dto/NotificationResponse.java create mode 100644 src/main/java/com/api/trip/domain/notification/domain/Notification.java create mode 100644 src/main/java/com/api/trip/domain/notification/emitter/SseEmitterMap.java create mode 100644 src/main/java/com/api/trip/domain/notification/repository/NotificationRepository.java create mode 100644 src/main/java/com/api/trip/domain/notification/repository/NotificationRepositoryCustom.java create mode 100644 src/main/java/com/api/trip/domain/notification/repository/NotificationRepositoryCustomImpl.java create mode 100644 src/main/java/com/api/trip/domain/notification/service/NotificationService.java create mode 100644 src/main/resources/static/index.html create mode 100644 src/main/resources/static/src/eventsource.js diff --git a/src/main/java/com/api/trip/common/exception/ErrorCode.java b/src/main/java/com/api/trip/common/exception/ErrorCode.java index 30d5034..6d501c1 100644 --- a/src/main/java/com/api/trip/common/exception/ErrorCode.java +++ b/src/main/java/com/api/trip/common/exception/ErrorCode.java @@ -45,7 +45,9 @@ public enum ErrorCode { INTEREST_ARTICLE_ALREADY_EXISTS("이미 좋아한 게시글입니다.", HttpStatus.BAD_REQUEST), INTEREST_ARTICLE_NOT_FOUND("좋아한 게시글이 아닙니다.", HttpStatus.NOT_FOUND), UPLOAD_FAILED("파일 업로드에 실패하였습니다.", HttpStatus.INTERNAL_SERVER_ERROR), - ; + + // 알림 + NOTIFICATION_NOT_FOUND("존재하지 않는 알림입니다.", HttpStatus.NOT_FOUND); private final String message; private final HttpStatus status; diff --git a/src/main/java/com/api/trip/domain/notification/controller/NotificationController.java b/src/main/java/com/api/trip/domain/notification/controller/NotificationController.java new file mode 100644 index 0000000..cb17d73 --- /dev/null +++ b/src/main/java/com/api/trip/domain/notification/controller/NotificationController.java @@ -0,0 +1,72 @@ +package com.api.trip.domain.notification.controller; + +import com.api.trip.common.exception.CustomException; +import com.api.trip.common.exception.ErrorCode; +import com.api.trip.domain.member.repository.MemberRepository; +import com.api.trip.domain.notification.controller.dto.GetMyNotificationsResponse; +import com.api.trip.domain.notification.emitter.SseEmitterMap; +import com.api.trip.domain.notification.service.NotificationService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +import java.time.LocalDateTime; + +@RestController +@RequestMapping("/api/notifications") +@RequiredArgsConstructor +public class NotificationController { + + private final MemberRepository memberRepository; + private final NotificationService notificationService; + private final SseEmitterMap sseEmitterMap; + + @GetMapping(value = "/connect", produces = MediaType.TEXT_EVENT_STREAM_VALUE) + public ResponseEntity connect() { + String email = SecurityContextHolder.getContext().getAuthentication().getName(); + Long memberId = memberRepository.findByEmail(email) + .orElseThrow(() -> new CustomException(ErrorCode.UNAUTHORIZED)) + .getId(); + + SseEmitter sseEmitter = new SseEmitter(); + sseEmitterMap.put(memberId, sseEmitter); + sseEmitterMap.send(memberId, "connect", LocalDateTime.now()); + return ResponseEntity.ok(sseEmitter); + } + + @GetMapping("/send-to-all") + public void sendToAll(@RequestParam String message) { + String email = SecurityContextHolder.getContext().getAuthentication().getName(); + sseEmitterMap.sendToAll("send-to-all", email + ": " + message); + } + + @GetMapping("/me") + public ResponseEntity getMyNotifications() { + String email = SecurityContextHolder.getContext().getAuthentication().getName(); + return ResponseEntity.ok(notificationService.getMyNotifications(email)); + } + + @PatchMapping("/{notificationId}") + public ResponseEntity readNotification(@PathVariable Long notificationId) { + String email = SecurityContextHolder.getContext().getAuthentication().getName(); + notificationService.readNotification(notificationId, email); + return ResponseEntity.ok().build(); + } + + @DeleteMapping("/{notificationId}") + public ResponseEntity deleteNotification(@PathVariable Long notificationId) { + String email = SecurityContextHolder.getContext().getAuthentication().getName(); + notificationService.deleteNotification(notificationId, email); + return ResponseEntity.ok().build(); + } + + @DeleteMapping("/me") + public ResponseEntity deleteMyNotifications() { + String email = SecurityContextHolder.getContext().getAuthentication().getName(); + notificationService.deleteMyNotifications(email); + return ResponseEntity.ok().build(); + } +} diff --git a/src/main/java/com/api/trip/domain/notification/controller/dto/GetMyNotificationsResponse.java b/src/main/java/com/api/trip/domain/notification/controller/dto/GetMyNotificationsResponse.java new file mode 100644 index 0000000..d1f85be --- /dev/null +++ b/src/main/java/com/api/trip/domain/notification/controller/dto/GetMyNotificationsResponse.java @@ -0,0 +1,36 @@ +package com.api.trip.domain.notification.controller.dto; + +import com.api.trip.domain.itemtag.model.ItemTag; +import com.api.trip.domain.notification.domain.Notification; +import lombok.Builder; +import lombok.Getter; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Getter +@Builder +public class GetMyNotificationsResponse { + + private List notifications; + private int unread; + + public static GetMyNotificationsResponse of(List notifications, List itemTags) { + Map> map = itemTags.stream() + .collect(Collectors.groupingBy(itemTag -> itemTag.getItem().getId())); + + List notificationResponses = notifications.stream() + .map(notification -> NotificationResponse.of(notification, map.get(notification.getItem().getId()))) + .toList(); + + int unread = (int) notifications.stream() + .filter(notification -> !notification.isRead()) + .count(); + + return builder() + .notifications(notificationResponses) + .unread(unread) + .build(); + } +} diff --git a/src/main/java/com/api/trip/domain/notification/controller/dto/NotificationResponse.java b/src/main/java/com/api/trip/domain/notification/controller/dto/NotificationResponse.java new file mode 100644 index 0000000..a5de459 --- /dev/null +++ b/src/main/java/com/api/trip/domain/notification/controller/dto/NotificationResponse.java @@ -0,0 +1,33 @@ +package com.api.trip.domain.notification.controller.dto; + +import com.api.trip.domain.itemtag.model.ItemTag; +import com.api.trip.domain.notification.domain.Notification; +import lombok.Builder; +import lombok.Getter; + +import java.time.LocalDateTime; +import java.util.List; + +@Getter +@Builder +public class NotificationResponse { + + private Long notificationId; + private Long itemId; + private String itemTitle; + private List tags; + private boolean read; + private LocalDateTime createdAt; + + public static NotificationResponse of(Notification notification, List itemTags) { + List tagNames = itemTags.stream().map(itemTag -> itemTag.getTag().getName()).toList(); + return builder() + .notificationId(notification.getId()) + .itemId(notification.getItem().getId()) + .itemTitle(notification.getItem().getTitle()) + .tags(tagNames) + .read(notification.isRead()) + .createdAt(notification.getCreatedAt()) + .build(); + } +} diff --git a/src/main/java/com/api/trip/domain/notification/domain/Notification.java b/src/main/java/com/api/trip/domain/notification/domain/Notification.java new file mode 100644 index 0000000..9b5f352 --- /dev/null +++ b/src/main/java/com/api/trip/domain/notification/domain/Notification.java @@ -0,0 +1,41 @@ +package com.api.trip.domain.notification.domain; + +import com.api.trip.common.auditing.entity.BaseTimeEntity; +import com.api.trip.domain.item.model.Item; +import com.api.trip.domain.member.model.Member; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Table(uniqueConstraints = {@UniqueConstraint(columnNames = {"member_id", "item_id"})}) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +public class Notification extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private Member member; + + @ManyToOne(fetch = FetchType.LAZY) + private Item item; + + @Column(nullable = false) + private boolean read; + + @Builder + private Notification(Member member, Item item, boolean read) { + this.member = member; + this.item = item; + this.read = read; + } + + public void read() { + read = true; + } +} diff --git a/src/main/java/com/api/trip/domain/notification/emitter/SseEmitterMap.java b/src/main/java/com/api/trip/domain/notification/emitter/SseEmitterMap.java new file mode 100644 index 0000000..26013c6 --- /dev/null +++ b/src/main/java/com/api/trip/domain/notification/emitter/SseEmitterMap.java @@ -0,0 +1,55 @@ +package com.api.trip.domain.notification.emitter; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +import java.io.IOException; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import static org.springframework.web.servlet.mvc.method.annotation.SseEmitter.SseEventBuilder; +import static org.springframework.web.servlet.mvc.method.annotation.SseEmitter.event; + +@Component +@Slf4j +public class SseEmitterMap { + + private final Map sseEmitterMap = new ConcurrentHashMap<>(); + + public void put(Long memberId, SseEmitter sseEmitter) { + sseEmitter.onCompletion(() -> remove(memberId)); + sseEmitter.onTimeout(sseEmitter::complete); + sseEmitterMap.put(memberId, sseEmitter); + log.info("connected with {}, the number of connections is {}", memberId, sseEmitterMap.size()); + } + + public void remove(Long memberId) { + sseEmitterMap.remove(memberId); + log.info("disconnected with {}, the number of connections is {}", memberId, sseEmitterMap.size()); + } + + public void send(Long memberId, String eventName, Object eventData) { + SseEmitter sseEmitter = sseEmitterMap.get(memberId); + try { + sseEmitter.send( + event() + .name(eventName) + .data(eventData) + ); + } catch (IOException | IllegalStateException e) { + remove(memberId); + } + } + + public void sendToAll(String eventName, Object eventData) { + SseEventBuilder sseEventBuilder = event().name(eventName).data(eventData); + sseEmitterMap.forEach((memberId, sseEmitter) -> { + try { + sseEmitter.send(sseEventBuilder); + } catch (IOException | IllegalStateException e) { + remove(memberId); + } + }); + } +} diff --git a/src/main/java/com/api/trip/domain/notification/repository/NotificationRepository.java b/src/main/java/com/api/trip/domain/notification/repository/NotificationRepository.java new file mode 100644 index 0000000..a9e1430 --- /dev/null +++ b/src/main/java/com/api/trip/domain/notification/repository/NotificationRepository.java @@ -0,0 +1,12 @@ +package com.api.trip.domain.notification.repository; + +import com.api.trip.domain.member.model.Member; +import com.api.trip.domain.notification.domain.Notification; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface NotificationRepository extends JpaRepository, NotificationRepositoryCustom { + + List findAllByMember(Member member); +} diff --git a/src/main/java/com/api/trip/domain/notification/repository/NotificationRepositoryCustom.java b/src/main/java/com/api/trip/domain/notification/repository/NotificationRepositoryCustom.java new file mode 100644 index 0000000..efa6246 --- /dev/null +++ b/src/main/java/com/api/trip/domain/notification/repository/NotificationRepositoryCustom.java @@ -0,0 +1,15 @@ +package com.api.trip.domain.notification.repository; + +import com.api.trip.domain.item.model.Item; +import com.api.trip.domain.itemtag.model.ItemTag; +import com.api.trip.domain.member.model.Member; +import com.api.trip.domain.notification.domain.Notification; + +import java.util.List; + +public interface NotificationRepositoryCustom { + + List findNotifications(Member member); + + List findItemTags(List items); +} diff --git a/src/main/java/com/api/trip/domain/notification/repository/NotificationRepositoryCustomImpl.java b/src/main/java/com/api/trip/domain/notification/repository/NotificationRepositoryCustomImpl.java new file mode 100644 index 0000000..b696953 --- /dev/null +++ b/src/main/java/com/api/trip/domain/notification/repository/NotificationRepositoryCustomImpl.java @@ -0,0 +1,42 @@ +package com.api.trip.domain.notification.repository; + +import com.api.trip.domain.item.model.Item; +import com.api.trip.domain.itemtag.model.ItemTag; +import com.api.trip.domain.member.model.Member; +import com.api.trip.domain.notification.domain.Notification; +import com.querydsl.jpa.impl.JPAQueryFactory; +import jakarta.persistence.EntityManager; + +import java.util.List; + +import static com.api.trip.domain.item.model.QItem.item; +import static com.api.trip.domain.itemtag.model.QItemTag.itemTag; +import static com.api.trip.domain.notification.domain.QNotification.notification; +import static com.api.trip.domain.tag.model.QTag.tag; + +public class NotificationRepositoryCustomImpl implements NotificationRepositoryCustom { + + private final JPAQueryFactory jpaQueryFactory; + + public NotificationRepositoryCustomImpl(EntityManager em) { + this.jpaQueryFactory = new JPAQueryFactory(em); + } + + @Override + public List findNotifications(Member member) { + return jpaQueryFactory + .selectFrom(notification) + .innerJoin(notification.item, item).fetchJoin() + .where(notification.member.eq(member)) + .fetch(); + } + + @Override + public List findItemTags(List items) { + return jpaQueryFactory + .selectFrom(itemTag) + .innerJoin(itemTag.tag, tag).fetchJoin() + .where(itemTag.item.in(items)) + .fetch(); + } +} diff --git a/src/main/java/com/api/trip/domain/notification/service/NotificationService.java b/src/main/java/com/api/trip/domain/notification/service/NotificationService.java new file mode 100644 index 0000000..6fb4a7d --- /dev/null +++ b/src/main/java/com/api/trip/domain/notification/service/NotificationService.java @@ -0,0 +1,77 @@ +package com.api.trip.domain.notification.service; + +import com.api.trip.common.exception.CustomException; +import com.api.trip.common.exception.ErrorCode; +import com.api.trip.domain.itemtag.model.ItemTag; +import com.api.trip.domain.member.model.Member; +import com.api.trip.domain.member.repository.MemberRepository; +import com.api.trip.domain.notification.controller.dto.GetMyNotificationsResponse; +import com.api.trip.domain.notification.domain.Notification; +import com.api.trip.domain.notification.repository.NotificationRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +@Transactional +@RequiredArgsConstructor +public class NotificationService { + + private final MemberRepository memberRepository; + private final NotificationRepository notificationRepository; + + @Transactional(readOnly = true) + public GetMyNotificationsResponse getMyNotifications(String email) { + Member member = memberRepository.findByEmail(email) + .orElseThrow(() -> new CustomException(ErrorCode.UNAUTHORIZED)); + + List notifications = notificationRepository.findNotifications(member); + + List itemTags = notificationRepository.findItemTags( + notifications.stream() + .map(Notification::getItem) + .toList() + ); + + return GetMyNotificationsResponse.of(notifications, itemTags); + } + + public void readNotification(Long notificationId, String email) { + Member member = memberRepository.findByEmail(email) + .orElseThrow(() -> new CustomException(ErrorCode.UNAUTHORIZED)); + + Notification notification = notificationRepository.findById(notificationId) + .orElseThrow(() -> new CustomException(ErrorCode.NOTIFICATION_NOT_FOUND)); + + if (notification.getMember() == member) { + throw new CustomException(ErrorCode.FORBIDDEN); + } + + notification.read(); + } + + public void deleteNotification(Long notificationId, String email) { + Member member = memberRepository.findByEmail(email) + .orElseThrow(() -> new CustomException(ErrorCode.UNAUTHORIZED)); + + Notification notification = notificationRepository.findById(notificationId) + .orElseThrow(() -> new CustomException(ErrorCode.NOTIFICATION_NOT_FOUND)); + + if (notification.getMember() == member) { + throw new CustomException(ErrorCode.FORBIDDEN); + } + + notificationRepository.delete(notification); + } + + public void deleteMyNotifications(String email) { + Member member = memberRepository.findByEmail(email) + .orElseThrow(() -> new CustomException(ErrorCode.UNAUTHORIZED)); + + List notifications = notificationRepository.findAllByMember(member); + + notificationRepository.deleteAllInBatch(notifications); + } +} diff --git a/src/main/resources/static/index.html b/src/main/resources/static/index.html new file mode 100644 index 0000000..0fe3af2 --- /dev/null +++ b/src/main/resources/static/index.html @@ -0,0 +1,42 @@ + + + + + Title + + + + +
+ + + + + + diff --git a/src/main/resources/static/src/eventsource.js b/src/main/resources/static/src/eventsource.js new file mode 100644 index 0000000..980b525 --- /dev/null +++ b/src/main/resources/static/src/eventsource.js @@ -0,0 +1,1048 @@ +/** @license + * eventsource.js + * Available under MIT License (MIT) + * https://github.com/Yaffle/EventSource/ + */ + +/*jslint indent: 2, vars: true, plusplus: true */ +/*global setTimeout, clearTimeout */ + +(function (global) { + "use strict"; + + var setTimeout = global.setTimeout; + var clearTimeout = global.clearTimeout; + var XMLHttpRequest = global.XMLHttpRequest; + var XDomainRequest = global.XDomainRequest; + var ActiveXObject = global.ActiveXObject; + var NativeEventSource = global.EventSource; + + var document = global.document; + var Promise = global.Promise; + var fetch = global.fetch; + var Response = global.Response; + var TextDecoder = global.TextDecoder; + var TextEncoder = global.TextEncoder; + var AbortController = global.AbortController; + + if (typeof window !== "undefined" && typeof document !== "undefined" && !("readyState" in document) && document.body == null) { // Firefox 2 + document.readyState = "loading"; + window.addEventListener("load", function (event) { + document.readyState = "complete"; + }, false); + } + + if (XMLHttpRequest == null && ActiveXObject != null) { // https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/Using_XMLHttpRequest_in_IE6 + XMLHttpRequest = function () { + return new ActiveXObject("Microsoft.XMLHTTP"); + }; + } + + if (Object.create == undefined) { + Object.create = function (C) { + function F(){} + F.prototype = C; + return new F(); + }; + } + + if (!Date.now) { + Date.now = function now() { + return new Date().getTime(); + }; + } + + // see #118 (Promise#finally with polyfilled Promise) + // see #123 (data URLs crash Edge) + // see #125 (CSP violations) + // see pull/#138 + // => No way to polyfill Promise#finally + + if (AbortController == undefined) { + var originalFetch2 = fetch; + fetch = function (url, options) { + var signal = options.signal; + return originalFetch2(url, {headers: options.headers, credentials: options.credentials, cache: options.cache}).then(function (response) { + var reader = response.body.getReader(); + signal._reader = reader; + if (signal._aborted) { + signal._reader.cancel(); + } + return { + status: response.status, + statusText: response.statusText, + headers: response.headers, + body: { + getReader: function () { + return reader; + } + } + }; + }); + }; + AbortController = function () { + this.signal = { + _reader: null, + _aborted: false + }; + this.abort = function () { + if (this.signal._reader != null) { + this.signal._reader.cancel(); + } + this.signal._aborted = true; + }; + }; + } + + function TextDecoderPolyfill() { + this.bitsNeeded = 0; + this.codePoint = 0; + } + + TextDecoderPolyfill.prototype.decode = function (octets) { + function valid(codePoint, shift, octetsCount) { + if (octetsCount === 1) { + return codePoint >= 0x0080 >> shift && codePoint << shift <= 0x07FF; + } + if (octetsCount === 2) { + return codePoint >= 0x0800 >> shift && codePoint << shift <= 0xD7FF || codePoint >= 0xE000 >> shift && codePoint << shift <= 0xFFFF; + } + if (octetsCount === 3) { + return codePoint >= 0x010000 >> shift && codePoint << shift <= 0x10FFFF; + } + throw new Error(); + } + function octetsCount(bitsNeeded, codePoint) { + if (bitsNeeded === 6 * 1) { + return codePoint >> 6 > 15 ? 3 : codePoint > 31 ? 2 : 1; + } + if (bitsNeeded === 6 * 2) { + return codePoint > 15 ? 3 : 2; + } + if (bitsNeeded === 6 * 3) { + return 3; + } + throw new Error(); + } + var REPLACER = 0xFFFD; + var string = ""; + var bitsNeeded = this.bitsNeeded; + var codePoint = this.codePoint; + for (var i = 0; i < octets.length; i += 1) { + var octet = octets[i]; + if (bitsNeeded !== 0) { + if (octet < 128 || octet > 191 || !valid(codePoint << 6 | octet & 63, bitsNeeded - 6, octetsCount(bitsNeeded, codePoint))) { + bitsNeeded = 0; + codePoint = REPLACER; + string += String.fromCharCode(codePoint); + } + } + if (bitsNeeded === 0) { + if (octet >= 0 && octet <= 127) { + bitsNeeded = 0; + codePoint = octet; + } else if (octet >= 192 && octet <= 223) { + bitsNeeded = 6 * 1; + codePoint = octet & 31; + } else if (octet >= 224 && octet <= 239) { + bitsNeeded = 6 * 2; + codePoint = octet & 15; + } else if (octet >= 240 && octet <= 247) { + bitsNeeded = 6 * 3; + codePoint = octet & 7; + } else { + bitsNeeded = 0; + codePoint = REPLACER; + } + if (bitsNeeded !== 0 && !valid(codePoint, bitsNeeded, octetsCount(bitsNeeded, codePoint))) { + bitsNeeded = 0; + codePoint = REPLACER; + } + } else { + bitsNeeded -= 6; + codePoint = codePoint << 6 | octet & 63; + } + if (bitsNeeded === 0) { + if (codePoint <= 0xFFFF) { + string += String.fromCharCode(codePoint); + } else { + string += String.fromCharCode(0xD800 + (codePoint - 0xFFFF - 1 >> 10)); + string += String.fromCharCode(0xDC00 + (codePoint - 0xFFFF - 1 & 0x3FF)); + } + } + } + this.bitsNeeded = bitsNeeded; + this.codePoint = codePoint; + return string; + }; + + // Firefox < 38 throws an error with stream option + var supportsStreamOption = function () { + try { + return new TextDecoder().decode(new TextEncoder().encode("test"), {stream: true}) === "test"; + } catch (error) { + console.debug("TextDecoder does not support streaming option. Using polyfill instead: " + error); + } + return false; + }; + + // IE, Edge + if (TextDecoder == undefined || TextEncoder == undefined || !supportsStreamOption()) { + TextDecoder = TextDecoderPolyfill; + } + + var k = function () { + }; + + function XHRWrapper(xhr) { + this.withCredentials = false; + this.readyState = 0; + this.status = 0; + this.statusText = ""; + this.responseText = ""; + this.onprogress = k; + this.onload = k; + this.onerror = k; + this.onreadystatechange = k; + this._contentType = ""; + this._xhr = xhr; + this._sendTimeout = 0; + this._abort = k; + } + + XHRWrapper.prototype.open = function (method, url) { + this._abort(true); + + var that = this; + var xhr = this._xhr; + var state = 1; + var timeout = 0; + + this._abort = function (silent) { + if (that._sendTimeout !== 0) { + clearTimeout(that._sendTimeout); + that._sendTimeout = 0; + } + if (state === 1 || state === 2 || state === 3) { + state = 4; + xhr.onload = k; + xhr.onerror = k; + xhr.onabort = k; + xhr.onprogress = k; + xhr.onreadystatechange = k; + // IE 8 - 9: XDomainRequest#abort() does not fire any event + // Opera < 10: XMLHttpRequest#abort() does not fire any event + xhr.abort(); + if (timeout !== 0) { + clearTimeout(timeout); + timeout = 0; + } + if (!silent) { + that.readyState = 4; + that.onabort(null); + that.onreadystatechange(); + } + } + state = 0; + }; + + var onStart = function () { + if (state === 1) { + //state = 2; + var status = 0; + var statusText = ""; + var contentType = undefined; + if (!("contentType" in xhr)) { + try { + status = xhr.status; + statusText = xhr.statusText; + contentType = xhr.getResponseHeader("Content-Type"); + } catch (error) { + // IE < 10 throws exception for `xhr.status` when xhr.readyState === 2 || xhr.readyState === 3 + // Opera < 11 throws exception for `xhr.status` when xhr.readyState === 2 + // https://bugs.webkit.org/show_bug.cgi?id=29121 + status = 0; + statusText = ""; + contentType = undefined; + // Firefox < 14, Chrome ?, Safari ? + // https://bugs.webkit.org/show_bug.cgi?id=29658 + // https://bugs.webkit.org/show_bug.cgi?id=77854 + } + } else { + status = 200; + statusText = "OK"; + contentType = xhr.contentType; + } + if (status !== 0) { + state = 2; + that.readyState = 2; + that.status = status; + that.statusText = statusText; + that._contentType = contentType; + that.onreadystatechange(); + } + } + }; + var onProgress = function () { + onStart(); + if (state === 2 || state === 3) { + state = 3; + var responseText = ""; + try { + responseText = xhr.responseText; + } catch (error) { + // IE 8 - 9 with XMLHttpRequest + } + that.readyState = 3; + that.responseText = responseText; + that.onprogress(); + } + }; + var onFinish = function (type, event) { + if (event == null || event.preventDefault == null) { + event = { + preventDefault: k + }; + } + // Firefox 52 fires "readystatechange" (xhr.readyState === 4) without final "readystatechange" (xhr.readyState === 3) + // IE 8 fires "onload" without "onprogress" + onProgress(); + if (state === 1 || state === 2 || state === 3) { + state = 4; + if (timeout !== 0) { + clearTimeout(timeout); + timeout = 0; + } + that.readyState = 4; + if (type === "load") { + that.onload(event); + } else if (type === "error") { + that.onerror(event); + } else if (type === "abort") { + that.onabort(event); + } else { + throw new TypeError(); + } + that.onreadystatechange(); + } + }; + var onReadyStateChange = function (event) { + if (xhr != undefined) { // Opera 12 + if (xhr.readyState === 4) { + if (!("onload" in xhr) || !("onerror" in xhr) || !("onabort" in xhr)) { + onFinish(xhr.responseText === "" ? "error" : "load", event); + } + } else if (xhr.readyState === 3) { + if (!("onprogress" in xhr)) { // testing XMLHttpRequest#responseText too many times is too slow in IE 11 + // and in Firefox 3.6 + onProgress(); + } + } else if (xhr.readyState === 2) { + onStart(); + } + } + }; + var onTimeout = function () { + timeout = setTimeout(function () { + onTimeout(); + }, 500); + if (xhr.readyState === 3) { + onProgress(); + } + }; + + // XDomainRequest#abort removes onprogress, onerror, onload + if ("onload" in xhr) { + xhr.onload = function (event) { + onFinish("load", event); + }; + } + if ("onerror" in xhr) { + xhr.onerror = function (event) { + onFinish("error", event); + }; + } + // improper fix to match Firefox behaviour, but it is better than just ignore abort + // see https://bugzilla.mozilla.org/show_bug.cgi?id=768596 + // https://bugzilla.mozilla.org/show_bug.cgi?id=880200 + // https://code.google.com/p/chromium/issues/detail?id=153570 + // IE 8 fires "onload" without "onprogress + if ("onabort" in xhr) { + xhr.onabort = function (event) { + onFinish("abort", event); + }; + } + + if ("onprogress" in xhr) { + xhr.onprogress = onProgress; + } + + // IE 8 - 9 (XMLHTTPRequest) + // Opera < 12 + // Firefox < 3.5 + // Firefox 3.5 - 3.6 - ? < 9.0 + // onprogress is not fired sometimes or delayed + // see also #64 (significant lag in IE 11) + if ("onreadystatechange" in xhr) { + xhr.onreadystatechange = function (event) { + onReadyStateChange(event); + }; + } + + if ("contentType" in xhr || !("ontimeout" in XMLHttpRequest.prototype)) { + url += (url.indexOf("?") === -1 ? "?" : "&") + "padding=true"; + } + xhr.open(method, url, true); + + if ("readyState" in xhr) { + // workaround for Opera 12 issue with "progress" events + // #91 (XMLHttpRequest onprogress not fired for streaming response in Edge 14-15-?) + timeout = setTimeout(function () { + onTimeout(); + }, 0); + } + }; + XHRWrapper.prototype.abort = function () { + this._abort(false); + }; + XHRWrapper.prototype.getResponseHeader = function (name) { + return this._contentType; + }; + XHRWrapper.prototype.setRequestHeader = function (name, value) { + var xhr = this._xhr; + if ("setRequestHeader" in xhr) { + xhr.setRequestHeader(name, value); + } + }; + XHRWrapper.prototype.getAllResponseHeaders = function () { + // XMLHttpRequest#getAllResponseHeaders returns null for CORS requests in Firefox 3.6.28 + return this._xhr.getAllResponseHeaders != undefined ? this._xhr.getAllResponseHeaders() || "" : ""; + }; + XHRWrapper.prototype.send = function () { + // loading indicator in Safari < ? (6), Chrome < 14, Firefox + // https://bugzilla.mozilla.org/show_bug.cgi?id=736723 + if ((!("ontimeout" in XMLHttpRequest.prototype) || (!("sendAsBinary" in XMLHttpRequest.prototype) && !("mozAnon" in XMLHttpRequest.prototype))) && + document != undefined && + document.readyState != undefined && + document.readyState !== "complete") { + var that = this; + that._sendTimeout = setTimeout(function () { + that._sendTimeout = 0; + that.send(); + }, 4); + return; + } + + var xhr = this._xhr; + // withCredentials should be set after "open" for Safari and Chrome (< 19 ?) + if ("withCredentials" in xhr) { + xhr.withCredentials = this.withCredentials; + } + try { + // xhr.send(); throws "Not enough arguments" in Firefox 3.0 + xhr.send(undefined); + } catch (error1) { + // Safari 5.1.7, Opera 12 + throw error1; + } + }; + + function toLowerCase(name) { + return name.replace(/[A-Z]/g, function (c) { + return String.fromCharCode(c.charCodeAt(0) + 0x20); + }); + } + + function HeadersPolyfill(all) { + // Get headers: implemented according to mozilla's example code: https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/getAllResponseHeaders#Example + var map = Object.create(null); + var array = all.split("\r\n"); + for (var i = 0; i < array.length; i += 1) { + var line = array[i]; + var parts = line.split(": "); + var name = parts.shift(); + var value = parts.join(": "); + map[toLowerCase(name)] = value; + } + this._map = map; + } + HeadersPolyfill.prototype.get = function (name) { + return this._map[toLowerCase(name)]; + }; + + if (XMLHttpRequest != null && XMLHttpRequest.HEADERS_RECEIVED == null) { // IE < 9, Firefox 3.6 + XMLHttpRequest.HEADERS_RECEIVED = 2; + } + + function XHRTransport() { + } + + XHRTransport.prototype.open = function (xhr, onStartCallback, onProgressCallback, onFinishCallback, url, withCredentials, headers) { + xhr.open("GET", url); + var offset = 0; + xhr.onprogress = function () { + var responseText = xhr.responseText; + var chunk = responseText.slice(offset); + offset += chunk.length; + onProgressCallback(chunk); + }; + xhr.onerror = function (event) { + event.preventDefault(); + onFinishCallback(new Error("NetworkError")); + }; + xhr.onload = function () { + onFinishCallback(null); + }; + xhr.onabort = function () { + onFinishCallback(null); + }; + xhr.onreadystatechange = function () { + if (xhr.readyState === XMLHttpRequest.HEADERS_RECEIVED) { + var status = xhr.status; + var statusText = xhr.statusText; + var contentType = xhr.getResponseHeader("Content-Type"); + var headers = xhr.getAllResponseHeaders(); + onStartCallback(status, statusText, contentType, new HeadersPolyfill(headers)); + } + }; + xhr.withCredentials = withCredentials; + for (var name in headers) { + if (Object.prototype.hasOwnProperty.call(headers, name)) { + xhr.setRequestHeader(name, headers[name]); + } + } + xhr.send(); + return xhr; + }; + + function HeadersWrapper(headers) { + this._headers = headers; + } + HeadersWrapper.prototype.get = function (name) { + return this._headers.get(name); + }; + + function FetchTransport() { + } + + FetchTransport.prototype.open = function (xhr, onStartCallback, onProgressCallback, onFinishCallback, url, withCredentials, headers) { + var reader = null; + var controller = new AbortController(); + var signal = controller.signal; + var textDecoder = new TextDecoder(); + fetch(url, { + headers: headers, + credentials: withCredentials ? "include" : "same-origin", + signal: signal, + cache: "no-store" + }).then(function (response) { + reader = response.body.getReader(); + onStartCallback(response.status, response.statusText, response.headers.get("Content-Type"), new HeadersWrapper(response.headers)); + // see https://github.com/promises-aplus/promises-spec/issues/179 + return new Promise(function (resolve, reject) { + var readNextChunk = function () { + reader.read().then(function (result) { + if (result.done) { + //Note: bytes in textDecoder are ignored + resolve(undefined); + } else { + var chunk = textDecoder.decode(result.value, {stream: true}); + onProgressCallback(chunk); + readNextChunk(); + } + })["catch"](function (error) { + reject(error); + }); + }; + readNextChunk(); + }); + })["catch"](function (error) { + if (error.name === "AbortError") { + return undefined; + } else { + return error; + } + }).then(function (error) { + onFinishCallback(error); + }); + return { + abort: function () { + if (reader != null) { + reader.cancel(); // https://bugzilla.mozilla.org/show_bug.cgi?id=1583815 + } + controller.abort(); + } + }; + }; + + function EventTarget() { + this._listeners = Object.create(null); + } + + function throwError(e) { + setTimeout(function () { + throw e; + }, 0); + } + + EventTarget.prototype.dispatchEvent = function (event) { + event.target = this; + var typeListeners = this._listeners[event.type]; + if (typeListeners != undefined) { + var length = typeListeners.length; + for (var i = 0; i < length; i += 1) { + var listener = typeListeners[i]; + try { + if (typeof listener.handleEvent === "function") { + listener.handleEvent(event); + } else { + listener.call(this, event); + } + } catch (e) { + throwError(e); + } + } + } + }; + EventTarget.prototype.addEventListener = function (type, listener) { + type = String(type); + var listeners = this._listeners; + var typeListeners = listeners[type]; + if (typeListeners == undefined) { + typeListeners = []; + listeners[type] = typeListeners; + } + var found = false; + for (var i = 0; i < typeListeners.length; i += 1) { + if (typeListeners[i] === listener) { + found = true; + } + } + if (!found) { + typeListeners.push(listener); + } + }; + EventTarget.prototype.removeEventListener = function (type, listener) { + type = String(type); + var listeners = this._listeners; + var typeListeners = listeners[type]; + if (typeListeners != undefined) { + var filtered = []; + for (var i = 0; i < typeListeners.length; i += 1) { + if (typeListeners[i] !== listener) { + filtered.push(typeListeners[i]); + } + } + if (filtered.length === 0) { + delete listeners[type]; + } else { + listeners[type] = filtered; + } + } + }; + + function Event(type) { + this.type = type; + this.target = undefined; + } + + function MessageEvent(type, options) { + Event.call(this, type); + this.data = options.data; + this.lastEventId = options.lastEventId; + } + + MessageEvent.prototype = Object.create(Event.prototype); + + function ConnectionEvent(type, options) { + Event.call(this, type); + this.status = options.status; + this.statusText = options.statusText; + this.headers = options.headers; + } + + ConnectionEvent.prototype = Object.create(Event.prototype); + + function ErrorEvent(type, options) { + Event.call(this, type); + this.error = options.error; + } + + ErrorEvent.prototype = Object.create(Event.prototype); + + var WAITING = -1; + var CONNECTING = 0; + var OPEN = 1; + var CLOSED = 2; + + var AFTER_CR = -1; + var FIELD_START = 0; + var FIELD = 1; + var VALUE_START = 2; + var VALUE = 3; + + var contentTypeRegExp = /^text\/event\-stream(;.*)?$/i; + + var MINIMUM_DURATION = 1000; + var MAXIMUM_DURATION = 18000000; + + var parseDuration = function (value, def) { + var n = value == null ? def : parseInt(value, 10); + if (n !== n) { + n = def; + } + return clampDuration(n); + }; + var clampDuration = function (n) { + return Math.min(Math.max(n, MINIMUM_DURATION), MAXIMUM_DURATION); + }; + + var fire = function (that, f, event) { + try { + if (typeof f === "function") { + f.call(that, event); + } + } catch (e) { + throwError(e); + } + }; + + function EventSourcePolyfill(url, options) { + EventTarget.call(this); + options = options || {}; + + this.onopen = undefined; + this.onmessage = undefined; + this.onerror = undefined; + + this.url = undefined; + this.readyState = undefined; + this.withCredentials = undefined; + this.headers = undefined; + + this._close = undefined; + + start(this, url, options); + } + + function getBestXHRTransport() { + return (XMLHttpRequest != undefined && ("withCredentials" in XMLHttpRequest.prototype)) || XDomainRequest == undefined + ? new XMLHttpRequest() + : new XDomainRequest(); + } + + var isFetchSupported = fetch != undefined && Response != undefined && "body" in Response.prototype; + + function start(es, url, options) { + url = String(url); + var withCredentials = Boolean(options.withCredentials); + var lastEventIdQueryParameterName = options.lastEventIdQueryParameterName || "lastEventId"; + + var initialRetry = clampDuration(1000); + var heartbeatTimeout = parseDuration(options.heartbeatTimeout, 45000); + + var lastEventId = ""; + var retry = initialRetry; + var wasActivity = false; + var textLength = 0; + var headers = options.headers || {}; + var TransportOption = options.Transport; + var xhr = isFetchSupported && TransportOption == undefined ? undefined : new XHRWrapper(TransportOption != undefined ? new TransportOption() : getBestXHRTransport()); + var transport = TransportOption != null && typeof TransportOption !== "string" ? new TransportOption() : (xhr == undefined ? new FetchTransport() : new XHRTransport()); + var abortController = undefined; + var timeout = 0; + var currentState = WAITING; + var dataBuffer = ""; + var lastEventIdBuffer = ""; + var eventTypeBuffer = ""; + + var textBuffer = ""; + var state = FIELD_START; + var fieldStart = 0; + var valueStart = 0; + + var onStart = function (status, statusText, contentType, headers) { + if (currentState === CONNECTING) { + if (status === 200 && contentType != undefined && contentTypeRegExp.test(contentType)) { + currentState = OPEN; + wasActivity = Date.now(); + retry = initialRetry; + es.readyState = OPEN; + var event = new ConnectionEvent("open", { + status: status, + statusText: statusText, + headers: headers + }); + es.dispatchEvent(event); + fire(es, es.onopen, event); + } else { + var message = ""; + if (status !== 200) { + if (statusText) { + statusText = statusText.replace(/\s+/g, " "); + } + message = "EventSource's response has a status " + status + " " + statusText + " that is not 200. Aborting the connection."; + } else { + message = "EventSource's response has a Content-Type specifying an unsupported type: " + (contentType == undefined ? "-" : contentType.replace(/\s+/g, " ")) + ". Aborting the connection."; + } + close(); + var event = new ConnectionEvent("error", { + status: status, + statusText: statusText, + headers: headers + }); + es.dispatchEvent(event); + fire(es, es.onerror, event); + console.error(message); + } + } + }; + + var onProgress = function (textChunk) { + if (currentState === OPEN) { + var n = -1; + for (var i = 0; i < textChunk.length; i += 1) { + var c = textChunk.charCodeAt(i); + if (c === "\n".charCodeAt(0) || c === "\r".charCodeAt(0)) { + n = i; + } + } + var chunk = (n !== -1 ? textBuffer : "") + textChunk.slice(0, n + 1); + textBuffer = (n === -1 ? textBuffer : "") + textChunk.slice(n + 1); + if (textChunk !== "") { + wasActivity = Date.now(); + textLength += textChunk.length; + } + for (var position = 0; position < chunk.length; position += 1) { + var c = chunk.charCodeAt(position); + if (state === AFTER_CR && c === "\n".charCodeAt(0)) { + state = FIELD_START; + } else { + if (state === AFTER_CR) { + state = FIELD_START; + } + if (c === "\r".charCodeAt(0) || c === "\n".charCodeAt(0)) { + if (state !== FIELD_START) { + if (state === FIELD) { + valueStart = position + 1; + } + var field = chunk.slice(fieldStart, valueStart - 1); + var value = chunk.slice(valueStart + (valueStart < position && chunk.charCodeAt(valueStart) === " ".charCodeAt(0) ? 1 : 0), position); + if (field === "data") { + dataBuffer += "\n"; + dataBuffer += value; + } else if (field === "id") { + lastEventIdBuffer = value; + } else if (field === "event") { + eventTypeBuffer = value; + } else if (field === "retry") { + initialRetry = parseDuration(value, initialRetry); + retry = initialRetry; + } else if (field === "heartbeatTimeout") { + heartbeatTimeout = parseDuration(value, heartbeatTimeout); + if (timeout !== 0) { + clearTimeout(timeout); + timeout = setTimeout(function () { + onTimeout(); + }, heartbeatTimeout); + } + } + } + if (state === FIELD_START) { + if (dataBuffer !== "") { + lastEventId = lastEventIdBuffer; + if (eventTypeBuffer === "") { + eventTypeBuffer = "message"; + } + var event = new MessageEvent(eventTypeBuffer, { + data: dataBuffer.slice(1), + lastEventId: lastEventIdBuffer + }); + es.dispatchEvent(event); + if (eventTypeBuffer === "open") { + fire(es, es.onopen, event); + } else if (eventTypeBuffer === "message") { + fire(es, es.onmessage, event); + } else if (eventTypeBuffer === "error") { + fire(es, es.onerror, event); + } + if (currentState === CLOSED) { + return; + } + } + dataBuffer = ""; + eventTypeBuffer = ""; + } + state = c === "\r".charCodeAt(0) ? AFTER_CR : FIELD_START; + } else { + if (state === FIELD_START) { + fieldStart = position; + state = FIELD; + } + if (state === FIELD) { + if (c === ":".charCodeAt(0)) { + valueStart = position + 1; + state = VALUE_START; + } + } else if (state === VALUE_START) { + state = VALUE; + } + } + } + } + } + }; + + var onFinish = function (error) { + if (currentState === OPEN || currentState === CONNECTING) { + currentState = WAITING; + if (timeout !== 0) { + clearTimeout(timeout); + timeout = 0; + } + timeout = setTimeout(function () { + onTimeout(); + }, retry); + retry = clampDuration(Math.min(initialRetry * 16, retry * 2)); + + es.readyState = CONNECTING; + var event = new ErrorEvent("error", {error: error}); + es.dispatchEvent(event); + fire(es, es.onerror, event); + if (error != undefined) { + console.error(error); + } + } + }; + + var close = function () { + currentState = CLOSED; + if (abortController != undefined) { + abortController.abort(); + abortController = undefined; + } + if (timeout !== 0) { + clearTimeout(timeout); + timeout = 0; + } + es.readyState = CLOSED; + }; + + var onTimeout = function () { + timeout = 0; + + if (currentState !== WAITING) { + if (!wasActivity && abortController != undefined) { + onFinish(new Error("No activity within " + heartbeatTimeout + " milliseconds." + " " + (currentState === CONNECTING ? "No response received." : textLength + " chars received.") + " " + "Reconnecting.")); + if (abortController != undefined) { + abortController.abort(); + abortController = undefined; + } + } else { + var nextHeartbeat = Math.max((wasActivity || Date.now()) + heartbeatTimeout - Date.now(), 1); + wasActivity = false; + timeout = setTimeout(function () { + onTimeout(); + }, nextHeartbeat); + } + return; + } + + wasActivity = false; + textLength = 0; + timeout = setTimeout(function () { + onTimeout(); + }, heartbeatTimeout); + + currentState = CONNECTING; + dataBuffer = ""; + eventTypeBuffer = ""; + lastEventIdBuffer = lastEventId; + textBuffer = ""; + fieldStart = 0; + valueStart = 0; + state = FIELD_START; + + // https://bugzilla.mozilla.org/show_bug.cgi?id=428916 + // Request header field Last-Event-ID is not allowed by Access-Control-Allow-Headers. + var requestURL = url; + if (url.slice(0, 5) !== "data:" && url.slice(0, 5) !== "blob:") { + if (lastEventId !== "") { + // Remove the lastEventId parameter if it's already part of the request URL. + var i = url.indexOf("?"); + requestURL = i === -1 ? url : url.slice(0, i + 1) + url.slice(i + 1).replace(/(?:^|&)([^=&]*)(?:=[^&]*)?/g, function (p, paramName) { + return paramName === lastEventIdQueryParameterName ? '' : p; + }); + // Append the current lastEventId to the request URL. + requestURL += (url.indexOf("?") === -1 ? "?" : "&") + lastEventIdQueryParameterName +"=" + encodeURIComponent(lastEventId); + } + } + var withCredentials = es.withCredentials; + var requestHeaders = {}; + requestHeaders["Accept"] = "text/event-stream"; + var headers = es.headers; + if (headers != undefined) { + for (var name in headers) { + if (Object.prototype.hasOwnProperty.call(headers, name)) { + requestHeaders[name] = headers[name]; + } + } + } + try { + abortController = transport.open(xhr, onStart, onProgress, onFinish, requestURL, withCredentials, requestHeaders); + } catch (error) { + close(); + throw error; + } + }; + + es.url = url; + es.readyState = CONNECTING; + es.withCredentials = withCredentials; + es.headers = headers; + es._close = close; + + onTimeout(); + } + + EventSourcePolyfill.prototype = Object.create(EventTarget.prototype); + EventSourcePolyfill.prototype.CONNECTING = CONNECTING; + EventSourcePolyfill.prototype.OPEN = OPEN; + EventSourcePolyfill.prototype.CLOSED = CLOSED; + EventSourcePolyfill.prototype.close = function () { + this._close(); + }; + + EventSourcePolyfill.CONNECTING = CONNECTING; + EventSourcePolyfill.OPEN = OPEN; + EventSourcePolyfill.CLOSED = CLOSED; + EventSourcePolyfill.prototype.withCredentials = undefined; + + var R = NativeEventSource + if (XMLHttpRequest != undefined && (NativeEventSource == undefined || !("withCredentials" in NativeEventSource.prototype))) { + // Why replace a native EventSource ? + // https://bugzilla.mozilla.org/show_bug.cgi?id=444328 + // https://bugzilla.mozilla.org/show_bug.cgi?id=831392 + // https://code.google.com/p/chromium/issues/detail?id=260144 + // https://code.google.com/p/chromium/issues/detail?id=225654 + // ... + R = EventSourcePolyfill; + } + + (function (factory) { + if (typeof module === "object" && typeof module.exports === "object") { + var v = factory(exports); + if (v !== undefined) module.exports = v; + } + else if (typeof define === "function" && define.amd) { + define(["exports"], factory); + } + else { + factory(global); + } + })(function (exports) { + exports.EventSourcePolyfill = EventSourcePolyfill; + exports.NativeEventSource = NativeEventSource; + exports.EventSource = R; + }); +}(typeof globalThis === 'undefined' ? (typeof window !== 'undefined' ? window : typeof self !== 'undefined' ? self : this) : globalThis));