mirror of https://github.com/usbharu/Hideout.git
feat: 削除された投稿はレコードを完全に削除せず、返信などの情報を維持したまま削除されるように
This commit is contained in:
parent
8d1f339a5d
commit
8a18780963
|
@ -8,15 +8,15 @@ import dev.usbharu.hideout.activitypub.service.common.ActivityPubProcessContext
|
|||
import dev.usbharu.hideout.activitypub.service.common.ActivityType
|
||||
import dev.usbharu.hideout.application.external.Transaction
|
||||
import dev.usbharu.hideout.core.domain.exception.FailedToGetResourcesException
|
||||
import dev.usbharu.hideout.core.domain.model.post.PostRepository
|
||||
import dev.usbharu.hideout.core.query.PostQueryService
|
||||
import dev.usbharu.hideout.core.service.post.PostService
|
||||
import org.springframework.stereotype.Service
|
||||
|
||||
@Service
|
||||
class APDeleteProcessor(
|
||||
transaction: Transaction,
|
||||
private val postQueryService: PostQueryService,
|
||||
private val postRepository: PostRepository
|
||||
private val postService: PostService
|
||||
) :
|
||||
AbstractActivityPubProcessor<Delete>(transaction) {
|
||||
override suspend fun internalProcess(activity: ActivityPubProcessContext<Delete>) {
|
||||
|
@ -33,7 +33,7 @@ class APDeleteProcessor(
|
|||
return
|
||||
}
|
||||
|
||||
postRepository.delete(post.id)
|
||||
postService.deleteRemote(post)
|
||||
}
|
||||
|
||||
override fun isSupported(activityType: ActivityType): Boolean = activityType == ActivityType.Delete
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
package dev.usbharu.hideout.activitypub.service.activity.delete
|
||||
|
||||
import dev.usbharu.hideout.activitypub.service.common.APRequestService
|
||||
import dev.usbharu.hideout.application.external.Transaction
|
||||
import dev.usbharu.hideout.core.external.job.DeliverDeleteJob
|
||||
import dev.usbharu.hideout.core.external.job.DeliverDeleteJobParam
|
||||
import dev.usbharu.hideout.core.query.ActorQueryService
|
||||
import dev.usbharu.hideout.core.service.job.JobProcessor
|
||||
import org.springframework.stereotype.Service
|
||||
|
||||
@Service
|
||||
class APDeliverDeleteJobProcessor(
|
||||
private val apRequestService: APRequestService,
|
||||
private val actorQueryService: ActorQueryService,
|
||||
private val transaction: Transaction,
|
||||
private val deliverDeleteJob: DeliverDeleteJob
|
||||
) : JobProcessor<DeliverDeleteJobParam, DeliverDeleteJob> {
|
||||
override suspend fun process(param: DeliverDeleteJobParam): Unit = transaction.transaction {
|
||||
apRequestService.apPost(param.inbox, param.delete, actorQueryService.findById(param.signer))
|
||||
}
|
||||
|
||||
override fun job(): DeliverDeleteJob = deliverDeleteJob
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
package dev.usbharu.hideout.activitypub.service.activity.delete
|
||||
|
||||
import dev.usbharu.hideout.activitypub.domain.model.Delete
|
||||
import dev.usbharu.hideout.activitypub.domain.model.Tombstone
|
||||
import dev.usbharu.hideout.application.config.ApplicationConfig
|
||||
import dev.usbharu.hideout.core.domain.model.post.Post
|
||||
import dev.usbharu.hideout.core.external.job.DeliverDeleteJob
|
||||
import dev.usbharu.hideout.core.external.job.DeliverDeleteJobParam
|
||||
import dev.usbharu.hideout.core.query.ActorQueryService
|
||||
import dev.usbharu.hideout.core.query.FollowerQueryService
|
||||
import dev.usbharu.hideout.core.service.job.JobQueueParentService
|
||||
import org.springframework.stereotype.Service
|
||||
import java.time.Instant
|
||||
|
||||
interface APSendDeleteService {
|
||||
suspend fun sendDeleteNote(deletedPost: Post)
|
||||
}
|
||||
|
||||
@Service
|
||||
class APSendDeleteServiceImpl(
|
||||
private val jobQueueParentService: JobQueueParentService,
|
||||
private val delverDeleteJob: DeliverDeleteJob,
|
||||
private val followerQueryService: FollowerQueryService,
|
||||
private val actorQueryService: ActorQueryService,
|
||||
private val applicationConfig: ApplicationConfig
|
||||
) : APSendDeleteService {
|
||||
override suspend fun sendDeleteNote(deletedPost: Post) {
|
||||
|
||||
|
||||
val actor = actorQueryService.findById(deletedPost.actorId)
|
||||
val followersById = followerQueryService.findFollowersById(deletedPost.actorId)
|
||||
|
||||
val delete = Delete(
|
||||
actor = actor.url,
|
||||
id = "${applicationConfig.url}/delete/note/${deletedPost.id}",
|
||||
published = Instant.now().toString(),
|
||||
`object` = Tombstone(id = deletedPost.apId)
|
||||
)
|
||||
|
||||
followersById.forEach {
|
||||
val jobProps = DeliverDeleteJobParam(
|
||||
delete,
|
||||
it.inbox,
|
||||
actor.id
|
||||
)
|
||||
jobQueueParentService.scheduleTypeSafe(delverDeleteJob, jobProps)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -2,6 +2,7 @@ package dev.usbharu.hideout.core.domain.model.post
|
|||
|
||||
import dev.usbharu.hideout.application.config.CharacterLimit
|
||||
import org.springframework.stereotype.Component
|
||||
import java.time.Instant
|
||||
|
||||
data class Post private constructor(
|
||||
val id: Long,
|
||||
|
@ -15,7 +16,8 @@ data class Post private constructor(
|
|||
val replyId: Long? = null,
|
||||
val sensitive: Boolean = false,
|
||||
val apId: String = url,
|
||||
val mediaIds: List<Long> = emptyList()
|
||||
val mediaIds: List<Long> = emptyList(),
|
||||
val delted: Boolean = false
|
||||
) {
|
||||
|
||||
@Component
|
||||
|
@ -71,8 +73,52 @@ data class Post private constructor(
|
|||
replyId = replyId,
|
||||
sensitive = sensitive,
|
||||
apId = apId,
|
||||
mediaIds = mediaIds
|
||||
mediaIds = mediaIds,
|
||||
delted = false
|
||||
)
|
||||
}
|
||||
|
||||
fun deleteOf(
|
||||
id: Long,
|
||||
visibility: Visibility,
|
||||
url: String,
|
||||
repostId: Long?,
|
||||
replyId: Long?,
|
||||
apId: String
|
||||
): Post {
|
||||
return Post(
|
||||
id = id,
|
||||
actorId = 0,
|
||||
overview = null,
|
||||
text = "",
|
||||
createdAt = Instant.EPOCH.toEpochMilli(),
|
||||
visibility = visibility,
|
||||
url = url,
|
||||
repostId = repostId,
|
||||
replyId = replyId,
|
||||
sensitive = false,
|
||||
apId = apId,
|
||||
mediaIds = emptyList(),
|
||||
delted = true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun delete(): Post {
|
||||
return Post(
|
||||
id = this.id,
|
||||
actorId = 0,
|
||||
overview = null,
|
||||
text = "",
|
||||
createdAt = Instant.EPOCH.toEpochMilli(),
|
||||
visibility = visibility,
|
||||
url = url,
|
||||
repostId = repostId,
|
||||
replyId = replyId,
|
||||
sensitive = false,
|
||||
apId = apId,
|
||||
mediaIds = emptyList(),
|
||||
delted = true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,4 +7,6 @@ interface ReactionRepository {
|
|||
suspend fun generateId(): Long
|
||||
suspend fun save(reaction: Reaction): Reaction
|
||||
suspend fun delete(reaction: Reaction): Reaction
|
||||
suspend fun deleteByPostId(postId: Long): Int
|
||||
suspend fun deleteByActorId(actorId: Long): Int
|
||||
}
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
package dev.usbharu.hideout.core.external.job
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import com.fasterxml.jackson.module.kotlin.readValue
|
||||
import dev.usbharu.hideout.activitypub.domain.model.Delete
|
||||
import kjob.core.dsl.ScheduleContext
|
||||
import kjob.core.job.JobProps
|
||||
import org.springframework.beans.factory.annotation.Qualifier
|
||||
import org.springframework.stereotype.Component
|
||||
|
||||
data class DeliverDeleteJobParam(
|
||||
val delete: Delete,
|
||||
val inbox: String,
|
||||
val signer: Long
|
||||
)
|
||||
|
||||
@Component
|
||||
class DeliverDeleteJob(@Qualifier("activitypub") private val objectMapper: ObjectMapper) :
|
||||
HideoutJob<DeliverDeleteJobParam, DeliverDeleteJob>("DeliverDeleteJob") {
|
||||
|
||||
val delete = string("delete")
|
||||
val inbox = string("inbox")
|
||||
val signer = long("signer")
|
||||
|
||||
override fun convert(value: DeliverDeleteJobParam): ScheduleContext<DeliverDeleteJob>.(DeliverDeleteJob) -> Unit = {
|
||||
props[delete] = objectMapper.writeValueAsString(value.delete)
|
||||
props[inbox] = value.inbox
|
||||
props[signer] = value.signer
|
||||
}
|
||||
|
||||
override fun convert(props: JobProps<DeliverDeleteJob>): DeliverDeleteJobParam = DeliverDeleteJobParam(
|
||||
objectMapper.readValue(props[delete]),
|
||||
props[inbox],
|
||||
props[signer]
|
||||
)
|
||||
}
|
|
@ -10,6 +10,17 @@ import org.springframework.stereotype.Component
|
|||
@Component
|
||||
class PostResultRowMapper(private val postBuilder: Post.PostBuilder) : ResultRowMapper<Post> {
|
||||
override fun map(resultRow: ResultRow): Post {
|
||||
if (resultRow[Posts.deleted]) {
|
||||
return postBuilder.deleteOf(
|
||||
resultRow[Posts.id],
|
||||
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],
|
||||
actorId = resultRow[Posts.actorId],
|
||||
|
|
|
@ -85,6 +85,7 @@ object Posts : Table() {
|
|||
val replyId: Column<Long?> = long("reply_id").references(id).nullable()
|
||||
val sensitive: Column<Boolean> = bool("sensitive").default(false)
|
||||
val apId: Column<String> = varchar("ap_id", 100).uniqueIndex()
|
||||
val deleted = bool("deleted").default(false)
|
||||
override val primaryKey: PrimaryKey = PrimaryKey(id)
|
||||
}
|
||||
|
||||
|
|
|
@ -42,6 +42,18 @@ class ReactionRepositoryImpl(
|
|||
}
|
||||
return reaction
|
||||
}
|
||||
|
||||
override suspend fun deleteByPostId(postId: Long): Int {
|
||||
return Reactions.deleteWhere {
|
||||
Reactions.postId eq postId
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun deleteByActorId(actorId: Long): Int {
|
||||
return Reactions.deleteWhere {
|
||||
Reactions.actorId eq actorId
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun ResultRow.toReaction(): Reaction {
|
||||
|
@ -55,8 +67,10 @@ fun ResultRow.toReaction(): Reaction {
|
|||
|
||||
object Reactions : LongIdTable("reactions") {
|
||||
val emojiId: Column<Long> = long("emoji_id")
|
||||
val postId: Column<Long> = long("post_id").references(Posts.id)
|
||||
val actorId: Column<Long> = long("actor_id").references(Actors.id)
|
||||
val postId: Column<Long> = long("post_id")
|
||||
.references(Posts.id, onDelete = ReferenceOption.CASCADE, onUpdate = ReferenceOption.CASCADE)
|
||||
val actorId: Column<Long> = long("actor_id")
|
||||
.references(Actors.id, onDelete = ReferenceOption.CASCADE, onUpdate = ReferenceOption.CASCADE)
|
||||
|
||||
init {
|
||||
uniqueIndex(emojiId, postId, actorId)
|
||||
|
|
|
@ -7,4 +7,6 @@ import org.springframework.stereotype.Service
|
|||
interface PostService {
|
||||
suspend fun createLocal(post: PostCreateDto): Post
|
||||
suspend fun createRemote(post: Post): Post
|
||||
suspend fun deleteLocal(post: Post)
|
||||
suspend fun deleteRemote(post: Post)
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import dev.usbharu.hideout.core.domain.exception.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
|
||||
import dev.usbharu.hideout.core.domain.model.reaction.ReactionRepository
|
||||
import dev.usbharu.hideout.core.query.PostQueryService
|
||||
import dev.usbharu.hideout.core.service.timeline.TimelineService
|
||||
import org.jetbrains.exposed.exceptions.ExposedSQLException
|
||||
|
@ -20,7 +21,8 @@ class PostServiceImpl(
|
|||
private val timelineService: TimelineService,
|
||||
private val postQueryService: PostQueryService,
|
||||
private val postBuilder: Post.PostBuilder,
|
||||
private val apSendCreateService: ApSendCreateService
|
||||
private val apSendCreateService: ApSendCreateService,
|
||||
private val reactionRepository: ReactionRepository
|
||||
) : PostService {
|
||||
|
||||
override suspend fun createLocal(post: PostCreateDto): Post {
|
||||
|
@ -38,6 +40,16 @@ class PostServiceImpl(
|
|||
return createdPost
|
||||
}
|
||||
|
||||
override suspend fun deleteLocal(post: Post) {
|
||||
reactionRepository.deleteByPostId(post.id)
|
||||
postRepository.save(post.delete())
|
||||
}
|
||||
|
||||
override suspend fun deleteRemote(post: Post) {
|
||||
reactionRepository.deleteByPostId(post.id)
|
||||
postRepository.save(post.delete())
|
||||
}
|
||||
|
||||
private suspend fun internalCreate(post: Post, isLocal: Boolean): Post {
|
||||
return try {
|
||||
if (postRepository.save(post)) {
|
||||
|
|
|
@ -76,7 +76,8 @@ create table if not exists posts
|
|||
repost_id bigint null,
|
||||
reply_id bigint null,
|
||||
"sensitive" boolean default false not null,
|
||||
ap_id varchar(100) not null unique
|
||||
ap_id varchar(100) not null unique,
|
||||
deleted boolean default false not null
|
||||
);
|
||||
alter table posts
|
||||
add constraint fk_posts_actor_id__id foreign key (actor_id) references actors (id) on delete restrict on update restrict;
|
||||
|
@ -196,4 +197,8 @@ create table if not exists relationships
|
|||
constraint fk_relationships_actor_id__id foreign key (actor_id) references actors (id) on delete restrict on update restrict,
|
||||
constraint fk_relationships_target_actor_id__id foreign key (target_actor_id) references actors (id) on delete restrict on update restrict,
|
||||
unique (actor_id, target_actor_id)
|
||||
)
|
||||
);
|
||||
|
||||
insert into actors (id, name, domain, screen_name, description, inbox, outbox, url, public_key, private_key, created_at,
|
||||
key_id, following, followers, instance, locked)
|
||||
values (0, 'ghost', '', '', '', '', '', '', '', null, 0, '', '', '', null, true);
|
||||
|
|
Loading…
Reference in New Issue