mirror of https://github.com/usbharu/Hideout.git
feat: Undoを実装
This commit is contained in:
parent
fbff97c247
commit
cb2ee6ca3f
|
@ -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
|
||||
|
|
|
@ -1,33 +1,43 @@
|
|||
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<String> = 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 {
|
||||
return "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()}"
|
||||
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ open class Follow : Object {
|
|||
protected constructor() : super()
|
||||
constructor(
|
||||
type: List<String> = emptyList(),
|
||||
name: String,
|
||||
name: String?,
|
||||
`object`: String?,
|
||||
actor: String?
|
||||
) : super(
|
||||
|
@ -16,4 +16,24 @@ 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 {
|
||||
return "Follow(`object`=$`object`) ${super.toString()}"
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ import com.fasterxml.jackson.databind.annotation.JsonSerialize
|
|||
|
||||
open class Object : JsonLd {
|
||||
@JsonSerialize(using = TypeSerializer::class)
|
||||
private var type: List<String> = emptyList()
|
||||
var type: List<String> = emptyList()
|
||||
var name: String? = null
|
||||
var actor: String? = null
|
||||
var id: String? = null
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
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<Object>() {
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
package dev.usbharu.hideout.domain.model.ap
|
||||
|
||||
class ObjectValue : Object {
|
||||
|
||||
var `object`: String? = null
|
||||
|
||||
protected constructor()
|
||||
constructor(type: List<String>, 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 {
|
||||
return "ObjectValue(`object`=$`object`) ${super.toString()}"
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
package dev.usbharu.hideout.domain.model.ap
|
||||
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
|
||||
import java.time.Instant
|
||||
|
||||
class Undo : Object {
|
||||
|
||||
@JsonDeserialize(using = ObjectDeserializer::class)
|
||||
var `object`: Object? = null
|
||||
var published: String? = null
|
||||
|
||||
protected constructor()
|
||||
constructor(
|
||||
type: List<String> = 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 {
|
||||
return "Undo(`object`=$`object`, published=$published) ${super.toString()}"
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
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()
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -432,7 +432,7 @@ class UsersTest {
|
|||
)
|
||||
}
|
||||
val userService = mock<IUserService> {
|
||||
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<IUserService> {
|
||||
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<IUserService> {
|
||||
onBlocking { addFollowers(eq(1235), eq(1234)) } doReturn false
|
||||
onBlocking { follow(eq(1235), eq(1234)) } doReturn false
|
||||
}
|
||||
application {
|
||||
configureSerialization()
|
||||
|
|
|
@ -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<Accept>(
|
||||
httpRequestData.body.toByteArray().decodeToString()
|
||||
content
|
||||
)
|
||||
)
|
||||
respondOk()
|
||||
|
|
Loading…
Reference in New Issue