mirror of https://github.com/usbharu/Hideout.git
feat: Createに対応
This commit is contained in:
parent
5b6e0a3657
commit
b8613e2bf1
|
@ -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<String> = emptyList()
|
||||
var cc: List<String> = 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<String> = emptyList(),
|
||||
cc: List<String> = 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 {
|
||||
|
|
|
@ -5,6 +5,9 @@ open class Note : Object {
|
|||
var content: String? = null
|
||||
var published: String? = null
|
||||
var to: List<String> = emptyList()
|
||||
var cc: List<String> = 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<String> = emptyList()
|
||||
to: List<String> = emptyList(),
|
||||
cc: List<String> = 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 {
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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]
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
|
@ -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<DeliverPostJob>)
|
||||
|
||||
suspend fun fetchNote(url: String, targetActor: String? = null): Note
|
||||
suspend fun fetchNote(note: Note, targetActor: String? = null): Note
|
||||
}
|
||||
|
|
|
@ -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<Note>()
|
||||
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"))
|
||||
}
|
||||
|
|
|
@ -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 -> {
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<pattern>%d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
<root level="DEBUG">
|
||||
<root level="TRACE">
|
||||
<appender-ref ref="STDOUT"/>
|
||||
</root>
|
||||
<logger name="org.eclipse.jetty" level="INFO"/>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -72,7 +72,8 @@ class ActivityPubNoteServiceImplTest {
|
|||
onBlocking { findFollowersById(eq(1L)) } doReturn followers
|
||||
}
|
||||
val jobQueueParentService = mock<JobQueueParentService>()
|
||||
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<String, Any>(
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue