Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

카톡 친구추가 API #242

Merged
merged 11 commits into from
May 14, 2024
17 changes: 17 additions & 0 deletions api/src/main/kotlin/handler/FriendHandler.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package com.wafflestudio.snu4t.handler

import com.wafflestudio.snu4t.common.dto.ListResponse
import com.wafflestudio.snu4t.common.dto.OkResponse
import com.wafflestudio.snu4t.friend.dto.FriendRequest
import com.wafflestudio.snu4t.friend.dto.FriendRequestLinkResponse
import com.wafflestudio.snu4t.friend.dto.FriendResponse
import com.wafflestudio.snu4t.friend.dto.FriendState
import com.wafflestudio.snu4t.friend.dto.UpdateFriendDisplayNameRequest
Expand Down Expand Up @@ -75,4 +77,19 @@ class FriendHandler(

friendService.breakFriend(friendId, userId)
}

suspend fun generateFriendLink(req: ServerRequest) = handle(req) {
val userId = req.userId
val friendRequestToken = friendService.generateFriendRequestLink(userId)

FriendRequestLinkResponse(friendRequestToken)
}

suspend fun acceptFriendByLink(req: ServerRequest) = handle(req) {
val userId = req.userId
val requestToken = req.pathVariable("requestToken")

friendService.acceptFriendByLink(userId, requestToken)
OkResponse()
}
}
2 changes: 2 additions & 0 deletions api/src/main/kotlin/router/MainRouter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,8 @@ class MainRouter(
DELETE("/{friendId}", friendHandler::breakFriend)
GET("/{friendId}/primary-table", friendTableHandler::getPrimaryTable)
GET("/{friendId}/coursebooks", friendTableHandler::getCoursebooks)
GET("/generate-link", friendHandler::generateFriendLink)
POST("/accept-link/{requestToken}", friendHandler::acceptFriendByLink)
GET("/{friendId}/registered-course-books", friendTableHandler::getCoursebooks) // TODO: delete
}
}
Expand Down
15 changes: 15 additions & 0 deletions api/src/main/kotlin/router/docs/UserDocs.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.wafflestudio.snu4t.router.docs

import com.wafflestudio.snu4t.common.dto.OkResponse
import com.wafflestudio.snu4t.friend.dto.FriendRequestLinkResponse
import com.wafflestudio.snu4t.users.dto.UserDto
import com.wafflestudio.snu4t.users.dto.UserLegacyDto
import com.wafflestudio.snu4t.users.dto.UserPatchRequest
Expand Down Expand Up @@ -67,5 +68,19 @@ import org.springframework.web.bind.annotation.RequestMethod
responses = [ApiResponse(responseCode = "200", content = [Content(schema = Schema(implementation = OkResponse::class))])]
),
),
RouterOperation(
path = "/v1/friend/generate-link", method = [RequestMethod.GET], produces = [MediaType.APPLICATION_JSON_VALUE],
operation = Operation(
operationId = "generateFriendLink",
responses = [ApiResponse(responseCode = "200", content = [Content(schema = Schema(implementation = FriendRequestLinkResponse::class))])]
),
),
RouterOperation(
path = "/v1/friend/accept-link/{requestToken}", method = [RequestMethod.POST], produces = [MediaType.APPLICATION_JSON_VALUE],
operation = Operation(
operationId = "acceptFriendByLink",
responses = [ApiResponse(responseCode = "200", content = [Content(schema = Schema(implementation = OkResponse::class))])]
),
),
)
annotation class UserDocs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.wafflestudio.snu4t.friend.dto

data class FriendRequestLinkResponse(
val requestToken: String,
)
46 changes: 46 additions & 0 deletions core/src/main/kotlin/friend/service/FriendService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import com.wafflestudio.snu4t.common.exception.FriendNotFoundException
import com.wafflestudio.snu4t.common.exception.InvalidDisplayNameException
import com.wafflestudio.snu4t.common.exception.InvalidFriendException
import com.wafflestudio.snu4t.common.exception.UserNotFoundByNicknameException
import com.wafflestudio.snu4t.common.exception.UserNotFoundException
import com.wafflestudio.snu4t.common.push.DeeplinkType
import com.wafflestudio.snu4t.common.push.dto.PushMessage
import com.wafflestudio.snu4t.friend.data.Friend
Expand All @@ -20,8 +21,15 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
import org.springframework.data.redis.core.ReactiveStringRedisTemplate
import org.springframework.data.redis.core.expireAndAwait
import org.springframework.data.redis.core.getAndAwait
import org.springframework.data.redis.core.setAndAwait
import org.springframework.stereotype.Service
import java.security.SecureRandom
import java.time.Duration
import java.time.LocalDateTime
import java.util.Base64

interface FriendService {
suspend fun getMyFriends(myUserId: String, state: FriendState): List<Pair<Friend, User>>
Expand All @@ -37,6 +45,10 @@ interface FriendService {
suspend fun breakFriend(friendId: String, userId: String)

suspend fun get(friendId: String): Friend?

suspend fun generateFriendRequestLink(userId: String): String

suspend fun acceptFriendByLink(userId: String, requestToken: String)
}

@Service
Expand All @@ -45,6 +57,7 @@ class FriendServiceImpl(
private val userNicknameService: UserNicknameService,
private val friendRepository: FriendRepository,
private val userRepository: UserRepository,
private val redisTemplate: ReactiveStringRedisTemplate
) : FriendService {
companion object {
private val coroutineScope = CoroutineScope(Dispatchers.Default + SupervisorJob())
Expand Down Expand Up @@ -148,4 +161,37 @@ class FriendServiceImpl(
override suspend fun get(friendId: String): Friend? {
return friendRepository.findById(friendId)
}

override suspend fun generateFriendRequestLink(userId: String): String {
val bytes = ByteArray(8)
SecureRandom.getInstanceStrong().nextBytes(bytes)
val encodedKey = Base64.getUrlEncoder().withoutPadding().encodeToString(bytes)
val redisKey = "friend-link:$encodedKey"

redisTemplate.opsForValue().setAndAwait(redisKey, userId)
redisTemplate.expireAndAwait(redisKey, Duration.ofDays(14))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

setAndAwait에 ttl 같이 넣을 수 있는 것 같아요

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

그리고 중복 체크하는거 하나 있으면 좋을 것 같아요
매우 낮은 확률이긴 한데 혹시 key 겹칠 수 있으니..

return encodedKey
}

override suspend fun acceptFriendByLink(userId: String, requestToken: String) {
val fromUserId = redisTemplate.opsForValue()
.getAndAwait("friend-link:$requestToken") ?: throw FriendNotFoundException
val fromUser = userRepository.findByIdAndActiveTrue(fromUserId) ?: throw UserNotFoundException
if (fromUser.id == userId)
throw InvalidFriendException
if (friendRepository.findByUserPair(fromUser.id!! to userId) != null)
throw DuplicateFriendException
val friend = friendRepository.save(
Friend(
fromUserId = fromUser.id,
toUserId = userId,
isAccepted = true,
)
)

coroutineScope.launch {
val toUser = requireNotNull(userRepository.findByIdAndActiveTrue(friend.toUserId))
sendFriendAcceptPush(friend.fromUserId, toUser)
}
}
}
Loading