diff --git a/src/main/kotlin/dev/usbharu/hideout/domain/model/ap/Create.kt b/src/main/kotlin/dev/usbharu/hideout/domain/model/ap/Create.kt index c187a0c7..5e9da1df 100644 --- a/src/main/kotlin/dev/usbharu/hideout/domain/model/ap/Create.kt +++ b/src/main/kotlin/dev/usbharu/hideout/domain/model/ap/Create.kt @@ -1,7 +1,12 @@ package dev.usbharu.hideout.domain.model.ap +import com.fasterxml.jackson.databind.annotation.JsonDeserialize + open class Create : Object { + @JsonDeserialize(using = ObjectDeserializer::class) var `object`: Object? = null + var to: List = emptyList() + var cc: List = emptyList() protected constructor() : super() constructor( @@ -9,14 +14,18 @@ open class Create : Object { name: String? = null, `object`: Object?, actor: String? = null, - id: String? = null + id: String? = null, + to: List = emptyList(), + cc: List = emptyList() ) : super( - add(type, "Create"), - name, - actor, - id + type = add(type, "Create"), + name = name, + actor = actor, + id = id ) { this.`object` = `object` + this.to = to + this.cc = cc } override fun equals(other: Any?): Boolean { diff --git a/src/main/kotlin/dev/usbharu/hideout/domain/model/ap/Note.kt b/src/main/kotlin/dev/usbharu/hideout/domain/model/ap/Note.kt index 97277e53..8425fc79 100644 --- a/src/main/kotlin/dev/usbharu/hideout/domain/model/ap/Note.kt +++ b/src/main/kotlin/dev/usbharu/hideout/domain/model/ap/Note.kt @@ -5,6 +5,9 @@ open class Note : Object { var content: String? = null var published: String? = null var to: List = emptyList() + var cc: List = emptyList() + var sensitive: Boolean = false + var inReplyTo: String? = null protected constructor() : super() constructor( @@ -14,7 +17,10 @@ open class Note : Object { attributedTo: String?, content: String?, published: String?, - to: List = emptyList() + to: List = emptyList(), + cc: List = emptyList(), + sensitive: Boolean = false, + inReplyTo: String? = null ) : super( type = add(type, "Note"), name = name, @@ -24,6 +30,9 @@ open class Note : Object { this.content = content this.published = published this.to = to + this.cc = cc + this.sensitive = sensitive + this.inReplyTo = inReplyTo } override fun equals(other: Any?): Boolean { diff --git a/src/main/kotlin/dev/usbharu/hideout/domain/model/ap/Person.kt b/src/main/kotlin/dev/usbharu/hideout/domain/model/ap/Person.kt index 661343a2..a6a5f8b8 100644 --- a/src/main/kotlin/dev/usbharu/hideout/domain/model/ap/Person.kt +++ b/src/main/kotlin/dev/usbharu/hideout/domain/model/ap/Person.kt @@ -5,7 +5,7 @@ open class Person : Object { var summary: String? = null var inbox: String? = null var outbox: String? = null - private var url: String? = null + var url: String? = null private var icon: Image? = null var publicKey: Key? = null diff --git a/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/entity/Post.kt b/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/entity/Post.kt index 2cfb45be..d58870d6 100644 --- a/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/entity/Post.kt +++ b/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/entity/Post.kt @@ -9,5 +9,7 @@ data class Post( val visibility: Visibility, val url: String, val repostId: Long? = null, - val replyId: Long? = null + val replyId: Long? = null, + val sensitive: Boolean = false, + val apId: String = url ) diff --git a/src/main/kotlin/dev/usbharu/hideout/repository/IPostRepository.kt b/src/main/kotlin/dev/usbharu/hideout/repository/IPostRepository.kt index 38223a97..3b77248e 100644 --- a/src/main/kotlin/dev/usbharu/hideout/repository/IPostRepository.kt +++ b/src/main/kotlin/dev/usbharu/hideout/repository/IPostRepository.kt @@ -5,6 +5,7 @@ import dev.usbharu.hideout.domain.model.hideout.entity.Post interface IPostRepository { suspend fun generateId(): Long suspend fun save(post: Post): Post - suspend fun findOneById(id: Long): Post + suspend fun findOneById(id: Long): Post? + suspend fun findByUrl(url: String): Post? suspend fun delete(id: Long) } diff --git a/src/main/kotlin/dev/usbharu/hideout/repository/PostRepositoryImpl.kt b/src/main/kotlin/dev/usbharu/hideout/repository/PostRepositoryImpl.kt index 45101190..2d7d59a1 100644 --- a/src/main/kotlin/dev/usbharu/hideout/repository/PostRepositoryImpl.kt +++ b/src/main/kotlin/dev/usbharu/hideout/repository/PostRepositoryImpl.kt @@ -16,6 +16,7 @@ class PostRepositoryImpl(database: Database, private val idGenerateService: IdGe init { transaction(database) { SchemaUtils.create(Posts) + SchemaUtils.createMissingTablesAndColumns(Posts) } } @@ -37,14 +38,22 @@ class PostRepositoryImpl(database: Database, private val idGenerateService: IdGe it[url] = post.url it[repostId] = post.repostId it[replyId] = post.replyId + it[sensitive] = post.sensitive + it[apId] = post.apId } return@query post } } - override suspend fun findOneById(id: Long): Post { + override suspend fun findOneById(id: Long): Post? { return query { - Posts.select { Posts.id eq id }.single().toPost() + Posts.select { Posts.id eq id }.singleOrNull()?.toPost() + } + } + + override suspend fun findByUrl(url: String): Post? { + return query { + Posts.select { Posts.url eq url }.singleOrNull()?.toPost() } } @@ -65,6 +74,8 @@ object Posts : Table() { val url = varchar("url", 500) val repostId = long("repostId").references(id).nullable() val replyId = long("replyId").references(id).nullable() + val sensitive = bool("sensitive").default(false) + val apId = varchar("ap_id", 100).uniqueIndex() override val primaryKey: PrimaryKey = PrimaryKey(id) } @@ -78,6 +89,8 @@ fun ResultRow.toPost(): Post { visibility = Visibility.values().first { visibility -> visibility.ordinal == this[Posts.visibility] }, url = this[Posts.url], repostId = this[Posts.repostId], - replyId = this[Posts.replyId] + replyId = this[Posts.replyId], + sensitive = this[Posts.sensitive], + apId = this[Posts.apId] ) } diff --git a/src/main/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubCreateService.kt b/src/main/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubCreateService.kt new file mode 100644 index 00000000..632c801e --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubCreateService.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.Create + +interface ActivityPubCreateService { + suspend fun receiveCreate(create: Create): ActivityPubResponse +} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubCreateServiceImpl.kt b/src/main/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubCreateServiceImpl.kt new file mode 100644 index 00000000..59a6b485 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubCreateServiceImpl.kt @@ -0,0 +1,25 @@ +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.Create +import dev.usbharu.hideout.domain.model.ap.Note +import dev.usbharu.hideout.exception.ap.IllegalActivityPubObjectException +import io.ktor.http.* +import org.koin.core.annotation.Single + +@Single +class ActivityPubCreateServiceImpl( + private val activityPubNoteService: ActivityPubNoteService +) : ActivityPubCreateService { + override suspend fun receiveCreate(create: Create): ActivityPubResponse { + val value = create.`object` ?: throw IllegalActivityPubObjectException("object is null") + if (value.type.contains("Note").not()) { + throw IllegalActivityPubObjectException("object is not Note") + } + + val note = value as Note + activityPubNoteService.fetchNote(note) + return ActivityPubStringResponse(HttpStatusCode.Created, "Created") + } +} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubNoteService.kt b/src/main/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubNoteService.kt index 5c6ccf96..2c289415 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubNoteService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubNoteService.kt @@ -1,5 +1,6 @@ package dev.usbharu.hideout.service.activitypub +import dev.usbharu.hideout.domain.model.ap.Note import dev.usbharu.hideout.domain.model.hideout.entity.Post import dev.usbharu.hideout.domain.model.job.DeliverPostJob import kjob.core.job.JobProps @@ -8,4 +9,7 @@ interface ActivityPubNoteService { suspend fun createNote(post: Post) suspend fun createNoteJob(props: JobProps) + + suspend fun fetchNote(url: String, targetActor: String? = null): Note + suspend fun fetchNote(note: Note, targetActor: String? = null): Note } diff --git a/src/main/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubNoteServiceImpl.kt b/src/main/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubNoteServiceImpl.kt index 42f0df20..1630832a 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubNoteServiceImpl.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubNoteServiceImpl.kt @@ -5,11 +5,16 @@ import dev.usbharu.hideout.config.Config import dev.usbharu.hideout.domain.model.ap.Create import dev.usbharu.hideout.domain.model.ap.Note import dev.usbharu.hideout.domain.model.hideout.entity.Post +import dev.usbharu.hideout.domain.model.hideout.entity.Visibility import dev.usbharu.hideout.domain.model.job.DeliverPostJob +import dev.usbharu.hideout.exception.ap.IllegalActivityPubObjectException +import dev.usbharu.hideout.plugins.getAp import dev.usbharu.hideout.plugins.postAp +import dev.usbharu.hideout.repository.IPostRepository import dev.usbharu.hideout.service.impl.IUserService import dev.usbharu.hideout.service.job.JobQueueParentService import io.ktor.client.* +import io.ktor.client.call.* import kjob.core.job.JobProps import org.koin.core.annotation.Single import org.slf4j.LoggerFactory @@ -19,7 +24,9 @@ import java.time.Instant class ActivityPubNoteServiceImpl( private val httpClient: HttpClient, private val jobQueueParentService: JobQueueParentService, - private val userService: IUserService + private val userService: IUserService, + private val postRepository: IPostRepository, + private val activityPubUserService: ActivityPubUserService ) : ActivityPubNoteService { private val logger = LoggerFactory.getLogger(this::class.java) @@ -61,4 +68,77 @@ class ActivityPubNoteServiceImpl( ) ) } + + override suspend fun fetchNote(url: String, targetActor: String?): Note { + val post = postRepository.findByUrl(url) + if (post != null) { + val user = userService.findById(post.userId) + val reply = post.replyId?.let { postRepository.findOneById(it) } + return Note( + name = "Post", + id = post.apId, + attributedTo = user.url, + content = post.text, + published = Instant.ofEpochMilli(post.createdAt).toString(), + to = listOf("https://www.w3.org/ns/activitystreams#Public", user.url + "/follower"), + sensitive = post.sensitive, + cc = listOf("https://www.w3.org/ns/activitystreams#Public", user.url + "/follower"), + inReplyTo = reply?.url + ) + } + val response = httpClient.getAp( + url, + "$targetActor#pubkey" + ) + val note = response.body() + return note(note, targetActor, url) + } + + private suspend fun ActivityPubNoteServiceImpl.note( + note: Note, + targetActor: String?, + url: String + ): Note { + val person = activityPubUserService.fetchPerson( + note.attributedTo ?: throw IllegalActivityPubObjectException("note.attributedTo is null"), targetActor + ) + val user = + userService.findByUrl(person.url ?: throw IllegalActivityPubObjectException("person.url is null")) + + val visibility = + if (note.to.contains("https://www.w3.org/ns/activitystreams#Public") && note.cc.contains("https://www.w3.org/ns/activitystreams#Public")) { + Visibility.PUBLIC + } else if (note.to.find { it.endsWith("/followers") } != null && note.cc.contains("https://www.w3.org/ns/activitystreams#Public")) { + Visibility.UNLISTED + } else if (note.to.find { it.endsWith("/followers") } != null) { + Visibility.FOLLOWERS + } else { + Visibility.DIRECT + } + + val reply = note.inReplyTo?.let { + fetchNote(it, targetActor) + postRepository.findByUrl(it) + } + + postRepository.save( + Post( + id = postRepository.generateId(), + userId = user.id, + overview = null, + text = note.content.orEmpty(), + createdAt = Instant.parse(note.published).toEpochMilli(), + visibility = visibility, + url = note.id ?: url, + repostId = null, + replyId = reply?.id, + sensitive = note.sensitive, + apId = note.id ?: url, + ) + ) + return note + } + + override suspend fun fetchNote(note: Note, targetActor: String?): Note = + note(note, targetActor, note.id ?: throw IllegalArgumentException("note.id is null")) } 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 4760f05b..821cecfd 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubServiceImpl.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubServiceImpl.kt @@ -20,7 +20,8 @@ class ActivityPubServiceImpl( private val activityPubReceiveFollowService: ActivityPubReceiveFollowService, private val activityPubNoteService: ActivityPubNoteService, private val activityPubUndoService: ActivityPubUndoService, - private val activityPubAcceptService: ActivityPubAcceptService + private val activityPubAcceptService: ActivityPubAcceptService, + private val activityPubCreateService: ActivityPubCreateService ) : ActivityPubService { val logger: Logger = LoggerFactory.getLogger(this::class.java) @@ -32,9 +33,9 @@ class ActivityPubServiceImpl( } val type = readTree["type"] if (type.isArray) { - return type.mapNotNull { jsonNode: JsonNode -> + return type.firstNotNullOf { jsonNode: JsonNode -> ActivityType.values().firstOrNull { it.name.equals(jsonNode.asText(), true) } - }.first() + } } return ActivityType.values().first { it.name.equals(type.asText(), true) } } @@ -51,6 +52,7 @@ class ActivityPubServiceImpl( ) ) + ActivityType.Create -> activityPubCreateService.receiveCreate(configData.objectMapper.readValue(json)) ActivityType.Undo -> activityPubUndoService.receiveUndo(configData.objectMapper.readValue(json)) else -> { diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml index 4593b633..ad457f2b 100644 --- a/src/main/resources/logback.xml +++ b/src/main/resources/logback.xml @@ -4,7 +4,7 @@ %d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n - + diff --git a/src/test/kotlin/dev/usbharu/hideout/repository/UserRepositoryTest.kt b/src/test/kotlin/dev/usbharu/hideout/repository/UserRepositoryTest.kt index aab82880..8fb127f7 100644 --- a/src/test/kotlin/dev/usbharu/hideout/repository/UserRepositoryTest.kt +++ b/src/test/kotlin/dev/usbharu/hideout/repository/UserRepositoryTest.kt @@ -28,6 +28,7 @@ class UserRepositoryTest { transaction(db) { SchemaUtils.create(Users) SchemaUtils.create(UsersFollowers) + SchemaUtils.create(FollowRequests) } } @@ -35,6 +36,7 @@ class UserRepositoryTest { fun tearDown() { transaction(db) { SchemaUtils.drop(UsersFollowers) + SchemaUtils.drop(FollowRequests) SchemaUtils.drop(Users) } } @@ -96,9 +98,7 @@ class UserRepositoryTest { ) userRepository.createFollower(user.id, follower.id) userRepository.createFollower(user.id, follower2.id) - userRepository.findFollowersById(user.id).let { - assertIterableEquals(listOf(follower, follower2), it) - } + assertIterableEquals(listOf(follower, follower2), userRepository.findFollowersById(user.id)) } @Test diff --git a/src/test/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubNoteServiceImplTest.kt b/src/test/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubNoteServiceImplTest.kt index 0372f88e..f4a9f206 100644 --- a/src/test/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubNoteServiceImplTest.kt +++ b/src/test/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubNoteServiceImplTest.kt @@ -72,7 +72,8 @@ class ActivityPubNoteServiceImplTest { onBlocking { findFollowersById(eq(1L)) } doReturn followers } val jobQueueParentService = mock() - val activityPubNoteService = ActivityPubNoteServiceImpl(mock(), jobQueueParentService, userService) + val activityPubNoteService = + ActivityPubNoteServiceImpl(mock(), jobQueueParentService, userService, mock(), mock()) val postEntity = Post( 1L, 1L, @@ -95,7 +96,7 @@ class ActivityPubNoteServiceImplTest { respondOk() } ) - val activityPubNoteService = ActivityPubNoteServiceImpl(httpClient, mock(), mock()) + val activityPubNoteService = ActivityPubNoteServiceImpl(httpClient, mock(), mock(), mock(), mock()) activityPubNoteService.createNoteJob( JobProps( data = mapOf( diff --git a/src/test/kotlin/dev/usbharu/hideout/service/impl/PostServiceTest.kt b/src/test/kotlin/dev/usbharu/hideout/service/impl/PostServiceTest.kt index 46e41fd2..47ea8518 100644 --- a/src/test/kotlin/dev/usbharu/hideout/service/impl/PostServiceTest.kt +++ b/src/test/kotlin/dev/usbharu/hideout/service/impl/PostServiceTest.kt @@ -53,7 +53,7 @@ class PostServiceTest { text, Instant.now().toEpochMilli(), visibility, - "https://example.com" + "https://example.com${(userId.toString() + text).hashCode()}" ) } @@ -88,6 +88,8 @@ class PostServiceTest { this[Posts.url] = it.url this[Posts.replyId] = it.replyId this[Posts.repostId] = it.repostId + this[Posts.sensitive] = it.sensitive + this[Posts.apId] = it.apId } } @@ -109,7 +111,7 @@ class PostServiceTest { text, Instant.now().toEpochMilli(), visibility, - "https://example.com" + "https://example.com${(userId.toString() + text).hashCode()}" ) } @@ -148,6 +150,8 @@ class PostServiceTest { this[Posts.url] = it.url this[Posts.replyId] = it.replyId this[Posts.repostId] = it.repostId + this[Posts.sensitive] = it.sensitive + this[Posts.apId] = it.apId } UsersFollowers.insert { it[id] = 100L