diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/activitypub/service/activity/undo/APUndoProcessor.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/activitypub/service/activity/undo/APUndoProcessor.kt index bd80e6f9..e788929e 100644 --- a/hideout-core/src/main/kotlin/dev/usbharu/hideout/activitypub/service/activity/undo/APUndoProcessor.kt +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/activitypub/service/activity/undo/APUndoProcessor.kt @@ -31,6 +31,7 @@ import dev.usbharu.hideout.core.domain.model.post.PostRepository import dev.usbharu.hideout.core.service.post.PostService import dev.usbharu.hideout.core.service.reaction.ReactionService import dev.usbharu.hideout.core.service.relationship.RelationshipService +import dev.usbharu.hideout.core.service.user.UserService import org.springframework.stereotype.Service @Service @@ -41,7 +42,8 @@ class APUndoProcessor( private val reactionService: ReactionService, private val actorRepository: ActorRepository, private val postRepository: PostRepository, - private val postService: PostService + private val postService: PostService, + private val userService: UserService, ) : AbstractActivityPubProcessor(transaction) { override suspend fun internalProcess(activity: ActivityPubProcessContext) { val undo = activity.activity @@ -71,6 +73,11 @@ class APUndoProcessor( return } + "Delete" -> { + delete(undo) + return + } + else -> {} } TODO() @@ -124,6 +131,14 @@ class APUndoProcessor( postService.deleteRemote(findByApId) } + private suspend fun delete(undo: Undo) { + val announce = undo.apObject as Delete + + val actor = actorRepository.findByUrl(announce.actor) ?: throw UserNotFoundException.withUrl(announce.actor) + + userService.restorationRemoteActor(actor.id) + } + override fun isSupported(activityType: ActivityType): Boolean = activityType == ActivityType.Undo override fun type(): Class = Undo::class.java diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/activitypub/service/objects/user/APUserService.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/activitypub/service/objects/user/APUserService.kt index 8f1aa04f..336a4de5 100644 --- a/hideout-core/src/main/kotlin/dev/usbharu/hideout/activitypub/service/objects/user/APUserService.kt +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/activitypub/service/objects/user/APUserService.kt @@ -40,9 +40,13 @@ interface APUserService { * @param targetActor 署名するユーザー * @return */ - suspend fun fetchPerson(url: String, targetActor: String? = null): Person + suspend fun fetchPerson(url: String, targetActor: String? = null, idOverride: Long? = null): Person - suspend fun fetchPersonWithEntity(url: String, targetActor: String? = null): Pair + suspend fun fetchPersonWithEntity( + url: String, + targetActor: String? = null, + idOverride: Long? = null, + ): Pair } @Service @@ -51,7 +55,7 @@ class APUserServiceImpl( private val transaction: Transaction, private val applicationConfig: ApplicationConfig, private val apResourceResolveService: APResourceResolveService, - private val actorRepository: ActorRepository + private val actorRepository: ActorRepository, ) : APUserService { @@ -88,13 +92,17 @@ class APUserServiceImpl( ) } - override suspend fun fetchPerson(url: String, targetActor: String?): Person = - fetchPersonWithEntity(url, targetActor).first + override suspend fun fetchPerson(url: String, targetActor: String?, idOverride: Long?): Person = + fetchPersonWithEntity(url, targetActor, idOverride).first - override suspend fun fetchPersonWithEntity(url: String, targetActor: String?): Pair { + override suspend fun fetchPersonWithEntity( + url: String, + targetActor: String?, + idOverride: Long?, + ): Pair { val userEntity = actorRepository.findByUrl(url) - if (userEntity != null) { + if (userEntity != null && idOverride == null) { return entityToPerson(userEntity, userEntity.url) to userEntity } @@ -104,7 +112,7 @@ class APUserServiceImpl( val actor = actorRepository.findByUrlWithLock(id) - if (actor != null) { + if (actor != null && idOverride == null) { return person to actor } @@ -123,13 +131,14 @@ class APUserServiceImpl( followers = person.followers, sharedInbox = person.endpoints["sharedInbox"], locked = person.manuallyApprovesFollowers - ) + ), + idOverride ) } private fun entityToPerson( actorEntity: Actor, - id: String + id: String, ) = Person( type = emptyList(), name = actorEntity.name, diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/deletedActor/DeletedActor.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/deletedActor/DeletedActor.kt index 43e296ee..0455435f 100644 --- a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/deletedActor/DeletedActor.kt +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/deletedActor/DeletedActor.kt @@ -22,6 +22,7 @@ data class DeletedActor( val id: Long, val name: String, val domain: String, + val apiId: String, val publicKey: String, - val deletedAt: Instant + val deletedAt: Instant, ) diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/post/Post.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/post/Post.kt index 881f6ca3..6e11792e 100644 --- a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/post/Post.kt +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/post/Post.kt @@ -43,7 +43,7 @@ data class Post private constructor( @get:URL val apId: String = url, val mediaIds: List = emptyList(), - val delted: Boolean = false, + val deleted: Boolean = false, val emojiIds: List = emptyList(), ) { @@ -67,7 +67,8 @@ data class Post private constructor( sensitive: Boolean = false, apId: String = url, mediaIds: List = emptyList(), - emojiIds: List = emptyList() + emojiIds: List = emptyList(), + deleted: Boolean = false, ): Post { require(id >= 0) { "id must be greater than or equal to 0." } @@ -109,7 +110,7 @@ data class Post private constructor( sensitive = sensitive, apId = apId, mediaIds = mediaIds, - delted = false, + deleted = deleted, emojiIds = emojiIds ) @@ -119,6 +120,10 @@ data class Post private constructor( throw IllegalArgumentException("${constraintViolation.propertyPath} : ${constraintViolation.message}") } + if (post.deleted) { + return post.delete() + } + return post } @@ -130,7 +135,7 @@ data class Post private constructor( createdAt: Instant, url: String, repost: Post, - apId: String + apId: String, ): Post { // リポストの公開範囲は元のポストより広くてはいけない val fixedVisibility = if (visibility.ordinal <= repost.visibility.ordinal) { @@ -139,16 +144,11 @@ data class Post private constructor( visibility } - require(id >= 0) { "id must be greater than or equal to 0." } - - require(actorId >= 0) { "actorId must be greater than or equal to 0." } - - val post = Post( + val post = of( id = id, actorId = actorId, overview = null, content = "", - text = "", createdAt = createdAt.toEpochMilli(), visibility = fixedVisibility, url = url, @@ -157,16 +157,9 @@ data class Post private constructor( sensitive = false, apId = apId, mediaIds = emptyList(), - delted = false, + deleted = false, emojiIds = emptyList() ) - - val validate = validator.validate(post) - - for (constraintViolation in validate) { - throw IllegalArgumentException("${constraintViolation.propertyPath} : ${constraintViolation.message}") - } - return post } @@ -184,7 +177,7 @@ data class Post private constructor( sensitive: Boolean = false, apId: String = url, mediaIds: List = emptyList(), - emojiIds: List = emptyList() + emojiIds: List = emptyList(), ): Post { // リポストの公開範囲は元のポストより広くてはいけない val fixedVisibility = if (visibility.ordinal <= repost.visibility.ordinal) { @@ -193,37 +186,11 @@ data class Post private constructor( visibility } - require(id >= 0) { "id must be greater than or equal to 0." } - - require(actorId >= 0) { "actorId must be greater than or equal to 0." } - - val limitedOverview = if ((overview?.length ?: 0) >= characterLimit.post.overview) { - overview?.substring(0, characterLimit.post.overview) - } else { - overview - } - - val limitedText = if (content.length >= characterLimit.post.text) { - content.substring(0, characterLimit.post.text) - } else { - content - } - - val (html, content1) = postContentFormatter.format(limitedText) - - require(url.isNotBlank()) { "url must contain non-blank characters" } - require(url.length <= characterLimit.general.url) { - "url must not exceed ${characterLimit.general.url} characters." - } - - require((replyId ?: 0) >= 0) { "replyId must be greater then or equal to 0." } - - val post = Post( + val post = of( id = id, actorId = actorId, - overview = limitedOverview, - content = html, - text = content1, + overview = overview, + content = content, createdAt = createdAt.toEpochMilli(), visibility = fixedVisibility, url = url, @@ -232,70 +199,26 @@ data class Post private constructor( sensitive = sensitive, apId = apId, mediaIds = mediaIds, - delted = false, + deleted = false, emojiIds = emojiIds ) - - val validate = validator.validate(post) - - for (constraintViolation in validate) { - throw IllegalArgumentException("${constraintViolation.propertyPath} : ${constraintViolation.message}") - } - return post } - @Suppress("LongParameterList") - fun deleteOf( - id: Long, - visibility: Visibility, - url: String, - repostId: Long?, - replyId: Long?, - apId: String - ): Post { - return Post( - id = id, - actorId = 0, - overview = null, - content = "", - text = "", - createdAt = Instant.EPOCH.toEpochMilli(), - visibility = visibility, - url = url, - repostId = repostId, - replyId = replyId, - sensitive = false, - apId = apId, - mediaIds = emptyList(), - delted = true - ) - } } fun isPureRepost(): Boolean = this.text.isEmpty() && - this.content.isEmpty() && - this.overview == null && - this.replyId == null && - this.repostId != null + this.content.isEmpty() && + this.overview == null && + this.replyId == null && + this.repostId != null fun delete(): Post { - return Post( - id = this.id, - actorId = 0, - overview = null, - content = "", - text = "", - createdAt = Instant.EPOCH.toEpochMilli(), - visibility = visibility, - url = url, - repostId = repostId, - replyId = replyId, - sensitive = false, - apId = apId, - mediaIds = emptyList(), - delted = true - ) + return copy(deleted = true) + } + + fun restore(): Post { + return copy(deleted = false) } } diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/post/PostRepository.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/post/PostRepository.kt index 390b8d0b..b6ced0ac 100644 --- a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/post/PostRepository.kt +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/domain/model/post/PostRepository.kt @@ -23,6 +23,7 @@ import org.springframework.stereotype.Repository interface PostRepository { suspend fun generateId(): Long suspend fun save(post: Post): Post + suspend fun saveAll(posts: List) suspend fun delete(id: Long) suspend fun findById(id: Long): Post? suspend fun findByUrl(url: String): Post? @@ -30,6 +31,7 @@ interface PostRepository { suspend fun findByApId(apId: String): Post? suspend fun existByApIdWithLock(apId: String): Boolean suspend fun findByActorId(actorId: Long): List + suspend fun findByActorIdAndDeleted(actorId: Long, deleted: Boolean): List suspend fun countByActorId(actorId: Long): Int } diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/external/job/UpdateActorTask.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/external/job/UpdateActorTask.kt new file mode 100644 index 00000000..8416e8a3 --- /dev/null +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/external/job/UpdateActorTask.kt @@ -0,0 +1,33 @@ +/* + * 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.core.external.job + +import dev.usbharu.owl.common.task.Task +import dev.usbharu.owl.common.task.TaskDefinition +import org.springframework.stereotype.Component + +data class UpdateActorTask( + val id: Long, + val apId: String, +) : Task() + + +@Component +data object UpdateActorTaskDef : TaskDefinition { + override val type: Class + get() = UpdateActorTask::class.java +} \ No newline at end of file diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposed/PostResultRowMapper.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposed/PostResultRowMapper.kt index fb222447..b912e929 100644 --- a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposed/PostResultRowMapper.kt +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposed/PostResultRowMapper.kt @@ -26,16 +26,6 @@ import org.springframework.stereotype.Component @Component class PostResultRowMapper(private val postBuilder: Post.PostBuilder) : ResultRowMapper { override fun map(resultRow: ResultRow): Post { - if (resultRow[Posts.deleted]) { - return postBuilder.deleteOf( - id = resultRow[Posts.id], - visibility = Visibility.values().first { it.ordinal == resultRow[Posts.visibility] }, - url = resultRow[Posts.url], - repostId = resultRow[Posts.repostId], - replyId = resultRow[Posts.replyId], - apId = resultRow[Posts.apId] - ) - } return postBuilder.of( id = resultRow[Posts.id], @@ -49,6 +39,7 @@ class PostResultRowMapper(private val postBuilder: Post.PostBuilder) : ResultRow replyId = resultRow[Posts.replyId], sensitive = resultRow[Posts.sensitive], apId = resultRow[Posts.apId], + deleted = resultRow[Posts.deleted], ) } } diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedrepository/DeletedActorRepositoryImpl.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedrepository/DeletedActorRepositoryImpl.kt index 6fc8f792..52398cf4 100644 --- a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedrepository/DeletedActorRepositoryImpl.kt +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedrepository/DeletedActorRepositoryImpl.kt @@ -39,6 +39,7 @@ class DeletedActorRepositoryImpl : DeletedActorRepository, AbstractRepository() it[id] = deletedActor.id it[name] = deletedActor.name it[domain] = deletedActor.domain + it[apId] = deletedActor.apiId it[publicKey] = deletedActor.publicKey it[deletedAt] = deletedActor.deletedAt } @@ -46,6 +47,7 @@ class DeletedActorRepositoryImpl : DeletedActorRepository, AbstractRepository() DeletedActors.update({ DeletedActors.id eq deletedActor.id }) { it[name] = deletedActor.name it[domain] = deletedActor.domain + it[apId] = deletedActor.apiId it[publicKey] = deletedActor.publicKey it[deletedAt] = deletedActor.deletedAt } @@ -84,6 +86,7 @@ private fun deletedActor(singleOr: ResultRow): DeletedActor { singleOr[DeletedActors.name], singleOr[DeletedActors.domain], singleOr[DeletedActors.publicKey], + singleOr[DeletedActors.apId], singleOr[DeletedActors.deletedAt] ) } @@ -92,6 +95,7 @@ object DeletedActors : Table("deleted_actors") { val id = long("id") val name = varchar("name", 300) val domain = varchar("domain", 255) + val apId = varchar("ap_id", 255).uniqueIndex() val publicKey = varchar("public_key", 10000).uniqueIndex() val deletedAt = timestamp("deleted_at") override val primaryKey: PrimaryKey = PrimaryKey(id) diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedrepository/PostRepositoryImpl.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedrepository/PostRepositoryImpl.kt index ce2ec9ea..c2dbcd28 100644 --- a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedrepository/PostRepositoryImpl.kt +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/infrastructure/exposedrepository/PostRepositoryImpl.kt @@ -20,6 +20,19 @@ import dev.usbharu.hideout.application.infrastructure.exposed.QueryMapper import dev.usbharu.hideout.application.service.id.IdGenerateService import dev.usbharu.hideout.core.domain.model.post.Post import dev.usbharu.hideout.core.domain.model.post.PostRepository +import dev.usbharu.hideout.core.infrastructure.exposedrepository.Posts.actorId +import dev.usbharu.hideout.core.infrastructure.exposedrepository.Posts.apId +import dev.usbharu.hideout.core.infrastructure.exposedrepository.Posts.content +import dev.usbharu.hideout.core.infrastructure.exposedrepository.Posts.createdAt +import dev.usbharu.hideout.core.infrastructure.exposedrepository.Posts.deleted +import dev.usbharu.hideout.core.infrastructure.exposedrepository.Posts.id +import dev.usbharu.hideout.core.infrastructure.exposedrepository.Posts.overview +import dev.usbharu.hideout.core.infrastructure.exposedrepository.Posts.replyId +import dev.usbharu.hideout.core.infrastructure.exposedrepository.Posts.repostId +import dev.usbharu.hideout.core.infrastructure.exposedrepository.Posts.sensitive +import dev.usbharu.hideout.core.infrastructure.exposedrepository.Posts.text +import dev.usbharu.hideout.core.infrastructure.exposedrepository.Posts.url +import dev.usbharu.hideout.core.infrastructure.exposedrepository.Posts.visibility import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq import org.slf4j.Logger @@ -29,7 +42,7 @@ import org.springframework.stereotype.Repository @Repository class PostRepositoryImpl( private val idGenerateService: IdGenerateService, - private val postQueryMapper: QueryMapper + private val postQueryMapper: QueryMapper, ) : PostRepository, AbstractRepository() { override val logger: Logger get() = Companion.logger @@ -37,7 +50,7 @@ class PostRepositoryImpl( override suspend fun generateId(): Long = idGenerateService.generateId() override suspend fun save(post: Post): Post = query { - val singleOrNull = Posts.selectAll().where { Posts.id eq post.id }.forUpdate().singleOrNull() + val singleOrNull = Posts.selectAll().where { id eq post.id }.forUpdate().singleOrNull() if (singleOrNull == null) { Posts.insert { it[id] = post.id @@ -52,7 +65,7 @@ class PostRepositoryImpl( it[replyId] = post.replyId it[sensitive] = post.sensitive it[apId] = post.apId - it[deleted] = post.delted + it[deleted] = post.deleted } PostsMedia.batchInsert(post.mediaIds) { this[PostsMedia.postId] = post.id @@ -77,7 +90,7 @@ class PostRepositoryImpl( this[PostsEmojis.postId] = post.id this[PostsEmojis.emojiId] = it } - Posts.update({ Posts.id eq post.id }) { + Posts.update({ id eq post.id }) { it[actorId] = post.actorId it[overview] = post.overview it[content] = post.content @@ -89,12 +102,45 @@ class PostRepositoryImpl( it[replyId] = post.replyId it[sensitive] = post.sensitive it[apId] = post.apId - it[deleted] = post.delted + it[deleted] = post.deleted } } return@query post } + override suspend fun saveAll(posts: List) { + Posts.batchUpsert( + posts, id, + ) { + this[id] = it.id + this[actorId] = it.actorId + this[overview] = it.overview + this[content] = it.content + this[text] = it.text + this[createdAt] = it.createdAt + this[visibility] = it.visibility.ordinal + this[url] = it.url + this[repostId] = it.repostId + this[replyId] = it.replyId + this[sensitive] = it.sensitive + this[apId] = it.apId + this[deleted] = it.deleted + } + val mediaIds = posts.flatMap { post -> post.mediaIds.map { post.id to it } } + PostsMedia.batchUpsert( + mediaIds, PostsMedia.postId + ) { + this[PostsMedia.postId] = it.first + this[PostsMedia.mediaId] = it.second + } + + val emojiIds = posts.flatMap { post -> post.emojiIds.map { post.id to it } } + PostsEmojis.batchUpsert(emojiIds, PostsEmojis.postId) { + this[PostsEmojis.postId] = it.first + this[PostsEmojis.emojiId] = it.second + } + } + override suspend fun findById(id: Long): Post? = query { return@query Posts .leftJoin(PostsMedia) @@ -133,6 +179,10 @@ class PostRepositoryImpl( .selectAll().where { Posts.actorId eq actorId }.let(postQueryMapper::map) } + override suspend fun findByActorIdAndDeleted(actorId: Long, deleted: Boolean): List { + TODO("Not yet implemented") + } + override suspend fun countByActorId(actorId: Long): Int = query { return@query Posts .selectAll() @@ -168,13 +218,13 @@ object Posts : Table() { } object PostsMedia : Table("posts_media") { - val postId = long("post_id").references(Posts.id, ReferenceOption.CASCADE, ReferenceOption.CASCADE) + val postId = long("post_id").references(id, ReferenceOption.CASCADE, ReferenceOption.CASCADE) val mediaId = long("media_id").references(Media.id, ReferenceOption.CASCADE, ReferenceOption.CASCADE) override val primaryKey = PrimaryKey(postId, mediaId) } object PostsEmojis : Table("posts_emojis") { - val postId = long("post_id").references(Posts.id) + val postId = long("post_id").references(id) val emojiId = long("emoji_id").references(CustomEmojis.id) override val primaryKey: PrimaryKey = PrimaryKey(postId, emojiId) } diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/service/post/PostService.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/service/post/PostService.kt index 8bc6a1d6..1a177666 100644 --- a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/service/post/PostService.kt +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/service/post/PostService.kt @@ -26,4 +26,5 @@ interface PostService { suspend fun deleteLocal(post: Post) suspend fun deleteRemote(post: Post) suspend fun deleteByActor(actorId: Long) + suspend fun restoreByRemoteActor(actorId: Long) } diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/service/post/PostServiceImpl.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/service/post/PostServiceImpl.kt index 879118a5..eb5c2dff 100644 --- a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/service/post/PostServiceImpl.kt +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/service/post/PostServiceImpl.kt @@ -18,9 +18,9 @@ package dev.usbharu.hideout.core.service.post import dev.usbharu.hideout.activitypub.service.activity.create.ApSendCreateService import dev.usbharu.hideout.activitypub.service.activity.delete.APSendDeleteService -import dev.usbharu.hideout.core.domain.exception.UserNotFoundException import dev.usbharu.hideout.core.domain.exception.resource.DuplicateException import dev.usbharu.hideout.core.domain.exception.resource.PostNotFoundException +import dev.usbharu.hideout.core.domain.exception.resource.UserNotFoundException import dev.usbharu.hideout.core.domain.model.actor.ActorRepository import dev.usbharu.hideout.core.domain.model.post.Post import dev.usbharu.hideout.core.domain.model.post.PostRepository @@ -38,7 +38,7 @@ class PostServiceImpl( private val postBuilder: Post.PostBuilder, private val apSendCreateService: ApSendCreateService, private val reactionRepository: ReactionRepository, - private val apSendDeleteService: APSendDeleteService + private val apSendDeleteService: APSendDeleteService, ) : PostService { override suspend fun createLocal(post: PostCreateDto): Post { @@ -52,14 +52,14 @@ class PostServiceImpl( override suspend fun createRemote(post: Post): Post { logger.info("START Create Remote Post user: {}, remote url: {}", post.actorId, post.apId) val actor = - actorRepository.findById(post.actorId) ?: throw UserNotFoundException("${post.actorId} was not found.") + actorRepository.findById(post.actorId) ?: throw UserNotFoundException.withId(post.actorId) val createdPost = internalCreate(post, false) logger.info("SUCCESS Create Remote Post url: {}", createdPost.url) return createdPost } override suspend fun deleteLocal(post: Post) { - if (post.delted) { + if (post.deleted) { return } reactionRepository.deleteByPostId(post.id) @@ -73,7 +73,7 @@ class PostServiceImpl( } override suspend fun deleteRemote(post: Post) { - if (post.delted) { + if (post.deleted) { return } reactionRepository.deleteByPostId(post.id) @@ -86,14 +86,25 @@ class PostServiceImpl( } override suspend fun deleteByActor(actorId: Long) { - postRepository.findByActorId(actorId).filterNot { it.delted }.forEach { postRepository.save(it.delete()) } - val actor = actorRepository.findById(actorId) ?: throw IllegalStateException("actor: $actorId was not found.") + postRepository.findByActorId(actorId).filterNot { it.deleted }.forEach { postRepository.save(it.delete()) } + + actorRepository.save(actor.copy(postsCount = 0, lastPostDate = null)) } + override suspend fun restoreByRemoteActor(actorId: Long) { + val actor = actorRepository.findById(actorId) ?: throw UserNotFoundException.withId(actorId) + + val postList = postRepository.findByActorIdAndDeleted(actorId, true).map { it.restore() } + + postRepository.saveAll(postList) + + actorRepository.save(actor.copy(postsCount = actor.postsCount.plus(postList.size))) + } + private suspend fun internalCreate(post: Post, isLocal: Boolean): Post { return try { val save = postRepository.save(post) diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/service/user/UserService.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/service/user/UserService.kt index 50e8d485..f1d42b42 100644 --- a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/service/user/UserService.kt +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/service/user/UserService.kt @@ -26,12 +26,14 @@ interface UserService { suspend fun createLocalUser(user: UserCreateDto): Actor - suspend fun createRemoteUser(user: RemoteUserCreateDto): Actor + suspend fun createRemoteUser(user: RemoteUserCreateDto, idOverride: Long? = null): Actor suspend fun updateUser(userId: Long, updateUserDto: UpdateUserDto) suspend fun deleteRemoteActor(actorId: Long) + suspend fun restorationRemoteActor(actorId: Long) + suspend fun deleteLocalUser(userId: Long) suspend fun updateUserStatistics(userId: Long) diff --git a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/service/user/UserServiceImpl.kt b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/service/user/UserServiceImpl.kt index 2149f343..f69e9510 100644 --- a/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/service/user/UserServiceImpl.kt +++ b/hideout-core/src/main/kotlin/dev/usbharu/hideout/core/service/user/UserServiceImpl.kt @@ -29,8 +29,10 @@ 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 import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailRepository +import dev.usbharu.hideout.core.external.job.UpdateActorTask import dev.usbharu.hideout.core.service.instance.InstanceService import dev.usbharu.hideout.core.service.post.PostService +import dev.usbharu.owl.producer.api.OwlProducer import org.slf4j.LoggerFactory import org.springframework.stereotype.Service import java.time.Instant @@ -50,6 +52,7 @@ class UserServiceImpl( private val postService: PostService, private val apSendDeleteService: APSendDeleteService, private val postRepository: PostRepository, + private val owlProducer: OwlProducer, ) : UserService { @@ -90,7 +93,7 @@ class UserServiceImpl( return save } - override suspend fun createRemoteUser(user: RemoteUserCreateDto): Actor { + override suspend fun createRemoteUser(user: RemoteUserCreateDto, idOverride: Long?): Actor { logger.info("START Create New remote user. name: {} url: {}", user.name, user.url) val deletedActor = deletedActorRepository.findByNameAndDomain(user.name, user.domain) @@ -104,7 +107,7 @@ class UserServiceImpl( val nextId = actorRepository.nextId() val userEntity = actorBuilder.of( - id = nextId, + id = idOverride ?: nextId, name = user.name, domain = user.domain, screenName = user.screenName, @@ -156,6 +159,7 @@ class UserServiceImpl( actor.id, actor.name, actor.domain, + actor.url, actor.publicKey, Instant.now() ) @@ -169,6 +173,15 @@ class UserServiceImpl( deletedActorRepository.save(deletedActor) } + override suspend fun restorationRemoteActor(actorId: Long) { + val deletedActor = deletedActorRepository.findById(actorId) + ?: return + + deletedActorRepository.delete(deletedActor) + + owlProducer.publishTask(UpdateActorTask(deletedActor.id, deletedActor.apiId)) + } + override suspend fun deleteLocalUser(userId: Long) { val actor = actorRepository.findByIdWithLock(userId) ?: throw UserNotFoundException.withId(userId) apSendDeleteService.sendDeleteActor(actor) @@ -176,6 +189,7 @@ class UserServiceImpl( actor.id, actor.name, actor.domain, + actor.url, actor.publicKey, Instant.now() ) diff --git a/hideout-core/src/test/kotlin/dev/usbharu/hideout/activitypub/service/objects/note/APNoteServiceImplTest.kt b/hideout-core/src/test/kotlin/dev/usbharu/hideout/activitypub/service/objects/note/APNoteServiceImplTest.kt index b5301461..3520c53a 100644 --- a/hideout-core/src/test/kotlin/dev/usbharu/hideout/activitypub/service/objects/note/APNoteServiceImplTest.kt +++ b/hideout-core/src/test/kotlin/dev/usbharu/hideout/activitypub/service/objects/note/APNoteServiceImplTest.kt @@ -154,7 +154,7 @@ class APNoteServiceImplTest { ) val apUserService = mock { - onBlocking { fetchPersonWithEntity(eq(note.attributedTo), isNull()) } doReturn (person to user) + onBlocking { fetchPersonWithEntity(eq(note.attributedTo), isNull(), anyOrNull()) } doReturn (person to user) } val postRepository = mock { onBlocking { generateId() } doReturn TwitterSnowflakeIdGenerateService.generateId() @@ -255,7 +255,7 @@ class APNoteServiceImplTest { followers = user.followers ) val apUserService = mock { - onBlocking { fetchPersonWithEntity(eq(user.url), anyOrNull()) } doReturn (person to user) + onBlocking { fetchPersonWithEntity(eq(user.url), anyOrNull(), anyOrNull()) } doReturn (person to user) } val postService = mock() val noteQueryService = mock { diff --git a/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/service/user/ActorServiceTest.kt b/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/service/user/ActorServiceTest.kt index 7a22a57b..834c3650 100644 --- a/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/service/user/ActorServiceTest.kt +++ b/hideout-core/src/test/kotlin/dev/usbharu/hideout/core/service/user/ActorServiceTest.kt @@ -69,7 +69,8 @@ class ActorServiceTest { relationshipRepository = mock(), postService = mock(), apSendDeleteService = mock(), - postRepository = mock() + postRepository = mock(), + owlProducer = mock() ) userService.createLocalUser(UserCreateDto("test", "testUser", "XXXXXXXXXXXXX", "test")) verify(actorRepository, times(1)).save(any()) @@ -112,7 +113,8 @@ class ActorServiceTest { relationshipRepository = mock(), postService = mock(), apSendDeleteService = mock(), - postRepository = mock() + postRepository = mock(), + owlProducer = mock() ) assertThrows { @@ -162,7 +164,8 @@ class ActorServiceTest { relationshipRepository = mock(), postService = mock(), apSendDeleteService = mock(), - postRepository = mock() + postRepository = mock(), + owlProducer = mock() ) val user = RemoteUserCreateDto( name = "test", diff --git a/hideout-worker/src/main/kotlin/dev/usbharu/hideout/worker/UpdateActorWorker.kt b/hideout-worker/src/main/kotlin/dev/usbharu/hideout/worker/UpdateActorWorker.kt new file mode 100644 index 00000000..f25b8dce --- /dev/null +++ b/hideout-worker/src/main/kotlin/dev/usbharu/hideout/worker/UpdateActorWorker.kt @@ -0,0 +1,44 @@ +/* + * 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.worker + +import dev.usbharu.hideout.activitypub.service.objects.user.APUserService +import dev.usbharu.hideout.application.external.Transaction +import dev.usbharu.hideout.core.external.job.UpdateActorTask +import dev.usbharu.hideout.core.external.job.UpdateActorTaskDef +import dev.usbharu.hideout.core.service.post.PostService +import dev.usbharu.owl.consumer.AbstractTaskRunner +import dev.usbharu.owl.consumer.TaskRequest +import dev.usbharu.owl.consumer.TaskResult +import org.springframework.stereotype.Component + +@Component +class UpdateActorWorker( + private val transaction: Transaction, + private val apUserService: APUserService, + private val postService: PostService, +) : AbstractTaskRunner(UpdateActorTaskDef) { + override suspend fun typedRun(typedParam: UpdateActorTask, taskRequest: TaskRequest): TaskResult { + transaction.transaction { + apUserService.fetchPerson(typedParam.apId, idOverride = typedParam.id) + + postService.restoreByRemoteActor(typedParam.id) + } + + return TaskResult.ok() + } +} \ No newline at end of file