diff --git a/detekt.yml b/detekt.yml index d0f97bdc..bf483322 100644 --- a/detekt.yml +++ b/detekt.yml @@ -92,7 +92,7 @@ exceptions: active: true NotImplementedDeclaration: - active: true + active: false ObjectExtendsThrowable: active: true diff --git a/gradle.properties b/gradle.properties index 9ee92fa5..56d141a1 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,6 +6,6 @@ exposed_version=0.41.1 h2_version=2.1.214 koin_version=3.3.1 org.gradle.parallel=true -org.gradle.configureondemand=true +#org.gradle.configureondemand=true org.gradle.caching=true org.gradle.jvmargs=-Xmx2048m -XX:+HeapDumpOnOutOfMemoryError -XX:+UseParallelGC diff --git a/src/main/kotlin/dev/usbharu/hideout/domain/model/ap/Accept.kt b/src/main/kotlin/dev/usbharu/hideout/domain/model/ap/Accept.kt index e71cbcbc..e25d0e6d 100644 --- a/src/main/kotlin/dev/usbharu/hideout/domain/model/ap/Accept.kt +++ b/src/main/kotlin/dev/usbharu/hideout/domain/model/ap/Accept.kt @@ -1,33 +1,38 @@ package dev.usbharu.hideout.domain.model.ap +import com.fasterxml.jackson.databind.annotation.JsonDeserialize + open class Accept : Object { + @JsonDeserialize(using = ObjectDeserializer::class) var `object`: Object? = null - protected constructor() : super() + protected constructor() constructor( type: List = emptyList(), name: String, `object`: Object?, actor: String? - ) : super(add(type, "Accept"), name, actor) { + ) : super( + type = add(type, "Accept"), + name = name, + actor = actor + ) { this.`object` = `object` } + override fun toString(): String = "Accept(`object`=$`object`) ${super.toString()}" + override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is Accept) return false if (!super.equals(other)) return false - if (`object` != other.`object`) return false - return actor == other.actor + return `object` == other.`object` } override fun hashCode(): Int { var result = super.hashCode() result = 31 * result + (`object`?.hashCode() ?: 0) - result = 31 * result + (actor?.hashCode() ?: 0) return result } - - override fun toString(): String = "Accept(`object`=$`object`, actor=$actor) ${super.toString()}" } diff --git a/src/main/kotlin/dev/usbharu/hideout/domain/model/ap/Follow.kt b/src/main/kotlin/dev/usbharu/hideout/domain/model/ap/Follow.kt index 8e29fbb5..d15d7631 100644 --- a/src/main/kotlin/dev/usbharu/hideout/domain/model/ap/Follow.kt +++ b/src/main/kotlin/dev/usbharu/hideout/domain/model/ap/Follow.kt @@ -6,7 +6,7 @@ open class Follow : Object { protected constructor() : super() constructor( type: List = emptyList(), - name: String, + name: String?, `object`: String?, actor: String? ) : super( @@ -16,4 +16,20 @@ open class Follow : Object { ) { this.`object` = `object` } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is Follow) return false + if (!super.equals(other)) return false + + return `object` == other.`object` + } + + override fun hashCode(): Int { + var result = super.hashCode() + result = 31 * result + (`object`?.hashCode() ?: 0) + return result + } + + override fun toString(): String = "Follow(`object`=$`object`) ${super.toString()}" } diff --git a/src/main/kotlin/dev/usbharu/hideout/domain/model/ap/Object.kt b/src/main/kotlin/dev/usbharu/hideout/domain/model/ap/Object.kt index 5d673244..80a282aa 100644 --- a/src/main/kotlin/dev/usbharu/hideout/domain/model/ap/Object.kt +++ b/src/main/kotlin/dev/usbharu/hideout/domain/model/ap/Object.kt @@ -7,7 +7,7 @@ import com.fasterxml.jackson.databind.annotation.JsonSerialize open class Object : JsonLd { @JsonSerialize(using = TypeSerializer::class) - private var type: List = emptyList() + var type: List = emptyList() var name: String? = null var actor: String? = null var id: String? = null diff --git a/src/main/kotlin/dev/usbharu/hideout/domain/model/ap/ObjectDeserializer.kt b/src/main/kotlin/dev/usbharu/hideout/domain/model/ap/ObjectDeserializer.kt new file mode 100644 index 00000000..70bcee3b --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/domain/model/ap/ObjectDeserializer.kt @@ -0,0 +1,48 @@ +package dev.usbharu.hideout.domain.model.ap + +import com.fasterxml.jackson.core.JsonParser +import com.fasterxml.jackson.databind.DeserializationContext +import com.fasterxml.jackson.databind.JsonDeserializer +import com.fasterxml.jackson.databind.JsonNode +import dev.usbharu.hideout.service.activitypub.ActivityType + +class ObjectDeserializer : JsonDeserializer() { + override fun deserialize(p: JsonParser?, ctxt: DeserializationContext?): Object { + requireNotNull(p) + val treeNode: JsonNode = requireNotNull(p.codec?.readTree(p)) + if (treeNode.isValueNode) { + return ObjectValue( + emptyList(), + null, + null, + null, + treeNode.asText() + ) + } else if (treeNode.isObject) { + val type = treeNode["type"] + val activityType = if (type.isArray) { + type.firstNotNullOf { jsonNode: JsonNode -> + ActivityType.values().firstOrNull { it.name.equals(jsonNode.asText(), true) } + } + } else if (type.isValueNode) { + ActivityType.values().first { it.name.equals(type.asText(), true) } + } else { + TODO() + } + + return when (activityType) { + ActivityType.Follow -> { + val readValue = p.codec.treeToValue(treeNode, Follow::class.java) + println(readValue) + readValue + } + + else -> { + TODO() + } + } + } else { + TODO() + } + } +} diff --git a/src/main/kotlin/dev/usbharu/hideout/domain/model/ap/ObjectValue.kt b/src/main/kotlin/dev/usbharu/hideout/domain/model/ap/ObjectValue.kt new file mode 100644 index 00000000..635d560d --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/domain/model/ap/ObjectValue.kt @@ -0,0 +1,32 @@ +package dev.usbharu.hideout.domain.model.ap + +open class ObjectValue : Object { + + var `object`: String? = null + + protected constructor() : super() + constructor(type: List, name: String?, actor: String?, id: String?, `object`: String?) : super( + type, + name, + actor, + id + ) { + this.`object` = `object` + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is ObjectValue) return false + if (!super.equals(other)) return false + + return `object` == other.`object` + } + + override fun hashCode(): Int { + var result = super.hashCode() + result = 31 * result + (`object`?.hashCode() ?: 0) + return result + } + + override fun toString(): String = "ObjectValue(`object`=$`object`) ${super.toString()}" +} diff --git a/src/main/kotlin/dev/usbharu/hideout/domain/model/ap/Undo.kt b/src/main/kotlin/dev/usbharu/hideout/domain/model/ap/Undo.kt new file mode 100644 index 00000000..8a175f22 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/domain/model/ap/Undo.kt @@ -0,0 +1,42 @@ +package dev.usbharu.hideout.domain.model.ap + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize +import java.time.Instant + +open class Undo : Object { + + @JsonDeserialize(using = ObjectDeserializer::class) + var `object`: Object? = null + var published: String? = null + + protected constructor() : super() + constructor( + type: List = emptyList(), + name: String, + actor: String, + id: String?, + `object`: Object, + published: Instant + ) : super(add(type, "Undo"), name, actor, id) { + this.`object` = `object` + this.published = published.toString() + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is Undo) return false + if (!super.equals(other)) return false + + if (`object` != other.`object`) return false + return published == other.published + } + + override fun hashCode(): Int { + var result = super.hashCode() + result = 31 * result + (`object`?.hashCode() ?: 0) + result = 31 * result + (published?.hashCode() ?: 0) + return result + } + + override fun toString(): String = "Undo(`object`=$`object`, published=$published) ${super.toString()}" +} diff --git a/src/main/kotlin/dev/usbharu/hideout/routing/api/internal/v1/Users.kt b/src/main/kotlin/dev/usbharu/hideout/routing/api/internal/v1/Users.kt index 5a51c4c2..bf8c4b11 100644 --- a/src/main/kotlin/dev/usbharu/hideout/routing/api/internal/v1/Users.kt +++ b/src/main/kotlin/dev/usbharu/hideout/routing/api/internal/v1/Users.kt @@ -71,7 +71,7 @@ fun Route.users(userService: IUserService, userApiService: IUserApiService) { val userParameter = call.parameters["name"] ?: throw ParameterNotExistException("Parameter(name='userName@domain') does not exist.") if (userParameter.toLongOrNull() != null) { - if (userService.addFollowers(userParameter.toLong(), userId)) { + if (userService.follow(userParameter.toLong(), userId)) { return@post call.respond(HttpStatusCode.OK) } else { return@post call.respond(HttpStatusCode.Accepted) @@ -79,7 +79,7 @@ fun Route.users(userService: IUserService, userApiService: IUserApiService) { } val acct = AcctUtil.parse(userParameter) val targetUser = userApiService.findByAcct(acct) - if (userService.addFollowers(targetUser.id, userId)) { + if (userService.follow(targetUser.id, userId)) { return@post call.respond(HttpStatusCode.OK) } else { return@post call.respond(HttpStatusCode.Accepted) diff --git a/src/main/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubFollowServiceImpl.kt b/src/main/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubFollowServiceImpl.kt index 787a0fcc..478629d8 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubFollowServiceImpl.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubFollowServiceImpl.kt @@ -49,6 +49,6 @@ class ActivityPubFollowServiceImpl( val users = userService.findByUrls(listOf(targetActor, follow.actor ?: throw IllegalArgumentException("actor is null"))) - userService.addFollowers(users.first { it.url == targetActor }.id, users.first { it.url == follow.actor }.id) + userService.follow(users.first { it.url == targetActor }.id, users.first { it.url == follow.actor }.id) } } diff --git a/src/main/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubServiceImpl.kt b/src/main/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubServiceImpl.kt index e7299c7f..38bb6bf4 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubServiceImpl.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubServiceImpl.kt @@ -1,6 +1,7 @@ package dev.usbharu.hideout.service.activitypub import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.module.kotlin.readValue import dev.usbharu.hideout.config.Config import dev.usbharu.hideout.domain.model.ActivityPubResponse import dev.usbharu.hideout.domain.model.ap.Follow @@ -17,7 +18,8 @@ import org.slf4j.LoggerFactory @Single class ActivityPubServiceImpl( private val activityPubFollowService: ActivityPubFollowService, - private val activityPubNoteService: ActivityPubNoteService + private val activityPubNoteService: ActivityPubNoteService, + private val activityPubUndoService: ActivityPubUndoService ) : ActivityPubService { val logger: Logger = LoggerFactory.getLogger(this::class.java) @@ -70,7 +72,7 @@ class ActivityPubServiceImpl( ActivityType.TentativeReject -> TODO() ActivityType.TentativeAccept -> TODO() ActivityType.Travel -> TODO() - ActivityType.Undo -> TODO() + ActivityType.Undo -> activityPubUndoService.receiveUndo(Config.configData.objectMapper.readValue(json)) ActivityType.Update -> TODO() ActivityType.View -> TODO() ActivityType.Other -> TODO() diff --git a/src/main/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubUndoService.kt b/src/main/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubUndoService.kt new file mode 100644 index 00000000..d0972608 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubUndoService.kt @@ -0,0 +1,8 @@ +package dev.usbharu.hideout.service.activitypub + +import dev.usbharu.hideout.domain.model.ActivityPubResponse +import dev.usbharu.hideout.domain.model.ap.Undo + +interface ActivityPubUndoService { + suspend fun receiveUndo(undo: Undo): ActivityPubResponse +} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubUndoServiceImpl.kt b/src/main/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubUndoServiceImpl.kt new file mode 100644 index 00000000..d8bf3e5d --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubUndoServiceImpl.kt @@ -0,0 +1,44 @@ +package dev.usbharu.hideout.service.activitypub + +import dev.usbharu.hideout.domain.model.ActivityPubResponse +import dev.usbharu.hideout.domain.model.ActivityPubStringResponse +import dev.usbharu.hideout.domain.model.ap.Follow +import dev.usbharu.hideout.domain.model.ap.Undo +import dev.usbharu.hideout.service.impl.IUserService +import io.ktor.http.* +import org.koin.core.annotation.Single + +@Single +class ActivityPubUndoServiceImpl( + private val userService: IUserService, + private val activityPubUserService: ActivityPubUserService +) : ActivityPubUndoService { + override suspend fun receiveUndo(undo: Undo): ActivityPubResponse { + if (undo.actor == null) { + return ActivityPubStringResponse(HttpStatusCode.BadRequest, "actor is null") + } + + val type = + undo.`object`?.type.orEmpty() + .firstOrNull { it == "Block" || it == "Follow" || it == "Like" || it == "Announce" || it == "Accept" } + ?: return ActivityPubStringResponse(HttpStatusCode.BadRequest, "unknown type ${undo.`object`?.type}") + + when (type) { + "Follow" -> { + val follow = undo.`object` as Follow + + if (follow.`object` == null) { + return ActivityPubStringResponse(HttpStatusCode.BadRequest, "object.object is null") + } + + activityPubUserService.fetchPerson(undo.actor!!, follow.`object`) + val follower = userService.findByUrl(undo.actor!!) + val target = userService.findByUrl(follow.`object`!!) + userService.unfollow(target.id, follower.id) + } + + else -> {} + } + TODO() + } +} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubUserService.kt b/src/main/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubUserService.kt index 659d134a..3ee34667 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubUserService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubUserService.kt @@ -5,5 +5,12 @@ import dev.usbharu.hideout.domain.model.ap.Person interface ActivityPubUserService { suspend fun getPersonByName(name: String): Person + /** + * Fetch person + * + * @param url + * @param targetActor 署名するユーザー + * @return + */ suspend fun fetchPerson(url: String, targetActor: String? = null): Person } diff --git a/src/main/kotlin/dev/usbharu/hideout/service/impl/IUserService.kt b/src/main/kotlin/dev/usbharu/hideout/service/impl/IUserService.kt index ab726ac1..c6b7c0b0 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/impl/IUserService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/impl/IUserService.kt @@ -45,5 +45,7 @@ interface IUserService { * @param follower * @return リクエストが成功したか */ - suspend fun addFollowers(id: Long, follower: Long): Boolean + suspend fun follow(id: Long, follower: Long): Boolean + + suspend fun unfollow(id: Long, follower: Long): Boolean } diff --git a/src/main/kotlin/dev/usbharu/hideout/service/impl/UserService.kt b/src/main/kotlin/dev/usbharu/hideout/service/impl/UserService.kt index 14f7b04b..d2037fa7 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/impl/UserService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/impl/UserService.kt @@ -105,8 +105,13 @@ class UserService(private val userRepository: IUserRepository, private val userA } // TODO APのフォロー処理を作る - override suspend fun addFollowers(id: Long, follower: Long): Boolean { + override suspend fun follow(id: Long, follower: Long): Boolean { userRepository.createFollower(id, follower) return false } + + override suspend fun unfollow(id: Long, follower: Long): Boolean { + userRepository.deleteFollower(id, follower) + return false + } } diff --git a/src/test/kotlin/dev/usbharu/hideout/domain/model/ap/UndoTest.kt b/src/test/kotlin/dev/usbharu/hideout/domain/model/ap/UndoTest.kt new file mode 100644 index 00000000..0e34e75e --- /dev/null +++ b/src/test/kotlin/dev/usbharu/hideout/domain/model/ap/UndoTest.kt @@ -0,0 +1,76 @@ +package dev.usbharu.hideout.domain.model.ap + +import org.intellij.lang.annotations.Language +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test +import utils.JsonObjectMapper +import java.time.Clock +import java.time.Instant +import java.time.ZoneId + +class UndoTest { + @Test + fun Undoのシリアライズができる() { + val undo = Undo( + emptyList(), + "Undo Follow", + "https://follower.example.com/", + "https://follower.example.com/undo/1", + Follow( + emptyList(), + null, + "https://follower.example.com/users/", + actor = "https://follower.exaple.com/users/1" + ), + Instant.now(Clock.tickMillis(ZoneId.systemDefault())) + ) + val writeValueAsString = JsonObjectMapper.objectMapper.writeValueAsString(undo) + println(writeValueAsString) + } + + @Test + fun Undoをデシリアライズ出来る() { + @Language("JSON") val json = """ + { + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", + { + "manuallyApprovesFollowers": "as:manuallyApprovesFollowers", + "sensitive": "as:sensitive", + "Hashtag": "as:Hashtag", + "quoteUrl": "as:quoteUrl", + "toot": "http://joinmastodon.org/ns#", + "Emoji": "toot:Emoji", + "featured": "toot:featured", + "discoverable": "toot:discoverable", + "schema": "http://schema.org#", + "PropertyValue": "schema:PropertyValue", + "value": "schema:value", + "misskey": "https://misskey-hub.net/ns#", + "_misskey_content": "misskey:_misskey_content", + "_misskey_quote": "misskey:_misskey_quote", + "_misskey_reaction": "misskey:_misskey_reaction", + "_misskey_votes": "misskey:_misskey_votes", + "isCat": "misskey:isCat", + "vcard": "http://www.w3.org/2006/vcard/ns#" + } + ], + "type": "Undo", + "id": "https://misskey.usbharu.dev/follows/97ws8y3rj6/9ezbh8qrh0/undo", + "actor": "https://misskey.usbharu.dev/users/97ws8y3rj6", + "object": { + "id": "https://misskey.usbharu.dev/follows/97ws8y3rj6/9ezbh8qrh0", + "type": "Follow", + "actor": "https://misskey.usbharu.dev/users/97ws8y3rj6", + "object": "https://test-hideout.usbharu.dev/users/test" + }, + "published": "2023-05-20T10:28:17.308Z" +} + + """.trimIndent() + + val undo = JsonObjectMapper.objectMapper.readValue(json, Undo::class.java) + println(undo) + } +} diff --git a/src/test/kotlin/dev/usbharu/hideout/routing/api/internal/v1/UsersTest.kt b/src/test/kotlin/dev/usbharu/hideout/routing/api/internal/v1/UsersTest.kt index 2fe03d92..acc7f9a3 100644 --- a/src/test/kotlin/dev/usbharu/hideout/routing/api/internal/v1/UsersTest.kt +++ b/src/test/kotlin/dev/usbharu/hideout/routing/api/internal/v1/UsersTest.kt @@ -432,7 +432,7 @@ class UsersTest { ) } val userService = mock { - onBlocking { addFollowers(eq(1235), eq(1234)) } doReturn true + onBlocking { follow(eq(1235), eq(1234)) } doReturn true } application { configureSerialization() @@ -482,7 +482,7 @@ class UsersTest { ) } val userService = mock { - onBlocking { addFollowers(eq(1235), eq(1234)) } doReturn false + onBlocking { follow(eq(1235), eq(1234)) } doReturn false } application { configureSerialization() @@ -532,7 +532,7 @@ class UsersTest { ) } val userService = mock { - onBlocking { addFollowers(eq(1235), eq(1234)) } doReturn false + onBlocking { follow(eq(1235), eq(1234)) } doReturn false } application { configureSerialization() diff --git a/src/test/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubFollowServiceImplTest.kt b/src/test/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubFollowServiceImplTest.kt index 12e54893..2e5b3af0 100644 --- a/src/test/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubFollowServiceImplTest.kt +++ b/src/test/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubFollowServiceImplTest.kt @@ -115,7 +115,7 @@ class ActivityPubFollowServiceImplTest { createdAt = Instant.now() ) ) - onBlocking { addFollowers(any(), any()) } doReturn false + onBlocking { follow(any(), any()) } doReturn false } val activityPubFollowService = ActivityPubFollowServiceImpl( @@ -137,10 +137,12 @@ class ActivityPubFollowServiceImplTest { actor = "https://example.com" ) accept.context += "https://www.w3.org/ns/activitystreams" + val content = httpRequestData.body.toByteArray().decodeToString() + println(content) assertEquals( accept, Config.configData.objectMapper.readValue( - httpRequestData.body.toByteArray().decodeToString() + content ) ) respondOk()