Compare commits

...

10 Commits

26 changed files with 1036 additions and 328 deletions

View File

@ -78,6 +78,7 @@ fun Application.parent() {
install(httpSignaturePlugin) { install(httpSignaturePlugin) {
keyMap = KtorKeyMap(get()) keyMap = KtorKeyMap(get())
} }
expectSuccess = true
} }
} }
single<IdGenerateService> { TwitterSnowflakeIdGenerateService } single<IdGenerateService> { TwitterSnowflakeIdGenerateService }
@ -107,12 +108,12 @@ fun Application.parent() {
inject<JwkProvider>().value, inject<JwkProvider>().value,
) )
configureRouting( configureRouting(
httpSignatureVerifyService = inject<HttpSignatureVerifyService>().value, httpSignatureVerifyService = inject<HttpSignatureVerifyService>().value,
activityPubService = inject<ActivityPubService>().value, activityPubService = inject<ActivityPubService>().value,
userService = inject<IUserService>().value, userService = inject<IUserService>().value,
activityPubUserService = inject<ActivityPubUserService>().value, activityPubUserService = inject<ActivityPubUserService>().value,
postService = inject<IPostApiService>().value, postService = inject<IPostApiService>().value,
userApiService = inject<IUserApiService>().value, userApiService = inject<IUserApiService>().value,
) )
} }

View File

@ -0,0 +1,42 @@
package dev.usbharu.hideout.domain.model.ap
open class Emoji : Object {
var updated: String? = null
var icon: Image? = null
protected constructor() : super()
constructor(
type: List<String>,
name: String?,
actor: String?,
id: String?,
updated: String?,
icon: Image?
) : super(
type = add(type, "Emoji"),
name = name,
actor = actor,
id = id
) {
this.updated = updated
this.icon = icon
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is Emoji) return false
if (!super.equals(other)) return false
if (updated != other.updated) return false
return icon == other.icon
}
override fun hashCode(): Int {
var result = super.hashCode()
result = 31 * result + (updated?.hashCode() ?: 0)
result = 31 * result + (icon?.hashCode() ?: 0)
return result
}
override fun toString(): String = "Emoji(updated=$updated, icon=$icon) ${super.toString()}"
}

View File

@ -0,0 +1,51 @@
package dev.usbharu.hideout.domain.model.ap
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
open class Like : Object {
var `object`: String? = null
var content: String? = null
@JsonDeserialize(contentUsing = ObjectDeserializer::class)
var tag: List<Object> = emptyList()
protected constructor() : super()
constructor(
type: List<String>,
name: String?,
actor: String?,
id: String?,
`object`: String?,
content: String?,
tag: List<Object>
) : super(
type = add(type, "Like"),
name = name,
actor = actor,
id = id
) {
this.`object` = `object`
this.content = content
this.tag = tag
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is Like) return false
if (!super.equals(other)) return false
if (`object` != other.`object`) return false
if (content != other.content) return false
return tag == other.tag
}
override fun hashCode(): Int {
var result = super.hashCode()
result = 31 * result + (`object`?.hashCode() ?: 0)
result = 31 * result + (content?.hashCode() ?: 0)
result = 31 * result + tag.hashCode()
return result
}
override fun toString(): String = "Like(`object`=$`object`, content=$content, tag=$tag) ${super.toString()}"
}

View File

@ -4,7 +4,7 @@ import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.databind.DeserializationContext import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.JsonDeserializer import com.fasterxml.jackson.databind.JsonDeserializer
import com.fasterxml.jackson.databind.JsonNode import com.fasterxml.jackson.databind.JsonNode
import dev.usbharu.hideout.service.activitypub.ActivityVocabulary import dev.usbharu.hideout.service.activitypub.ExtendedActivityVocabulary
class ObjectDeserializer : JsonDeserializer<Object>() { class ObjectDeserializer : JsonDeserializer<Object>() {
override fun deserialize(p: JsonParser?, ctxt: DeserializationContext?): Object { override fun deserialize(p: JsonParser?, ctxt: DeserializationContext?): Object {
@ -22,28 +22,78 @@ class ObjectDeserializer : JsonDeserializer<Object>() {
val type = treeNode["type"] val type = treeNode["type"]
val activityType = if (type.isArray) { val activityType = if (type.isArray) {
type.firstNotNullOf { jsonNode: JsonNode -> type.firstNotNullOf { jsonNode: JsonNode ->
ActivityVocabulary.values().firstOrNull { it.name.equals(jsonNode.asText(), true) } ExtendedActivityVocabulary.values().firstOrNull { it.name.equals(jsonNode.asText(), true) }
} }
} else if (type.isValueNode) { } else if (type.isValueNode) {
ActivityVocabulary.values().first { it.name.equals(type.asText(), true) } ExtendedActivityVocabulary.values().first { it.name.equals(type.asText(), true) }
} else { } else {
TODO() TODO()
} }
return when (activityType) { return when (activityType) {
ActivityVocabulary.Follow -> { ExtendedActivityVocabulary.Follow -> {
val readValue = p.codec.treeToValue(treeNode, Follow::class.java) val readValue = p.codec.treeToValue(treeNode, Follow::class.java)
println(readValue) println(readValue)
readValue readValue
} }
ActivityVocabulary.Note -> { ExtendedActivityVocabulary.Note -> {
p.codec.treeToValue(treeNode, Note::class.java) p.codec.treeToValue(treeNode, Note::class.java)
} }
else -> { ExtendedActivityVocabulary.Object -> p.codec.treeToValue(treeNode, Object::class.java)
TODO() ExtendedActivityVocabulary.Link -> TODO()
} ExtendedActivityVocabulary.Activity -> TODO()
ExtendedActivityVocabulary.IntransitiveActivity -> TODO()
ExtendedActivityVocabulary.Collection -> TODO()
ExtendedActivityVocabulary.OrderedCollection -> TODO()
ExtendedActivityVocabulary.CollectionPage -> TODO()
ExtendedActivityVocabulary.OrderedCollectionPage -> TODO()
ExtendedActivityVocabulary.Accept -> p.codec.treeToValue(treeNode, Accept::class.java)
ExtendedActivityVocabulary.Add -> TODO()
ExtendedActivityVocabulary.Announce -> TODO()
ExtendedActivityVocabulary.Arrive -> TODO()
ExtendedActivityVocabulary.Block -> TODO()
ExtendedActivityVocabulary.Create -> p.codec.treeToValue(treeNode, Create::class.java)
ExtendedActivityVocabulary.Delete -> TODO()
ExtendedActivityVocabulary.Dislike -> TODO()
ExtendedActivityVocabulary.Flag -> TODO()
ExtendedActivityVocabulary.Ignore -> TODO()
ExtendedActivityVocabulary.Invite -> TODO()
ExtendedActivityVocabulary.Join -> TODO()
ExtendedActivityVocabulary.Leave -> TODO()
ExtendedActivityVocabulary.Like -> p.codec.treeToValue(treeNode, Like::class.java)
ExtendedActivityVocabulary.Listen -> TODO()
ExtendedActivityVocabulary.Move -> TODO()
ExtendedActivityVocabulary.Offer -> TODO()
ExtendedActivityVocabulary.Question -> TODO()
ExtendedActivityVocabulary.Reject -> TODO()
ExtendedActivityVocabulary.Read -> TODO()
ExtendedActivityVocabulary.Remove -> TODO()
ExtendedActivityVocabulary.TentativeReject -> TODO()
ExtendedActivityVocabulary.TentativeAccept -> TODO()
ExtendedActivityVocabulary.Travel -> TODO()
ExtendedActivityVocabulary.Undo -> p.codec.treeToValue(treeNode, Undo::class.java)
ExtendedActivityVocabulary.Update -> TODO()
ExtendedActivityVocabulary.View -> TODO()
ExtendedActivityVocabulary.Application -> TODO()
ExtendedActivityVocabulary.Group -> TODO()
ExtendedActivityVocabulary.Organization -> TODO()
ExtendedActivityVocabulary.Person -> p.codec.treeToValue(treeNode, Person::class.java)
ExtendedActivityVocabulary.Service -> TODO()
ExtendedActivityVocabulary.Article -> TODO()
ExtendedActivityVocabulary.Audio -> TODO()
ExtendedActivityVocabulary.Document -> TODO()
ExtendedActivityVocabulary.Event -> TODO()
ExtendedActivityVocabulary.Image -> p.codec.treeToValue(treeNode, Image::class.java)
ExtendedActivityVocabulary.Page -> TODO()
ExtendedActivityVocabulary.Place -> TODO()
ExtendedActivityVocabulary.Profile -> TODO()
ExtendedActivityVocabulary.Relationship -> TODO()
ExtendedActivityVocabulary.Tombstone -> TODO()
ExtendedActivityVocabulary.Video -> TODO()
ExtendedActivityVocabulary.Mention -> TODO()
ExtendedActivityVocabulary.Emoji -> p.codec.treeToValue(treeNode, Emoji::class.java)
} }
} else { } else {
TODO() TODO()

View File

@ -0,0 +1,3 @@
package dev.usbharu.hideout.domain.model.hideout.entity
data class Reaction(val id: Long, val emojiId: Long, val postId: Long, val userId: Long)

View File

@ -44,10 +44,12 @@ suspend fun HttpClient.postAp(urlString: String, username: String, jsonLd: JsonL
} }
} }
suspend fun HttpClient.getAp(urlString: String, username: String): HttpResponse { suspend fun HttpClient.getAp(urlString: String, username: String?): HttpResponse {
return this.get(urlString) { return this.get(urlString) {
header("Accept", ContentType.Application.Activity) header("Accept", ContentType.Application.Activity)
header("Signature", "keyId=\"$username\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date\"") username?.let {
header("Signature", "keyId=\"$username\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date\"")
}
} }
} }

View File

@ -18,12 +18,12 @@ import io.ktor.server.routing.*
@Suppress("LongParameterList") @Suppress("LongParameterList")
fun Application.configureRouting( fun Application.configureRouting(
httpSignatureVerifyService: HttpSignatureVerifyService, httpSignatureVerifyService: HttpSignatureVerifyService,
activityPubService: ActivityPubService, activityPubService: ActivityPubService,
userService: IUserService, userService: IUserService,
activityPubUserService: ActivityPubUserService, activityPubUserService: ActivityPubUserService,
postService: IPostApiService, postService: IPostApiService,
userApiService: IUserApiService userApiService: IUserApiService
) { ) {
install(AutoHeadResponse) install(AutoHeadResponse)
routing { routing {

View File

@ -11,33 +11,33 @@ interface IPostRepository {
suspend fun findByUrl(url: String): Post? suspend fun findByUrl(url: String): Post?
suspend fun delete(id: Long) suspend fun delete(id: Long)
suspend fun findAll( suspend fun findAll(
since: Instant?, since: Instant?,
until: Instant?, until: Instant?,
minId: Long?, minId: Long?,
maxId: Long?, maxId: Long?,
limit: Int?, limit: Int?,
userId: Long? userId: Long?
): List<Post> ): List<Post>
suspend fun findByUserNameAndDomain( suspend fun findByUserNameAndDomain(
username: String, username: String,
s: String, s: String,
since: Instant?, since: Instant?,
until: Instant?, until: Instant?,
minId: Long?, minId: Long?,
maxId: Long?, maxId: Long?,
limit: Int?, limit: Int?,
userId: Long? userId: Long?
): List<Post> ): List<Post>
suspend fun findByUserId( suspend fun findByUserId(
idOrNull: Long, idOrNull: Long,
since: Instant?, since: Instant?,
until: Instant?, until: Instant?,
minId: Long?, minId: Long?,
maxId: Long?, maxId: Long?,
limit: Int?, limit: Int?,
userId: Long? userId: Long?
): List<Post> ): List<Post>
suspend fun findByApId(id: String): Post? suspend fun findByApId(id: String): Post?

View File

@ -63,7 +63,9 @@ class PostRepositoryImpl(database: Database, private val idGenerateService: IdGe
} }
override suspend fun findOneById(id: Long, userId: Long?): Post? { override suspend fun findOneById(id: Long, userId: Long?): Post? {
TODO("Not yet implemented") return query {
Posts.select { Posts.id eq id }.singleOrNull()?.toPost()
}
} }
override suspend fun findByUrl(url: String): Post? { override suspend fun findByUrl(url: String): Post? {
@ -79,37 +81,37 @@ class PostRepositoryImpl(database: Database, private val idGenerateService: IdGe
} }
override suspend fun findAll( override suspend fun findAll(
since: Instant?, since: Instant?,
until: Instant?, until: Instant?,
minId: Long?, minId: Long?,
maxId: Long?, maxId: Long?,
limit: Int?, limit: Int?,
userId: Long? userId: Long?
): List<Post> { ): List<Post> {
TODO("Not yet implemented") TODO("Not yet implemented")
} }
override suspend fun findByUserNameAndDomain( override suspend fun findByUserNameAndDomain(
username: String, username: String,
s: String, s: String,
since: Instant?, since: Instant?,
until: Instant?, until: Instant?,
minId: Long?, minId: Long?,
maxId: Long?, maxId: Long?,
limit: Int?, limit: Int?,
userId: Long? userId: Long?
): List<Post> { ): List<Post> {
TODO("Not yet implemented") TODO("Not yet implemented")
} }
override suspend fun findByUserId( override suspend fun findByUserId(
idOrNull: Long, idOrNull: Long,
since: Instant?, since: Instant?,
until: Instant?, until: Instant?,
minId: Long?, minId: Long?,
maxId: Long?, maxId: Long?,
limit: Int?, limit: Int?,
userId: Long? userId: Long?
): List<Post> { ): List<Post> {
TODO("Not yet implemented") TODO("Not yet implemented")
} }
@ -138,16 +140,16 @@ object Posts : Table() {
fun ResultRow.toPost(): Post { fun ResultRow.toPost(): Post {
return Post( return Post(
id = this[Posts.id], id = this[Posts.id],
userId = this[Posts.userId], userId = this[Posts.userId],
overview = this[Posts.overview], overview = this[Posts.overview],
text = this[Posts.text], text = this[Posts.text],
createdAt = this[Posts.createdAt], createdAt = this[Posts.createdAt],
visibility = Visibility.values().first { visibility -> visibility.ordinal == this[Posts.visibility] }, visibility = Visibility.values().first { visibility -> visibility.ordinal == this[Posts.visibility] },
url = this[Posts.url], url = this[Posts.url],
repostId = this[Posts.repostId], repostId = this[Posts.repostId],
replyId = this[Posts.replyId], replyId = this[Posts.replyId],
sensitive = this[Posts.sensitive], sensitive = this[Posts.sensitive],
apId = this[Posts.apId] apId = this[Posts.apId]
) )
} }

View File

@ -0,0 +1,9 @@
package dev.usbharu.hideout.repository
import dev.usbharu.hideout.domain.model.hideout.entity.Reaction
interface ReactionRepository {
suspend fun generateId(): Long
suspend fun save(reaction: Reaction): Reaction
suspend fun reactionAlreadyExist(postId: Long, userId: Long, emojiId: Long): Boolean
}

View File

@ -0,0 +1,79 @@
package dev.usbharu.hideout.repository
import dev.usbharu.hideout.domain.model.hideout.entity.Reaction
import dev.usbharu.hideout.service.core.IdGenerateService
import kotlinx.coroutines.Dispatchers
import org.jetbrains.exposed.dao.id.LongIdTable
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction
import org.jetbrains.exposed.sql.transactions.transaction
import org.koin.core.annotation.Single
@Single
class ReactionRepositoryImpl(
private val database: Database,
private val idGenerateService: IdGenerateService
) : ReactionRepository {
init {
transaction(database) {
SchemaUtils.create(Reactions)
SchemaUtils.createMissingTablesAndColumns(Reactions)
}
}
@Suppress("InjectDispatcher")
suspend fun <T> query(block: suspend () -> T): T =
newSuspendedTransaction(Dispatchers.IO) { block() }
override suspend fun generateId(): Long = idGenerateService.generateId()
override suspend fun save(reaction: Reaction): Reaction {
query {
if (Reactions.select { Reactions.id eq reaction.id }.empty()) {
Reactions.insert {
it[id] = reaction.id
it[emojiId] = reaction.emojiId
it[postId] = reaction.postId
it[userId] = reaction.userId
}
} else {
Reactions.update({ Reactions.id eq reaction.id }) {
it[emojiId] = reaction.emojiId
it[postId] = reaction.postId
it[userId] = reaction.userId
}
}
}
return reaction
}
override suspend fun reactionAlreadyExist(postId: Long, userId: Long, emojiId: Long): Boolean {
return query {
Reactions.select {
Reactions.postId.eq(postId).and(Reactions.userId.eq(userId)).and(
Reactions.emojiId.eq(emojiId)
)
}.empty().not()
}
}
}
fun ResultRow.toReaction(): Reaction {
return Reaction(
this[Reactions.id].value,
this[Reactions.emojiId],
this[Reactions.postId],
this[Reactions.userId]
)
}
object Reactions : LongIdTable("reactions") {
val emojiId = long("emoji_id")
val postId = long("post_id").references(Posts.id)
val userId = long("user_id").references(Users.id)
init {
uniqueIndex(emojiId, postId, userId)
}
}

View File

@ -51,14 +51,14 @@ fun Route.posts(postApiService: IPostApiService) {
get { get {
val userId = call.principal<JWTPrincipal>()?.payload?.getClaim("uid")?.asLong() val userId = call.principal<JWTPrincipal>()?.payload?.getClaim("uid")?.asLong()
val targetUserName = call.parameters["name"] val targetUserName = call.parameters["name"]
?: throw ParameterNotExistException("Parameter(name='userName@domain') does not exist.") ?: throw ParameterNotExistException("Parameter(name='userName@domain') does not exist.")
val posts = postApiService.getByUser(targetUserName, userId = userId) val posts = postApiService.getByUser(targetUserName, userId = userId)
call.respond(posts) call.respond(posts)
} }
get("/{id}") { get("/{id}") {
val userId = call.principal<JWTPrincipal>()?.payload?.getClaim("uid")?.asLong() val userId = call.principal<JWTPrincipal>()?.payload?.getClaim("uid")?.asLong()
val id = call.parameters["id"]?.toLong() val id = call.parameters["id"]?.toLong()
?: throw ParameterNotExistException("Parameter(name='postsId' does not exist.") ?: throw ParameterNotExistException("Parameter(name='postsId' does not exist.")
val post = postApiService.getById(id, userId) val post = postApiService.getById(id, userId)
call.respond(post) call.respond(post)
} }

View File

@ -43,9 +43,9 @@ fun Route.users(userService: IUserService, userApiService: IUserApiService) {
get { get {
val userParameter = ( val userParameter = (
call.parameters["name"] call.parameters["name"]
?: throw ParameterNotExistException( ?: throw ParameterNotExistException(
"Parameter(name='userName@domain') does not exist." "Parameter(name='userName@domain') does not exist."
) )
) )
if (userParameter.toLongOrNull() != null) { if (userParameter.toLongOrNull() != null) {
return@get call.respond(userApiService.findById(userParameter.toLong())) return@get call.respond(userApiService.findById(userParameter.toLong()))
@ -93,9 +93,9 @@ fun Route.users(userService: IUserService, userApiService: IUserApiService) {
get { get {
val userParameter = ( val userParameter = (
call.parameters["name"] call.parameters["name"]
?: throw ParameterNotExistException( ?: throw ParameterNotExistException(
"Parameter(name='userName@domain') does not exist." "Parameter(name='userName@domain') does not exist."
) )
) )
if (userParameter.toLongOrNull() != null) { if (userParameter.toLongOrNull() != null) {
return@get call.respond(userApiService.findFollowings(userParameter.toLong())) return@get call.respond(userApiService.findFollowings(userParameter.toLong()))

View File

@ -0,0 +1,8 @@
package dev.usbharu.hideout.service.activitypub
import dev.usbharu.hideout.domain.model.ActivityPubResponse
import dev.usbharu.hideout.domain.model.ap.Like
interface ActivityPubLikeService {
suspend fun receiveLike(like: Like): ActivityPubResponse
}

View File

@ -0,0 +1,40 @@
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.Like
import dev.usbharu.hideout.exception.PostNotFoundException
import dev.usbharu.hideout.exception.ap.IllegalActivityPubObjectException
import dev.usbharu.hideout.repository.IPostRepository
import dev.usbharu.hideout.service.reaction.IReactionService
import dev.usbharu.hideout.service.user.IUserService
import io.ktor.http.*
import org.koin.core.annotation.Single
@Single
class ActivityPubLikeServiceImpl(
private val reactionService: IReactionService,
private val activityPubUserService: ActivityPubUserService,
private val userService: IUserService,
private val postService: IPostRepository,
private val activityPubNoteService: ActivityPubNoteService
) : ActivityPubLikeService {
override suspend fun receiveLike(like: Like): ActivityPubResponse {
val actor = like.actor ?: throw IllegalActivityPubObjectException("actor is null")
val content = like.content ?: throw IllegalActivityPubObjectException("content is null")
like.`object` ?: throw IllegalActivityPubObjectException("object is null")
val person = activityPubUserService.fetchPerson(actor)
activityPubNoteService.fetchNote(like.`object`!!)
val user = userService.findByUrl(
person.url
?: throw IllegalActivityPubObjectException("actor is not found")
)
val post = postService.findByUrl(like.`object`!!)
?: throw PostNotFoundException("${like.`object`} was not found")
reactionService.receiveReaction(content, actor.substringAfter("://").substringBefore("/"), user.id, post.id)
return ActivityPubStringResponse(HttpStatusCode.OK, "")
}
}

View File

@ -14,7 +14,7 @@ import dev.usbharu.hideout.repository.IPostRepository
import dev.usbharu.hideout.service.job.JobQueueParentService import dev.usbharu.hideout.service.job.JobQueueParentService
import dev.usbharu.hideout.service.user.IUserService import dev.usbharu.hideout.service.user.IUserService
import io.ktor.client.* import io.ktor.client.*
import io.ktor.client.call.* import io.ktor.client.statement.*
import kjob.core.job.JobProps import kjob.core.job.JobProps
import org.koin.core.annotation.Single import org.koin.core.annotation.Single
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
@ -22,11 +22,11 @@ import java.time.Instant
@Single @Single
class ActivityPubNoteServiceImpl( class ActivityPubNoteServiceImpl(
private val httpClient: HttpClient, private val httpClient: HttpClient,
private val jobQueueParentService: JobQueueParentService, private val jobQueueParentService: JobQueueParentService,
private val userService: IUserService, private val userService: IUserService,
private val postRepository: IPostRepository, private val postRepository: IPostRepository,
private val activityPubUserService: ActivityPubUserService private val activityPubUserService: ActivityPubUserService
) : ActivityPubNoteService { ) : ActivityPubNoteService {
private val logger = LoggerFactory.getLogger(this::class.java) private val logger = LoggerFactory.getLogger(this::class.java)
@ -48,24 +48,24 @@ class ActivityPubNoteServiceImpl(
val actor = props[DeliverPostJob.actor] val actor = props[DeliverPostJob.actor]
val postEntity = Config.configData.objectMapper.readValue<Post>(props[DeliverPostJob.post]) val postEntity = Config.configData.objectMapper.readValue<Post>(props[DeliverPostJob.post])
val note = Note( val note = Note(
name = "Note", name = "Note",
id = postEntity.url, id = postEntity.url,
attributedTo = actor, attributedTo = actor,
content = postEntity.text, content = postEntity.text,
published = Instant.ofEpochMilli(postEntity.createdAt).toString(), published = Instant.ofEpochMilli(postEntity.createdAt).toString(),
to = listOf(public, actor + "/follower") to = listOf(public, actor + "/follower")
) )
val inbox = props[DeliverPostJob.inbox] val inbox = props[DeliverPostJob.inbox]
logger.debug("createNoteJob: actor={}, note={}, inbox={}", actor, postEntity, inbox) logger.debug("createNoteJob: actor={}, note={}, inbox={}", actor, postEntity, inbox)
httpClient.postAp( httpClient.postAp(
urlString = inbox, urlString = inbox,
username = "$actor#pubkey", username = "$actor#pubkey",
jsonLd = Create( jsonLd = Create(
name = "Create Note", name = "Create Note",
`object` = note, `object` = note,
actor = note.attributedTo, actor = note.attributedTo,
id = "${Config.configData.url}/create/note/${postEntity.id}" id = "${Config.configData.url}/create/note/${postEntity.id}"
) )
) )
} }
@ -75,10 +75,10 @@ class ActivityPubNoteServiceImpl(
return postToNote(post) return postToNote(post)
} }
val response = httpClient.getAp( val response = httpClient.getAp(
url, url,
"$targetActor#pubkey" targetActor?.let { "$targetActor#pubkey" }
) )
val note = response.body<Note>() val note = Config.configData.objectMapper.readValue<Note>(response.bodyAsText())
return note(note, targetActor, url) return note(note, targetActor, url)
} }
@ -86,44 +86,44 @@ class ActivityPubNoteServiceImpl(
val user = userService.findById(post.userId) val user = userService.findById(post.userId)
val reply = post.replyId?.let { postRepository.findOneById(it) } val reply = post.replyId?.let { postRepository.findOneById(it) }
return Note( return Note(
name = "Post", name = "Post",
id = post.apId, id = post.apId,
attributedTo = user.url, attributedTo = user.url,
content = post.text, content = post.text,
published = Instant.ofEpochMilli(post.createdAt).toString(), published = Instant.ofEpochMilli(post.createdAt).toString(),
to = listOf(public, user.url + "/follower"), to = listOf(public, user.url + "/follower"),
sensitive = post.sensitive, sensitive = post.sensitive,
cc = listOf(public, user.url + "/follower"), cc = listOf(public, user.url + "/follower"),
inReplyTo = reply?.url inReplyTo = reply?.url
) )
} }
private suspend fun ActivityPubNoteServiceImpl.note( private suspend fun ActivityPubNoteServiceImpl.note(
note: Note, note: Note,
targetActor: String?, targetActor: String?,
url: String url: String
): Note { ): Note {
val findByApId = postRepository.findByApId(url) val findByApId = postRepository.findByApId(url)
if (findByApId != null) { if (findByApId != null) {
return postToNote(findByApId) return postToNote(findByApId)
} }
val person = activityPubUserService.fetchPerson( val person = activityPubUserService.fetchPerson(
note.attributedTo ?: throw IllegalActivityPubObjectException("note.attributedTo is null"), note.attributedTo ?: throw IllegalActivityPubObjectException("note.attributedTo is null"),
targetActor targetActor
) )
val user = val user =
userService.findByUrl(person.url ?: throw IllegalActivityPubObjectException("person.url is null")) userService.findByUrl(person.url ?: throw IllegalActivityPubObjectException("person.url is null"))
val visibility = val visibility =
if (note.to.contains(public) && note.cc.contains(public)) { if (note.to.contains(public) && note.cc.contains(public)) {
Visibility.PUBLIC Visibility.PUBLIC
} else if (note.to.find { it.endsWith("/followers") } != null && note.cc.contains(public)) { } else if (note.to.find { it.endsWith("/followers") } != null && note.cc.contains(public)) {
Visibility.UNLISTED Visibility.UNLISTED
} else if (note.to.find { it.endsWith("/followers") } != null) { } else if (note.to.find { it.endsWith("/followers") } != null) {
Visibility.FOLLOWERS Visibility.FOLLOWERS
} else { } else {
Visibility.DIRECT Visibility.DIRECT
} }
val reply = note.inReplyTo?.let { val reply = note.inReplyTo?.let {
fetchNote(it, targetActor) fetchNote(it, targetActor)
@ -131,25 +131,25 @@ class ActivityPubNoteServiceImpl(
} }
postRepository.save( postRepository.save(
Post( Post(
id = postRepository.generateId(), id = postRepository.generateId(),
userId = user.id, userId = user.id,
overview = null, overview = null,
text = note.content.orEmpty(), text = note.content.orEmpty(),
createdAt = Instant.parse(note.published).toEpochMilli(), createdAt = Instant.parse(note.published).toEpochMilli(),
visibility = visibility, visibility = visibility,
url = note.id ?: url, url = note.id ?: url,
repostId = null, repostId = null,
replyId = reply?.id, replyId = reply?.id,
sensitive = note.sensitive, sensitive = note.sensitive,
apId = note.id ?: url, apId = note.id ?: url,
) )
) )
return note return note
} }
override suspend fun fetchNote(note: Note, targetActor: String?): Note = override suspend fun fetchNote(note: Note, targetActor: String?): Note =
note(note, targetActor, note.id ?: throw IllegalArgumentException("note.id is null")) note(note, targetActor, note.id ?: throw IllegalArgumentException("note.id is null"))
companion object { companion object {
const val public: String = "https://www.w3.org/ns/activitystreams#Public" const val public: String = "https://www.w3.org/ns/activitystreams#Public"

View File

@ -100,3 +100,65 @@ enum class ActivityVocabulary {
Video, Video,
Mention, Mention,
} }
enum class ExtendedActivityVocabulary {
Object,
Link,
Activity,
IntransitiveActivity,
Collection,
OrderedCollection,
CollectionPage,
OrderedCollectionPage,
Accept,
Add,
Announce,
Arrive,
Block,
Create,
Delete,
Dislike,
Flag,
Follow,
Ignore,
Invite,
Join,
Leave,
Like,
Listen,
Move,
Offer,
Question,
Reject,
Read,
Remove,
TentativeReject,
TentativeAccept,
Travel,
Undo,
Update,
View,
Application,
Group,
Organization,
Person,
Service,
Article,
Audio,
Document,
Event,
Image,
Note,
Page,
Place,
Profile,
Relationship,
Tombstone,
Video,
Mention,
Emoji
}
enum class ExtendedVocabulary {
Emoji
}

View File

@ -21,7 +21,8 @@ class ActivityPubServiceImpl(
private val activityPubNoteService: ActivityPubNoteService, private val activityPubNoteService: ActivityPubNoteService,
private val activityPubUndoService: ActivityPubUndoService, private val activityPubUndoService: ActivityPubUndoService,
private val activityPubAcceptService: ActivityPubAcceptService, private val activityPubAcceptService: ActivityPubAcceptService,
private val activityPubCreateService: ActivityPubCreateService private val activityPubCreateService: ActivityPubCreateService,
private val activityPubLikeService: ActivityPubLikeService
) : ActivityPubService { ) : ActivityPubService {
val logger: Logger = LoggerFactory.getLogger(this::class.java) val logger: Logger = LoggerFactory.getLogger(this::class.java)
@ -53,6 +54,7 @@ class ActivityPubServiceImpl(
) )
ActivityType.Create -> activityPubCreateService.receiveCreate(configData.objectMapper.readValue(json)) ActivityType.Create -> activityPubCreateService.receiveCreate(configData.objectMapper.readValue(json))
ActivityType.Like -> activityPubLikeService.receiveLike(configData.objectMapper.readValue(json))
ActivityType.Undo -> activityPubUndoService.receiveUndo(configData.objectMapper.readValue(json)) ActivityType.Undo -> activityPubUndoService.receiveUndo(configData.objectMapper.readValue(json))
else -> { else -> {

View File

@ -93,7 +93,7 @@ class ActivityPubUserServiceImpl(
RemoteUserCreateDto( RemoteUserCreateDto(
name = person.preferredUsername name = person.preferredUsername
?: throw IllegalActivityPubObjectException("preferredUsername is null"), ?: throw IllegalActivityPubObjectException("preferredUsername is null"),
domain = url.substringAfter("://").substringBeforeLast("/"), domain = url.substringAfter("://").substringBefore("/"),
screenName = (person.name ?: person.preferredUsername) screenName = (person.name ?: person.preferredUsername)
?: throw IllegalActivityPubObjectException("preferredUsername is null"), ?: throw IllegalActivityPubObjectException("preferredUsername is null"),
description = person.summary.orEmpty(), description = person.summary.orEmpty(),

View File

@ -3,25 +3,26 @@ package dev.usbharu.hideout.service.api
import dev.usbharu.hideout.domain.model.hideout.entity.Post import dev.usbharu.hideout.domain.model.hideout.entity.Post
import java.time.Instant import java.time.Instant
@Suppress("LongParameterList")
interface IPostApiService { interface IPostApiService {
suspend fun createPost(postForm: dev.usbharu.hideout.domain.model.hideout.form.Post, userId: Long): Post suspend fun createPost(postForm: dev.usbharu.hideout.domain.model.hideout.form.Post, userId: Long): Post
suspend fun getById(id: Long, userId: Long?): Post suspend fun getById(id: Long, userId: Long?): Post
suspend fun getAll( suspend fun getAll(
since: Instant? = null, since: Instant? = null,
until: Instant? = null, until: Instant? = null,
minId: Long? = null, minId: Long? = null,
maxId: Long? = null, maxId: Long? = null,
limit: Int? = null, limit: Int? = null,
userId: Long? = null userId: Long? = null
): List<Post> ): List<Post>
suspend fun getByUser( suspend fun getByUser(
nameOrId: String, nameOrId: String,
since: Instant? = null, since: Instant? = null,
until: Instant? = null, until: Instant? = null,
minId: Long? = null, minId: Long? = null,
maxId: Long? = null, maxId: Long? = null,
limit: Int? = null, limit: Int? = null,
userId: Long? = null userId: Long? = null
): List<Post> ): List<Post>
} }

View File

@ -13,58 +13,58 @@ import dev.usbharu.hideout.domain.model.hideout.form.Post as FormPost
@Single @Single
class PostApiServiceImpl( class PostApiServiceImpl(
private val postService: IPostService, private val postService: IPostService,
private val postRepository: IPostRepository private val postRepository: IPostRepository
) : IPostApiService { ) : IPostApiService {
override suspend fun createPost(postForm: FormPost, userId: Long): Post { override suspend fun createPost(postForm: FormPost, userId: Long): Post {
return postService.createLocal( return postService.createLocal(
PostCreateDto( PostCreateDto(
text = postForm.text, text = postForm.text,
overview = postForm.overview, overview = postForm.overview,
visibility = postForm.visibility, visibility = postForm.visibility,
repostId = postForm.repostId, repostId = postForm.repostId,
repolyId = postForm.replyId, repolyId = postForm.replyId,
userId = userId userId = userId
) )
) )
} }
override suspend fun getById(id: Long, userId: Long?): Post { override suspend fun getById(id: Long, userId: Long?): Post {
return postRepository.findOneById(id, userId) return postRepository.findOneById(id, userId)
?: throw PostNotFoundException("$id was not found or is not authorized.") ?: throw PostNotFoundException("$id was not found or is not authorized.")
} }
override suspend fun getAll( override suspend fun getAll(
since: Instant?, since: Instant?,
until: Instant?, until: Instant?,
minId: Long?, minId: Long?,
maxId: Long?, maxId: Long?,
limit: Int?, limit: Int?,
userId: Long? userId: Long?
): List<Post> = postRepository.findAll(since, until, minId, maxId, limit, userId) ): List<Post> = postRepository.findAll(since, until, minId, maxId, limit, userId)
override suspend fun getByUser( override suspend fun getByUser(
nameOrId: String, nameOrId: String,
since: Instant?, since: Instant?,
until: Instant?, until: Instant?,
minId: Long?, minId: Long?,
maxId: Long?, maxId: Long?,
limit: Int?, limit: Int?,
userId: Long? userId: Long?
): List<Post> { ): List<Post> {
val idOrNull = nameOrId.toLongOrNull() val idOrNull = nameOrId.toLongOrNull()
return if (idOrNull == null) { return if (idOrNull == null) {
val acct = AcctUtil.parse(nameOrId) val acct = AcctUtil.parse(nameOrId)
postRepository.findByUserNameAndDomain( postRepository.findByUserNameAndDomain(
acct.username, username = acct.username,
acct.domain s = acct.domain
?: Config.configData.domain, ?: Config.configData.domain,
since, since = since,
until, until = until,
minId, minId = minId,
maxId, maxId = maxId,
limit, limit = limit,
userId userId = userId
) )
} else { } else {
postRepository.findByUserId(idOrNull, since, until, minId, maxId, limit, userId) postRepository.findByUserId(idOrNull, since, until, minId, maxId, limit, userId)

View File

@ -11,23 +11,23 @@ import java.time.Instant
@Single @Single
class PostServiceImpl( class PostServiceImpl(
private val postRepository: IPostRepository, private val postRepository: IPostRepository,
private val userRepository: IUserRepository, private val userRepository: IUserRepository,
private val activityPubNoteService: ActivityPubNoteService private val activityPubNoteService: ActivityPubNoteService
) : IPostService { ) : IPostService {
override suspend fun createLocal(post: PostCreateDto): Post { override suspend fun createLocal(post: PostCreateDto): Post {
val user = userRepository.findById(post.userId) ?: throw UserNotFoundException("${post.userId} was not found") val user = userRepository.findById(post.userId) ?: throw UserNotFoundException("${post.userId} was not found")
val id = postRepository.generateId() val id = postRepository.generateId()
val createPost = Post( val createPost = Post(
id = id, id = id,
userId = post.userId, userId = post.userId,
overview = post.overview, overview = post.overview,
text = post.text, text = post.text,
createdAt = Instant.now().toEpochMilli(), createdAt = Instant.now().toEpochMilli(),
visibility = post.visibility, visibility = post.visibility,
url = "${user.url}/posts/$id", url = "${user.url}/posts/$id",
repostId = null, repostId = null,
replyId = null replyId = null
) )
activityPubNoteService.createNote(createPost) activityPubNoteService.createNote(createPost)
return internalCreate(createPost) return internalCreate(createPost)

View File

@ -0,0 +1,5 @@
package dev.usbharu.hideout.service.reaction
interface IReactionService {
suspend fun receiveReaction(name: String, domain: String, userId: Long, postId: Long)
}

View File

@ -0,0 +1,16 @@
package dev.usbharu.hideout.service.reaction
import dev.usbharu.hideout.domain.model.hideout.entity.Reaction
import dev.usbharu.hideout.repository.ReactionRepository
import org.koin.core.annotation.Single
@Single
class ReactionServiceImpl(private val reactionRepository: ReactionRepository) : IReactionService {
override suspend fun receiveReaction(name: String, domain: String, userId: Long, postId: Long) {
if (reactionRepository.reactionAlreadyExist(postId, userId, 0).not()) {
reactionRepository.save(
Reaction(reactionRepository.generateId(), 0, postId, userId)
)
}
}
}

View File

@ -0,0 +1,335 @@
openapi: 3.0.3
info:
title: Hideout API
description: Hideout API
version: 1.0.0
servers:
- url: 'https://test-hideout.usbharu.dev/api/internal/v1'
paths:
/posts:
get:
summary: 権限に応じて投稿一覧を返す
security:
- { }
- BearerAuth: [ ]
responses:
200:
description: 成功
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/Post"
401:
$ref: "#/components/responses/Unauthorized"
403:
$ref: "#/components/responses/Forbidden"
429:
$ref: "#/components/responses/TooManyRequests"
post:
summary: 投稿する
security:
- BearerAuth: [ ]
requestBody:
description: 投稿する内容
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/Post"
responses:
200:
description: 成功
headers:
Location:
description: 作成した投稿のURL
schema:
type: string
format: uri
401:
$ref: "#/components/responses/Unauthorized"
429:
$ref: "#/components/responses/TooManyRequests"
/posts/{postId}:
get:
summary: 権限に応じてIDの投稿を返す
security:
- { }
- BearerAuth: [ ]
parameters:
- $ref: "#/components/parameters/postId"
responses:
200:
description: 成功
content:
application/json:
schema:
$ref: "#/components/schemas/Post"
401:
$ref: "#/components/responses/Unauthorized"
403:
$ref: "#/components/responses/Forbidden"
404:
$ref: "#/components/responses/NotFoundOrForbidden"
429:
$ref: "#/components/responses/TooManyRequests"
/users/{userName}/posts:
get:
summary: 権限に応じてユーザーの投稿一覧を返す
security:
- { }
- BearerAuth: [ ]
parameters:
- $ref: "#/components/parameters/userName"
responses:
200:
description: 成功
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/Post"
401:
$ref: "#/components/responses/Unauthorized"
403:
$ref: "#/components/responses/Forbidden"
429:
$ref: "#/components/responses/TooManyRequests"
/users/{userName}/posts/{postId}:
get:
summary: 権限に応じてIDの投稿を返す
description: userNameが間違っていても取得できます。
security:
- { }
- BearerAuth: [ ]
parameters:
- $ref: "#/components/parameters/userName"
- $ref: "#/components/parameters/postId"
responses:
200:
description: 成功
content:
application/json:
schema:
$ref: "#/components/schemas/Post"
401:
$ref: "#/components/responses/Unauthorized"
403:
$ref: "#/components/responses/Forbidden"
404:
$ref: "#/components/responses/NotFoundOrForbidden"
429:
$ref: "#/components/responses/TooManyRequests"
/users:
get:
summary: ユーザー一覧を返す
security:
- { }
responses:
200:
description: 成功
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/User"
post:
summary: ユーザーを作成する
security:
- { }
requestBody:
description: 作成するユーザーの詳細
required: true
content:
application/json:
schema:
type: object
properties:
username:
type: string
password:
type: string
responses:
201:
description: ユーザーが作成された
headers:
Location:
description: 作成されたユーザーのURL
schema:
type: string
format: url
400:
description: ユーザー名が既に仕様されている。またはリクエストが異常
/users/{userName}:
get:
summary: ユーザーの詳細を返す
security:
- { }
- BearerAuth: [ ]
parameters:
- $ref: "#/components/parameters/userName"
responses:
200:
description: 成功
content:
application/json:
schema:
$ref: "#/components/schemas/User"
404:
$ref: "#/components/responses/NotFound"
/users/{userName}/followers:
get:
summary: ユーザーのフォロワー一覧を返す
parameters:
- $ref: "#/components/parameters/userName"
responses:
200:
description: 成功
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/User"
post:
summary: ユーザーをフォローする
security:
- BearerAuth: [ ]
parameters:
- $ref: "#/components/parameters/userName"
responses:
200:
description: 成功
202:
description: 受け付けられたが完了していない
401:
$ref: "#/components/responses/Unauthorized"
404:
$ref: "#/components/responses/NotFound"
/users/{userName}/following:
get:
summary: ユーザーのフォロイー一覧を返す
parameters:
- $ref: "#/components/parameters/userName"
responses:
200:
description: 成功
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/User"
components:
responses:
Unauthorized:
description: トークンが無効
Forbidden:
description: 権限がない
NotFoundOrForbidden:
description: 存在しないか権限がない
NotFound:
description: 存在しない
TooManyRequests:
description: レートリミット
parameters:
postId:
name: postId
in: path
description: 投稿ID
required: true
schema:
type: integer
format: int64
userName:
name: userName
in: path
description: ユーザーIDまたはAcctなど @name@domain name@domain name
required: true
schema:
type: string
schemas:
User:
type: object
properties:
id:
type: number
format: int64
readOnly: true
name:
type: string
domain:
type: string
readOnly: true
screenName:
type: string
description:
type: string
url:
type: string
readOnly: true
createdAt:
type: number
readOnly: true
Post:
type: object
properties:
id:
type: integer
format: int64
readOnly: true
userId:
type: integer
format: int64
readOnly: true
overview:
type: string
text:
type: string
createdAt:
type: integer
format: int64
readOnly: true
visibility:
type: string
enum:
- public
- unlisted
- followers
- direct
url:
type: string
format: uri
readOnly: true
repostId:
type: integer
format: int64
readOnly: true
replyId:
type: integer
format: int64
readOnly: true
sensitive:
type: boolean
apId:
type: string
format: url
readOnly: true
securitySchemes:
BearerAuth:
type: http
scheme: bearer

View File

@ -41,24 +41,24 @@ class PostsTest {
createdAt = Instant.now().toEpochMilli(), createdAt = Instant.now().toEpochMilli(),
url = "https://example.com/posts/1" url = "https://example.com/posts/1"
), ),
Post( Post(
id = 123456, id = 123456,
userId = 4322, userId = 4322,
text = "test2", text = "test2",
visibility = Visibility.PUBLIC, visibility = Visibility.PUBLIC,
createdAt = Instant.now().toEpochMilli(), createdAt = Instant.now().toEpochMilli(),
url = "https://example.com/posts/2" url = "https://example.com/posts/2"
) )
) )
val postService = mock<IPostApiService> { val postService = mock<IPostApiService> {
onBlocking { onBlocking {
getAll( getAll(
since = anyOrNull(), since = anyOrNull(),
until = anyOrNull(), until = anyOrNull(),
minId = anyOrNull(), minId = anyOrNull(),
maxId = anyOrNull(), maxId = anyOrNull(),
limit = anyOrNull(), limit = anyOrNull(),
userId = isNull() userId = isNull()
) )
} doReturn posts } doReturn posts
} }
@ -120,12 +120,12 @@ class PostsTest {
val postService = mock<IPostApiService> { val postService = mock<IPostApiService> {
onBlocking { onBlocking {
getAll( getAll(
since = anyOrNull(), since = anyOrNull(),
until = anyOrNull(), until = anyOrNull(),
minId = anyOrNull(), minId = anyOrNull(),
maxId = anyOrNull(), maxId = anyOrNull(),
limit = anyOrNull(), limit = anyOrNull(),
userId = isNotNull() userId = isNotNull()
) )
} doReturn posts } doReturn posts
} }
@ -157,12 +157,12 @@ class PostsTest {
config = ApplicationConfig("empty.conf") config = ApplicationConfig("empty.conf")
} }
val post = Post( val post = Post(
12345, 12345,
1234, 1234,
text = "aaa", text = "aaa",
visibility = Visibility.PUBLIC, visibility = Visibility.PUBLIC,
createdAt = Instant.now().toEpochMilli(), createdAt = Instant.now().toEpochMilli(),
url = "https://example.com/posts/1" url = "https://example.com/posts/1"
) )
val postService = mock<IPostApiService> { val postService = mock<IPostApiService> {
onBlocking { getById(any(), anyOrNull()) } doReturn post onBlocking { getById(any(), anyOrNull()) } doReturn post
@ -188,12 +188,12 @@ class PostsTest {
config = ApplicationConfig("empty.conf") config = ApplicationConfig("empty.conf")
} }
val post = Post( val post = Post(
12345, 12345,
1234, 1234,
text = "aaa", text = "aaa",
visibility = Visibility.FOLLOWERS, visibility = Visibility.FOLLOWERS,
createdAt = Instant.now().toEpochMilli(), createdAt = Instant.now().toEpochMilli(),
url = "https://example.com/posts/1" url = "https://example.com/posts/1"
) )
val postService = mock<IPostApiService> { val postService = mock<IPostApiService> {
onBlocking { getById(any(), isNotNull()) } doReturn post onBlocking { getById(any(), isNotNull()) } doReturn post
@ -243,13 +243,13 @@ class PostsTest {
val argument = it.getArgument<dev.usbharu.hideout.domain.model.hideout.form.Post>(0) val argument = it.getArgument<dev.usbharu.hideout.domain.model.hideout.form.Post>(0)
val userId = it.getArgument<Long>(1) val userId = it.getArgument<Long>(1)
Post( Post(
123L, 123L,
userId, userId,
null, null,
argument.text, argument.text,
Instant.now().toEpochMilli(), Instant.now().toEpochMilli(),
Visibility.PUBLIC, Visibility.PUBLIC,
"https://example.com" "https://example.com"
) )
} }
} }
@ -299,25 +299,25 @@ class PostsTest {
createdAt = Instant.now().toEpochMilli(), createdAt = Instant.now().toEpochMilli(),
url = "https://example.com/posts/1" url = "https://example.com/posts/1"
), ),
Post( Post(
id = 123456, id = 123456,
userId = 1, userId = 1,
text = "test2", text = "test2",
visibility = Visibility.PUBLIC, visibility = Visibility.PUBLIC,
createdAt = Instant.now().toEpochMilli(), createdAt = Instant.now().toEpochMilli(),
url = "https://example.com/posts/2" url = "https://example.com/posts/2"
) )
) )
val postService = mock<IPostApiService> { val postService = mock<IPostApiService> {
onBlocking { onBlocking {
getByUser( getByUser(
nameOrId = any(), nameOrId = any(),
since = anyOrNull(), since = anyOrNull(),
until = anyOrNull(), until = anyOrNull(),
minId = anyOrNull(), minId = anyOrNull(),
maxId = anyOrNull(), maxId = anyOrNull(),
limit = anyOrNull(), limit = anyOrNull(),
userId = anyOrNull() userId = anyOrNull()
) )
} doReturn posts } doReturn posts
} }
@ -351,25 +351,25 @@ class PostsTest {
createdAt = Instant.now().toEpochMilli(), createdAt = Instant.now().toEpochMilli(),
url = "https://example.com/posts/1" url = "https://example.com/posts/1"
), ),
Post( Post(
id = 123456, id = 123456,
userId = 1, userId = 1,
text = "test2", text = "test2",
visibility = Visibility.PUBLIC, visibility = Visibility.PUBLIC,
createdAt = Instant.now().toEpochMilli(), createdAt = Instant.now().toEpochMilli(),
url = "https://example.com/posts/2" url = "https://example.com/posts/2"
) )
) )
val postService = mock<IPostApiService> { val postService = mock<IPostApiService> {
onBlocking { onBlocking {
getByUser( getByUser(
nameOrId = eq("test1"), nameOrId = eq("test1"),
since = anyOrNull(), since = anyOrNull(),
until = anyOrNull(), until = anyOrNull(),
minId = anyOrNull(), minId = anyOrNull(),
maxId = anyOrNull(), maxId = anyOrNull(),
limit = anyOrNull(), limit = anyOrNull(),
userId = anyOrNull() userId = anyOrNull()
) )
} doReturn posts } doReturn posts
} }
@ -403,25 +403,25 @@ class PostsTest {
createdAt = Instant.now().toEpochMilli(), createdAt = Instant.now().toEpochMilli(),
url = "https://example.com/posts/1" url = "https://example.com/posts/1"
), ),
Post( Post(
id = 123456, id = 123456,
userId = 1, userId = 1,
text = "test2", text = "test2",
visibility = Visibility.PUBLIC, visibility = Visibility.PUBLIC,
createdAt = Instant.now().toEpochMilli(), createdAt = Instant.now().toEpochMilli(),
url = "https://example.com/posts/2" url = "https://example.com/posts/2"
) )
) )
val postService = mock<IPostApiService> { val postService = mock<IPostApiService> {
onBlocking { onBlocking {
getByUser( getByUser(
nameOrId = eq("test1@example.com"), nameOrId = eq("test1@example.com"),
since = anyOrNull(), since = anyOrNull(),
until = anyOrNull(), until = anyOrNull(),
minId = anyOrNull(), minId = anyOrNull(),
maxId = anyOrNull(), maxId = anyOrNull(),
limit = anyOrNull(), limit = anyOrNull(),
userId = anyOrNull() userId = anyOrNull()
) )
} doReturn posts } doReturn posts
} }
@ -455,25 +455,25 @@ class PostsTest {
createdAt = Instant.now().toEpochMilli(), createdAt = Instant.now().toEpochMilli(),
url = "https://example.com/posts/1" url = "https://example.com/posts/1"
), ),
Post( Post(
id = 123456, id = 123456,
userId = 1, userId = 1,
text = "test2", text = "test2",
visibility = Visibility.PUBLIC, visibility = Visibility.PUBLIC,
createdAt = Instant.now().toEpochMilli(), createdAt = Instant.now().toEpochMilli(),
url = "https://example.com/posts/2" url = "https://example.com/posts/2"
) )
) )
val postService = mock<IPostApiService> { val postService = mock<IPostApiService> {
onBlocking { onBlocking {
getByUser( getByUser(
nameOrId = eq("@test1@example.com"), nameOrId = eq("@test1@example.com"),
since = anyOrNull(), since = anyOrNull(),
until = anyOrNull(), until = anyOrNull(),
minId = anyOrNull(), minId = anyOrNull(),
maxId = anyOrNull(), maxId = anyOrNull(),
limit = anyOrNull(), limit = anyOrNull(),
userId = anyOrNull() userId = anyOrNull()
) )
} doReturn posts } doReturn posts
} }
@ -499,12 +499,12 @@ class PostsTest {
config = ApplicationConfig("empty.conf") config = ApplicationConfig("empty.conf")
} }
val post = Post( val post = Post(
id = 123456, id = 123456,
userId = 1, userId = 1,
text = "test2", text = "test2",
visibility = Visibility.PUBLIC, visibility = Visibility.PUBLIC,
createdAt = Instant.now().toEpochMilli(), createdAt = Instant.now().toEpochMilli(),
url = "https://example.com/posts/2" url = "https://example.com/posts/2"
) )
val postService = mock<IPostApiService> { val postService = mock<IPostApiService> {
onBlocking { getById(eq(12345L), anyOrNull()) } doReturn post onBlocking { getById(eq(12345L), anyOrNull()) } doReturn post
@ -531,12 +531,12 @@ class PostsTest {
config = ApplicationConfig("empty.conf") config = ApplicationConfig("empty.conf")
} }
val post = Post( val post = Post(
id = 123456, id = 123456,
userId = 1, userId = 1,
text = "test2", text = "test2",
visibility = Visibility.PUBLIC, visibility = Visibility.PUBLIC,
createdAt = Instant.now().toEpochMilli(), createdAt = Instant.now().toEpochMilli(),
url = "https://example.com/posts/2" url = "https://example.com/posts/2"
) )
val postService = mock<IPostApiService> { val postService = mock<IPostApiService> {
onBlocking { getById(eq(12345L), anyOrNull()) } doReturn post onBlocking { getById(eq(12345L), anyOrNull()) } doReturn post
@ -563,12 +563,12 @@ class PostsTest {
config = ApplicationConfig("empty.conf") config = ApplicationConfig("empty.conf")
} }
val post = Post( val post = Post(
id = 123456, id = 123456,
userId = 1, userId = 1,
text = "test2", text = "test2",
visibility = Visibility.PUBLIC, visibility = Visibility.PUBLIC,
createdAt = Instant.now().toEpochMilli(), createdAt = Instant.now().toEpochMilli(),
url = "https://example.com/posts/2" url = "https://example.com/posts/2"
) )
val postService = mock<IPostApiService> { val postService = mock<IPostApiService> {
onBlocking { getById(eq(12345L), anyOrNull()) } doReturn post onBlocking { getById(eq(12345L), anyOrNull()) } doReturn post
@ -595,12 +595,12 @@ class PostsTest {
config = ApplicationConfig("empty.conf") config = ApplicationConfig("empty.conf")
} }
val post = Post( val post = Post(
id = 123456, id = 123456,
userId = 1, userId = 1,
text = "test2", text = "test2",
visibility = Visibility.PUBLIC, visibility = Visibility.PUBLIC,
createdAt = Instant.now().toEpochMilli(), createdAt = Instant.now().toEpochMilli(),
url = "https://example.com/posts/2" url = "https://example.com/posts/2"
) )
val postService = mock<IPostApiService> { val postService = mock<IPostApiService> {
onBlocking { getById(eq(12345L), anyOrNull()) } doReturn post onBlocking { getById(eq(12345L), anyOrNull()) } doReturn post