Merge pull request #375 from usbharu/feature/undo-delete

ActorのUndo Deleteに対応
This commit is contained in:
usbharu 2024-05-17 23:21:03 +09:00 committed by GitHub
commit 07b240961a
16 changed files with 249 additions and 146 deletions

View File

@ -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.post.PostService
import dev.usbharu.hideout.core.service.reaction.ReactionService import dev.usbharu.hideout.core.service.reaction.ReactionService
import dev.usbharu.hideout.core.service.relationship.RelationshipService import dev.usbharu.hideout.core.service.relationship.RelationshipService
import dev.usbharu.hideout.core.service.user.UserService
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
@Service @Service
@ -41,7 +42,8 @@ class APUndoProcessor(
private val reactionService: ReactionService, private val reactionService: ReactionService,
private val actorRepository: ActorRepository, private val actorRepository: ActorRepository,
private val postRepository: PostRepository, private val postRepository: PostRepository,
private val postService: PostService private val postService: PostService,
private val userService: UserService,
) : AbstractActivityPubProcessor<Undo>(transaction) { ) : AbstractActivityPubProcessor<Undo>(transaction) {
override suspend fun internalProcess(activity: ActivityPubProcessContext<Undo>) { override suspend fun internalProcess(activity: ActivityPubProcessContext<Undo>) {
val undo = activity.activity val undo = activity.activity
@ -71,6 +73,11 @@ class APUndoProcessor(
return return
} }
"Delete" -> {
delete(undo)
return
}
else -> {} else -> {}
} }
TODO() TODO()
@ -124,6 +131,14 @@ class APUndoProcessor(
postService.deleteRemote(findByApId) 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 isSupported(activityType: ActivityType): Boolean = activityType == ActivityType.Undo
override fun type(): Class<Undo> = Undo::class.java override fun type(): Class<Undo> = Undo::class.java

View File

@ -40,9 +40,13 @@ interface APUserService {
* @param targetActor 署名するユーザー * @param targetActor 署名するユーザー
* @return * @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<Person, Actor> suspend fun fetchPersonWithEntity(
url: String,
targetActor: String? = null,
idOverride: Long? = null,
): Pair<Person, Actor>
} }
@Service @Service
@ -51,7 +55,7 @@ class APUserServiceImpl(
private val transaction: Transaction, private val transaction: Transaction,
private val applicationConfig: ApplicationConfig, private val applicationConfig: ApplicationConfig,
private val apResourceResolveService: APResourceResolveService, private val apResourceResolveService: APResourceResolveService,
private val actorRepository: ActorRepository private val actorRepository: ActorRepository,
) : ) :
APUserService { APUserService {
@ -88,13 +92,17 @@ class APUserServiceImpl(
) )
} }
override suspend fun fetchPerson(url: String, targetActor: String?): Person = override suspend fun fetchPerson(url: String, targetActor: String?, idOverride: Long?): Person =
fetchPersonWithEntity(url, targetActor).first fetchPersonWithEntity(url, targetActor, idOverride).first
override suspend fun fetchPersonWithEntity(url: String, targetActor: String?): Pair<Person, Actor> { override suspend fun fetchPersonWithEntity(
url: String,
targetActor: String?,
idOverride: Long?,
): Pair<Person, Actor> {
val userEntity = actorRepository.findByUrl(url) val userEntity = actorRepository.findByUrl(url)
if (userEntity != null) { if (userEntity != null && idOverride == null) {
return entityToPerson(userEntity, userEntity.url) to userEntity return entityToPerson(userEntity, userEntity.url) to userEntity
} }
@ -104,7 +112,7 @@ class APUserServiceImpl(
val actor = actorRepository.findByUrlWithLock(id) val actor = actorRepository.findByUrlWithLock(id)
if (actor != null) { if (actor != null && idOverride == null) {
return person to actor return person to actor
} }
@ -123,13 +131,14 @@ class APUserServiceImpl(
followers = person.followers, followers = person.followers,
sharedInbox = person.endpoints["sharedInbox"], sharedInbox = person.endpoints["sharedInbox"],
locked = person.manuallyApprovesFollowers locked = person.manuallyApprovesFollowers
) ),
idOverride
) )
} }
private fun entityToPerson( private fun entityToPerson(
actorEntity: Actor, actorEntity: Actor,
id: String id: String,
) = Person( ) = Person(
type = emptyList(), type = emptyList(),
name = actorEntity.name, name = actorEntity.name,

View File

@ -22,6 +22,7 @@ data class DeletedActor(
val id: Long, val id: Long,
val name: String, val name: String,
val domain: String, val domain: String,
val apiId: String,
val publicKey: String, val publicKey: String,
val deletedAt: Instant val deletedAt: Instant,
) )

View File

@ -43,7 +43,7 @@ data class Post private constructor(
@get:URL @get:URL
val apId: String = url, val apId: String = url,
val mediaIds: List<Long> = emptyList(), val mediaIds: List<Long> = emptyList(),
val delted: Boolean = false, val deleted: Boolean = false,
val emojiIds: List<Long> = emptyList(), val emojiIds: List<Long> = emptyList(),
) { ) {
@ -67,7 +67,8 @@ data class Post private constructor(
sensitive: Boolean = false, sensitive: Boolean = false,
apId: String = url, apId: String = url,
mediaIds: List<Long> = emptyList(), mediaIds: List<Long> = emptyList(),
emojiIds: List<Long> = emptyList() emojiIds: List<Long> = emptyList(),
deleted: Boolean = false,
): Post { ): Post {
require(id >= 0) { "id must be greater than or equal to 0." } require(id >= 0) { "id must be greater than or equal to 0." }
@ -109,7 +110,7 @@ data class Post private constructor(
sensitive = sensitive, sensitive = sensitive,
apId = apId, apId = apId,
mediaIds = mediaIds, mediaIds = mediaIds,
delted = false, deleted = deleted,
emojiIds = emojiIds emojiIds = emojiIds
) )
@ -119,6 +120,10 @@ data class Post private constructor(
throw IllegalArgumentException("${constraintViolation.propertyPath} : ${constraintViolation.message}") throw IllegalArgumentException("${constraintViolation.propertyPath} : ${constraintViolation.message}")
} }
if (post.deleted) {
return post.delete()
}
return post return post
} }
@ -130,7 +135,7 @@ data class Post private constructor(
createdAt: Instant, createdAt: Instant,
url: String, url: String,
repost: Post, repost: Post,
apId: String apId: String,
): Post { ): Post {
// リポストの公開範囲は元のポストより広くてはいけない // リポストの公開範囲は元のポストより広くてはいけない
val fixedVisibility = if (visibility.ordinal <= repost.visibility.ordinal) { val fixedVisibility = if (visibility.ordinal <= repost.visibility.ordinal) {
@ -139,16 +144,11 @@ data class Post private constructor(
visibility visibility
} }
require(id >= 0) { "id must be greater than or equal to 0." } val post = of(
require(actorId >= 0) { "actorId must be greater than or equal to 0." }
val post = Post(
id = id, id = id,
actorId = actorId, actorId = actorId,
overview = null, overview = null,
content = "", content = "",
text = "",
createdAt = createdAt.toEpochMilli(), createdAt = createdAt.toEpochMilli(),
visibility = fixedVisibility, visibility = fixedVisibility,
url = url, url = url,
@ -157,16 +157,9 @@ data class Post private constructor(
sensitive = false, sensitive = false,
apId = apId, apId = apId,
mediaIds = emptyList(), mediaIds = emptyList(),
delted = false, deleted = false,
emojiIds = emptyList() emojiIds = emptyList()
) )
val validate = validator.validate(post)
for (constraintViolation in validate) {
throw IllegalArgumentException("${constraintViolation.propertyPath} : ${constraintViolation.message}")
}
return post return post
} }
@ -184,7 +177,7 @@ data class Post private constructor(
sensitive: Boolean = false, sensitive: Boolean = false,
apId: String = url, apId: String = url,
mediaIds: List<Long> = emptyList(), mediaIds: List<Long> = emptyList(),
emojiIds: List<Long> = emptyList() emojiIds: List<Long> = emptyList(),
): Post { ): Post {
// リポストの公開範囲は元のポストより広くてはいけない // リポストの公開範囲は元のポストより広くてはいけない
val fixedVisibility = if (visibility.ordinal <= repost.visibility.ordinal) { val fixedVisibility = if (visibility.ordinal <= repost.visibility.ordinal) {
@ -193,37 +186,11 @@ data class Post private constructor(
visibility visibility
} }
require(id >= 0) { "id must be greater than or equal to 0." } val post = of(
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(
id = id, id = id,
actorId = actorId, actorId = actorId,
overview = limitedOverview, overview = overview,
content = html, content = content,
text = content1,
createdAt = createdAt.toEpochMilli(), createdAt = createdAt.toEpochMilli(),
visibility = fixedVisibility, visibility = fixedVisibility,
url = url, url = url,
@ -232,70 +199,26 @@ data class Post private constructor(
sensitive = sensitive, sensitive = sensitive,
apId = apId, apId = apId,
mediaIds = mediaIds, mediaIds = mediaIds,
delted = false, deleted = false,
emojiIds = emojiIds emojiIds = emojiIds
) )
val validate = validator.validate(post)
for (constraintViolation in validate) {
throw IllegalArgumentException("${constraintViolation.propertyPath} : ${constraintViolation.message}")
}
return post 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 = fun isPureRepost(): Boolean =
this.text.isEmpty() && this.text.isEmpty() &&
this.content.isEmpty() && this.content.isEmpty() &&
this.overview == null && this.overview == null &&
this.replyId == null && this.replyId == null &&
this.repostId != null this.repostId != null
fun delete(): Post { fun delete(): Post {
return Post( return copy(deleted = true)
id = this.id, }
actorId = 0,
overview = null, fun restore(): Post {
content = "", return copy(deleted = false)
text = "",
createdAt = Instant.EPOCH.toEpochMilli(),
visibility = visibility,
url = url,
repostId = repostId,
replyId = replyId,
sensitive = false,
apId = apId,
mediaIds = emptyList(),
delted = true
)
} }
} }

View File

@ -23,6 +23,7 @@ import org.springframework.stereotype.Repository
interface PostRepository { interface PostRepository {
suspend fun generateId(): Long suspend fun generateId(): Long
suspend fun save(post: Post): Post suspend fun save(post: Post): Post
suspend fun saveAll(posts: List<Post>)
suspend fun delete(id: Long) suspend fun delete(id: Long)
suspend fun findById(id: Long): Post? suspend fun findById(id: Long): Post?
suspend fun findByUrl(url: String): Post? suspend fun findByUrl(url: String): Post?
@ -30,6 +31,7 @@ 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 findByActorIdAndDeleted(actorId: Long, deleted: Boolean): List<Post>
suspend fun countByActorId(actorId: Long): Int suspend fun countByActorId(actorId: Long): Int
} }

View File

@ -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<UpdateActorTask> {
override val type: Class<UpdateActorTask>
get() = UpdateActorTask::class.java
}

View File

@ -26,16 +26,6 @@ import org.springframework.stereotype.Component
@Component @Component
class PostResultRowMapper(private val postBuilder: Post.PostBuilder) : ResultRowMapper<Post> { class PostResultRowMapper(private val postBuilder: Post.PostBuilder) : ResultRowMapper<Post> {
override fun map(resultRow: ResultRow): Post { 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( return postBuilder.of(
id = resultRow[Posts.id], id = resultRow[Posts.id],
@ -49,6 +39,7 @@ class PostResultRowMapper(private val postBuilder: Post.PostBuilder) : ResultRow
replyId = resultRow[Posts.replyId], replyId = resultRow[Posts.replyId],
sensitive = resultRow[Posts.sensitive], sensitive = resultRow[Posts.sensitive],
apId = resultRow[Posts.apId], apId = resultRow[Posts.apId],
deleted = resultRow[Posts.deleted],
) )
} }
} }

View File

@ -39,6 +39,7 @@ class DeletedActorRepositoryImpl : DeletedActorRepository, AbstractRepository()
it[id] = deletedActor.id it[id] = deletedActor.id
it[name] = deletedActor.name it[name] = deletedActor.name
it[domain] = deletedActor.domain it[domain] = deletedActor.domain
it[apId] = deletedActor.apiId
it[publicKey] = deletedActor.publicKey it[publicKey] = deletedActor.publicKey
it[deletedAt] = deletedActor.deletedAt it[deletedAt] = deletedActor.deletedAt
} }
@ -46,6 +47,7 @@ class DeletedActorRepositoryImpl : DeletedActorRepository, AbstractRepository()
DeletedActors.update({ DeletedActors.id eq deletedActor.id }) { DeletedActors.update({ DeletedActors.id eq deletedActor.id }) {
it[name] = deletedActor.name it[name] = deletedActor.name
it[domain] = deletedActor.domain it[domain] = deletedActor.domain
it[apId] = deletedActor.apiId
it[publicKey] = deletedActor.publicKey it[publicKey] = deletedActor.publicKey
it[deletedAt] = deletedActor.deletedAt it[deletedAt] = deletedActor.deletedAt
} }
@ -84,6 +86,7 @@ private fun deletedActor(singleOr: ResultRow): DeletedActor {
singleOr[DeletedActors.name], singleOr[DeletedActors.name],
singleOr[DeletedActors.domain], singleOr[DeletedActors.domain],
singleOr[DeletedActors.publicKey], singleOr[DeletedActors.publicKey],
singleOr[DeletedActors.apId],
singleOr[DeletedActors.deletedAt] singleOr[DeletedActors.deletedAt]
) )
} }
@ -92,6 +95,7 @@ object DeletedActors : Table("deleted_actors") {
val id = long("id") val id = long("id")
val name = varchar("name", 300) val name = varchar("name", 300)
val domain = varchar("domain", 255) val domain = varchar("domain", 255)
val apId = varchar("ap_id", 255).uniqueIndex()
val publicKey = varchar("public_key", 10000).uniqueIndex() val publicKey = varchar("public_key", 10000).uniqueIndex()
val deletedAt = timestamp("deleted_at") val deletedAt = timestamp("deleted_at")
override val primaryKey: PrimaryKey = PrimaryKey(id) override val primaryKey: PrimaryKey = PrimaryKey(id)

View File

@ -20,6 +20,19 @@ import dev.usbharu.hideout.application.infrastructure.exposed.QueryMapper
import dev.usbharu.hideout.application.service.id.IdGenerateService 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.Post
import dev.usbharu.hideout.core.domain.model.post.PostRepository 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.*
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.slf4j.Logger import org.slf4j.Logger
@ -29,7 +42,7 @@ import org.springframework.stereotype.Repository
@Repository @Repository
class PostRepositoryImpl( class PostRepositoryImpl(
private val idGenerateService: IdGenerateService, private val idGenerateService: IdGenerateService,
private val postQueryMapper: QueryMapper<Post> private val postQueryMapper: QueryMapper<Post>,
) : PostRepository, AbstractRepository() { ) : PostRepository, AbstractRepository() {
override val logger: Logger override val logger: Logger
get() = Companion.logger get() = Companion.logger
@ -37,7 +50,7 @@ class PostRepositoryImpl(
override suspend fun generateId(): Long = idGenerateService.generateId() override suspend fun generateId(): Long = idGenerateService.generateId()
override suspend fun save(post: Post): Post = query { 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) { if (singleOrNull == null) {
Posts.insert { Posts.insert {
it[id] = post.id it[id] = post.id
@ -52,7 +65,7 @@ class PostRepositoryImpl(
it[replyId] = post.replyId it[replyId] = post.replyId
it[sensitive] = post.sensitive it[sensitive] = post.sensitive
it[apId] = post.apId it[apId] = post.apId
it[deleted] = post.delted it[deleted] = post.deleted
} }
PostsMedia.batchInsert(post.mediaIds) { PostsMedia.batchInsert(post.mediaIds) {
this[PostsMedia.postId] = post.id this[PostsMedia.postId] = post.id
@ -77,7 +90,7 @@ class PostRepositoryImpl(
this[PostsEmojis.postId] = post.id this[PostsEmojis.postId] = post.id
this[PostsEmojis.emojiId] = it this[PostsEmojis.emojiId] = it
} }
Posts.update({ Posts.id eq post.id }) { Posts.update({ id eq post.id }) {
it[actorId] = post.actorId it[actorId] = post.actorId
it[overview] = post.overview it[overview] = post.overview
it[content] = post.content it[content] = post.content
@ -89,12 +102,45 @@ class PostRepositoryImpl(
it[replyId] = post.replyId it[replyId] = post.replyId
it[sensitive] = post.sensitive it[sensitive] = post.sensitive
it[apId] = post.apId it[apId] = post.apId
it[deleted] = post.delted it[deleted] = post.deleted
} }
} }
return@query post return@query post
} }
override suspend fun saveAll(posts: List<Post>) {
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 { override suspend fun findById(id: Long): Post? = query {
return@query Posts return@query Posts
.leftJoin(PostsMedia) .leftJoin(PostsMedia)
@ -133,6 +179,10 @@ class PostRepositoryImpl(
.selectAll().where { Posts.actorId eq actorId }.let(postQueryMapper::map) .selectAll().where { Posts.actorId eq actorId }.let(postQueryMapper::map)
} }
override suspend fun findByActorIdAndDeleted(actorId: Long, deleted: Boolean): List<Post> {
TODO("Not yet implemented")
}
override suspend fun countByActorId(actorId: Long): Int = query { override suspend fun countByActorId(actorId: Long): Int = query {
return@query Posts return@query Posts
.selectAll() .selectAll()
@ -168,13 +218,13 @@ object Posts : Table() {
} }
object PostsMedia : Table("posts_media") { 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) val mediaId = long("media_id").references(Media.id, ReferenceOption.CASCADE, ReferenceOption.CASCADE)
override val primaryKey = PrimaryKey(postId, mediaId) override val primaryKey = PrimaryKey(postId, mediaId)
} }
object PostsEmojis : Table("posts_emojis") { 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) val emojiId = long("emoji_id").references(CustomEmojis.id)
override val primaryKey: PrimaryKey = PrimaryKey(postId, emojiId) override val primaryKey: PrimaryKey = PrimaryKey(postId, emojiId)
} }

View File

@ -26,4 +26,5 @@ interface PostService {
suspend fun deleteLocal(post: Post) suspend fun deleteLocal(post: Post)
suspend fun deleteRemote(post: Post) suspend fun deleteRemote(post: Post)
suspend fun deleteByActor(actorId: Long) suspend fun deleteByActor(actorId: Long)
suspend fun restoreByRemoteActor(actorId: Long)
} }

View File

@ -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.create.ApSendCreateService
import dev.usbharu.hideout.activitypub.service.activity.delete.APSendDeleteService 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.DuplicateException
import dev.usbharu.hideout.core.domain.exception.resource.PostNotFoundException 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.actor.ActorRepository
import dev.usbharu.hideout.core.domain.model.post.Post import dev.usbharu.hideout.core.domain.model.post.Post
import dev.usbharu.hideout.core.domain.model.post.PostRepository import dev.usbharu.hideout.core.domain.model.post.PostRepository
@ -38,7 +38,7 @@ class PostServiceImpl(
private val postBuilder: Post.PostBuilder, private val postBuilder: Post.PostBuilder,
private val apSendCreateService: ApSendCreateService, private val apSendCreateService: ApSendCreateService,
private val reactionRepository: ReactionRepository, private val reactionRepository: ReactionRepository,
private val apSendDeleteService: APSendDeleteService private val apSendDeleteService: APSendDeleteService,
) : PostService { ) : PostService {
override suspend fun createLocal(post: PostCreateDto): Post { override suspend fun createLocal(post: PostCreateDto): Post {
@ -52,14 +52,14 @@ class PostServiceImpl(
override suspend fun createRemote(post: Post): Post { override suspend fun createRemote(post: Post): Post {
logger.info("START Create Remote Post user: {}, remote url: {}", post.actorId, post.apId) logger.info("START Create Remote Post user: {}, remote url: {}", post.actorId, post.apId)
val actor = 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) val createdPost = internalCreate(post, false)
logger.info("SUCCESS Create Remote Post url: {}", createdPost.url) logger.info("SUCCESS Create Remote Post url: {}", createdPost.url)
return createdPost return createdPost
} }
override suspend fun deleteLocal(post: Post) { override suspend fun deleteLocal(post: Post) {
if (post.delted) { if (post.deleted) {
return return
} }
reactionRepository.deleteByPostId(post.id) reactionRepository.deleteByPostId(post.id)
@ -73,7 +73,7 @@ class PostServiceImpl(
} }
override suspend fun deleteRemote(post: Post) { override suspend fun deleteRemote(post: Post) {
if (post.delted) { if (post.deleted) {
return return
} }
reactionRepository.deleteByPostId(post.id) reactionRepository.deleteByPostId(post.id)
@ -86,14 +86,25 @@ class PostServiceImpl(
} }
override suspend fun deleteByActor(actorId: Long) { override suspend fun deleteByActor(actorId: Long) {
postRepository.findByActorId(actorId).filterNot { it.delted }.forEach { postRepository.save(it.delete()) }
val actor = actorRepository.findById(actorId) val actor = actorRepository.findById(actorId)
?: throw IllegalStateException("actor: $actorId was not found.") ?: 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)) 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 { private suspend fun internalCreate(post: Post, isLocal: Boolean): Post {
return try { return try {
val save = postRepository.save(post) val save = postRepository.save(post)

View File

@ -26,12 +26,14 @@ interface UserService {
suspend fun createLocalUser(user: UserCreateDto): Actor 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 updateUser(userId: Long, updateUserDto: UpdateUserDto)
suspend fun deleteRemoteActor(actorId: Long) suspend fun deleteRemoteActor(actorId: Long)
suspend fun restorationRemoteActor(actorId: Long)
suspend fun deleteLocalUser(userId: Long) suspend fun deleteLocalUser(userId: Long)
suspend fun updateUserStatistics(userId: Long) suspend fun updateUserStatistics(userId: Long)

View File

@ -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.relationship.RelationshipRepository
import dev.usbharu.hideout.core.domain.model.userdetails.UserDetail import dev.usbharu.hideout.core.domain.model.userdetails.UserDetail
import dev.usbharu.hideout.core.domain.model.userdetails.UserDetailRepository 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.instance.InstanceService
import dev.usbharu.hideout.core.service.post.PostService import dev.usbharu.hideout.core.service.post.PostService
import dev.usbharu.owl.producer.api.OwlProducer
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import java.time.Instant import java.time.Instant
@ -50,6 +52,7 @@ class UserServiceImpl(
private val postService: PostService, private val postService: PostService,
private val apSendDeleteService: APSendDeleteService, private val apSendDeleteService: APSendDeleteService,
private val postRepository: PostRepository, private val postRepository: PostRepository,
private val owlProducer: OwlProducer,
) : ) :
UserService { UserService {
@ -90,7 +93,7 @@ class UserServiceImpl(
return save 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) logger.info("START Create New remote user. name: {} url: {}", user.name, user.url)
val deletedActor = deletedActorRepository.findByNameAndDomain(user.name, user.domain) val deletedActor = deletedActorRepository.findByNameAndDomain(user.name, user.domain)
@ -104,7 +107,7 @@ class UserServiceImpl(
val nextId = actorRepository.nextId() val nextId = actorRepository.nextId()
val userEntity = actorBuilder.of( val userEntity = actorBuilder.of(
id = nextId, id = idOverride ?: nextId,
name = user.name, name = user.name,
domain = user.domain, domain = user.domain,
screenName = user.screenName, screenName = user.screenName,
@ -156,6 +159,7 @@ class UserServiceImpl(
actor.id, actor.id,
actor.name, actor.name,
actor.domain, actor.domain,
actor.url,
actor.publicKey, actor.publicKey,
Instant.now() Instant.now()
) )
@ -169,6 +173,15 @@ class UserServiceImpl(
deletedActorRepository.save(deletedActor) 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) { override suspend fun deleteLocalUser(userId: Long) {
val actor = actorRepository.findByIdWithLock(userId) ?: throw UserNotFoundException.withId(userId) val actor = actorRepository.findByIdWithLock(userId) ?: throw UserNotFoundException.withId(userId)
apSendDeleteService.sendDeleteActor(actor) apSendDeleteService.sendDeleteActor(actor)
@ -176,6 +189,7 @@ class UserServiceImpl(
actor.id, actor.id,
actor.name, actor.name,
actor.domain, actor.domain,
actor.url,
actor.publicKey, actor.publicKey,
Instant.now() Instant.now()
) )

View File

@ -154,7 +154,7 @@ class APNoteServiceImplTest {
) )
val apUserService = mock<APUserService> { val apUserService = mock<APUserService> {
onBlocking { fetchPersonWithEntity(eq(note.attributedTo), isNull()) } doReturn (person to user) onBlocking { fetchPersonWithEntity(eq(note.attributedTo), isNull(), anyOrNull()) } doReturn (person to user)
} }
val postRepository = mock<PostRepository> { val postRepository = mock<PostRepository> {
onBlocking { generateId() } doReturn TwitterSnowflakeIdGenerateService.generateId() onBlocking { generateId() } doReturn TwitterSnowflakeIdGenerateService.generateId()
@ -255,7 +255,7 @@ class APNoteServiceImplTest {
followers = user.followers followers = user.followers
) )
val apUserService = mock<APUserService> { val apUserService = mock<APUserService> {
onBlocking { fetchPersonWithEntity(eq(user.url), anyOrNull()) } doReturn (person to user) onBlocking { fetchPersonWithEntity(eq(user.url), anyOrNull(), anyOrNull()) } doReturn (person to user)
} }
val postService = mock<PostService>() val postService = mock<PostService>()
val noteQueryService = mock<NoteQueryService> { val noteQueryService = mock<NoteQueryService> {

View File

@ -69,7 +69,8 @@ class ActorServiceTest {
relationshipRepository = mock(), relationshipRepository = mock(),
postService = mock(), postService = mock(),
apSendDeleteService = mock(), apSendDeleteService = mock(),
postRepository = mock() postRepository = mock(),
owlProducer = 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())
@ -112,7 +113,8 @@ class ActorServiceTest {
relationshipRepository = mock(), relationshipRepository = mock(),
postService = mock(), postService = mock(),
apSendDeleteService = mock(), apSendDeleteService = mock(),
postRepository = mock() postRepository = mock(),
owlProducer = mock()
) )
assertThrows<IllegalStateException> { assertThrows<IllegalStateException> {
@ -162,7 +164,8 @@ class ActorServiceTest {
relationshipRepository = mock(), relationshipRepository = mock(),
postService = mock(), postService = mock(),
apSendDeleteService = mock(), apSendDeleteService = mock(),
postRepository = mock() postRepository = mock(),
owlProducer = mock()
) )
val user = RemoteUserCreateDto( val user = RemoteUserCreateDto(
name = "test", name = "test",

View File

@ -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<UpdateActorTask, UpdateActorTaskDef>(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()
}
}