diff --git a/src/main/kotlin/dev/usbharu/hideout/Application.kt b/src/main/kotlin/dev/usbharu/hideout/Application.kt index bd2d6774..bd41e609 100644 --- a/src/main/kotlin/dev/usbharu/hideout/Application.kt +++ b/src/main/kotlin/dev/usbharu/hideout/Application.kt @@ -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 { ActivityPubFollowServiceImpl(get(), get(), get(),get()) } - single { ActivityPubServiceImpl(get()) } + single { ActivityPubFollowServiceImpl(get(), get(), get(), get()) } + single { ActivityPubServiceImpl(get(), get()) } single { UserService(get()) } single { ActivityPubUserServiceImpl(get(), get(), get()) } + single { ActivityPubNoteServiceImpl(get(), get(), get()) } + single { PostService(get(), get()) } + single { PostRepositoryImpl(get(), TwitterSnowflakeIdGenerateService) } } @@ -98,9 +97,11 @@ fun Application.parent() { inject().value, inject().value, inject().value, - inject().value + inject().value, + inject().value ) } + @Suppress("unused") fun Application.worker() { val kJob = kjob(ExposedKJob) { @@ -109,9 +110,14 @@ fun Application.worker() { val activityPubService = inject().value - kJob.register(ReceiveFollowJob){ + kJob.register(ReceiveFollowJob) { execute { - activityPubService.processActivity(this,it) + activityPubService.processActivity(this, it) + } + } + kJob.register(DeliverPostJob){ + execute { + activityPubService.processActivity(this, it) } } } diff --git a/src/main/kotlin/dev/usbharu/hideout/domain/model/Posts.kt b/src/main/kotlin/dev/usbharu/hideout/domain/model/Posts.kt new file mode 100644 index 00000000..3a6867db --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/domain/model/Posts.kt @@ -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] + ) +} diff --git a/src/main/kotlin/dev/usbharu/hideout/domain/model/User.kt b/src/main/kotlin/dev/usbharu/hideout/domain/model/User.kt index 28e57f99..7239465e 100644 --- a/src/main/kotlin/dev/usbharu/hideout/domain/model/User.kt +++ b/src/main/kotlin/dev/usbharu/hideout/domain/model/User.kt @@ -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] + ) +} 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 new file mode 100644 index 00000000..a0766fd6 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/domain/model/ap/Create.kt @@ -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 = 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()}" + } + + +} 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 new file mode 100644 index 00000000..caced1da --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/domain/model/ap/Note.kt @@ -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 = emptyList() + protected constructor() : super() + constructor( + type: List = emptyList(), + name: String, + id: String?, + attributedTo: String?, + content: String?, + published: String?, + to: List = 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()}" + } + + +} diff --git a/src/main/kotlin/dev/usbharu/hideout/domain/model/api/Status.kt b/src/main/kotlin/dev/usbharu/hideout/domain/model/api/Status.kt new file mode 100644 index 00000000..e8e9bfe0 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/domain/model/api/Status.kt @@ -0,0 +1,6 @@ +package dev.usbharu.hideout.domain.model.api + +data class StatusForPost( + val status:String, + val userId:Long +) diff --git a/src/main/kotlin/dev/usbharu/hideout/domain/model/job/HideoutJob.kt b/src/main/kotlin/dev/usbharu/hideout/domain/model/job/HideoutJob.kt index c499807c..6bce8a95 100644 --- a/src/main/kotlin/dev/usbharu/hideout/domain/model/job/HideoutJob.kt +++ b/src/main/kotlin/dev/usbharu/hideout/domain/model/job/HideoutJob.kt @@ -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") +} diff --git a/src/main/kotlin/dev/usbharu/hideout/plugins/Routing.kt b/src/main/kotlin/dev/usbharu/hideout/plugins/Routing.kt index 6b44c0fd..fc4f0742 100644 --- a/src/main/kotlin/dev/usbharu/hideout/plugins/Routing.kt +++ b/src/main/kotlin/dev/usbharu/hideout/plugins/Routing.kt @@ -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) + } + } } diff --git a/src/main/kotlin/dev/usbharu/hideout/repository/IPostRepository.kt b/src/main/kotlin/dev/usbharu/hideout/repository/IPostRepository.kt new file mode 100644 index 00000000..d7f523db --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/repository/IPostRepository.kt @@ -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) +} diff --git a/src/main/kotlin/dev/usbharu/hideout/repository/IUserKeyRepository.kt b/src/main/kotlin/dev/usbharu/hideout/repository/IUserKeyRepository.kt deleted file mode 100644 index d72bac45..00000000 --- a/src/main/kotlin/dev/usbharu/hideout/repository/IUserKeyRepository.kt +++ /dev/null @@ -1,3 +0,0 @@ -package dev.usbharu.hideout.repository - -interface IUserKeyRepository diff --git a/src/main/kotlin/dev/usbharu/hideout/repository/PostRepositoryImpl.kt b/src/main/kotlin/dev/usbharu/hideout/repository/PostRepositoryImpl.kt new file mode 100644 index 00000000..4d30dc68 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/repository/PostRepositoryImpl.kt @@ -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 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 } + } + } +} diff --git a/src/main/kotlin/dev/usbharu/hideout/repository/UserKeyRepository.kt b/src/main/kotlin/dev/usbharu/hideout/repository/UserKeyRepository.kt deleted file mode 100644 index b8a8de36..00000000 --- a/src/main/kotlin/dev/usbharu/hideout/repository/UserKeyRepository.kt +++ /dev/null @@ -1,4 +0,0 @@ -package dev.usbharu.hideout.repository - -class UserKeyRepository { -} diff --git a/src/main/kotlin/dev/usbharu/hideout/repository/UserRepository.kt b/src/main/kotlin/dev/usbharu/hideout/repository/UserRepository.kt index f51240f0..dfaf32aa 100644 --- a/src/main/kotlin/dev/usbharu/hideout/repository/UserRepository.kt +++ b/src/main/kotlin/dev/usbharu/hideout/repository/UserRepository.kt @@ -112,7 +112,9 @@ class UserRepository(private val database: Database) : IUserRepository { } override suspend fun findByUrls(urls: List): List { - TODO("Not yet implemented") + return query { + Users.select { Users.url inList urls }.map { it.toUserEntity() } + } } override suspend fun findFollowersById(id: Long): List { diff --git a/src/main/kotlin/dev/usbharu/hideout/routing/activitypub/UserRouting.kt b/src/main/kotlin/dev/usbharu/hideout/routing/activitypub/UserRouting.kt index 36b4d80d..66fc511c 100644 --- a/src/main/kotlin/dev/usbharu/hideout/routing/activitypub/UserRouting.kt +++ b/src/main/kotlin/dev/usbharu/hideout/routing/activitypub/UserRouting.kt @@ -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)) + } } } diff --git a/src/main/kotlin/dev/usbharu/hideout/routing/api/v1/Statuses.kt b/src/main/kotlin/dev/usbharu/hideout/routing/api/v1/Statuses.kt new file mode 100644 index 00000000..710354a0 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/routing/api/v1/Statuses.kt @@ -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) + } + } +} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/IPostService.kt b/src/main/kotlin/dev/usbharu/hideout/service/IPostService.kt new file mode 100644 index 00000000..52e3ba05 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/service/IPostService.kt @@ -0,0 +1,7 @@ +package dev.usbharu.hideout.service + +import dev.usbharu.hideout.domain.model.Post + +interface IPostService { + suspend fun create(post:Post) +} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/IdGenerateService.kt b/src/main/kotlin/dev/usbharu/hideout/service/IdGenerateService.kt new file mode 100644 index 00000000..8a74d9e4 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/service/IdGenerateService.kt @@ -0,0 +1,5 @@ +package dev.usbharu.hideout.service + +interface IdGenerateService { + suspend fun generateId():Long +} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/SnowflakeIdGenerateService.kt b/src/main/kotlin/dev/usbharu/hideout/service/SnowflakeIdGenerateService.kt new file mode 100644 index 00000000..69cda875 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/service/SnowflakeIdGenerateService.kt @@ -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() + } +} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/TwitterSnowflakeIdGenerateService.kt b/src/main/kotlin/dev/usbharu/hideout/service/TwitterSnowflakeIdGenerateService.kt new file mode 100644 index 00000000..7eb6b391 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/service/TwitterSnowflakeIdGenerateService.kt @@ -0,0 +1,4 @@ +package dev.usbharu.hideout.service + +// 2010-11-04T01:42:54.657 +object TwitterSnowflakeIdGenerateService : SnowflakeIdGenerateService(1288834974657L) diff --git a/src/main/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubNoteService.kt b/src/main/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubNoteService.kt new file mode 100644 index 00000000..b36aeec4 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubNoteService.kt @@ -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) +} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubNoteServiceImpl.kt b/src/main/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubNoteServiceImpl.kt new file mode 100644 index 00000000..445e5a48 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubNoteServiceImpl.kt @@ -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) { + val actor = props[DeliverPostJob.actor] + val postEntity = Config.configData.objectMapper.readValue(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 + ) + ) + } +} 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 c6424167..e86f967f 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubServiceImpl.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubServiceImpl.kt @@ -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 processActivity(job: JobContextWithProps, hideoutJob: HideoutJob) { + logger.debug("processActivity: ${hideoutJob.name}") when (hideoutJob) { ReceiveFollowJob -> activityPubFollowService.receiveFollowJob(job.props as JobProps) + DeliverPostJob -> activityPubNoteService.createNoteJob(job.props as JobProps) } } diff --git a/src/main/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubUserServiceImpl.kt b/src/main/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubUserServiceImpl.kt index 09c4b601..88272bdf 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubUserServiceImpl.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubUserServiceImpl.kt @@ -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 diff --git a/src/main/kotlin/dev/usbharu/hideout/service/impl/PostService.kt b/src/main/kotlin/dev/usbharu/hideout/service/impl/PostService.kt new file mode 100644 index 00000000..71b1b10f --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/service/impl/PostService.kt @@ -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) + } +} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/job/KJobJobQueueParentService.kt b/src/main/kotlin/dev/usbharu/hideout/service/job/KJobJobQueueParentService.kt index 1f4178b5..6324fde7 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/job/KJobJobQueueParentService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/job/KJobJobQueueParentService.kt @@ -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 schedule(job: J,block:ScheduleContext.(J)->Unit) { + logger.debug("schedule job={}",job.name) kjob.schedule(job,block) } } diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml index 60418308..bae7027f 100644 --- a/src/main/resources/logback.xml +++ b/src/main/resources/logback.xml @@ -9,4 +9,7 @@ + + + diff --git a/src/test/kotlin/dev/usbharu/hideout/routing/activitypub/InboxRoutingKtTest.kt b/src/test/kotlin/dev/usbharu/hideout/routing/activitypub/InboxRoutingKtTest.kt index c4de8bfb..5fa97051 100644 --- a/src/test/kotlin/dev/usbharu/hideout/routing/activitypub/InboxRoutingKtTest.kt +++ b/src/test/kotlin/dev/usbharu/hideout/routing/activitypub/InboxRoutingKtTest.kt @@ -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) diff --git a/src/test/kotlin/dev/usbharu/hideout/routing/activitypub/UsersAPTest.kt b/src/test/kotlin/dev/usbharu/hideout/routing/activitypub/UsersAPTest.kt index 6d177521..fd537617 100644 --- a/src/test/kotlin/dev/usbharu/hideout/routing/activitypub/UsersAPTest.kt +++ b/src/test/kotlin/dev/usbharu/hideout/routing/activitypub/UsersAPTest.kt @@ -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) diff --git a/src/test/kotlin/dev/usbharu/hideout/service/TwitterSnowflakeIdGenerateServiceTest.kt b/src/test/kotlin/dev/usbharu/hideout/service/TwitterSnowflakeIdGenerateServiceTest.kt new file mode 100644 index 00000000..d1d5ff60 --- /dev/null +++ b/src/test/kotlin/dev/usbharu/hideout/service/TwitterSnowflakeIdGenerateServiceTest.kt @@ -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() + coroutineScope { + + repeat(500000) { + + launch(Dispatchers.IO) { + val id = TwitterSnowflakeIdGenerateService.generateId() + mutex.withLock { + mutableListOf.add(id) + + } + } + + } + } + + assertEquals(0, mutableListOf.size - mutableListOf.toSet().size) + } +} diff --git a/src/test/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubNoteServiceImplTest.kt b/src/test/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubNoteServiceImplTest.kt new file mode 100644 index 00000000..8d820be6 --- /dev/null +++ b/src/test/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubNoteServiceImplTest.kt @@ -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( + 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 { + 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() + 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( + 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 + ) + ) + } +}