mirror of https://github.com/usbharu/Hideout.git
feat: Announceを受け取れるように
This commit is contained in:
parent
8e516420cf
commit
ac5be2e2df
|
@ -42,7 +42,7 @@ class ObjectDeserializer : JsonDeserializer<Object>() {
|
|||
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)
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
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(
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>?
|
||||
}
|
|
@ -1,8 +1,10 @@
|
|||
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.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
|
||||
|
@ -23,6 +25,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<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
|
||||
|
@ -35,7 +40,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 {
|
||||
|
||||
|
@ -67,6 +73,68 @@ class APNoteServiceImpl(
|
|||
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(
|
||||
note: Note,
|
||||
targetActor: String?,
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
@ -212,27 +201,27 @@ data class Actor private constructor(
|
|||
fun withLastPostAt(lastPostDate: Instant): Actor = this.copy(lastPostDate = lastPostDate)
|
||||
override fun toString(): String {
|
||||
return "Actor(" +
|
||||
"id=$id, " +
|
||||
"name='$name', " +
|
||||
"domain='$domain', " +
|
||||
"screenName='$screenName', " +
|
||||
"description='$description', " +
|
||||
"inbox='$inbox', " +
|
||||
"outbox='$outbox', " +
|
||||
"url='$url', " +
|
||||
"publicKey='$publicKey', " +
|
||||
"privateKey=$privateKey, " +
|
||||
"createdAt=$createdAt, " +
|
||||
"keyId='$keyId', " +
|
||||
"followers=$followers, " +
|
||||
"following=$following, " +
|
||||
"instance=$instance, " +
|
||||
"locked=$locked, " +
|
||||
"followersCount=$followersCount, " +
|
||||
"followingCount=$followingCount, " +
|
||||
"postsCount=$postsCount, " +
|
||||
"lastPostDate=$lastPostDate, " +
|
||||
"emojis=$emojis" +
|
||||
")"
|
||||
"id=$id, " +
|
||||
"name='$name', " +
|
||||
"domain='$domain', " +
|
||||
"screenName='$screenName', " +
|
||||
"description='$description', " +
|
||||
"inbox='$inbox', " +
|
||||
"outbox='$outbox', " +
|
||||
"url='$url', " +
|
||||
"publicKey='$publicKey', " +
|
||||
"privateKey=$privateKey, " +
|
||||
"createdAt=$createdAt, " +
|
||||
"keyId='$keyId', " +
|
||||
"followers=$followers, " +
|
||||
"following=$following, " +
|
||||
"instance=$instance, " +
|
||||
"locked=$locked, " +
|
||||
"followersCount=$followersCount, " +
|
||||
"followingCount=$followingCount, " +
|
||||
"postsCount=$postsCount, " +
|
||||
"lastPostDate=$lastPostDate, " +
|
||||
"emojis=$emojis" +
|
||||
")"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -89,6 +89,114 @@ 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")
|
||||
fun deleteOf(
|
||||
id: Long,
|
||||
|
@ -117,6 +225,9 @@ 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,
|
||||
|
|
|
@ -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<FailedToGetActivityPubResourceException> { 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()
|
||||
)
|
||||
|
||||
|
|
Loading…
Reference in New Issue