mirror of https://github.com/usbharu/Hideout.git
commit
2e441bb063
|
@ -5,15 +5,16 @@ import com.fasterxml.jackson.databind.DeserializationFeature
|
|||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||
import dev.usbharu.hideout.config.Config
|
||||
import dev.usbharu.hideout.config.ConfigData
|
||||
import dev.usbharu.hideout.domain.model.job.DeliverPostJob
|
||||
import dev.usbharu.hideout.domain.model.job.ReceiveFollowJob
|
||||
import dev.usbharu.hideout.plugins.*
|
||||
import dev.usbharu.hideout.repository.IUserAuthRepository
|
||||
import dev.usbharu.hideout.repository.IUserRepository
|
||||
import dev.usbharu.hideout.repository.UserAuthRepository
|
||||
import dev.usbharu.hideout.repository.UserRepository
|
||||
import dev.usbharu.hideout.repository.*
|
||||
import dev.usbharu.hideout.routing.register
|
||||
import dev.usbharu.hideout.service.IPostService
|
||||
import dev.usbharu.hideout.service.IUserAuthService
|
||||
import dev.usbharu.hideout.service.TwitterSnowflakeIdGenerateService
|
||||
import dev.usbharu.hideout.service.activitypub.*
|
||||
import dev.usbharu.hideout.service.impl.PostService
|
||||
import dev.usbharu.hideout.service.impl.UserAuthService
|
||||
import dev.usbharu.hideout.service.impl.UserService
|
||||
import dev.usbharu.hideout.service.job.JobQueueParentService
|
||||
|
@ -25,11 +26,6 @@ import io.ktor.client.*
|
|||
import io.ktor.client.engine.cio.*
|
||||
import io.ktor.client.plugins.logging.*
|
||||
import io.ktor.server.application.*
|
||||
import kjob.core.Job
|
||||
import kjob.core.KJob
|
||||
import kjob.core.dsl.JobContextWithProps
|
||||
import kjob.core.dsl.JobRegisterContext
|
||||
import kjob.core.dsl.KJobFunctions
|
||||
import kjob.core.kjob
|
||||
import org.jetbrains.exposed.sql.Database
|
||||
import org.koin.ktor.ext.inject
|
||||
|
@ -76,15 +72,18 @@ fun Application.parent() {
|
|||
logger = Logger.DEFAULT
|
||||
level = LogLevel.ALL
|
||||
}
|
||||
install(httpSignaturePlugin){
|
||||
install(httpSignaturePlugin) {
|
||||
keyMap = KtorKeyMap(get())
|
||||
}
|
||||
}
|
||||
}
|
||||
single<ActivityPubFollowService> { ActivityPubFollowServiceImpl(get(), get(), get(),get()) }
|
||||
single<ActivityPubService> { ActivityPubServiceImpl(get()) }
|
||||
single<ActivityPubFollowService> { ActivityPubFollowServiceImpl(get(), get(), get(), get()) }
|
||||
single<ActivityPubService> { ActivityPubServiceImpl(get(), get()) }
|
||||
single<UserService> { UserService(get()) }
|
||||
single<ActivityPubUserService> { ActivityPubUserServiceImpl(get(), get(), get()) }
|
||||
single<ActivityPubNoteService> { ActivityPubNoteServiceImpl(get(), get(), get()) }
|
||||
single<IPostService> { PostService(get(), get()) }
|
||||
single<IPostRepository> { PostRepositoryImpl(get(), TwitterSnowflakeIdGenerateService) }
|
||||
}
|
||||
|
||||
|
||||
|
@ -98,9 +97,11 @@ fun Application.parent() {
|
|||
inject<HttpSignatureVerifyService>().value,
|
||||
inject<ActivityPubService>().value,
|
||||
inject<UserService>().value,
|
||||
inject<ActivityPubUserService>().value
|
||||
inject<ActivityPubUserService>().value,
|
||||
inject<IPostService>().value
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
fun Application.worker() {
|
||||
val kJob = kjob(ExposedKJob) {
|
||||
|
@ -109,9 +110,14 @@ fun Application.worker() {
|
|||
|
||||
val activityPubService = inject<ActivityPubService>().value
|
||||
|
||||
kJob.register(ReceiveFollowJob){
|
||||
kJob.register(ReceiveFollowJob) {
|
||||
execute {
|
||||
activityPubService.processActivity(this,it)
|
||||
activityPubService.processActivity(this, it)
|
||||
}
|
||||
}
|
||||
kJob.register(DeliverPostJob){
|
||||
execute {
|
||||
activityPubService.processActivity(this, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
package dev.usbharu.hideout.domain.model
|
||||
|
||||
import org.jetbrains.exposed.sql.ResultRow
|
||||
import org.jetbrains.exposed.sql.Table
|
||||
|
||||
object Posts : Table() {
|
||||
val id = long("id")
|
||||
val userId = long("userId").references(Users.id)
|
||||
val overview = varchar("overview", 100).nullable()
|
||||
val text = varchar("text", 3000)
|
||||
val createdAt = long("createdAt")
|
||||
val visibility = integer("visibility").default(0)
|
||||
val url = varchar("url", 500)
|
||||
val repostId = long("repostId").references(id).nullable()
|
||||
val replyId = long("replyId").references(id).nullable()
|
||||
override val primaryKey: PrimaryKey = PrimaryKey(id)
|
||||
}
|
||||
|
||||
data class Post(
|
||||
val userId: Long,
|
||||
val overview: String? = null,
|
||||
val text: String,
|
||||
val createdAt: Long,
|
||||
val visibility: Int,
|
||||
val repostId: Long? = null,
|
||||
val replyId: Long? = null
|
||||
)
|
||||
|
||||
data class PostEntity(
|
||||
val id: Long,
|
||||
val userId:Long,
|
||||
val overview: String? = null,
|
||||
val text: String,
|
||||
val createdAt: Long,
|
||||
val visibility: Int,
|
||||
val url: String,
|
||||
val repostId: Long? = null,
|
||||
val replyId: Long? = null
|
||||
)
|
||||
|
||||
fun ResultRow.toPost():PostEntity{
|
||||
return PostEntity(
|
||||
id = this[Posts.id],
|
||||
userId = this[Posts.userId],
|
||||
overview = this[Posts.overview],
|
||||
text = this[Posts.text],
|
||||
createdAt = this[Posts.createdAt],
|
||||
visibility = this[Posts.visibility],
|
||||
url = this[Posts.url],
|
||||
repostId = this[Posts.repostId],
|
||||
replyId = this[Posts.replyId]
|
||||
)
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
package dev.usbharu.hideout.domain.model
|
||||
|
||||
import org.jetbrains.exposed.dao.id.LongIdTable
|
||||
import org.jetbrains.exposed.sql.ResultRow
|
||||
|
||||
data class User(
|
||||
val name: String,
|
||||
|
@ -47,3 +48,16 @@ object Users : LongIdTable("users") {
|
|||
uniqueIndex(name, domain)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun ResultRow.toUser(): User {
|
||||
return User(
|
||||
this[Users.name],
|
||||
this[Users.domain],
|
||||
this[Users.screenName],
|
||||
this[Users.description],
|
||||
this[Users.inbox],
|
||||
this[Users.outbox],
|
||||
this[Users.url]
|
||||
)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
package dev.usbharu.hideout.domain.model.ap
|
||||
|
||||
open class Create : Object {
|
||||
var `object` : Object? = null
|
||||
|
||||
protected constructor() : super()
|
||||
constructor(type: List<String> = emptyList(), name: String, `object`: Object?) : super(add(type,"Create"), name) {
|
||||
this.`object` = `object`
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is Create) 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 "Create(`object`=$`object`) ${super.toString()}"
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
package dev.usbharu.hideout.domain.model.ap
|
||||
|
||||
open class Note : Object {
|
||||
var id:String? = null
|
||||
var attributedTo:String? = null
|
||||
var content:String? = null
|
||||
var published:String? = null
|
||||
var to:List<String> = emptyList()
|
||||
protected constructor() : super()
|
||||
constructor(
|
||||
type: List<String> = emptyList(),
|
||||
name: String,
|
||||
id: String?,
|
||||
attributedTo: String?,
|
||||
content: String?,
|
||||
published: String?,
|
||||
to: List<String> = emptyList()
|
||||
) : super(add(type,"Note"), name) {
|
||||
this.id = id
|
||||
this.attributedTo = attributedTo
|
||||
this.content = content
|
||||
this.published = published
|
||||
this.to = to
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is Note) return false
|
||||
if (!super.equals(other)) return false
|
||||
|
||||
if (id != other.id) return false
|
||||
if (attributedTo != other.attributedTo) return false
|
||||
if (content != other.content) return false
|
||||
if (published != other.published) return false
|
||||
return to == other.to
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = super.hashCode()
|
||||
result = 31 * result + (id?.hashCode() ?: 0)
|
||||
result = 31 * result + (attributedTo?.hashCode() ?: 0)
|
||||
result = 31 * result + (content?.hashCode() ?: 0)
|
||||
result = 31 * result + (published?.hashCode() ?: 0)
|
||||
result = 31 * result + to.hashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "Note(id=$id, attributedTo=$attributedTo, content=$content, published=$published, to=$to) ${super.toString()}"
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
package dev.usbharu.hideout.domain.model.api
|
||||
|
||||
data class StatusForPost(
|
||||
val status:String,
|
||||
val userId:Long
|
||||
)
|
|
@ -9,3 +9,9 @@ object ReceiveFollowJob : HideoutJob("ReceiveFollowJob"){
|
|||
val follow = string("follow")
|
||||
val targetActor = string("targetActor")
|
||||
}
|
||||
|
||||
object DeliverPostJob : HideoutJob("DeliverPostJob"){
|
||||
val post = string("post")
|
||||
val actor = string("actor")
|
||||
val inbox = string("inbox")
|
||||
}
|
||||
|
|
|
@ -3,7 +3,9 @@ package dev.usbharu.hideout.plugins
|
|||
import dev.usbharu.hideout.routing.activitypub.inbox
|
||||
import dev.usbharu.hideout.routing.activitypub.outbox
|
||||
import dev.usbharu.hideout.routing.activitypub.usersAP
|
||||
import dev.usbharu.hideout.routing.api.v1.statuses
|
||||
import dev.usbharu.hideout.routing.wellknown.webfinger
|
||||
import dev.usbharu.hideout.service.IPostService
|
||||
import dev.usbharu.hideout.service.activitypub.ActivityPubService
|
||||
import dev.usbharu.hideout.service.activitypub.ActivityPubUserService
|
||||
import dev.usbharu.hideout.service.impl.UserService
|
||||
|
@ -15,14 +17,20 @@ import io.ktor.server.routing.*
|
|||
fun Application.configureRouting(
|
||||
httpSignatureVerifyService: HttpSignatureVerifyService,
|
||||
activityPubService: ActivityPubService,
|
||||
userService:UserService,
|
||||
activityPubUserService: ActivityPubUserService
|
||||
userService: UserService,
|
||||
activityPubUserService: ActivityPubUserService,
|
||||
postService: IPostService
|
||||
) {
|
||||
install(AutoHeadResponse)
|
||||
routing {
|
||||
inbox(httpSignatureVerifyService, activityPubService)
|
||||
outbox()
|
||||
usersAP(activityPubUserService)
|
||||
usersAP(activityPubUserService,userService)
|
||||
webfinger(userService)
|
||||
|
||||
route("/api/v1") {
|
||||
statuses(postService)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
package dev.usbharu.hideout.repository
|
||||
|
||||
import dev.usbharu.hideout.domain.model.Post
|
||||
import dev.usbharu.hideout.domain.model.PostEntity
|
||||
|
||||
interface IPostRepository {
|
||||
suspend fun insert(post:Post):PostEntity
|
||||
suspend fun findOneById(id:Long):PostEntity
|
||||
suspend fun delete(id:Long)
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
package dev.usbharu.hideout.repository
|
||||
|
||||
interface IUserKeyRepository
|
|
@ -0,0 +1,65 @@
|
|||
package dev.usbharu.hideout.repository
|
||||
|
||||
import dev.usbharu.hideout.config.Config
|
||||
import dev.usbharu.hideout.domain.model.*
|
||||
import dev.usbharu.hideout.service.IdGenerateService
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import org.jetbrains.exposed.sql.*
|
||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
||||
import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
|
||||
class PostRepositoryImpl(database: Database, private val idGenerateService: IdGenerateService) : IPostRepository {
|
||||
|
||||
init {
|
||||
transaction(database) {
|
||||
SchemaUtils.create(Posts)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun <T> query(block: suspend () -> T): T =
|
||||
newSuspendedTransaction(Dispatchers.IO) { block() }
|
||||
|
||||
override suspend fun insert(post: Post): PostEntity {
|
||||
return query {
|
||||
|
||||
val generateId = idGenerateService.generateId()
|
||||
val name = Users.select { Users.id eq post.userId }.single().toUser().name
|
||||
val postUrl = Config.configData.url + "/users/$name/posts/$generateId"
|
||||
Posts.insert {
|
||||
it[id] = generateId
|
||||
it[userId] = post.userId
|
||||
it[overview] = post.overview
|
||||
it[text] = post.text
|
||||
it[createdAt] = post.createdAt
|
||||
it[visibility] = post.visibility
|
||||
it[url] = postUrl
|
||||
it[repostId] = post.repostId
|
||||
it[replyId] = post.replyId
|
||||
}
|
||||
return@query PostEntity(
|
||||
generateId,
|
||||
post.userId,
|
||||
post.overview,
|
||||
post.text,
|
||||
post.createdAt,
|
||||
post.visibility,
|
||||
postUrl,
|
||||
post.repostId,
|
||||
post.replyId
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun findOneById(id: Long): PostEntity {
|
||||
return query {
|
||||
Posts.select { Posts.id eq id }.single().toPost()
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun delete(id: Long) {
|
||||
return query {
|
||||
Posts.deleteWhere { Posts.id eq id }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +0,0 @@
|
|||
package dev.usbharu.hideout.repository
|
||||
|
||||
class UserKeyRepository {
|
||||
}
|
|
@ -112,7 +112,9 @@ class UserRepository(private val database: Database) : IUserRepository {
|
|||
}
|
||||
|
||||
override suspend fun findByUrls(urls: List<String>): List<UserEntity> {
|
||||
TODO("Not yet implemented")
|
||||
return query {
|
||||
Users.select { Users.url inList urls }.map { it.toUserEntity() }
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun findFollowersById(id: Long): List<UserEntity> {
|
||||
|
|
|
@ -3,14 +3,16 @@ package dev.usbharu.hideout.routing.activitypub
|
|||
import dev.usbharu.hideout.exception.ParameterNotExistException
|
||||
import dev.usbharu.hideout.plugins.respondAp
|
||||
import dev.usbharu.hideout.service.activitypub.ActivityPubUserService
|
||||
import dev.usbharu.hideout.service.impl.UserService
|
||||
import dev.usbharu.hideout.util.HttpUtil.Activity
|
||||
import dev.usbharu.hideout.util.HttpUtil.JsonLd
|
||||
import io.ktor.http.*
|
||||
import io.ktor.server.application.*
|
||||
import io.ktor.server.request.*
|
||||
import io.ktor.server.response.*
|
||||
import io.ktor.server.routing.*
|
||||
|
||||
fun Routing.usersAP(activityPubUserService: ActivityPubUserService) {
|
||||
fun Routing.usersAP(activityPubUserService: ActivityPubUserService, userService: UserService) {
|
||||
route("/users/{name}") {
|
||||
createChild(ContentTypeRouteSelector(ContentType.Application.Activity, ContentType.Application.JsonLd)).handle {
|
||||
val name =
|
||||
|
@ -21,6 +23,10 @@ fun Routing.usersAP(activityPubUserService: ActivityPubUserService) {
|
|||
HttpStatusCode.OK
|
||||
)
|
||||
}
|
||||
get {
|
||||
val userEntity = userService.findByName(call.parameters["name"]!!)
|
||||
call.respondText(userEntity.toString() + "\n" + userService.findFollowersById(userEntity.id))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
package dev.usbharu.hideout.routing.api.v1
|
||||
|
||||
import dev.usbharu.hideout.domain.model.Post
|
||||
import dev.usbharu.hideout.domain.model.api.StatusForPost
|
||||
import dev.usbharu.hideout.service.IPostService
|
||||
import dev.usbharu.hideout.service.impl.PostService
|
||||
import io.ktor.server.application.*
|
||||
import io.ktor.server.request.*
|
||||
import io.ktor.server.response.*
|
||||
import io.ktor.server.routing.*
|
||||
|
||||
fun Route.statuses(postService: IPostService) {
|
||||
route("/statuses") {
|
||||
post {
|
||||
val status: StatusForPost = call.receive()
|
||||
val post = Post(
|
||||
userId = status.userId,
|
||||
createdAt = System.currentTimeMillis(),
|
||||
text = status.status,
|
||||
visibility = 1
|
||||
)
|
||||
postService.create(post)
|
||||
call.respond(status)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
package dev.usbharu.hideout.service
|
||||
|
||||
import dev.usbharu.hideout.domain.model.Post
|
||||
|
||||
interface IPostService {
|
||||
suspend fun create(post:Post)
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
package dev.usbharu.hideout.service
|
||||
|
||||
interface IdGenerateService {
|
||||
suspend fun generateId():Long
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
package dev.usbharu.hideout.service
|
||||
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import java.time.Instant
|
||||
|
||||
open class SnowflakeIdGenerateService(private val baseTime:Long) : IdGenerateService {
|
||||
var lastTimeStamp: Long = -1
|
||||
var sequenceId: Int = 0
|
||||
val mutex = Mutex()
|
||||
|
||||
@Throws(IllegalStateException::class)
|
||||
override suspend fun generateId(): Long {
|
||||
return mutex.withLock {
|
||||
|
||||
var timestamp = getTime()
|
||||
if (timestamp < lastTimeStamp) {
|
||||
while (timestamp <= lastTimeStamp) {
|
||||
delay(1L)
|
||||
timestamp = getTime()
|
||||
}
|
||||
// throw IllegalStateException(" $lastTimeStamp $timestamp ${lastTimeStamp-timestamp} ")
|
||||
}
|
||||
if (timestamp == lastTimeStamp) {
|
||||
sequenceId++
|
||||
if (sequenceId >= 4096) {
|
||||
while (timestamp <= lastTimeStamp) {
|
||||
delay(1L)
|
||||
timestamp = getTime()
|
||||
}
|
||||
sequenceId = 0
|
||||
}
|
||||
} else {
|
||||
sequenceId = 0
|
||||
}
|
||||
lastTimeStamp = timestamp
|
||||
return@withLock (timestamp - baseTime).shl(22).or(1L.shl(12)).or(sequenceId.toLong())
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
private fun getTime(): Long {
|
||||
return Instant.now().toEpochMilli()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
package dev.usbharu.hideout.service
|
||||
|
||||
// 2010-11-04T01:42:54.657
|
||||
object TwitterSnowflakeIdGenerateService : SnowflakeIdGenerateService(1288834974657L)
|
|
@ -0,0 +1,11 @@
|
|||
package dev.usbharu.hideout.service.activitypub
|
||||
|
||||
import dev.usbharu.hideout.domain.model.PostEntity
|
||||
import dev.usbharu.hideout.domain.model.job.DeliverPostJob
|
||||
import kjob.core.job.JobProps
|
||||
|
||||
interface ActivityPubNoteService {
|
||||
|
||||
suspend fun createNote(post:PostEntity)
|
||||
suspend fun createNoteJob(props:JobProps<DeliverPostJob>)
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
package dev.usbharu.hideout.service.activitypub
|
||||
|
||||
import com.fasterxml.jackson.module.kotlin.readValue
|
||||
import dev.usbharu.hideout.config.Config
|
||||
import dev.usbharu.hideout.domain.model.PostEntity
|
||||
import dev.usbharu.hideout.domain.model.ap.Create
|
||||
import dev.usbharu.hideout.domain.model.ap.Note
|
||||
import dev.usbharu.hideout.domain.model.job.DeliverPostJob
|
||||
import dev.usbharu.hideout.plugins.postAp
|
||||
import dev.usbharu.hideout.service.impl.UserService
|
||||
import dev.usbharu.hideout.service.job.JobQueueParentService
|
||||
import io.ktor.client.*
|
||||
import kjob.core.job.JobProps
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.time.Instant
|
||||
|
||||
class ActivityPubNoteServiceImpl(
|
||||
private val httpClient: HttpClient,
|
||||
private val jobQueueParentService: JobQueueParentService,
|
||||
private val userService: UserService
|
||||
) : ActivityPubNoteService {
|
||||
|
||||
private val logger = LoggerFactory.getLogger(this::class.java)
|
||||
|
||||
override suspend fun createNote(post: PostEntity) {
|
||||
val followers = userService.findFollowersById(post.userId)
|
||||
val userEntity = userService.findById(post.userId)
|
||||
val note = Config.configData.objectMapper.writeValueAsString(post)
|
||||
followers.forEach { followerEntity ->
|
||||
jobQueueParentService.schedule(DeliverPostJob) {
|
||||
props[it.actor] = userEntity.url
|
||||
props[it.post] = note
|
||||
props[it.inbox] = followerEntity.inbox
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override suspend fun createNoteJob(props: JobProps<DeliverPostJob>) {
|
||||
val actor = props[DeliverPostJob.actor]
|
||||
val postEntity = Config.configData.objectMapper.readValue<PostEntity>(props[DeliverPostJob.post])
|
||||
val note = Note(
|
||||
name = "Note",
|
||||
id = postEntity.url,
|
||||
attributedTo = actor,
|
||||
content = postEntity.text,
|
||||
published = Instant.ofEpochMilli(postEntity.createdAt).toString(),
|
||||
to = listOf("https://www.w3.org/ns/activitystreams#Public", actor + "/followers")
|
||||
)
|
||||
val inbox = props[DeliverPostJob.inbox]
|
||||
logger.debug("createNoteJob: actor={}, note={}, inbox={}", actor, postEntity, inbox)
|
||||
httpClient.postAp(
|
||||
urlString = inbox,
|
||||
username = "$actor#pubkey",
|
||||
jsonLd = Create(
|
||||
name = "Create Note",
|
||||
`object` = note
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,9 +1,10 @@
|
|||
package dev.usbharu.hideout.service.activitypub
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode
|
||||
import dev.usbharu.hideout.domain.model.ap.Follow
|
||||
import dev.usbharu.hideout.config.Config
|
||||
import dev.usbharu.hideout.domain.model.ActivityPubResponse
|
||||
import dev.usbharu.hideout.domain.model.ap.Follow
|
||||
import dev.usbharu.hideout.domain.model.job.DeliverPostJob
|
||||
import dev.usbharu.hideout.domain.model.job.HideoutJob
|
||||
import dev.usbharu.hideout.domain.model.job.ReceiveFollowJob
|
||||
import dev.usbharu.hideout.exception.JsonParseException
|
||||
|
@ -11,7 +12,10 @@ import kjob.core.dsl.JobContextWithProps
|
|||
import kjob.core.job.JobProps
|
||||
import org.slf4j.LoggerFactory
|
||||
|
||||
class ActivityPubServiceImpl(private val activityPubFollowService: ActivityPubFollowService) : ActivityPubService {
|
||||
class ActivityPubServiceImpl(
|
||||
private val activityPubFollowService: ActivityPubFollowService,
|
||||
private val activityPubNoteService: ActivityPubNoteService
|
||||
) : ActivityPubService {
|
||||
|
||||
val logger = LoggerFactory.getLogger(this::class.java)
|
||||
override fun parseActivity(json: String): ActivityType {
|
||||
|
@ -70,8 +74,10 @@ class ActivityPubServiceImpl(private val activityPubFollowService: ActivityPubFo
|
|||
}
|
||||
|
||||
override suspend fun <T : HideoutJob> processActivity(job: JobContextWithProps<T>, hideoutJob: HideoutJob) {
|
||||
logger.debug("processActivity: ${hideoutJob.name}")
|
||||
when (hideoutJob) {
|
||||
ReceiveFollowJob -> activityPubFollowService.receiveFollowJob(job.props as JobProps<ReceiveFollowJob>)
|
||||
DeliverPostJob -> activityPubNoteService.createNoteJob(job.props as JobProps<DeliverPostJob>)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@ import io.ktor.client.*
|
|||
import io.ktor.client.request.*
|
||||
import io.ktor.client.statement.*
|
||||
import io.ktor.http.*
|
||||
import org.slf4j.LoggerFactory
|
||||
|
||||
class ActivityPubUserServiceImpl(
|
||||
private val userService: UserService,
|
||||
|
@ -23,6 +24,8 @@ class ActivityPubUserServiceImpl(
|
|||
private val httpClient: HttpClient
|
||||
) :
|
||||
ActivityPubUserService {
|
||||
|
||||
private val logger = LoggerFactory.getLogger(this::class.java)
|
||||
override suspend fun getPersonByName(name: String): Person {
|
||||
// TODO: JOINで書き直し
|
||||
val userEntity = userService.findByName(name)
|
||||
|
@ -91,8 +94,8 @@ class ActivityPubUserServiceImpl(
|
|||
name = person.preferredUsername
|
||||
?: throw IllegalActivityPubObjectException("preferredUsername is null"),
|
||||
domain = url.substringAfter(":").substringBeforeLast("/"),
|
||||
screenName = person.name ?: throw IllegalActivityPubObjectException("name is null"),
|
||||
description = person.summary ?: throw IllegalActivityPubObjectException("summary is null"),
|
||||
screenName = (person.name ?: person.preferredUsername) ?: throw IllegalActivityPubObjectException("preferredUsername is null"),
|
||||
description = person.summary ?: "",
|
||||
inbox = person.inbox ?: throw IllegalActivityPubObjectException("inbox is null"),
|
||||
outbox = person.outbox ?: throw IllegalActivityPubObjectException("outbox is null"),
|
||||
url = url
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
package dev.usbharu.hideout.service.impl
|
||||
|
||||
import dev.usbharu.hideout.domain.model.Post
|
||||
import dev.usbharu.hideout.repository.IPostRepository
|
||||
import dev.usbharu.hideout.service.IPostService
|
||||
import dev.usbharu.hideout.service.activitypub.ActivityPubNoteService
|
||||
import dev.usbharu.hideout.service.job.JobQueueParentService
|
||||
import org.slf4j.LoggerFactory
|
||||
|
||||
class PostService(private val postRepository:IPostRepository,private val activityPubNoteService: ActivityPubNoteService) : IPostService {
|
||||
|
||||
private val logger = LoggerFactory.getLogger(this::class.java)
|
||||
override suspend fun create(post: Post) {
|
||||
logger.debug("create post={}",post)
|
||||
val postEntity = postRepository.insert(post)
|
||||
activityPubNoteService.createNote(postEntity)
|
||||
}
|
||||
}
|
|
@ -6,9 +6,12 @@ import kjob.core.KJob
|
|||
import kjob.core.dsl.ScheduleContext
|
||||
import kjob.core.kjob
|
||||
import org.jetbrains.exposed.sql.Database
|
||||
import org.slf4j.LoggerFactory
|
||||
|
||||
class KJobJobQueueParentService(private val database: Database) : JobQueueParentService {
|
||||
|
||||
private val logger = LoggerFactory.getLogger(this::class.java)
|
||||
|
||||
val kjob: KJob = kjob(ExposedKJob) {
|
||||
connectionDatabase = database
|
||||
isWorker = false
|
||||
|
@ -19,6 +22,7 @@ class KJobJobQueueParentService(private val database: Database) : JobQueueParent
|
|||
}
|
||||
|
||||
override suspend fun <J : Job> schedule(job: J,block:ScheduleContext<J>.(J)->Unit) {
|
||||
logger.debug("schedule job={}",job.name)
|
||||
kjob.schedule(job,block)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,4 +9,7 @@
|
|||
</root>
|
||||
<logger name="org.eclipse.jetty" level="INFO"/>
|
||||
<logger name="io.netty" level="INFO"/>
|
||||
<logger name="kjob.core.internal.scheduler.JobServiceImpl" level="INFO"/>
|
||||
<logger name="Exposed" level="INFO"/>
|
||||
<logger name="io.ktor.server.plugins.contentnegotiation" level="INFO"/>
|
||||
</configuration>
|
||||
|
|
|
@ -27,7 +27,7 @@ class InboxRoutingKtTest {
|
|||
}
|
||||
application {
|
||||
configureSerialization()
|
||||
configureRouting(mock(), mock(), mock(), mock())
|
||||
configureRouting(mock(), mock(), mock(), mock(),mock())
|
||||
}
|
||||
client.get("/inbox").let {
|
||||
Assertions.assertEquals(HttpStatusCode.MethodNotAllowed, it.status)
|
||||
|
@ -50,7 +50,7 @@ class InboxRoutingKtTest {
|
|||
application {
|
||||
configureStatusPages()
|
||||
configureSerialization()
|
||||
configureRouting(httpSignatureVerifyService, activityPubService, userService, activityPubUserService)
|
||||
configureRouting(httpSignatureVerifyService, activityPubService, userService, activityPubUserService,mock())
|
||||
}
|
||||
client.post("/inbox").let {
|
||||
Assertions.assertEquals(HttpStatusCode.BadRequest, it.status)
|
||||
|
@ -64,7 +64,7 @@ class InboxRoutingKtTest {
|
|||
}
|
||||
application {
|
||||
configureSerialization()
|
||||
configureRouting(mock(), mock(), mock(), mock())
|
||||
configureRouting(mock(), mock(), mock(), mock(),mock())
|
||||
}
|
||||
client.get("/users/test/inbox").let {
|
||||
Assertions.assertEquals(HttpStatusCode.MethodNotAllowed, it.status)
|
||||
|
@ -87,7 +87,7 @@ class InboxRoutingKtTest {
|
|||
application {
|
||||
configureStatusPages()
|
||||
configureSerialization()
|
||||
configureRouting(httpSignatureVerifyService, activityPubService, userService, activityPubUserService)
|
||||
configureRouting(httpSignatureVerifyService, activityPubService, userService, activityPubUserService,mock())
|
||||
}
|
||||
client.post("/users/test/inbox").let {
|
||||
Assertions.assertEquals(HttpStatusCode.BadRequest, it.status)
|
||||
|
|
|
@ -70,7 +70,7 @@ class UsersAPTest {
|
|||
|
||||
application {
|
||||
configureSerialization()
|
||||
configureRouting(httpSignatureVerifyService, activityPubService, userService, activityPubUserService)
|
||||
configureRouting(httpSignatureVerifyService, activityPubService, userService, activityPubUserService,mock())
|
||||
}
|
||||
client.get("/users/test") {
|
||||
accept(ContentType.Application.Activity)
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
package dev.usbharu.hideout.service
|
||||
|
||||
//import kotlinx.coroutines.NonCancellable.message
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class TwitterSnowflakeIdGenerateServiceTest {
|
||||
@Test
|
||||
fun noDuplicateTest() = runBlocking {
|
||||
val mutex = Mutex()
|
||||
val mutableListOf = mutableListOf<Long>()
|
||||
coroutineScope {
|
||||
|
||||
repeat(500000) {
|
||||
|
||||
launch(Dispatchers.IO) {
|
||||
val id = TwitterSnowflakeIdGenerateService.generateId()
|
||||
mutex.withLock {
|
||||
mutableListOf.add(id)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
assertEquals(0, mutableListOf.size - mutableListOf.toSet().size)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,92 @@
|
|||
@file:OptIn(ExperimentalCoroutinesApi::class)
|
||||
@file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
|
||||
|
||||
package dev.usbharu.hideout.service.activitypub
|
||||
|
||||
import dev.usbharu.hideout.config.Config
|
||||
import dev.usbharu.hideout.config.ConfigData
|
||||
import dev.usbharu.hideout.domain.model.PostEntity
|
||||
import dev.usbharu.hideout.domain.model.UserEntity
|
||||
import dev.usbharu.hideout.domain.model.job.DeliverPostJob
|
||||
import dev.usbharu.hideout.service.impl.UserService
|
||||
import dev.usbharu.hideout.service.job.JobQueueParentService
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.engine.mock.*
|
||||
import kjob.core.job.JobProps
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlinx.serialization.json.Json
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.mockito.Mockito.eq
|
||||
import org.mockito.kotlin.*
|
||||
import utils.JsonObjectMapper
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class ActivityPubNoteServiceImplTest {
|
||||
@Test
|
||||
fun `createPost 新しい投稿`() = runTest {
|
||||
val followers = listOf<UserEntity>(
|
||||
UserEntity(
|
||||
2L,
|
||||
"follower",
|
||||
"follower.example.com",
|
||||
"followerUser",
|
||||
"test follower user",
|
||||
"https://follower.example.com/inbox",
|
||||
"https://follower.example.com/outbox",
|
||||
"https://follower.example.com"
|
||||
),
|
||||
UserEntity(
|
||||
3L,
|
||||
"follower2",
|
||||
"follower2.example.com",
|
||||
"follower2User",
|
||||
"test follower2 user",
|
||||
"https://follower2.example.com/inbox",
|
||||
"https://follower2.example.com/outbox",
|
||||
"https:.//follower2.example.com"
|
||||
)
|
||||
)
|
||||
val userService = mock<UserService> {
|
||||
onBlocking { findById(eq(1L)) } doReturn UserEntity(
|
||||
1L,
|
||||
"test",
|
||||
"example.com",
|
||||
"testUser",
|
||||
"test user",
|
||||
"https://example.com/inbox",
|
||||
"https://example.com/outbox",
|
||||
"https:.//example.com"
|
||||
)
|
||||
onBlocking { findFollowersById(eq(1L)) } doReturn followers
|
||||
}
|
||||
val jobQueueParentService = mock<JobQueueParentService>()
|
||||
val activityPubNoteService = ActivityPubNoteServiceImpl(mock(), jobQueueParentService, userService)
|
||||
val postEntity = PostEntity(
|
||||
1L, 1L, null, "test text", 1L, 1, "https://example.com"
|
||||
)
|
||||
activityPubNoteService.createNote(postEntity)
|
||||
verify(jobQueueParentService, times(2)).schedule(eq(DeliverPostJob), any())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `createPostJob 新しい投稿のJob`() = runTest {
|
||||
Config.configData = ConfigData(objectMapper = JsonObjectMapper.objectMapper)
|
||||
val httpClient = HttpClient(MockEngine { httpRequestData ->
|
||||
assertEquals("https://follower.example.com/inbox", httpRequestData.url.toString())
|
||||
respondOk()
|
||||
})
|
||||
val activityPubNoteService = ActivityPubNoteServiceImpl(httpClient, mock(), mock())
|
||||
activityPubNoteService.createNoteJob(
|
||||
JobProps(
|
||||
data = mapOf<String, Any>(
|
||||
DeliverPostJob.actor.name to "https://follower.example.com",
|
||||
DeliverPostJob.post.name to "{\"id\":1,\"userId\":1,\"inReplyToId\":null,\"text\":\"test text\"," +
|
||||
"\"createdAt\":1,\"updatedAt\":1,\"url\":\"https://example.com\"}",
|
||||
DeliverPostJob.inbox.name to "https://follower.example.com/inbox"
|
||||
),
|
||||
json = Json
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue