mirror of https://github.com/usbharu/Hideout.git
commit
bd5d0fadf0
|
@ -0,0 +1,60 @@
|
||||||
|
package dev.usbharu.hideout.activitypub.domain.model
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonCreator
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
|
import dev.usbharu.hideout.activitypub.domain.model.objects.Object
|
||||||
|
|
||||||
|
open class Announce @JsonCreator constructor(
|
||||||
|
type: List<String> = emptyList(),
|
||||||
|
@JsonProperty("object")
|
||||||
|
val apObject: String,
|
||||||
|
override val actor: String,
|
||||||
|
override val id: String,
|
||||||
|
val published: String,
|
||||||
|
val to: List<String> = emptyList(),
|
||||||
|
val cc: List<String> = emptyList()
|
||||||
|
) : Object(
|
||||||
|
type = add(type, "Announce")
|
||||||
|
),
|
||||||
|
HasActor,
|
||||||
|
HasId {
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (javaClass != other?.javaClass) return false
|
||||||
|
if (!super.equals(other)) return false
|
||||||
|
|
||||||
|
other as Announce
|
||||||
|
|
||||||
|
if (apObject != other.apObject) return false
|
||||||
|
if (actor != other.actor) return false
|
||||||
|
if (id != other.id) return false
|
||||||
|
if (published != other.published) return false
|
||||||
|
if (to != other.to) return false
|
||||||
|
if (cc != other.cc) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = super.hashCode()
|
||||||
|
result = 31 * result + apObject.hashCode()
|
||||||
|
result = 31 * result + actor.hashCode()
|
||||||
|
result = 31 * result + id.hashCode()
|
||||||
|
result = 31 * result + published.hashCode()
|
||||||
|
result = 31 * result + to.hashCode()
|
||||||
|
result = 31 * result + cc.hashCode()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
return "Announce(" +
|
||||||
|
"apObject='$apObject', " +
|
||||||
|
"actor='$actor', " +
|
||||||
|
"id='$id', " +
|
||||||
|
"published='$published', " +
|
||||||
|
"to=$to, " +
|
||||||
|
"cc=$cc" +
|
||||||
|
")" +
|
||||||
|
" ${super.toString()}"
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,11 +1,12 @@
|
||||||
package dev.usbharu.hideout.activitypub.domain.model
|
package dev.usbharu.hideout.activitypub.domain.model
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
|
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
|
||||||
import dev.usbharu.hideout.activitypub.domain.model.objects.Object
|
import dev.usbharu.hideout.activitypub.domain.model.objects.Object
|
||||||
import dev.usbharu.hideout.activitypub.domain.model.objects.ObjectDeserializer
|
import dev.usbharu.hideout.activitypub.domain.model.objects.ObjectDeserializer
|
||||||
|
|
||||||
open class Note
|
open class Note
|
||||||
@Suppress("LongParameterList")
|
@Suppress("LongParameterList", "CyclomaticComplexMethod")
|
||||||
constructor(
|
constructor(
|
||||||
type: List<String> = emptyList(),
|
type: List<String> = emptyList(),
|
||||||
override val id: String,
|
override val id: String,
|
||||||
|
@ -18,12 +19,17 @@ constructor(
|
||||||
val inReplyTo: String? = null,
|
val inReplyTo: String? = null,
|
||||||
val attachment: List<Document> = emptyList(),
|
val attachment: List<Document> = emptyList(),
|
||||||
@JsonDeserialize(contentUsing = ObjectDeserializer::class)
|
@JsonDeserialize(contentUsing = ObjectDeserializer::class)
|
||||||
val tag: List<Object> = emptyList()
|
val tag: List<Object> = emptyList(),
|
||||||
|
val quoteUri: String? = null,
|
||||||
|
val quoteUrl: String? = null,
|
||||||
|
@JsonProperty("_misskey_quote")
|
||||||
|
val misskeyQuote: String? = null
|
||||||
) : Object(
|
) : Object(
|
||||||
type = add(type, "Note")
|
type = add(type, "Note")
|
||||||
),
|
),
|
||||||
HasId {
|
HasId {
|
||||||
|
|
||||||
|
@Suppress("CyclomaticComplexMethod", "CognitiveComplexMethod")
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
if (this === other) return true
|
if (this === other) return true
|
||||||
if (javaClass != other?.javaClass) return false
|
if (javaClass != other?.javaClass) return false
|
||||||
|
@ -41,10 +47,14 @@ constructor(
|
||||||
if (inReplyTo != other.inReplyTo) return false
|
if (inReplyTo != other.inReplyTo) return false
|
||||||
if (attachment != other.attachment) return false
|
if (attachment != other.attachment) return false
|
||||||
if (tag != other.tag) return false
|
if (tag != other.tag) return false
|
||||||
|
if (quoteUri != other.quoteUri) return false
|
||||||
|
if (quoteUrl != other.quoteUrl) return false
|
||||||
|
if (misskeyQuote != other.misskeyQuote) return false
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("CyclomaticComplexMethod")
|
||||||
override fun hashCode(): Int {
|
override fun hashCode(): Int {
|
||||||
var result = super.hashCode()
|
var result = super.hashCode()
|
||||||
result = 31 * result + id.hashCode()
|
result = 31 * result + id.hashCode()
|
||||||
|
@ -57,6 +67,9 @@ constructor(
|
||||||
result = 31 * result + (inReplyTo?.hashCode() ?: 0)
|
result = 31 * result + (inReplyTo?.hashCode() ?: 0)
|
||||||
result = 31 * result + attachment.hashCode()
|
result = 31 * result + attachment.hashCode()
|
||||||
result = 31 * result + tag.hashCode()
|
result = 31 * result + tag.hashCode()
|
||||||
|
result = 31 * result + (quoteUri?.hashCode() ?: 0)
|
||||||
|
result = 31 * result + (quoteUrl?.hashCode() ?: 0)
|
||||||
|
result = 31 * result + (misskeyQuote?.hashCode() ?: 0)
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -71,7 +84,10 @@ constructor(
|
||||||
"sensitive=$sensitive, " +
|
"sensitive=$sensitive, " +
|
||||||
"inReplyTo=$inReplyTo, " +
|
"inReplyTo=$inReplyTo, " +
|
||||||
"attachment=$attachment, " +
|
"attachment=$attachment, " +
|
||||||
"tag=$tag" +
|
"tag=$tag, " +
|
||||||
|
"quoteUri=$quoteUri, " +
|
||||||
|
"quoteUrl=$quoteUrl, " +
|
||||||
|
"misskeyQuote=$misskeyQuote" +
|
||||||
")" +
|
")" +
|
||||||
" ${super.toString()}"
|
" ${super.toString()}"
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,7 +42,7 @@ class ObjectDeserializer : JsonDeserializer<Object>() {
|
||||||
ExtendedActivityVocabulary.OrderedCollectionPage -> null
|
ExtendedActivityVocabulary.OrderedCollectionPage -> null
|
||||||
ExtendedActivityVocabulary.Accept -> p.codec.treeToValue(treeNode, Accept::class.java)
|
ExtendedActivityVocabulary.Accept -> p.codec.treeToValue(treeNode, Accept::class.java)
|
||||||
ExtendedActivityVocabulary.Add -> null
|
ExtendedActivityVocabulary.Add -> null
|
||||||
ExtendedActivityVocabulary.Announce -> null
|
ExtendedActivityVocabulary.Announce -> p.codec.treeToValue(treeNode, Announce::class.java)
|
||||||
ExtendedActivityVocabulary.Arrive -> null
|
ExtendedActivityVocabulary.Arrive -> null
|
||||||
ExtendedActivityVocabulary.Block -> p.codec.treeToValue(treeNode, Block::class.java)
|
ExtendedActivityVocabulary.Block -> p.codec.treeToValue(treeNode, Block::class.java)
|
||||||
ExtendedActivityVocabulary.Create -> p.codec.treeToValue(treeNode, Create::class.java)
|
ExtendedActivityVocabulary.Create -> p.codec.treeToValue(treeNode, Create::class.java)
|
||||||
|
|
|
@ -0,0 +1,66 @@
|
||||||
|
package dev.usbharu.hideout.activitypub.infrastructure.exposedquery
|
||||||
|
|
||||||
|
import dev.usbharu.hideout.activitypub.domain.model.Announce
|
||||||
|
import dev.usbharu.hideout.activitypub.query.AnnounceQueryService
|
||||||
|
import dev.usbharu.hideout.activitypub.service.objects.note.APNoteServiceImpl
|
||||||
|
import dev.usbharu.hideout.application.infrastructure.exposed.ResultRowMapper
|
||||||
|
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.Visibility
|
||||||
|
import dev.usbharu.hideout.core.infrastructure.exposedrepository.Actors
|
||||||
|
import dev.usbharu.hideout.core.infrastructure.exposedrepository.Posts
|
||||||
|
import org.jetbrains.exposed.sql.ResultRow
|
||||||
|
import org.jetbrains.exposed.sql.select
|
||||||
|
import org.springframework.stereotype.Repository
|
||||||
|
import java.time.Instant
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
class ExposedAnnounceQueryService(
|
||||||
|
private val postRepository: PostRepository,
|
||||||
|
private val postResultRowMapper: ResultRowMapper<Post>
|
||||||
|
) : AnnounceQueryService {
|
||||||
|
override suspend fun findById(id: Long): Pair<Announce, Post>? {
|
||||||
|
return Posts
|
||||||
|
.leftJoin(Actors)
|
||||||
|
.select { Posts.id eq id }
|
||||||
|
.singleOrNull()
|
||||||
|
?.let { (it.toAnnounce() ?: return null) to (postResultRowMapper.map(it)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun findByApId(apId: String): Pair<Announce, Post>? {
|
||||||
|
return Posts
|
||||||
|
.leftJoin(Actors)
|
||||||
|
.select { Posts.apId eq apId }
|
||||||
|
.singleOrNull()
|
||||||
|
?.let { (it.toAnnounce() ?: return null) to (postResultRowMapper.map(it)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun ResultRow.toAnnounce(): Announce? {
|
||||||
|
val repostId = this[Posts.repostId] ?: return null
|
||||||
|
val repost = postRepository.findById(repostId)?.url ?: return null
|
||||||
|
|
||||||
|
val (to, cc) = visibility(
|
||||||
|
Visibility.values().first { visibility -> visibility.ordinal == this[Posts.visibility] },
|
||||||
|
this[Actors.followers]
|
||||||
|
)
|
||||||
|
|
||||||
|
return Announce(
|
||||||
|
type = emptyList(),
|
||||||
|
id = this[Posts.apId],
|
||||||
|
apObject = repost,
|
||||||
|
actor = this[Actors.url],
|
||||||
|
published = Instant.ofEpochMilli(this[Posts.createdAt]).toString(),
|
||||||
|
to = to,
|
||||||
|
cc = cc
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun visibility(visibility: Visibility, followers: String?): Pair<List<String>, List<String>> {
|
||||||
|
return when (visibility) {
|
||||||
|
Visibility.PUBLIC -> listOf(APNoteServiceImpl.public) to listOf(APNoteServiceImpl.public)
|
||||||
|
Visibility.UNLISTED -> listOfNotNull(followers) to listOf(APNoteServiceImpl.public)
|
||||||
|
Visibility.FOLLOWERS -> listOfNotNull(followers) to listOfNotNull(followers)
|
||||||
|
Visibility.DIRECT -> TODO()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -59,6 +59,17 @@ class NoteQueryServiceImpl(private val postRepository: PostRepository, private v
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val repostId = this[Posts.repostId]
|
||||||
|
val repost = if (repostId != null) {
|
||||||
|
val url = postRepository.findById(repostId)?.url
|
||||||
|
if (url == null) {
|
||||||
|
logger.warn("Failed to get repostId: $repostId")
|
||||||
|
}
|
||||||
|
url
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
val visibility1 =
|
val visibility1 =
|
||||||
visibility(
|
visibility(
|
||||||
Visibility.values().first { visibility -> visibility.ordinal == this[Posts.visibility] },
|
Visibility.values().first { visibility -> visibility.ordinal == this[Posts.visibility] },
|
||||||
|
@ -72,6 +83,9 @@ class NoteQueryServiceImpl(private val postRepository: PostRepository, private v
|
||||||
to = visibility1.first,
|
to = visibility1.first,
|
||||||
cc = visibility1.second,
|
cc = visibility1.second,
|
||||||
inReplyTo = replyTo,
|
inReplyTo = replyTo,
|
||||||
|
misskeyQuote = repost,
|
||||||
|
quoteUri = repost,
|
||||||
|
quoteUrl = repost,
|
||||||
sensitive = this[Posts.sensitive],
|
sensitive = this[Posts.sensitive],
|
||||||
attachment = mediaList.map { Document(url = it.url, mediaType = "image/jpeg") }
|
attachment = mediaList.map { Document(url = it.url, mediaType = "image/jpeg") }
|
||||||
)
|
)
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
package dev.usbharu.hideout.activitypub.query
|
||||||
|
|
||||||
|
import dev.usbharu.hideout.activitypub.domain.model.Announce
|
||||||
|
import dev.usbharu.hideout.core.domain.model.post.Post
|
||||||
|
import org.springframework.stereotype.Repository
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
interface AnnounceQueryService {
|
||||||
|
suspend fun findById(id: Long): Pair<Announce, Post>?
|
||||||
|
suspend fun findByApId(apId: String): Pair<Announce, Post>?
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
package dev.usbharu.hideout.activitypub.service.activity.announce
|
||||||
|
|
||||||
|
import dev.usbharu.hideout.activitypub.domain.model.Announce
|
||||||
|
import dev.usbharu.hideout.activitypub.service.common.AbstractActivityPubProcessor
|
||||||
|
import dev.usbharu.hideout.activitypub.service.common.ActivityPubProcessContext
|
||||||
|
import dev.usbharu.hideout.activitypub.service.common.ActivityType
|
||||||
|
import dev.usbharu.hideout.activitypub.service.objects.note.APNoteService
|
||||||
|
import dev.usbharu.hideout.application.external.Transaction
|
||||||
|
import org.springframework.stereotype.Service
|
||||||
|
|
||||||
|
@Service
|
||||||
|
class ApAnnounceProcessor(transaction: Transaction, private val apNoteService: APNoteService) :
|
||||||
|
AbstractActivityPubProcessor<Announce>(transaction) {
|
||||||
|
override suspend fun internalProcess(activity: ActivityPubProcessContext<Announce>) {
|
||||||
|
apNoteService.fetchAnnounce(activity.activity)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isSupported(activityType: ActivityType): Boolean = ActivityType.Announce == activityType
|
||||||
|
|
||||||
|
override fun type(): Class<Announce> = Announce::class.java
|
||||||
|
}
|
|
@ -12,6 +12,7 @@ import dev.usbharu.hideout.core.domain.exception.resource.UserNotFoundException
|
||||||
import dev.usbharu.hideout.core.domain.exception.resource.local.LocalUserNotFoundException
|
import dev.usbharu.hideout.core.domain.exception.resource.local.LocalUserNotFoundException
|
||||||
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.PostRepository
|
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.reaction.ReactionService
|
||||||
import dev.usbharu.hideout.core.service.relationship.RelationshipService
|
import dev.usbharu.hideout.core.service.relationship.RelationshipService
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
|
@ -23,7 +24,8 @@ class APUndoProcessor(
|
||||||
private val relationshipService: RelationshipService,
|
private val relationshipService: RelationshipService,
|
||||||
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
|
||||||
) : 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
|
||||||
|
@ -53,6 +55,11 @@ class APUndoProcessor(
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
"Announce" -> {
|
||||||
|
announce(undo)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
else -> {}
|
else -> {}
|
||||||
}
|
}
|
||||||
TODO()
|
TODO()
|
||||||
|
@ -109,6 +116,13 @@ class APUndoProcessor(
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun announce(undo: Undo) {
|
||||||
|
val announce = undo.apObject as Announce
|
||||||
|
|
||||||
|
val findByApId = postRepository.findByApId(announce.id) ?: return
|
||||||
|
postService.deleteRemote(findByApId)
|
||||||
|
}
|
||||||
|
|
||||||
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
|
||||||
|
|
|
@ -1,13 +1,17 @@
|
||||||
package dev.usbharu.hideout.activitypub.service.objects.note
|
package dev.usbharu.hideout.activitypub.service.objects.note
|
||||||
|
|
||||||
import dev.usbharu.hideout.activitypub.domain.exception.FailedToGetActivityPubResourceException
|
import dev.usbharu.hideout.activitypub.domain.exception.FailedToGetActivityPubResourceException
|
||||||
|
import dev.usbharu.hideout.activitypub.domain.model.Announce
|
||||||
import dev.usbharu.hideout.activitypub.domain.model.Emoji
|
import dev.usbharu.hideout.activitypub.domain.model.Emoji
|
||||||
import dev.usbharu.hideout.activitypub.domain.model.Note
|
import dev.usbharu.hideout.activitypub.domain.model.Note
|
||||||
|
import dev.usbharu.hideout.activitypub.domain.model.Person
|
||||||
|
import dev.usbharu.hideout.activitypub.query.AnnounceQueryService
|
||||||
import dev.usbharu.hideout.activitypub.query.NoteQueryService
|
import dev.usbharu.hideout.activitypub.query.NoteQueryService
|
||||||
import dev.usbharu.hideout.activitypub.service.common.APResourceResolveService
|
import dev.usbharu.hideout.activitypub.service.common.APResourceResolveService
|
||||||
import dev.usbharu.hideout.activitypub.service.common.resolve
|
import dev.usbharu.hideout.activitypub.service.common.resolve
|
||||||
import dev.usbharu.hideout.activitypub.service.objects.emoji.EmojiService
|
import dev.usbharu.hideout.activitypub.service.objects.emoji.EmojiService
|
||||||
import dev.usbharu.hideout.activitypub.service.objects.user.APUserService
|
import dev.usbharu.hideout.activitypub.service.objects.user.APUserService
|
||||||
|
import dev.usbharu.hideout.core.domain.model.actor.Actor
|
||||||
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.domain.model.post.Visibility
|
import dev.usbharu.hideout.core.domain.model.post.Visibility
|
||||||
|
@ -23,6 +27,9 @@ interface APNoteService {
|
||||||
suspend fun fetchNote(url: String, targetActor: String? = null): Note = fetchNoteWithEntity(url, targetActor).first
|
suspend fun fetchNote(url: String, targetActor: String? = null): Note = fetchNoteWithEntity(url, targetActor).first
|
||||||
suspend fun fetchNote(note: Note, targetActor: String? = null): Note
|
suspend fun fetchNote(note: Note, targetActor: String? = null): Note
|
||||||
suspend fun fetchNoteWithEntity(url: String, targetActor: String? = null): Pair<Note, Post>
|
suspend fun fetchNoteWithEntity(url: String, targetActor: String? = null): Pair<Note, Post>
|
||||||
|
|
||||||
|
suspend fun fetchAnnounce(url: String, signerId: Long? = null): Pair<Announce, Post>
|
||||||
|
suspend fun fetchAnnounce(announce: Announce, signerId: Long? = null): Pair<Announce, Post>
|
||||||
}
|
}
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
|
@ -35,7 +42,8 @@ class APNoteServiceImpl(
|
||||||
private val postBuilder: Post.PostBuilder,
|
private val postBuilder: Post.PostBuilder,
|
||||||
private val noteQueryService: NoteQueryService,
|
private val noteQueryService: NoteQueryService,
|
||||||
private val mediaService: MediaService,
|
private val mediaService: MediaService,
|
||||||
private val emojiService: EmojiService
|
private val emojiService: EmojiService,
|
||||||
|
private val announceQueryService: AnnounceQueryService
|
||||||
|
|
||||||
) : APNoteService {
|
) : APNoteService {
|
||||||
|
|
||||||
|
@ -62,41 +70,90 @@ class APNoteServiceImpl(
|
||||||
)
|
)
|
||||||
throw FailedToGetActivityPubResourceException("Could not retrieve $url.", e)
|
throw FailedToGetActivityPubResourceException("Could not retrieve $url.", e)
|
||||||
}
|
}
|
||||||
val savedNote = saveIfMissing(note, targetActor, url)
|
val savedNote = saveIfMissing(note, targetActor)
|
||||||
logger.debug("SUCCESS Fetch Note url: {}", url)
|
logger.debug("SUCCESS Fetch Note url: {}", url)
|
||||||
return savedNote
|
return savedNote
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun fetchAnnounce(url: String, signerId: Long?): Pair<Announce, Post> {
|
||||||
|
logger.debug("START Fetch Announce url: {}", url)
|
||||||
|
|
||||||
|
val post: Pair<Announce, Post>? = announceQueryService.findByApId(url)
|
||||||
|
|
||||||
|
if (post != null) {
|
||||||
|
logger.debug("SUCCESS Found in local url: {}", url)
|
||||||
|
return post
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("AP GET url: {}", url)
|
||||||
|
|
||||||
|
val announce = try {
|
||||||
|
apResourceResolveService.resolve<Announce>(url, signerId)
|
||||||
|
} catch (e: ClientRequestException) {
|
||||||
|
logger.warn(
|
||||||
|
"FAILED Failed to retrieve ActivityPub resource. HTTP Status Code: {} url: {}",
|
||||||
|
e.response.status,
|
||||||
|
url
|
||||||
|
)
|
||||||
|
throw FailedToGetActivityPubResourceException("Could not retrieve $url.", e)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fetchAnnounce(announce, signerId)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun fetchAnnounce(announce: Announce, signerId: Long?): Pair<Announce, Post> {
|
||||||
|
val findByApId = announceQueryService.findByApId(announce.id)
|
||||||
|
|
||||||
|
if (findByApId != null) {
|
||||||
|
return findByApId
|
||||||
|
}
|
||||||
|
|
||||||
|
val (_, actor) = apUserService.fetchPersonWithEntity(announce.actor, null)
|
||||||
|
|
||||||
|
val (_, post) = fetchNoteWithEntity(announce.apObject, null)
|
||||||
|
|
||||||
|
val visibility = if (announce.to.contains(public)) {
|
||||||
|
Visibility.PUBLIC
|
||||||
|
} else if (announce.to.contains(actor.followers) && announce.cc.contains(public)) {
|
||||||
|
Visibility.UNLISTED
|
||||||
|
} else if (announce.to.contains(actor.followers)) {
|
||||||
|
Visibility.FOLLOWERS
|
||||||
|
} else {
|
||||||
|
Visibility.DIRECT
|
||||||
|
}
|
||||||
|
|
||||||
|
val createRemote = postService.createRemote(
|
||||||
|
postBuilder.pureRepostOf(
|
||||||
|
id = postRepository.generateId(),
|
||||||
|
actorId = actor.id,
|
||||||
|
visibility = visibility,
|
||||||
|
createdAt = Instant.parse(announce.published),
|
||||||
|
url = announce.id,
|
||||||
|
repost = post,
|
||||||
|
apId = announce.id
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return announce to createRemote
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun saveIfMissing(
|
private suspend fun saveIfMissing(
|
||||||
note: Note,
|
note: Note,
|
||||||
targetActor: String?,
|
targetActor: String?
|
||||||
url: String
|
): Pair<Note, Post> = noteQueryService.findByApid(note.id) ?: saveNote(note, targetActor)
|
||||||
): Pair<Note, Post> = noteQueryService.findByApid(note.id) ?: saveNote(note, targetActor, url)
|
|
||||||
|
|
||||||
private suspend fun saveNote(note: Note, targetActor: String?, url: String): Pair<Note, Post> {
|
private suspend fun saveNote(note: Note, targetActor: String?): Pair<Note, Post> {
|
||||||
val person = apUserService.fetchPersonWithEntity(
|
val person = apUserService.fetchPersonWithEntity(
|
||||||
note.attributedTo,
|
note.attributedTo,
|
||||||
targetActor
|
targetActor
|
||||||
)
|
)
|
||||||
|
|
||||||
val post = postRepository.findByApId(note.id)
|
val post = postRepository.findByApId(note.id)
|
||||||
|
|
||||||
if (post != null) {
|
if (post != null) {
|
||||||
return note to post
|
return note to post
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.debug("VISIBILITY url: {} to: {} cc: {}", note.id, note.to, note.cc)
|
logger.debug("VISIBILITY url: {} to: {} cc: {}", note.id, note.to, note.cc)
|
||||||
|
val visibility = visibility(note, person)
|
||||||
val visibility = if (note.to.contains(public)) {
|
|
||||||
Visibility.PUBLIC
|
|
||||||
} else if (note.to.contains(person.second.followers) && note.cc.contains(public)) {
|
|
||||||
Visibility.UNLISTED
|
|
||||||
} else if (note.to.contains(person.second.followers)) {
|
|
||||||
Visibility.FOLLOWERS
|
|
||||||
} else {
|
|
||||||
Visibility.DIRECT
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.debug("VISIBILITY is {} url: {}", visibility.name, note.id)
|
logger.debug("VISIBILITY is {} url: {}", visibility.name, note.id)
|
||||||
|
|
||||||
val reply = note.inReplyTo?.let {
|
val reply = note.inReplyTo?.let {
|
||||||
|
@ -104,14 +161,12 @@ class APNoteServiceImpl(
|
||||||
postRepository.findByUrl(it)
|
postRepository.findByUrl(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
val emojis = note.tag
|
val quote = (note.misskeyQuote ?: note.quoteUri ?: note.quoteUrl)?.let {
|
||||||
.filterIsInstance<Emoji>()
|
fetchNote(it, targetActor)
|
||||||
.map {
|
postRepository.findByUrl(it)
|
||||||
emojiService.fetchEmoji(it).second
|
}
|
||||||
}
|
|
||||||
.map {
|
val emojis = buildEmojis(note)
|
||||||
it.id
|
|
||||||
}
|
|
||||||
|
|
||||||
val mediaList = note.attachment.map {
|
val mediaList = note.attachment.map {
|
||||||
mediaService.uploadRemoteMedia(
|
mediaService.uploadRemoteMedia(
|
||||||
|
@ -124,26 +179,69 @@ class APNoteServiceImpl(
|
||||||
)
|
)
|
||||||
}.map { it.id }
|
}.map { it.id }
|
||||||
|
|
||||||
val createRemote = postService.createRemote(
|
val createPost =
|
||||||
postBuilder.of(
|
if (quote != null) {
|
||||||
id = postRepository.generateId(),
|
postBuilder.quoteRepostOf(
|
||||||
actorId = person.second.id,
|
id = postRepository.generateId(),
|
||||||
content = note.content,
|
actorId = person.second.id,
|
||||||
createdAt = Instant.parse(note.published).toEpochMilli(),
|
content = note.content,
|
||||||
visibility = visibility,
|
createdAt = Instant.parse(note.published),
|
||||||
url = note.id,
|
visibility = visibility,
|
||||||
replyId = reply?.id,
|
url = note.id,
|
||||||
sensitive = note.sensitive,
|
replyId = reply?.id,
|
||||||
apId = note.id,
|
sensitive = note.sensitive,
|
||||||
mediaIds = mediaList,
|
apId = note.id,
|
||||||
emojiIds = emojis
|
mediaIds = mediaList,
|
||||||
)
|
emojiIds = emojis,
|
||||||
)
|
repost = quote
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
postBuilder.of(
|
||||||
|
id = postRepository.generateId(),
|
||||||
|
actorId = person.second.id,
|
||||||
|
content = note.content,
|
||||||
|
createdAt = Instant.parse(note.published).toEpochMilli(),
|
||||||
|
visibility = visibility,
|
||||||
|
url = note.id,
|
||||||
|
replyId = reply?.id,
|
||||||
|
sensitive = note.sensitive,
|
||||||
|
apId = note.id,
|
||||||
|
mediaIds = mediaList,
|
||||||
|
emojiIds = emojis
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val createRemote = postService.createRemote(createPost)
|
||||||
return note to createRemote
|
return note to createRemote
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun buildEmojis(note: Note) = note.tag
|
||||||
|
.filterIsInstance<Emoji>()
|
||||||
|
.map {
|
||||||
|
emojiService.fetchEmoji(it).second
|
||||||
|
}
|
||||||
|
.map {
|
||||||
|
it.id
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun visibility(
|
||||||
|
note: Note,
|
||||||
|
person: Pair<Person, Actor>
|
||||||
|
): Visibility {
|
||||||
|
val visibility = if (note.to.contains(public)) {
|
||||||
|
Visibility.PUBLIC
|
||||||
|
} else if (note.to.contains(person.second.followers) && note.cc.contains(public)) {
|
||||||
|
Visibility.UNLISTED
|
||||||
|
} else if (note.to.contains(person.second.followers)) {
|
||||||
|
Visibility.FOLLOWERS
|
||||||
|
} else {
|
||||||
|
Visibility.DIRECT
|
||||||
|
}
|
||||||
|
return visibility
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun fetchNote(note: Note, targetActor: String?): Note =
|
override suspend fun fetchNote(note: Note, targetActor: String?): Note =
|
||||||
saveIfMissing(note, targetActor, note.id).first
|
saveIfMissing(note, targetActor).first
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val public: String = "https://www.w3.org/ns/activitystreams#Public"
|
const val public: String = "https://www.w3.org/ns/activitystreams#Public"
|
||||||
|
|
|
@ -5,6 +5,7 @@ import dev.usbharu.hideout.application.config.CharacterLimit
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import org.springframework.stereotype.Component
|
import org.springframework.stereotype.Component
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
|
import kotlin.math.max
|
||||||
|
|
||||||
data class Actor private constructor(
|
data class Actor private constructor(
|
||||||
val id: Long,
|
val id: Long,
|
||||||
|
@ -159,18 +160,6 @@ data class Actor private constructor(
|
||||||
"keyId must contain non-blank characters."
|
"keyId must contain non-blank characters."
|
||||||
}
|
}
|
||||||
|
|
||||||
require(postsCount >= 0) {
|
|
||||||
"postsCount must be greater than or equal to 0"
|
|
||||||
}
|
|
||||||
|
|
||||||
require(followersCount >= 0) {
|
|
||||||
"followersCount must be greater than or equal to 0"
|
|
||||||
}
|
|
||||||
|
|
||||||
require(followingCount >= 0) {
|
|
||||||
"followingCount must be greater than or equal to 0"
|
|
||||||
}
|
|
||||||
|
|
||||||
return Actor(
|
return Actor(
|
||||||
id = id,
|
id = id,
|
||||||
name = limitedName,
|
name = limitedName,
|
||||||
|
@ -188,9 +177,9 @@ data class Actor private constructor(
|
||||||
following = following,
|
following = following,
|
||||||
instance = instance,
|
instance = instance,
|
||||||
locked = locked,
|
locked = locked,
|
||||||
followersCount = followersCount,
|
followersCount = max(0, followersCount),
|
||||||
followingCount = followingCount,
|
followingCount = max(0, followingCount),
|
||||||
postsCount = postsCount,
|
postsCount = max(0, postsCount),
|
||||||
lastPostDate = lastPostDate,
|
lastPostDate = lastPostDate,
|
||||||
emojis = emojis
|
emojis = emojis
|
||||||
)
|
)
|
||||||
|
|
|
@ -89,6 +89,111 @@ data class Post private constructor(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun pureRepostOf(
|
||||||
|
id: Long,
|
||||||
|
actorId: Long,
|
||||||
|
visibility: Visibility,
|
||||||
|
createdAt: Instant,
|
||||||
|
url: String,
|
||||||
|
repost: Post,
|
||||||
|
apId: String
|
||||||
|
): Post {
|
||||||
|
// リポストの公開範囲は元のポストより広くてはいけない
|
||||||
|
val fixedVisibility = if (visibility.ordinal <= repost.visibility.ordinal) {
|
||||||
|
repost.visibility
|
||||||
|
} else {
|
||||||
|
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." }
|
||||||
|
|
||||||
|
return Post(
|
||||||
|
id,
|
||||||
|
actorId,
|
||||||
|
null,
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
createdAt.toEpochMilli(),
|
||||||
|
fixedVisibility,
|
||||||
|
url,
|
||||||
|
repost.id,
|
||||||
|
null,
|
||||||
|
false,
|
||||||
|
apId,
|
||||||
|
emptyList(),
|
||||||
|
false,
|
||||||
|
emptyList()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun quoteRepostOf(
|
||||||
|
id: Long,
|
||||||
|
actorId: Long,
|
||||||
|
overview: String? = null,
|
||||||
|
content: String,
|
||||||
|
createdAt: Instant,
|
||||||
|
visibility: Visibility,
|
||||||
|
url: String,
|
||||||
|
repost: Post,
|
||||||
|
replyId: Long? = null,
|
||||||
|
sensitive: Boolean = false,
|
||||||
|
apId: String = url,
|
||||||
|
mediaIds: List<Long> = emptyList(),
|
||||||
|
emojiIds: List<Long> = emptyList()
|
||||||
|
): Post {
|
||||||
|
// リポストの公開範囲は元のポストより広くてはいけない
|
||||||
|
val fixedVisibility = if (visibility.ordinal <= repost.visibility.ordinal) {
|
||||||
|
repost.visibility
|
||||||
|
} else {
|
||||||
|
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." }
|
||||||
|
|
||||||
|
return Post(
|
||||||
|
id = id,
|
||||||
|
actorId = actorId,
|
||||||
|
overview = limitedOverview,
|
||||||
|
content = html,
|
||||||
|
text = content1,
|
||||||
|
createdAt = createdAt.toEpochMilli(),
|
||||||
|
visibility = fixedVisibility,
|
||||||
|
url = url,
|
||||||
|
repostId = repost.id,
|
||||||
|
replyId = replyId,
|
||||||
|
sensitive = sensitive,
|
||||||
|
apId = apId,
|
||||||
|
mediaIds = mediaIds,
|
||||||
|
delted = false,
|
||||||
|
emojiIds = emojiIds
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Suppress("LongParameterList")
|
@Suppress("LongParameterList")
|
||||||
fun deleteOf(
|
fun deleteOf(
|
||||||
id: Long,
|
id: Long,
|
||||||
|
@ -117,6 +222,13 @@ data class Post private constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun isPureRepost(): Boolean =
|
||||||
|
this.text.isEmpty() &&
|
||||||
|
this.content.isEmpty() &&
|
||||||
|
this.overview == null &&
|
||||||
|
this.replyId == null &&
|
||||||
|
this.repostId != null
|
||||||
|
|
||||||
fun delete(): Post {
|
fun delete(): Post {
|
||||||
return Post(
|
return Post(
|
||||||
id = this.id,
|
id = this.id,
|
||||||
|
|
|
@ -36,6 +36,7 @@ interface RelationshipRepository {
|
||||||
|
|
||||||
suspend fun findByTargetIdAndFollowing(targetId: Long, following: Boolean): List<Relationship>
|
suspend fun findByTargetIdAndFollowing(targetId: Long, following: Boolean): List<Relationship>
|
||||||
|
|
||||||
|
@Suppress("FunctionMaxLength")
|
||||||
suspend fun findByTargetIdAndFollowRequestAndIgnoreFollowRequest(
|
suspend fun findByTargetIdAndFollowRequestAndIgnoreFollowRequest(
|
||||||
targetId: Long,
|
targetId: Long,
|
||||||
followRequest: Boolean,
|
followRequest: Boolean,
|
||||||
|
|
|
@ -8,6 +8,7 @@ interface MastodonNotificationRepository {
|
||||||
suspend fun deleteById(id: Long)
|
suspend fun deleteById(id: Long)
|
||||||
suspend fun findById(id: Long): MastodonNotification?
|
suspend fun findById(id: Long): MastodonNotification?
|
||||||
|
|
||||||
|
@Suppress("FunctionMaxLength")
|
||||||
suspend fun findByUserIdAndInTypesAndInSourceActorId(
|
suspend fun findByUserIdAndInTypesAndInSourceActorId(
|
||||||
loginUser: Long,
|
loginUser: Long,
|
||||||
types: List<NotificationType>,
|
types: List<NotificationType>,
|
||||||
|
|
|
@ -84,7 +84,7 @@ class MastodonAccountApiController(
|
||||||
maxId?.toLongOrNull(),
|
maxId?.toLongOrNull(),
|
||||||
sinceId?.toLongOrNull(),
|
sinceId?.toLongOrNull(),
|
||||||
minId?.toLongOrNull(),
|
minId?.toLongOrNull(),
|
||||||
limit.coerceIn(0, 80) ?: 40
|
limit.coerceIn(0, 80)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
val httpHeader = statuses.toHttpHeader(
|
val httpHeader = statuses.toHttpHeader(
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
package dev.usbharu.hideout.activitypub.domain.model
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.module.kotlin.readValue
|
||||||
|
import dev.usbharu.hideout.application.config.ActivityPubConfig
|
||||||
|
import org.junit.jupiter.api.Assertions.*
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
|
||||||
|
class AnnounceTest{
|
||||||
|
@Test
|
||||||
|
fun mastodonのjsonをデシリアライズできる() {
|
||||||
|
//language=JSON
|
||||||
|
val json = """{
|
||||||
|
"@context": "https://www.w3.org/ns/activitystreams",
|
||||||
|
"id": "https://kb.usbharu.dev/users/usbharu/statuses/111859915842276344/activity",
|
||||||
|
"type": "Announce",
|
||||||
|
"actor": "https://kb.usbharu.dev/users/usbharu",
|
||||||
|
"published": "2024-02-02T04:07:40Z",
|
||||||
|
"to": [
|
||||||
|
"https://kb.usbharu.dev/users/usbharu/followers"
|
||||||
|
],
|
||||||
|
"cc": [
|
||||||
|
"https://kb.usbharu.dev/users/usbharu"
|
||||||
|
],
|
||||||
|
"object": "https://kb.usbharu.dev/users/usbharu/statuses/111850484548963326"
|
||||||
|
}"""
|
||||||
|
|
||||||
|
val objectMapper = ActivityPubConfig().objectMapper()
|
||||||
|
|
||||||
|
val readValue = objectMapper.readValue<Announce>(json)
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -79,6 +79,7 @@ class APNoteServiceImplTest {
|
||||||
),
|
),
|
||||||
noteQueryService = noteQueryService,
|
noteQueryService = noteQueryService,
|
||||||
mock(),
|
mock(),
|
||||||
|
mock(),
|
||||||
mock()
|
mock()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -152,7 +153,8 @@ class APNoteServiceImplTest {
|
||||||
),
|
),
|
||||||
noteQueryService = noteQueryService,
|
noteQueryService = noteQueryService,
|
||||||
mock(),
|
mock(),
|
||||||
mock { }
|
mock { },
|
||||||
|
mock()
|
||||||
)
|
)
|
||||||
|
|
||||||
val actual = apNoteServiceImpl.fetchNote(url)
|
val actual = apNoteServiceImpl.fetchNote(url)
|
||||||
|
@ -204,7 +206,8 @@ class APNoteServiceImplTest {
|
||||||
),
|
),
|
||||||
noteQueryService = noteQueryService,
|
noteQueryService = noteQueryService,
|
||||||
mock(),
|
mock(),
|
||||||
mock()
|
mock(),
|
||||||
|
mock { }
|
||||||
)
|
)
|
||||||
|
|
||||||
assertThrows<FailedToGetActivityPubResourceException> { apNoteServiceImpl.fetchNote(url) }
|
assertThrows<FailedToGetActivityPubResourceException> { apNoteServiceImpl.fetchNote(url) }
|
||||||
|
@ -255,6 +258,7 @@ class APNoteServiceImplTest {
|
||||||
postBuilder = postBuilder,
|
postBuilder = postBuilder,
|
||||||
noteQueryService = noteQueryService,
|
noteQueryService = noteQueryService,
|
||||||
mock(),
|
mock(),
|
||||||
|
mock(),
|
||||||
mock()
|
mock()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -308,6 +312,7 @@ class APNoteServiceImplTest {
|
||||||
postBuilder = postBuilder,
|
postBuilder = postBuilder,
|
||||||
noteQueryService = noteQueryService,
|
noteQueryService = noteQueryService,
|
||||||
mock(),
|
mock(),
|
||||||
|
mock(),
|
||||||
mock()
|
mock()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue