diff --git a/src/main/kotlin/dev/usbharu/hideout/core/domain/model/post/PostRepository.kt b/src/main/kotlin/dev/usbharu/hideout/core/domain/model/post/PostRepository.kt index d02c8506..390b8d0b 100644 --- a/src/main/kotlin/dev/usbharu/hideout/core/domain/model/post/PostRepository.kt +++ b/src/main/kotlin/dev/usbharu/hideout/core/domain/model/post/PostRepository.kt @@ -30,4 +30,6 @@ interface PostRepository { suspend fun findByApId(apId: String): Post? suspend fun existByApIdWithLock(apId: String): Boolean suspend fun findByActorId(actorId: Long): List + + suspend fun countByActorId(actorId: Long): Int } diff --git a/src/main/kotlin/dev/usbharu/hideout/core/domain/model/relationship/RelationshipRepository.kt b/src/main/kotlin/dev/usbharu/hideout/core/domain/model/relationship/RelationshipRepository.kt index 827b7f49..4780d30d 100644 --- a/src/main/kotlin/dev/usbharu/hideout/core/domain/model/relationship/RelationshipRepository.kt +++ b/src/main/kotlin/dev/usbharu/hideout/core/domain/model/relationship/RelationshipRepository.kt @@ -52,17 +52,21 @@ interface RelationshipRepository { suspend fun findByTargetIdAndFollowing(targetId: Long, following: Boolean): List + suspend fun countByTargetIdAndFollowing(targetId: Long, following: Boolean): Int + + suspend fun countByUserIdAndFollowing(userId: Long, following: Boolean): Int + @Suppress("FunctionMaxLength") suspend fun findByTargetIdAndFollowRequestAndIgnoreFollowRequest( targetId: Long, followRequest: Boolean, ignoreFollowRequest: Boolean, - page: Page.PageByMaxId + page: Page.PageByMaxId, ): PaginationList suspend fun findByActorIdAndMuting( actorId: Long, muting: Boolean, - page: Page.PageByMaxId + page: Page.PageByMaxId, ): PaginationList } diff --git a/src/main/kotlin/dev/usbharu/hideout/core/domain/model/relationship/RelationshipRepositoryImpl.kt b/src/main/kotlin/dev/usbharu/hideout/core/domain/model/relationship/RelationshipRepositoryImpl.kt index ebf46d50..481c3dd6 100644 --- a/src/main/kotlin/dev/usbharu/hideout/core/domain/model/relationship/RelationshipRepositoryImpl.kt +++ b/src/main/kotlin/dev/usbharu/hideout/core/domain/model/relationship/RelationshipRepositoryImpl.kt @@ -92,11 +92,31 @@ class RelationshipRepositoryImpl : RelationshipRepository, AbstractRepository() .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( targetId: Long, followRequest: Boolean, ignoreFollowRequest: Boolean, - page: Page.PageByMaxId + page: Page.PageByMaxId, ): PaginationList = query { val query = Relationships.selectAll().where { Relationships.targetActorId.eq(targetId).and(Relationships.followRequest.eq(followRequest)) @@ -115,7 +135,7 @@ class RelationshipRepositoryImpl : RelationshipRepository, AbstractRepository() override suspend fun findByActorIdAndMuting( actorId: Long, muting: Boolean, - page: Page.PageByMaxId + page: Page.PageByMaxId, ): PaginationList = query { val query = Relationships.selectAll().where { Relationships.actorId.eq(actorId).and(Relationships.muting.eq(muting)) } diff --git a/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedrepository/PostRepositoryImpl.kt b/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedrepository/PostRepositoryImpl.kt index 76b9b82b..ce2ec9ea 100644 --- a/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedrepository/PostRepositoryImpl.kt +++ b/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedrepository/PostRepositoryImpl.kt @@ -133,6 +133,14 @@ class PostRepositoryImpl( .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 { Posts.deleteWhere { Posts.id eq id } } diff --git a/src/main/kotlin/dev/usbharu/hideout/core/service/user/UserService.kt b/src/main/kotlin/dev/usbharu/hideout/core/service/user/UserService.kt index 2c632f57..50e8d485 100644 --- a/src/main/kotlin/dev/usbharu/hideout/core/service/user/UserService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/core/service/user/UserService.kt @@ -33,4 +33,6 @@ interface UserService { suspend fun deleteRemoteActor(actorId: Long) suspend fun deleteLocalUser(userId: Long) + + suspend fun updateUserStatistics(userId: Long) } diff --git a/src/main/kotlin/dev/usbharu/hideout/core/service/user/UserServiceImpl.kt b/src/main/kotlin/dev/usbharu/hideout/core/service/user/UserServiceImpl.kt index c15e5f79..5a459405 100644 --- a/src/main/kotlin/dev/usbharu/hideout/core/service/user/UserServiceImpl.kt +++ b/src/main/kotlin/dev/usbharu/hideout/core/service/user/UserServiceImpl.kt @@ -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.deletedActor.DeletedActor 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.relationship.RelationshipRepository import dev.usbharu.hideout.core.domain.model.userdetails.UserDetail @@ -47,8 +48,8 @@ class UserServiceImpl( private val reactionRepository: ReactionRepository, private val relationshipRepository: RelationshipRepository, private val postService: PostService, - private val apSendDeleteService: APSendDeleteService - + private val apSendDeleteService: APSendDeleteService, + private val postRepository: PostRepository, ) : UserService { @@ -191,6 +192,22 @@ class UserServiceImpl( 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 { private val logger = LoggerFactory.getLogger(UserServiceImpl::class.java) } diff --git a/src/main/kotlin/dev/usbharu/hideout/mastodon/domain/exception/AccountNotFoundException.kt b/src/main/kotlin/dev/usbharu/hideout/mastodon/domain/exception/AccountNotFoundException.kt new file mode 100644 index 00000000..0e52b36a --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/mastodon/domain/exception/AccountNotFoundException.kt @@ -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) : super(response) + constructor(message: String?, response: MastodonApiErrorResponse) : super(message, response) + constructor(message: String?, cause: Throwable?, response: MastodonApiErrorResponse) : super( + message, + cause, + response + ) + + constructor(cause: Throwable?, response: MastodonApiErrorResponse) : super(cause, response) + constructor( + message: String?, + cause: Throwable?, + enableSuppression: Boolean, + writableStackTrace: Boolean, + response: MastodonApiErrorResponse, + ) : super(message, cause, enableSuppression, writableStackTrace, response) + + fun getTypedResponse(): MastodonApiErrorResponse = + response as MastodonApiErrorResponse + + companion object { + fun ofId(id: Long): AccountNotFoundException = AccountNotFoundException( + "id: $id was not found.", + MastodonApiErrorResponse( + NotFoundResponse( + "Record not found" + ), + 404 + ), + ) + } +} \ No newline at end of file diff --git a/src/main/kotlin/dev/usbharu/hideout/mastodon/infrastructure/springweb/MastodonApiControllerAdvice.kt b/src/main/kotlin/dev/usbharu/hideout/mastodon/infrastructure/springweb/MastodonApiControllerAdvice.kt index 003b071f..b154f52b 100644 --- a/src/main/kotlin/dev/usbharu/hideout/mastodon/infrastructure/springweb/MastodonApiControllerAdvice.kt +++ b/src/main/kotlin/dev/usbharu/hideout/mastodon/infrastructure/springweb/MastodonApiControllerAdvice.kt @@ -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.UnprocessableEntityResponse 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.interfaces.api.account.MastodonAccountApiController 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) } + @ExceptionHandler(AccountNotFoundException::class) + fun handleException(ex: AccountNotFoundException): ResponseEntity { + logger.warn("Account not found.", ex) + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ex.getTypedResponse().response) + } + companion object { private val logger = LoggerFactory.getLogger(MastodonApiControllerAdvice::class.java) } diff --git a/src/main/kotlin/dev/usbharu/hideout/mastodon/service/account/AccountApiService.kt b/src/main/kotlin/dev/usbharu/hideout/mastodon/service/account/AccountApiService.kt index de3ab853..c9f15567 100644 --- a/src/main/kotlin/dev/usbharu/hideout/mastodon/service/account/AccountApiService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/mastodon/service/account/AccountApiService.kt @@ -19,6 +19,7 @@ package dev.usbharu.hideout.mastodon.service.account import dev.usbharu.hideout.application.external.Transaction import dev.usbharu.hideout.application.infrastructure.exposed.Page 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.service.media.MediaService 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.UserService 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.query.StatusQueryService import org.slf4j.LoggerFactory @@ -127,6 +129,8 @@ class AccountApiServiceImpl( } override suspend fun verifyCredentials(userid: Long): CredentialAccount = transaction.transaction { + userService.updateUserStatistics(userid) + val account = accountService.findById(userid) from(account) } @@ -141,8 +145,15 @@ class AccountApiServiceImpl( return@transaction fetchRelationship(loginUser, followTargetUserId) } - override suspend fun account(id: Long): Account = transaction.transaction { - return@transaction accountService.findById(id) + override suspend fun account(id: Long): Account { + return try { + transaction.transaction { + userService.updateUserStatistics(id) + return@transaction accountService.findById(id) + } + } catch (e: UserNotFoundException) { + throw AccountNotFoundException.ofId(id) + } } override suspend fun relationships(userid: Long, id: List, withSuspended: Boolean): List = @@ -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) fetchRelationship(userid, target) @@ -339,6 +350,9 @@ class AccountApiServiceImpl( ignoreFollowRequestToTarget = false ) + userService.updateUserStatistics(userid) + userService.updateUserStatistics(targetId) + return Relationship( id = targetId.toString(), following = relationship.following, diff --git a/src/test/kotlin/dev/usbharu/hideout/core/service/user/ActorServiceTest.kt b/src/test/kotlin/dev/usbharu/hideout/core/service/user/ActorServiceTest.kt index 0ff56011..e0eac4e4 100644 --- a/src/test/kotlin/dev/usbharu/hideout/core/service/user/ActorServiceTest.kt +++ b/src/test/kotlin/dev/usbharu/hideout/core/service/user/ActorServiceTest.kt @@ -62,7 +62,8 @@ class ActorServiceTest { reactionRepository = mock(), relationshipRepository = mock(), postService = mock(), - apSendDeleteService = mock() + apSendDeleteService = mock(), + postRepository = mock() ) userService.createLocalUser(UserCreateDto("test", "testUser", "XXXXXXXXXXXXX", "test")) verify(actorRepository, times(1)).save(any()) @@ -100,7 +101,8 @@ class ActorServiceTest { reactionRepository = mock(), relationshipRepository = mock(), postService = mock(), - apSendDeleteService = mock() + apSendDeleteService = mock(), + postRepository = mock() ) val user = RemoteUserCreateDto( name = "test",