Merge pull request #289 from usbharu/bugfix/posts-count

アカウントの投稿数が増えなかった問題を修正
This commit is contained in:
usbharu 2024-02-24 16:06:58 +09:00 committed by GitHub
commit df68cd237a
10 changed files with 141 additions and 11 deletions

View File

@ -30,4 +30,6 @@ interface PostRepository {
suspend fun findByApId(apId: String): Post? suspend fun findByApId(apId: String): Post?
suspend fun existByApIdWithLock(apId: String): Boolean suspend fun existByApIdWithLock(apId: String): Boolean
suspend fun findByActorId(actorId: Long): List<Post> suspend fun findByActorId(actorId: Long): List<Post>
suspend fun countByActorId(actorId: Long): Int
} }

View File

@ -52,17 +52,21 @@ interface RelationshipRepository {
suspend fun findByTargetIdAndFollowing(targetId: Long, following: Boolean): List<Relationship> suspend fun findByTargetIdAndFollowing(targetId: Long, following: Boolean): List<Relationship>
suspend fun countByTargetIdAndFollowing(targetId: Long, following: Boolean): Int
suspend fun countByUserIdAndFollowing(userId: Long, following: Boolean): Int
@Suppress("FunctionMaxLength") @Suppress("FunctionMaxLength")
suspend fun findByTargetIdAndFollowRequestAndIgnoreFollowRequest( suspend fun findByTargetIdAndFollowRequestAndIgnoreFollowRequest(
targetId: Long, targetId: Long,
followRequest: Boolean, followRequest: Boolean,
ignoreFollowRequest: Boolean, ignoreFollowRequest: Boolean,
page: Page.PageByMaxId page: Page.PageByMaxId,
): PaginationList<Relationship, Long> ): PaginationList<Relationship, Long>
suspend fun findByActorIdAndMuting( suspend fun findByActorIdAndMuting(
actorId: Long, actorId: Long,
muting: Boolean, muting: Boolean,
page: Page.PageByMaxId page: Page.PageByMaxId,
): PaginationList<Relationship, Long> ): PaginationList<Relationship, Long>
} }

View File

@ -92,11 +92,31 @@ class RelationshipRepositoryImpl : RelationshipRepository, AbstractRepository()
.map { it.toRelationships() } .map { it.toRelationships() }
} }
override suspend fun countByTargetIdAndFollowing(targetId: Long, following: Boolean): Int = query {
return@query Relationships
.selectAll()
.where {
Relationships.targetActorId eq targetId and (Relationships.following eq following)
}
.count()
.toInt()
}
override suspend fun countByUserIdAndFollowing(userId: Long, following: Boolean): Int = query {
return@query Relationships
.selectAll()
.where {
Relationships.actorId eq userId and (Relationships.following eq following)
}
.count()
.toInt()
}
override suspend fun findByTargetIdAndFollowRequestAndIgnoreFollowRequest( override suspend fun findByTargetIdAndFollowRequestAndIgnoreFollowRequest(
targetId: Long, targetId: Long,
followRequest: Boolean, followRequest: Boolean,
ignoreFollowRequest: Boolean, ignoreFollowRequest: Boolean,
page: Page.PageByMaxId page: Page.PageByMaxId,
): PaginationList<Relationship, Long> = query { ): PaginationList<Relationship, Long> = query {
val query = Relationships.selectAll().where { val query = Relationships.selectAll().where {
Relationships.targetActorId.eq(targetId).and(Relationships.followRequest.eq(followRequest)) Relationships.targetActorId.eq(targetId).and(Relationships.followRequest.eq(followRequest))
@ -115,7 +135,7 @@ class RelationshipRepositoryImpl : RelationshipRepository, AbstractRepository()
override suspend fun findByActorIdAndMuting( override suspend fun findByActorIdAndMuting(
actorId: Long, actorId: Long,
muting: Boolean, muting: Boolean,
page: Page.PageByMaxId page: Page.PageByMaxId,
): PaginationList<Relationship, Long> = query { ): PaginationList<Relationship, Long> = query {
val query = val query =
Relationships.selectAll().where { Relationships.actorId.eq(actorId).and(Relationships.muting.eq(muting)) } Relationships.selectAll().where { Relationships.actorId.eq(actorId).and(Relationships.muting.eq(muting)) }

View File

@ -133,6 +133,14 @@ class PostRepositoryImpl(
.selectAll().where { Posts.actorId eq actorId }.let(postQueryMapper::map) .selectAll().where { Posts.actorId eq actorId }.let(postQueryMapper::map)
} }
override suspend fun countByActorId(actorId: Long): Int = query {
return@query Posts
.selectAll()
.where { Posts.actorId eq actorId }
.count()
.toInt()
}
override suspend fun delete(id: Long): Unit = query { override suspend fun delete(id: Long): Unit = query {
Posts.deleteWhere { Posts.id eq id } Posts.deleteWhere { Posts.id eq id }
} }

View File

@ -33,4 +33,6 @@ interface UserService {
suspend fun deleteRemoteActor(actorId: Long) suspend fun deleteRemoteActor(actorId: Long)
suspend fun deleteLocalUser(userId: Long) suspend fun deleteLocalUser(userId: Long)
suspend fun updateUserStatistics(userId: Long)
} }

View File

@ -24,6 +24,7 @@ import dev.usbharu.hideout.core.domain.model.actor.Actor
import dev.usbharu.hideout.core.domain.model.actor.ActorRepository import dev.usbharu.hideout.core.domain.model.actor.ActorRepository
import dev.usbharu.hideout.core.domain.model.deletedActor.DeletedActor import dev.usbharu.hideout.core.domain.model.deletedActor.DeletedActor
import dev.usbharu.hideout.core.domain.model.deletedActor.DeletedActorRepository import dev.usbharu.hideout.core.domain.model.deletedActor.DeletedActorRepository
import dev.usbharu.hideout.core.domain.model.post.PostRepository
import dev.usbharu.hideout.core.domain.model.reaction.ReactionRepository import dev.usbharu.hideout.core.domain.model.reaction.ReactionRepository
import dev.usbharu.hideout.core.domain.model.relationship.RelationshipRepository import dev.usbharu.hideout.core.domain.model.relationship.RelationshipRepository
import dev.usbharu.hideout.core.domain.model.userdetails.UserDetail import dev.usbharu.hideout.core.domain.model.userdetails.UserDetail
@ -47,8 +48,8 @@ class UserServiceImpl(
private val reactionRepository: ReactionRepository, private val reactionRepository: ReactionRepository,
private val relationshipRepository: RelationshipRepository, private val relationshipRepository: RelationshipRepository,
private val postService: PostService, private val postService: PostService,
private val apSendDeleteService: APSendDeleteService private val apSendDeleteService: APSendDeleteService,
private val postRepository: PostRepository,
) : ) :
UserService { UserService {
@ -191,6 +192,22 @@ class UserServiceImpl(
deletedActorRepository.save(deletedActor) deletedActorRepository.save(deletedActor)
} }
override suspend fun updateUserStatistics(userId: Long) {
val actor = actorRepository.findByIdWithLock(userId) ?: throw UserNotFoundException.withId(userId)
val followerCount = relationshipRepository.countByTargetIdAndFollowing(userId, true)
val followingCount = relationshipRepository.countByUserIdAndFollowing(userId, true)
val postsCount = postRepository.countByActorId(userId)
actorRepository.save(
actor.copy(
followersCount = followerCount,
followingCount = followingCount,
postsCount = postsCount
)
)
}
companion object { companion object {
private val logger = LoggerFactory.getLogger(UserServiceImpl::class.java) private val logger = LoggerFactory.getLogger(UserServiceImpl::class.java)
} }

View File

@ -0,0 +1,54 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.mastodon.domain.exception
import dev.usbharu.hideout.domain.mastodon.model.generated.NotFoundResponse
import dev.usbharu.hideout.mastodon.domain.model.MastodonApiErrorResponse
class AccountNotFoundException : ClientException {
constructor(response: MastodonApiErrorResponse<NotFoundResponse>) : super(response)
constructor(message: String?, response: MastodonApiErrorResponse<NotFoundResponse>) : super(message, response)
constructor(message: String?, cause: Throwable?, response: MastodonApiErrorResponse<NotFoundResponse>) : super(
message,
cause,
response
)
constructor(cause: Throwable?, response: MastodonApiErrorResponse<NotFoundResponse>) : super(cause, response)
constructor(
message: String?,
cause: Throwable?,
enableSuppression: Boolean,
writableStackTrace: Boolean,
response: MastodonApiErrorResponse<NotFoundResponse>,
) : super(message, cause, enableSuppression, writableStackTrace, response)
fun getTypedResponse(): MastodonApiErrorResponse<NotFoundResponse> =
response as MastodonApiErrorResponse<NotFoundResponse>
companion object {
fun ofId(id: Long): AccountNotFoundException = AccountNotFoundException(
"id: $id was not found.",
MastodonApiErrorResponse(
NotFoundResponse(
"Record not found"
),
404
),
)
}
}

View File

@ -19,6 +19,7 @@ package dev.usbharu.hideout.mastodon.infrastructure.springweb
import dev.usbharu.hideout.domain.mastodon.model.generated.NotFoundResponse import dev.usbharu.hideout.domain.mastodon.model.generated.NotFoundResponse
import dev.usbharu.hideout.domain.mastodon.model.generated.UnprocessableEntityResponse import dev.usbharu.hideout.domain.mastodon.model.generated.UnprocessableEntityResponse
import dev.usbharu.hideout.domain.mastodon.model.generated.UnprocessableEntityResponseDetails import dev.usbharu.hideout.domain.mastodon.model.generated.UnprocessableEntityResponseDetails
import dev.usbharu.hideout.mastodon.domain.exception.AccountNotFoundException
import dev.usbharu.hideout.mastodon.domain.exception.StatusNotFoundException import dev.usbharu.hideout.mastodon.domain.exception.StatusNotFoundException
import dev.usbharu.hideout.mastodon.interfaces.api.account.MastodonAccountApiController import dev.usbharu.hideout.mastodon.interfaces.api.account.MastodonAccountApiController
import dev.usbharu.hideout.mastodon.interfaces.api.apps.MastodonAppsApiController import dev.usbharu.hideout.mastodon.interfaces.api.apps.MastodonAppsApiController
@ -98,6 +99,12 @@ class MastodonApiControllerAdvice {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ex.getTypedResponse().response) return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ex.getTypedResponse().response)
} }
@ExceptionHandler(AccountNotFoundException::class)
fun handleException(ex: AccountNotFoundException): ResponseEntity<NotFoundResponse> {
logger.warn("Account not found.", ex)
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ex.getTypedResponse().response)
}
companion object { companion object {
private val logger = LoggerFactory.getLogger(MastodonApiControllerAdvice::class.java) private val logger = LoggerFactory.getLogger(MastodonApiControllerAdvice::class.java)
} }

View File

@ -19,6 +19,7 @@ package dev.usbharu.hideout.mastodon.service.account
import dev.usbharu.hideout.application.external.Transaction import dev.usbharu.hideout.application.external.Transaction
import dev.usbharu.hideout.application.infrastructure.exposed.Page import dev.usbharu.hideout.application.infrastructure.exposed.Page
import dev.usbharu.hideout.application.infrastructure.exposed.PaginationList import dev.usbharu.hideout.application.infrastructure.exposed.PaginationList
import dev.usbharu.hideout.core.domain.exception.resource.UserNotFoundException
import dev.usbharu.hideout.core.domain.model.relationship.RelationshipRepository import dev.usbharu.hideout.core.domain.model.relationship.RelationshipRepository
import dev.usbharu.hideout.core.service.media.MediaService import dev.usbharu.hideout.core.service.media.MediaService
import dev.usbharu.hideout.core.service.relationship.RelationshipService import dev.usbharu.hideout.core.service.relationship.RelationshipService
@ -26,6 +27,7 @@ import dev.usbharu.hideout.core.service.user.UpdateUserDto
import dev.usbharu.hideout.core.service.user.UserCreateDto import dev.usbharu.hideout.core.service.user.UserCreateDto
import dev.usbharu.hideout.core.service.user.UserService import dev.usbharu.hideout.core.service.user.UserService
import dev.usbharu.hideout.domain.mastodon.model.generated.* import dev.usbharu.hideout.domain.mastodon.model.generated.*
import dev.usbharu.hideout.mastodon.domain.exception.AccountNotFoundException
import dev.usbharu.hideout.mastodon.interfaces.api.media.MediaRequest import dev.usbharu.hideout.mastodon.interfaces.api.media.MediaRequest
import dev.usbharu.hideout.mastodon.query.StatusQueryService import dev.usbharu.hideout.mastodon.query.StatusQueryService
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
@ -127,6 +129,8 @@ class AccountApiServiceImpl(
} }
override suspend fun verifyCredentials(userid: Long): CredentialAccount = transaction.transaction { override suspend fun verifyCredentials(userid: Long): CredentialAccount = transaction.transaction {
userService.updateUserStatistics(userid)
val account = accountService.findById(userid) val account = accountService.findById(userid)
from(account) from(account)
} }
@ -141,9 +145,16 @@ class AccountApiServiceImpl(
return@transaction fetchRelationship(loginUser, followTargetUserId) return@transaction fetchRelationship(loginUser, followTargetUserId)
} }
override suspend fun account(id: Long): Account = transaction.transaction { override suspend fun account(id: Long): Account {
return try {
transaction.transaction {
userService.updateUserStatistics(id)
return@transaction accountService.findById(id) return@transaction accountService.findById(id)
} }
} catch (e: UserNotFoundException) {
throw AccountNotFoundException.ofId(id)
}
}
override suspend fun relationships(userid: Long, id: List<Long>, withSuspended: Boolean): List<Relationship> = override suspend fun relationships(userid: Long, id: List<Long>, withSuspended: Boolean): List<Relationship> =
transaction.transaction { transaction.transaction {
@ -160,7 +171,7 @@ class AccountApiServiceImpl(
} }
} }
override suspend fun block(userid: Long, target: Long): Relationship = transaction.transaction { override suspend fun block(userid: Long, target: Long) = transaction.transaction {
relationshipService.block(userid, target) relationshipService.block(userid, target)
fetchRelationship(userid, target) fetchRelationship(userid, target)
@ -339,6 +350,9 @@ class AccountApiServiceImpl(
ignoreFollowRequestToTarget = false ignoreFollowRequestToTarget = false
) )
userService.updateUserStatistics(userid)
userService.updateUserStatistics(targetId)
return Relationship( return Relationship(
id = targetId.toString(), id = targetId.toString(),
following = relationship.following, following = relationship.following,

View File

@ -62,7 +62,8 @@ class ActorServiceTest {
reactionRepository = mock(), reactionRepository = mock(),
relationshipRepository = mock(), relationshipRepository = mock(),
postService = mock(), postService = mock(),
apSendDeleteService = mock() apSendDeleteService = mock(),
postRepository = mock()
) )
userService.createLocalUser(UserCreateDto("test", "testUser", "XXXXXXXXXXXXX", "test")) userService.createLocalUser(UserCreateDto("test", "testUser", "XXXXXXXXXXXXX", "test"))
verify(actorRepository, times(1)).save(any()) verify(actorRepository, times(1)).save(any())
@ -100,7 +101,8 @@ class ActorServiceTest {
reactionRepository = mock(), reactionRepository = mock(),
relationshipRepository = mock(), relationshipRepository = mock(),
postService = mock(), postService = mock(),
apSendDeleteService = mock() apSendDeleteService = mock(),
postRepository = mock()
) )
val user = RemoteUserCreateDto( val user = RemoteUserCreateDto(
name = "test", name = "test",