feat: フォローリクエストに対応。フォローした後無限ループする問題を修正

This commit is contained in:
usbharu 2023-05-22 23:33:33 +09:00
parent c1f7f838ca
commit 5b6e0a3657
12 changed files with 83 additions and 48 deletions

View File

@ -66,7 +66,7 @@ fun Application.parent() {
HttpClient(CIO).config { HttpClient(CIO).config {
install(Logging) { install(Logging) {
logger = Logger.DEFAULT logger = Logger.DEFAULT
level = LogLevel.ALL level = LogLevel.INFO
} }
install(httpSignaturePlugin) { install(httpSignaturePlugin) {
keyMap = KtorKeyMap(get()) keyMap = KtorKeyMap(get())

View File

@ -0,0 +1,3 @@
package dev.usbharu.hideout.domain.model.hideout.entity
data class FollowRequest(val userId: Long, val followerId: Long)

View File

@ -35,5 +35,9 @@ interface IUserRepository {
suspend fun deleteFollower(id: Long, follower: Long) suspend fun deleteFollower(id: Long, follower: Long)
suspend fun findFollowersById(id: Long): List<User> suspend fun findFollowersById(id: Long): List<User>
suspend fun addFollowRequest(id: Long, follower: Long)
suspend fun deleteFollowRequest(id: Long, follower: Long)
suspend fun findFollowRequestsById(id: Long, follower: Long): Boolean
suspend fun nextId(): Long suspend fun nextId(): Long
} }

View File

@ -20,6 +20,8 @@ class UserRepository(private val database: Database, private val idGenerateServi
SchemaUtils.create(UsersFollowers) SchemaUtils.create(UsersFollowers)
SchemaUtils.createMissingTablesAndColumns(Users) SchemaUtils.createMissingTablesAndColumns(Users)
SchemaUtils.createMissingTablesAndColumns(UsersFollowers) SchemaUtils.createMissingTablesAndColumns(UsersFollowers)
SchemaUtils.create(FollowRequests)
SchemaUtils.createMissingTablesAndColumns(FollowRequests)
} }
} }
@ -180,6 +182,28 @@ class UserRepository(private val database: Database, private val idGenerateServi
} }
} }
override suspend fun addFollowRequest(id: Long, follower: Long) {
query {
FollowRequests.insert {
it[userId] = id
it[followerId] = follower
}
}
}
override suspend fun deleteFollowRequest(id: Long, follower: Long) {
query {
FollowRequests.deleteWhere { userId.eq(id) and followerId.eq(follower) }
}
}
override suspend fun findFollowRequestsById(id: Long, follower: Long): Boolean {
return query {
FollowRequests.select { (FollowRequests.userId eq id) and (FollowRequests.followerId eq follower) }
.singleOrNull() != null
}
}
override suspend fun delete(id: Long) { override suspend fun delete(id: Long) {
query { query {
Users.deleteWhere { Users.id.eq(id) } Users.deleteWhere { Users.id.eq(id) }
@ -253,3 +277,12 @@ object UsersFollowers : LongIdTable("users_followers") {
uniqueIndex(userId, followerId) uniqueIndex(userId, followerId)
} }
} }
object FollowRequests : LongIdTable("follow_requests") {
val userId = long("user_id").references(Users.id)
val followerId = long("follower_id").references(Users.id)
init {
uniqueIndex(userId, followerId)
}
}

View File

@ -71,7 +71,7 @@ fun Route.users(userService: IUserService, userApiService: IUserApiService) {
val userParameter = call.parameters["name"] val userParameter = call.parameters["name"]
?: throw ParameterNotExistException("Parameter(name='userName@domain') does not exist.") ?: throw ParameterNotExistException("Parameter(name='userName@domain') does not exist.")
if (userParameter.toLongOrNull() != null) { if (userParameter.toLongOrNull() != null) {
if (userService.follow(userParameter.toLong(), userId)) { if (userService.followRequest(userParameter.toLong(), userId)) {
return@post call.respond(HttpStatusCode.OK) return@post call.respond(HttpStatusCode.OK)
} else { } else {
return@post call.respond(HttpStatusCode.Accepted) return@post call.respond(HttpStatusCode.Accepted)
@ -79,7 +79,7 @@ fun Route.users(userService: IUserService, userApiService: IUserApiService) {
} }
val acct = AcctUtil.parse(userParameter) val acct = AcctUtil.parse(userParameter)
val targetUser = userApiService.findByAcct(acct) val targetUser = userApiService.findByAcct(acct)
if (userService.follow(targetUser.id, userId)) { if (userService.followRequest(targetUser.id, userId)) {
return@post call.respond(HttpStatusCode.OK) return@post call.respond(HttpStatusCode.OK)
} else { } else {
return@post call.respond(HttpStatusCode.Accepted) return@post call.respond(HttpStatusCode.Accepted)

View File

@ -49,6 +49,6 @@ class ActivityPubReceiveFollowServiceImpl(
val users = val users =
userService.findByUrls(listOf(targetActor, follow.actor ?: throw IllegalArgumentException("actor is null"))) userService.findByUrls(listOf(targetActor, follow.actor ?: throw IllegalArgumentException("actor is null")))
userService.follow(users.first { it.url == targetActor }.id, users.first { it.url == follow.actor }.id) userService.followRequest(users.first { it.url == targetActor }.id, users.first { it.url == follow.actor }.id)
} }
} }

View File

@ -26,7 +26,7 @@ class ActivityPubServiceImpl(
val logger: Logger = LoggerFactory.getLogger(this::class.java) val logger: Logger = LoggerFactory.getLogger(this::class.java)
override fun parseActivity(json: String): ActivityType { override fun parseActivity(json: String): ActivityType {
val readTree = configData.objectMapper.readTree(json) val readTree = configData.objectMapper.readTree(json)
logger.debug("readTree: {}", readTree) logger.trace("readTree: {}", readTree)
if (readTree.isObject.not()) { if (readTree.isObject.not()) {
throw JsonParseException("Json is not object.") throw JsonParseException("Json is not object.")
} }
@ -41,16 +41,9 @@ class ActivityPubServiceImpl(
@Suppress("CyclomaticComplexMethod", "NotImplementedDeclaration") @Suppress("CyclomaticComplexMethod", "NotImplementedDeclaration")
override suspend fun processActivity(json: String, type: ActivityType): ActivityPubResponse { override suspend fun processActivity(json: String, type: ActivityType): ActivityPubResponse {
logger.debug("proccess activity: {}", type)
return when (type) { return when (type) {
ActivityType.Accept -> activityPubAcceptService.receiveAccept(configData.objectMapper.readValue(json)) ActivityType.Accept -> activityPubAcceptService.receiveAccept(configData.objectMapper.readValue(json))
ActivityType.Add -> TODO()
ActivityType.Announce -> TODO()
ActivityType.Arrive -> TODO()
ActivityType.Block -> TODO()
ActivityType.Create -> TODO()
ActivityType.Delete -> TODO()
ActivityType.Dislike -> TODO()
ActivityType.Flag -> TODO()
ActivityType.Follow -> activityPubReceiveFollowService.receiveFollow( ActivityType.Follow -> activityPubReceiveFollowService.receiveFollow(
configData.objectMapper.readValue( configData.objectMapper.readValue(
json, json,
@ -58,25 +51,11 @@ class ActivityPubServiceImpl(
) )
) )
ActivityType.Ignore -> TODO()
ActivityType.Invite -> TODO()
ActivityType.Join -> TODO()
ActivityType.Leave -> TODO()
ActivityType.Like -> TODO()
ActivityType.Listen -> TODO()
ActivityType.Move -> TODO()
ActivityType.Offer -> TODO()
ActivityType.Question -> TODO()
ActivityType.Reject -> TODO()
ActivityType.Read -> TODO()
ActivityType.Remove -> TODO()
ActivityType.TentativeReject -> TODO()
ActivityType.TentativeAccept -> TODO()
ActivityType.Travel -> TODO()
ActivityType.Undo -> activityPubUndoService.receiveUndo(configData.objectMapper.readValue(json)) ActivityType.Undo -> activityPubUndoService.receiveUndo(configData.objectMapper.readValue(json))
ActivityType.Update -> TODO()
ActivityType.View -> TODO() else -> {
ActivityType.Other -> TODO() throw IllegalArgumentException("$type is not supported.")
}
} }
} }

View File

@ -39,13 +39,21 @@ interface IUserService {
suspend fun findFollowingByNameAndDomain(name: String, domain: String?): List<User> suspend fun findFollowingByNameAndDomain(name: String, domain: String?): List<User>
/** /**
* フォロワーを追加する * フォローリクエストを送信する
* *
* @param id * @param id
* @param follower * @param followerId
* @return リクエストが成功したか * @return リクエストが成功したか
*/ */
suspend fun follow(id: Long, follower: Long): Boolean suspend fun followRequest(id: Long, followerId: Long): Boolean
suspend fun unfollow(id: Long, follower: Long): Boolean /**
* フォローする
*
* @param id
* @param followerId
*/
suspend fun follow(id: Long, followerId: Long)
suspend fun unfollow(id: Long, followerId: Long): Boolean
} }

View File

@ -111,23 +111,31 @@ class UserService(
} }
// TODO APのフォロー処理を作る // TODO APのフォロー処理を作る
override suspend fun follow(id: Long, followerId: Long): Boolean { override suspend fun followRequest(id: Long, followerId: Long): Boolean {
val user = userRepository.findById(id) ?: throw UserNotFoundException("$id was not found.") val user = userRepository.findById(id) ?: throw UserNotFoundException("$id was not found.")
val follower = userRepository.findById(followerId) ?: throw UserNotFoundException("$followerId was not found.") val follower = userRepository.findById(followerId) ?: throw UserNotFoundException("$followerId was not found.")
if (follower.domain != Config.configData.domain) {
throw IllegalArgumentException("follower is not local user.")
}
return if (user.domain == Config.configData.domain) { return if (user.domain == Config.configData.domain) {
userRepository.createFollower(id, followerId) follow(id, followerId)
true true
} else { } else {
activityPubSendFollowService.sendFollow(SendFollowDto(follower, user)) if (userRepository.findFollowRequestsById(id, followerId)) {
// do-nothing
} else {
activityPubSendFollowService.sendFollow(SendFollowDto(follower, user))
}
false false
} }
} }
override suspend fun unfollow(id: Long, follower: Long): Boolean { override suspend fun follow(id: Long, followerId: Long) {
userRepository.deleteFollower(id, follower) userRepository.createFollower(id, followerId)
if (userRepository.findFollowRequestsById(id, followerId)) {
userRepository.deleteFollowRequest(id, followerId)
}
}
override suspend fun unfollow(id: Long, followerId: Long): Boolean {
userRepository.deleteFollower(id, followerId)
return false return false
} }
} }

View File

@ -4,7 +4,7 @@
<pattern>%d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern> <pattern>%d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder> </encoder>
</appender> </appender>
<root level="trace"> <root level="DEBUG">
<appender-ref ref="STDOUT"/> <appender-ref ref="STDOUT"/>
</root> </root>
<logger name="org.eclipse.jetty" level="INFO"/> <logger name="org.eclipse.jetty" level="INFO"/>

View File

@ -432,7 +432,7 @@ class UsersTest {
) )
} }
val userService = mock<IUserService> { val userService = mock<IUserService> {
onBlocking { follow(eq(1235), eq(1234)) } doReturn true onBlocking { followRequest(eq(1235), eq(1234)) } doReturn true
} }
application { application {
configureSerialization() configureSerialization()
@ -482,7 +482,7 @@ class UsersTest {
) )
} }
val userService = mock<IUserService> { val userService = mock<IUserService> {
onBlocking { follow(eq(1235), eq(1234)) } doReturn false onBlocking { followRequest(eq(1235), eq(1234)) } doReturn false
} }
application { application {
configureSerialization() configureSerialization()
@ -532,7 +532,7 @@ class UsersTest {
) )
} }
val userService = mock<IUserService> { val userService = mock<IUserService> {
onBlocking { follow(eq(1235), eq(1234)) } doReturn false onBlocking { followRequest(eq(1235), eq(1234)) } doReturn false
} }
application { application {
configureSerialization() configureSerialization()

View File

@ -116,7 +116,7 @@ class ActivityPubReceiveFollowServiceImplTest {
createdAt = Instant.now() createdAt = Instant.now()
) )
) )
onBlocking { follow(any(), any()) } doReturn false onBlocking { followRequest(any(), any()) } doReturn false
} }
val activityPubFollowService = val activityPubFollowService =
ActivityPubReceiveFollowServiceImpl( ActivityPubReceiveFollowServiceImpl(