diff --git a/src/main/kotlin/dev/usbharu/hideout/activitypub/domain/model/Announce.kt b/src/main/kotlin/dev/usbharu/hideout/activitypub/domain/model/Announce.kt new file mode 100644 index 00000000..513d4429 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/activitypub/domain/model/Announce.kt @@ -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 = emptyList(), + @JsonProperty("object") + val apObject: String, + override val actor: String, + override val id: String, + val published: String, + val to: List = emptyList(), + val cc: List = 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()}" + } +} diff --git a/src/main/kotlin/dev/usbharu/hideout/activitypub/domain/model/Note.kt b/src/main/kotlin/dev/usbharu/hideout/activitypub/domain/model/Note.kt index 60e7858c..cb51a489 100644 --- a/src/main/kotlin/dev/usbharu/hideout/activitypub/domain/model/Note.kt +++ b/src/main/kotlin/dev/usbharu/hideout/activitypub/domain/model/Note.kt @@ -1,11 +1,12 @@ package dev.usbharu.hideout.activitypub.domain.model +import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.databind.annotation.JsonDeserialize import dev.usbharu.hideout.activitypub.domain.model.objects.Object import dev.usbharu.hideout.activitypub.domain.model.objects.ObjectDeserializer open class Note -@Suppress("LongParameterList") +@Suppress("LongParameterList", "CyclomaticComplexMethod") constructor( type: List = emptyList(), override val id: String, @@ -18,12 +19,17 @@ constructor( val inReplyTo: String? = null, val attachment: List = emptyList(), @JsonDeserialize(contentUsing = ObjectDeserializer::class) - val tag: List = emptyList() + val tag: List = emptyList(), + val quoteUri: String? = null, + val quoteUrl: String? = null, + @JsonProperty("_misskey_quote") + val misskeyQuote: String? = null ) : Object( type = add(type, "Note") ), HasId { + @Suppress("CyclomaticComplexMethod", "CognitiveComplexMethod") override fun equals(other: Any?): Boolean { if (this === other) return true if (javaClass != other?.javaClass) return false @@ -41,10 +47,14 @@ constructor( if (inReplyTo != other.inReplyTo) return false if (attachment != other.attachment) 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 } + @Suppress("CyclomaticComplexMethod") override fun hashCode(): Int { var result = super.hashCode() result = 31 * result + id.hashCode() @@ -57,6 +67,9 @@ constructor( result = 31 * result + (inReplyTo?.hashCode() ?: 0) result = 31 * result + attachment.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 } @@ -71,7 +84,10 @@ constructor( "sensitive=$sensitive, " + "inReplyTo=$inReplyTo, " + "attachment=$attachment, " + - "tag=$tag" + + "tag=$tag, " + + "quoteUri=$quoteUri, " + + "quoteUrl=$quoteUrl, " + + "misskeyQuote=$misskeyQuote" + ")" + " ${super.toString()}" } diff --git a/src/main/kotlin/dev/usbharu/hideout/activitypub/domain/model/objects/ObjectDeserializer.kt b/src/main/kotlin/dev/usbharu/hideout/activitypub/domain/model/objects/ObjectDeserializer.kt index f35b0c4f..781857d3 100644 --- a/src/main/kotlin/dev/usbharu/hideout/activitypub/domain/model/objects/ObjectDeserializer.kt +++ b/src/main/kotlin/dev/usbharu/hideout/activitypub/domain/model/objects/ObjectDeserializer.kt @@ -42,7 +42,7 @@ class ObjectDeserializer : JsonDeserializer() { ExtendedActivityVocabulary.OrderedCollectionPage -> null ExtendedActivityVocabulary.Accept -> p.codec.treeToValue(treeNode, Accept::class.java) ExtendedActivityVocabulary.Add -> null - ExtendedActivityVocabulary.Announce -> null + ExtendedActivityVocabulary.Announce -> p.codec.treeToValue(treeNode, Announce::class.java) ExtendedActivityVocabulary.Arrive -> null ExtendedActivityVocabulary.Block -> p.codec.treeToValue(treeNode, Block::class.java) ExtendedActivityVocabulary.Create -> p.codec.treeToValue(treeNode, Create::class.java) diff --git a/src/main/kotlin/dev/usbharu/hideout/activitypub/infrastructure/exposedquery/ExposedAnnounceQueryService.kt b/src/main/kotlin/dev/usbharu/hideout/activitypub/infrastructure/exposedquery/ExposedAnnounceQueryService.kt new file mode 100644 index 00000000..c89a6ce1 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/activitypub/infrastructure/exposedquery/ExposedAnnounceQueryService.kt @@ -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 +) : AnnounceQueryService { + override suspend fun findById(id: Long): Pair? { + 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? { + 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> { + 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() + } + } +} diff --git a/src/main/kotlin/dev/usbharu/hideout/activitypub/infrastructure/exposedquery/NoteQueryServiceImpl.kt b/src/main/kotlin/dev/usbharu/hideout/activitypub/infrastructure/exposedquery/NoteQueryServiceImpl.kt index 1ebc9511..ffe2ee83 100644 --- a/src/main/kotlin/dev/usbharu/hideout/activitypub/infrastructure/exposedquery/NoteQueryServiceImpl.kt +++ b/src/main/kotlin/dev/usbharu/hideout/activitypub/infrastructure/exposedquery/NoteQueryServiceImpl.kt @@ -59,6 +59,17 @@ class NoteQueryServiceImpl(private val postRepository: PostRepository, private v 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 = 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, cc = visibility1.second, inReplyTo = replyTo, + misskeyQuote = repost, + quoteUri = repost, + quoteUrl = repost, sensitive = this[Posts.sensitive], attachment = mediaList.map { Document(url = it.url, mediaType = "image/jpeg") } ) diff --git a/src/main/kotlin/dev/usbharu/hideout/activitypub/query/AnnounceQueryService.kt b/src/main/kotlin/dev/usbharu/hideout/activitypub/query/AnnounceQueryService.kt new file mode 100644 index 00000000..2e01f9ee --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/activitypub/query/AnnounceQueryService.kt @@ -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? + suspend fun findByApId(apId: String): Pair? +} diff --git a/src/main/kotlin/dev/usbharu/hideout/activitypub/service/activity/announce/ApAnnounceProcessor.kt b/src/main/kotlin/dev/usbharu/hideout/activitypub/service/activity/announce/ApAnnounceProcessor.kt new file mode 100644 index 00000000..99303538 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/activitypub/service/activity/announce/ApAnnounceProcessor.kt @@ -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(transaction) { + override suspend fun internalProcess(activity: ActivityPubProcessContext) { + apNoteService.fetchAnnounce(activity.activity) + } + + override fun isSupported(activityType: ActivityType): Boolean = ActivityType.Announce == activityType + + override fun type(): Class = Announce::class.java +} diff --git a/src/main/kotlin/dev/usbharu/hideout/activitypub/service/activity/undo/APUndoProcessor.kt b/src/main/kotlin/dev/usbharu/hideout/activitypub/service/activity/undo/APUndoProcessor.kt index 864c848f..c6abf698 100644 --- a/src/main/kotlin/dev/usbharu/hideout/activitypub/service/activity/undo/APUndoProcessor.kt +++ b/src/main/kotlin/dev/usbharu/hideout/activitypub/service/activity/undo/APUndoProcessor.kt @@ -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.model.actor.ActorRepository 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 org.springframework.stereotype.Service @@ -23,7 +24,8 @@ class APUndoProcessor( private val relationshipService: RelationshipService, private val reactionService: ReactionService, private val actorRepository: ActorRepository, - private val postRepository: PostRepository + private val postRepository: PostRepository, + private val postService: PostService ) : AbstractActivityPubProcessor(transaction) { override suspend fun internalProcess(activity: ActivityPubProcessContext) { val undo = activity.activity @@ -53,6 +55,11 @@ class APUndoProcessor( return } + "Announce" -> { + announce(undo) + return + } + else -> {} } TODO() @@ -109,6 +116,13 @@ class APUndoProcessor( 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 type(): Class = Undo::class.java diff --git a/src/main/kotlin/dev/usbharu/hideout/activitypub/service/objects/note/APNoteService.kt b/src/main/kotlin/dev/usbharu/hideout/activitypub/service/objects/note/APNoteService.kt index 7c788ae0..8cd1ad11 100644 --- a/src/main/kotlin/dev/usbharu/hideout/activitypub/service/objects/note/APNoteService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/activitypub/service/objects/note/APNoteService.kt @@ -1,13 +1,17 @@ package dev.usbharu.hideout.activitypub.service.objects.note 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.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.service.common.APResourceResolveService import dev.usbharu.hideout.activitypub.service.common.resolve import dev.usbharu.hideout.activitypub.service.objects.emoji.EmojiService 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.PostRepository 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(note: Note, targetActor: String? = null): Note suspend fun fetchNoteWithEntity(url: String, targetActor: String? = null): Pair + + suspend fun fetchAnnounce(url: String, signerId: Long? = null): Pair + suspend fun fetchAnnounce(announce: Announce, signerId: Long? = null): Pair } @Service @@ -35,7 +42,8 @@ class APNoteServiceImpl( private val postBuilder: Post.PostBuilder, private val noteQueryService: NoteQueryService, private val mediaService: MediaService, - private val emojiService: EmojiService + private val emojiService: EmojiService, + private val announceQueryService: AnnounceQueryService ) : APNoteService { @@ -62,41 +70,90 @@ class APNoteServiceImpl( ) 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) return savedNote } + override suspend fun fetchAnnounce(url: String, signerId: Long?): Pair { + logger.debug("START Fetch Announce url: {}", url) + + val post: Pair? = 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(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 { + 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( note: Note, - targetActor: String?, - url: String - ): Pair = noteQueryService.findByApid(note.id) ?: saveNote(note, targetActor, url) + targetActor: String? + ): Pair = noteQueryService.findByApid(note.id) ?: saveNote(note, targetActor) - private suspend fun saveNote(note: Note, targetActor: String?, url: String): Pair { + private suspend fun saveNote(note: Note, targetActor: String?): Pair { val person = apUserService.fetchPersonWithEntity( note.attributedTo, targetActor ) val post = postRepository.findByApId(note.id) - if (post != null) { return note to post } logger.debug("VISIBILITY url: {} to: {} cc: {}", note.id, note.to, note.cc) - - 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 - } - + val visibility = visibility(note, person) logger.debug("VISIBILITY is {} url: {}", visibility.name, note.id) val reply = note.inReplyTo?.let { @@ -104,14 +161,12 @@ class APNoteServiceImpl( postRepository.findByUrl(it) } - val emojis = note.tag - .filterIsInstance() - .map { - emojiService.fetchEmoji(it).second - } - .map { - it.id - } + val quote = (note.misskeyQuote ?: note.quoteUri ?: note.quoteUrl)?.let { + fetchNote(it, targetActor) + postRepository.findByUrl(it) + } + + val emojis = buildEmojis(note) val mediaList = note.attachment.map { mediaService.uploadRemoteMedia( @@ -124,26 +179,69 @@ class APNoteServiceImpl( ) }.map { it.id } - val createRemote = postService.createRemote( - 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 createPost = + if (quote != null) { + postBuilder.quoteRepostOf( + id = postRepository.generateId(), + actorId = person.second.id, + content = note.content, + createdAt = Instant.parse(note.published), + visibility = visibility, + url = note.id, + replyId = reply?.id, + sensitive = note.sensitive, + apId = note.id, + 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 } + private suspend fun buildEmojis(note: Note) = note.tag + .filterIsInstance() + .map { + emojiService.fetchEmoji(it).second + } + .map { + it.id + } + + private fun visibility( + note: Note, + person: Pair + ): 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 = - saveIfMissing(note, targetActor, note.id).first + saveIfMissing(note, targetActor).first companion object { const val public: String = "https://www.w3.org/ns/activitystreams#Public" diff --git a/src/main/kotlin/dev/usbharu/hideout/core/domain/model/actor/Actor.kt b/src/main/kotlin/dev/usbharu/hideout/core/domain/model/actor/Actor.kt index 5e8b4264..59f16059 100644 --- a/src/main/kotlin/dev/usbharu/hideout/core/domain/model/actor/Actor.kt +++ b/src/main/kotlin/dev/usbharu/hideout/core/domain/model/actor/Actor.kt @@ -5,6 +5,7 @@ import dev.usbharu.hideout.application.config.CharacterLimit import org.slf4j.LoggerFactory import org.springframework.stereotype.Component import java.time.Instant +import kotlin.math.max data class Actor private constructor( val id: Long, @@ -159,18 +160,6 @@ data class Actor private constructor( "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( id = id, name = limitedName, @@ -188,9 +177,9 @@ data class Actor private constructor( following = following, instance = instance, locked = locked, - followersCount = followersCount, - followingCount = followingCount, - postsCount = postsCount, + followersCount = max(0, followersCount), + followingCount = max(0, followingCount), + postsCount = max(0, postsCount), lastPostDate = lastPostDate, emojis = emojis ) diff --git a/src/main/kotlin/dev/usbharu/hideout/core/domain/model/post/Post.kt b/src/main/kotlin/dev/usbharu/hideout/core/domain/model/post/Post.kt index 0eabb4aa..e9b73fe2 100644 --- a/src/main/kotlin/dev/usbharu/hideout/core/domain/model/post/Post.kt +++ b/src/main/kotlin/dev/usbharu/hideout/core/domain/model/post/Post.kt @@ -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 = emptyList(), + emojiIds: List = 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") fun deleteOf( 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 { return Post( id = this.id, diff --git a/src/main/kotlin/dev/usbharu/hideout/core/domain/model/relationship/RelationshipRepository.kt b/src/main/kotlin/dev/usbharu/hideout/core/domain/model/relationship/RelationshipRepository.kt index b08180a6..c934e18b 100644 --- a/src/main/kotlin/dev/usbharu/hideout/core/domain/model/relationship/RelationshipRepository.kt +++ b/src/main/kotlin/dev/usbharu/hideout/core/domain/model/relationship/RelationshipRepository.kt @@ -36,6 +36,7 @@ interface RelationshipRepository { suspend fun findByTargetIdAndFollowing(targetId: Long, following: Boolean): List + @Suppress("FunctionMaxLength") suspend fun findByTargetIdAndFollowRequestAndIgnoreFollowRequest( targetId: Long, followRequest: Boolean, diff --git a/src/main/kotlin/dev/usbharu/hideout/mastodon/domain/model/MastodonNotificationRepository.kt b/src/main/kotlin/dev/usbharu/hideout/mastodon/domain/model/MastodonNotificationRepository.kt index e1294843..7ee7342c 100644 --- a/src/main/kotlin/dev/usbharu/hideout/mastodon/domain/model/MastodonNotificationRepository.kt +++ b/src/main/kotlin/dev/usbharu/hideout/mastodon/domain/model/MastodonNotificationRepository.kt @@ -8,6 +8,7 @@ interface MastodonNotificationRepository { suspend fun deleteById(id: Long) suspend fun findById(id: Long): MastodonNotification? + @Suppress("FunctionMaxLength") suspend fun findByUserIdAndInTypesAndInSourceActorId( loginUser: Long, types: List, diff --git a/src/main/kotlin/dev/usbharu/hideout/mastodon/interfaces/api/account/MastodonAccountApiController.kt b/src/main/kotlin/dev/usbharu/hideout/mastodon/interfaces/api/account/MastodonAccountApiController.kt index f577f48d..16c5900f 100644 --- a/src/main/kotlin/dev/usbharu/hideout/mastodon/interfaces/api/account/MastodonAccountApiController.kt +++ b/src/main/kotlin/dev/usbharu/hideout/mastodon/interfaces/api/account/MastodonAccountApiController.kt @@ -84,7 +84,7 @@ class MastodonAccountApiController( maxId?.toLongOrNull(), sinceId?.toLongOrNull(), minId?.toLongOrNull(), - limit.coerceIn(0, 80) ?: 40 + limit.coerceIn(0, 80) ) ) val httpHeader = statuses.toHttpHeader( diff --git a/src/test/kotlin/dev/usbharu/hideout/activitypub/domain/model/AnnounceTest.kt b/src/test/kotlin/dev/usbharu/hideout/activitypub/domain/model/AnnounceTest.kt new file mode 100644 index 00000000..5d01861e --- /dev/null +++ b/src/test/kotlin/dev/usbharu/hideout/activitypub/domain/model/AnnounceTest.kt @@ -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(json) + + + } +} \ No newline at end of file diff --git a/src/test/kotlin/dev/usbharu/hideout/activitypub/service/objects/note/APNoteServiceImplTest.kt b/src/test/kotlin/dev/usbharu/hideout/activitypub/service/objects/note/APNoteServiceImplTest.kt index 8109bb2d..97f34d09 100644 --- a/src/test/kotlin/dev/usbharu/hideout/activitypub/service/objects/note/APNoteServiceImplTest.kt +++ b/src/test/kotlin/dev/usbharu/hideout/activitypub/service/objects/note/APNoteServiceImplTest.kt @@ -79,6 +79,7 @@ class APNoteServiceImplTest { ), noteQueryService = noteQueryService, mock(), + mock(), mock() ) @@ -152,7 +153,8 @@ class APNoteServiceImplTest { ), noteQueryService = noteQueryService, mock(), - mock { } + mock { }, + mock() ) val actual = apNoteServiceImpl.fetchNote(url) @@ -204,7 +206,8 @@ class APNoteServiceImplTest { ), noteQueryService = noteQueryService, mock(), - mock() + mock(), + mock { } ) assertThrows { apNoteServiceImpl.fetchNote(url) } @@ -255,6 +258,7 @@ class APNoteServiceImplTest { postBuilder = postBuilder, noteQueryService = noteQueryService, mock(), + mock(), mock() ) @@ -308,6 +312,7 @@ class APNoteServiceImplTest { postBuilder = postBuilder, noteQueryService = noteQueryService, mock(), + mock(), mock() )