diff --git a/build.gradle.kts b/build.gradle.kts index ae1ed371..17396d06 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -110,6 +110,7 @@ dependencies { implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310") implementation("com.fasterxml.jackson.module:jackson-module-kotlin") implementation("org.springframework.security:spring-security-oauth2-jose") + implementation("org.springframework.boot:spring-boot-starter-data-mongodb") implementation("io.ktor:ktor-client-logging-jvm:$ktor_version") diff --git a/src/main/kotlin/dev/usbharu/hideout/controller/mastodon/MastodonStatusesApiContoller.kt b/src/main/kotlin/dev/usbharu/hideout/controller/mastodon/MastodonStatusesApiContoller.kt index 0d40471e..d5111577 100644 --- a/src/main/kotlin/dev/usbharu/hideout/controller/mastodon/MastodonStatusesApiContoller.kt +++ b/src/main/kotlin/dev/usbharu/hideout/controller/mastodon/MastodonStatusesApiContoller.kt @@ -22,5 +22,5 @@ class MastodonStatusesApiContoller(private val statusesApiService: StatusesApiSe statusesApiService.postStatus(statusesRequest, jwt.getClaim("uid").toLong()), HttpStatus.OK ) - } + } } diff --git a/src/main/kotlin/dev/usbharu/hideout/controller/mastodon/MastodonTimelineApiController.kt b/src/main/kotlin/dev/usbharu/hideout/controller/mastodon/MastodonTimelineApiController.kt new file mode 100644 index 00000000..5d7b1bdb --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/controller/mastodon/MastodonTimelineApiController.kt @@ -0,0 +1,52 @@ +package dev.usbharu.hideout.controller.mastodon + +import dev.usbharu.hideout.controller.mastodon.generated.TimelineApi +import dev.usbharu.hideout.domain.mastodon.model.generated.Status +import dev.usbharu.hideout.service.api.mastodon.TimelineApiService +import kotlinx.coroutines.runBlocking +import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity +import org.springframework.security.core.context.SecurityContextHolder +import org.springframework.security.oauth2.jwt.Jwt +import org.springframework.stereotype.Controller + +@Controller +class MastodonTimelineApiController(private val timelineApiService: TimelineApiService) : TimelineApi { + override fun apiV1TimelinesHomeGet( + maxId: String?, + sinceId: String?, + minId: String?, + limit: Int? + ): ResponseEntity> = runBlocking { + val jwt = SecurityContextHolder.getContext().authentication.principal as Jwt + val homeTimeline = timelineApiService.homeTimeline( + userId = jwt.getClaim("uid").toLong(), + maxId = maxId?.toLongOrNull(), + minId = minId?.toLongOrNull(), + sinceId = sinceId?.toLongOrNull(), + limit = limit ?: 20 + ) + ResponseEntity(homeTimeline, HttpStatus.OK) + } + + override fun apiV1TimelinesPublicGet( + local: Boolean?, + remote: Boolean?, + onlyMedia: Boolean?, + maxId: String?, + sinceId: String?, + minId: String?, + limit: Int? + ): ResponseEntity> = runBlocking { + val publicTimeline = timelineApiService.publicTimeline( + localOnly = local ?: false, + remoteOnly = remote ?: false, + mediaOnly = onlyMedia ?: false, + maxId = maxId?.toLongOrNull(), + minId = minId?.toLongOrNull(), + sinceId = sinceId?.toLongOrNull(), + limit = limit ?: 20 + ) + ResponseEntity(publicTimeline, HttpStatus.OK) + } +} diff --git a/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/entity/Timeline.kt b/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/entity/Timeline.kt new file mode 100644 index 00000000..64f450a8 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/entity/Timeline.kt @@ -0,0 +1,18 @@ +package dev.usbharu.hideout.domain.model.hideout.entity + +import org.springframework.data.annotation.Id + +data class Timeline( + @Id + val id: Long, + val userId: Long, + val timelineId: Long, + val postId: Long, + val postUserId: Long, + val createdAt: Long, + val replyId: Long?, + val repostId: Long?, + val visibility: Visibility, + val sensitive: Boolean, + val isLocal: Boolean +) diff --git a/src/main/kotlin/dev/usbharu/hideout/query/mastodon/StatusQueryService.kt b/src/main/kotlin/dev/usbharu/hideout/query/mastodon/StatusQueryService.kt new file mode 100644 index 00000000..a4ed048c --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/query/mastodon/StatusQueryService.kt @@ -0,0 +1,7 @@ +package dev.usbharu.hideout.query.mastodon + +import dev.usbharu.hideout.domain.mastodon.model.generated.Status + +interface StatusQueryService { + suspend fun findByPostIds(ids: List): List +} diff --git a/src/main/kotlin/dev/usbharu/hideout/query/mastodon/StatusQueryServiceImpl.kt b/src/main/kotlin/dev/usbharu/hideout/query/mastodon/StatusQueryServiceImpl.kt new file mode 100644 index 00000000..71d3e0e1 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/query/mastodon/StatusQueryServiceImpl.kt @@ -0,0 +1,102 @@ +package dev.usbharu.hideout.query.mastodon + +import dev.usbharu.hideout.domain.mastodon.model.generated.Account +import dev.usbharu.hideout.domain.mastodon.model.generated.Status +import dev.usbharu.hideout.repository.Posts +import dev.usbharu.hideout.repository.Users +import org.jetbrains.exposed.sql.innerJoin +import org.jetbrains.exposed.sql.select +import org.springframework.stereotype.Repository +import java.time.Instant + +@Repository +class StatusQueryServiceImpl : StatusQueryService { + @Suppress("LongMethod") + override suspend fun findByPostIds(ids: List): List { + val pairs = Posts.innerJoin(Users, onColumn = { userId }, otherColumn = { id }) + .select { Posts.id inList ids } + .map { + Status( + id = it[Posts.id].toString(), + uri = it[Posts.apId], + createdAt = Instant.ofEpochMilli(it[Posts.createdAt]).toString(), + account = Account( + id = it[Users.id].toString(), + username = it[Users.name], + acct = "${it[Users.name]}@${it[Users.domain]}", + url = it[Users.url], + displayName = it[Users.screenName], + note = it[Users.description], + avatar = it[Users.url] + "/icon.jpg", + avatarStatic = it[Users.url] + "/icon.jpg", + header = it[Users.url] + "/header.jpg", + headerStatic = it[Users.url] + "/header.jpg", + locked = false, + fields = emptyList(), + emojis = emptyList(), + bot = false, + group = false, + discoverable = true, + createdAt = Instant.ofEpochMilli(it[Users.createdAt]).toString(), + lastStatusAt = Instant.ofEpochMilli(it[Users.createdAt]).toString(), + statusesCount = 0, + followersCount = 0, + followingCount = 0, + noindex = false, + moved = false, + suspendex = false, + limited = false + ), + content = it[Posts.text], + visibility = when (it[Posts.visibility]) { + 0 -> Status.Visibility.public + 1 -> Status.Visibility.unlisted + 2 -> Status.Visibility.private + 3 -> Status.Visibility.direct + else -> Status.Visibility.public + }, + sensitive = it[Posts.sensitive], + spoilerText = it[Posts.overview].orEmpty(), + mediaAttachments = emptyList(), + mentions = emptyList(), + tags = emptyList(), + emojis = emptyList(), + reblogsCount = 0, + favouritesCount = 0, + repliesCount = 0, + url = it[Posts.apId], + inReplyToId = it[Posts.replyId].toString(), + inReplyToAccountId = null, + language = null, + text = it[Posts.text], + editedAt = null, + application = null, + poll = null, + card = null, + favourited = null, + reblogged = null, + muted = null, + bookmarked = null, + pinned = null, + filtered = null + ) to it[Posts.repostId] + } + + val statuses = pairs.map { it.first } + return pairs + .map { + if (it.second != null) { + it.first.copy(reblog = statuses.find { status -> status.id == it.second.toString() }) + } else { + it.first + } + } + .map { + if (it.inReplyToId != null) { + it.copy(inReplyToAccountId = statuses.find { status -> status.id == it.inReplyToId }?.id) + } else { + it + } + } + } +} diff --git a/src/main/kotlin/dev/usbharu/hideout/repository/MongoTimelineRepository.kt b/src/main/kotlin/dev/usbharu/hideout/repository/MongoTimelineRepository.kt new file mode 100644 index 00000000..0b8937cf --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/repository/MongoTimelineRepository.kt @@ -0,0 +1,19 @@ +package dev.usbharu.hideout.repository + +import dev.usbharu.hideout.domain.model.hideout.entity.Timeline +import org.springframework.data.domain.Pageable +import org.springframework.data.mongodb.repository.MongoRepository + +@Suppress("LongParameterList", "FunctionMaxLength") +interface MongoTimelineRepository : MongoRepository { + fun findByUserId(id: Long): List + fun findByUserIdAndTimelineId(userId: Long, timelineId: Long): List + fun findByUserIdAndTimelineIdAndPostIdBetweenAndIsLocal( + userId: Long?, + timelineId: Long?, + postIdMin: Long?, + postIdMax: Long?, + isLocal: Boolean?, + pageable: Pageable + ): List +} diff --git a/src/main/kotlin/dev/usbharu/hideout/repository/MongoTimelineRepositoryWrapper.kt b/src/main/kotlin/dev/usbharu/hideout/repository/MongoTimelineRepositoryWrapper.kt new file mode 100644 index 00000000..a3e31b6f --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/repository/MongoTimelineRepositoryWrapper.kt @@ -0,0 +1,38 @@ +package dev.usbharu.hideout.repository + +import dev.usbharu.hideout.domain.model.hideout.entity.Timeline +import dev.usbharu.hideout.service.core.IdGenerateService +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.springframework.stereotype.Repository + +@Repository +@Suppress("InjectDispatcher") +class MongoTimelineRepositoryWrapper( + private val mongoTimelineRepository: MongoTimelineRepository, + private val idGenerateService: IdGenerateService +) : + TimelineRepository { + override suspend fun generateId(): Long = idGenerateService.generateId() + + override suspend fun save(timeline: Timeline): Timeline { + return withContext(Dispatchers.IO) { + mongoTimelineRepository.save(timeline) + } + } + + override suspend fun saveAll(timelines: List): List = + mongoTimelineRepository.saveAll(timelines) + + override suspend fun findByUserId(id: Long): List { + return withContext(Dispatchers.IO) { + mongoTimelineRepository.findByUserId(id) + } + } + + override suspend fun findByUserIdAndTimelineId(userId: Long, timelineId: Long): List { + return withContext(Dispatchers.IO) { + mongoTimelineRepository.findByUserIdAndTimelineId(userId, timelineId) + } + } +} diff --git a/src/main/kotlin/dev/usbharu/hideout/repository/TimelineRepository.kt b/src/main/kotlin/dev/usbharu/hideout/repository/TimelineRepository.kt new file mode 100644 index 00000000..76e9755c --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/repository/TimelineRepository.kt @@ -0,0 +1,11 @@ +package dev.usbharu.hideout.repository + +import dev.usbharu.hideout.domain.model.hideout.entity.Timeline + +interface TimelineRepository { + suspend fun generateId(): Long + suspend fun save(timeline: Timeline): Timeline + suspend fun saveAll(timelines: List): List + suspend fun findByUserId(id: Long): List + suspend fun findByUserIdAndTimelineId(userId: Long, timelineId: Long): List +} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/ap/APNoteService.kt b/src/main/kotlin/dev/usbharu/hideout/service/ap/APNoteService.kt index 3c719104..2b6ce229 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/ap/APNoteService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/ap/APNoteService.kt @@ -17,6 +17,8 @@ import dev.usbharu.hideout.query.PostQueryService import dev.usbharu.hideout.query.UserQueryService import dev.usbharu.hideout.repository.PostRepository import dev.usbharu.hideout.service.job.JobQueueParentService +import dev.usbharu.hideout.service.post.PostCreateInterceptor +import dev.usbharu.hideout.service.post.PostService import io.ktor.client.* import io.ktor.client.statement.* import kjob.core.job.JobProps @@ -36,6 +38,7 @@ interface APNoteService { } @Service +@Suppress("LongParameterList") class APNoteServiceImpl( private val httpClient: HttpClient, private val jobQueueParentService: JobQueueParentService, @@ -45,9 +48,14 @@ class APNoteServiceImpl( private val followerQueryService: FollowerQueryService, private val postQueryService: PostQueryService, @Qualifier("activitypub") private val objectMapper: ObjectMapper, - private val applicationConfig: ApplicationConfig + private val applicationConfig: ApplicationConfig, + private val postService: PostService -) : APNoteService { +) : APNoteService, PostCreateInterceptor { + + init { + postService.addInterceptor(this) + } private val logger = LoggerFactory.getLogger(this::class.java) @@ -161,7 +169,7 @@ class APNoteServiceImpl( postQueryService.findByUrl(it) } - postRepository.save( + postService.createRemote( Post.of( id = postRepository.generateId(), userId = person.second.id, @@ -182,6 +190,10 @@ class APNoteServiceImpl( override suspend fun fetchNote(note: Note, targetActor: String?): Note = note(note, targetActor, note.id ?: throw IllegalArgumentException("note.id is null")) + override suspend fun run(post: Post) { + createNote(post) + } + companion object { const val public: String = "https://www.w3.org/ns/activitystreams#Public" } diff --git a/src/main/kotlin/dev/usbharu/hideout/service/api/mastodon/TimelineApiService.kt b/src/main/kotlin/dev/usbharu/hideout/service/api/mastodon/TimelineApiService.kt new file mode 100644 index 00000000..2dfc1f24 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/service/api/mastodon/TimelineApiService.kt @@ -0,0 +1,71 @@ +package dev.usbharu.hideout.service.api.mastodon + +import dev.usbharu.hideout.domain.mastodon.model.generated.Status +import dev.usbharu.hideout.service.core.Transaction +import dev.usbharu.hideout.service.post.GenerateTimelineService +import org.springframework.stereotype.Service + +@Suppress("LongParameterList") +interface TimelineApiService { + suspend fun publicTimeline( + localOnly: Boolean = false, + remoteOnly: Boolean = false, + mediaOnly: Boolean = false, + maxId: Long?, + minId: Long?, + sinceId: Long?, + limit: Int = 20 + ): List + + suspend fun homeTimeline( + userId: Long, + maxId: Long?, + minId: Long?, + sinceId: Long?, + limit: Int = 20 + ): List +} + +@Service +class TimelineApiServiceImpl( + private val generateTimelineService: GenerateTimelineService, + private val transaction: Transaction +) : TimelineApiService { + override suspend fun publicTimeline( + localOnly: Boolean, + remoteOnly: Boolean, + mediaOnly: Boolean, + maxId: Long?, + minId: Long?, + sinceId: Long?, + limit: Int + ): List = transaction.transaction { + generateTimelineService.getTimeline( + forUserId = 0, + localOnly = localOnly, + mediaOnly = mediaOnly, + maxId = maxId, + minId = minId, + sinceId = sinceId, + limit = limit + ) + } + + override suspend fun homeTimeline( + userId: Long, + maxId: Long?, + minId: Long?, + sinceId: Long?, + limit: Int + ): List = transaction.transaction { + generateTimelineService.getTimeline( + forUserId = userId, + localOnly = false, + mediaOnly = false, + maxId = maxId, + minId = minId, + sinceId = sinceId, + limit = limit + ) + } +} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/post/GenerateTimelineService.kt b/src/main/kotlin/dev/usbharu/hideout/service/post/GenerateTimelineService.kt new file mode 100644 index 00000000..34f42678 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/service/post/GenerateTimelineService.kt @@ -0,0 +1,18 @@ +package dev.usbharu.hideout.service.post + +import dev.usbharu.hideout.domain.mastodon.model.generated.Status +import org.springframework.stereotype.Service + +@Service +@Suppress("LongParameterList") +interface GenerateTimelineService { + suspend fun getTimeline( + forUserId: Long? = null, + localOnly: Boolean = false, + mediaOnly: Boolean = false, + maxId: Long? = null, + minId: Long? = null, + sinceId: Long? = null, + limit: Int = 20 + ): List +} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/post/MongoGenerateTimelineService.kt b/src/main/kotlin/dev/usbharu/hideout/service/post/MongoGenerateTimelineService.kt new file mode 100644 index 00000000..fa5c9800 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/service/post/MongoGenerateTimelineService.kt @@ -0,0 +1,48 @@ +package dev.usbharu.hideout.service.post + +import dev.usbharu.hideout.domain.mastodon.model.generated.Status +import dev.usbharu.hideout.domain.model.hideout.entity.Timeline +import dev.usbharu.hideout.query.mastodon.StatusQueryService +import org.springframework.data.mongodb.core.MongoTemplate +import org.springframework.data.mongodb.core.query.Criteria +import org.springframework.data.mongodb.core.query.Query +import org.springframework.stereotype.Service + +@Service +class MongoGenerateTimelineService( + private val statusQueryService: StatusQueryService, + private val mongoTemplate: MongoTemplate +) : + GenerateTimelineService { + override suspend fun getTimeline( + forUserId: Long?, + localOnly: Boolean, + mediaOnly: Boolean, + maxId: Long?, + minId: Long?, + sinceId: Long?, + limit: Int + ): List { + val query = Query() + if (forUserId != null) { + val criteria = Criteria.where("userId").`is`(forUserId) + query.addCriteria(criteria) + } + if (localOnly) { + val criteria = Criteria.where("isLocal").`is`(true) + query.addCriteria(criteria) + } + if (maxId != null) { + val criteria = Criteria.where("postId").lt(maxId) + query.addCriteria(criteria) + } + if (minId != null) { + val criteria = Criteria.where("postId").gt(minId) + query.addCriteria(criteria) + } + + val timelines = mongoTemplate.find(query.limit(limit), Timeline::class.java) + + return statusQueryService.findByPostIds(timelines.flatMap { setOfNotNull(it.postId, it.replyId, it.repostId) }) + } +} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/post/PostService.kt b/src/main/kotlin/dev/usbharu/hideout/service/post/PostService.kt index 0eed9d72..641ac4cd 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/post/PostService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/post/PostService.kt @@ -7,4 +7,10 @@ import org.springframework.stereotype.Service @Service interface PostService { suspend fun createLocal(post: PostCreateDto): Post + suspend fun createRemote(post: Post): Post + fun addInterceptor(postCreateInterceptor: PostCreateInterceptor) +} + +interface PostCreateInterceptor { + suspend fun run(post: Post) } diff --git a/src/main/kotlin/dev/usbharu/hideout/service/post/PostServiceImpl.kt b/src/main/kotlin/dev/usbharu/hideout/service/post/PostServiceImpl.kt index 0ba6b536..a86c2227 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/post/PostServiceImpl.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/post/PostServiceImpl.kt @@ -5,17 +5,36 @@ import dev.usbharu.hideout.domain.model.hideout.entity.Post import dev.usbharu.hideout.exception.UserNotFoundException import dev.usbharu.hideout.repository.PostRepository import dev.usbharu.hideout.repository.UserRepository -import dev.usbharu.hideout.service.ap.APNoteService import org.springframework.stereotype.Service import java.time.Instant +import java.util.* @Service class PostServiceImpl( private val postRepository: PostRepository, private val userRepository: UserRepository, - private val apNoteService: APNoteService + private val timelineService: TimelineService ) : PostService { + private val interceptors = Collections.synchronizedList(mutableListOf()) + override suspend fun createLocal(post: PostCreateDto): Post { + val create = internalCreate(post, true) + interceptors.forEach { it.run(create) } + return create + } + + override suspend fun createRemote(post: Post): Post = internalCreate(post, false) + + override fun addInterceptor(postCreateInterceptor: PostCreateInterceptor) { + interceptors.add(postCreateInterceptor) + } + + private suspend fun internalCreate(post: Post, isLocal: Boolean): Post { + timelineService.publishTimeline(post, isLocal) + return postRepository.save(post) + } + + private suspend fun internalCreate(post: PostCreateDto, isLocal: Boolean): Post { val user = userRepository.findById(post.userId) ?: throw UserNotFoundException("${post.userId} was not found") val id = postRepository.generateId() val createPost = Post.of( @@ -29,9 +48,6 @@ class PostServiceImpl( repostId = null, replyId = null ) - apNoteService.createNote(createPost) - return internalCreate(createPost) + return internalCreate(createPost, isLocal) } - - private suspend fun internalCreate(post: Post): Post = postRepository.save(post) } diff --git a/src/main/kotlin/dev/usbharu/hideout/service/post/TimelineService.kt b/src/main/kotlin/dev/usbharu/hideout/service/post/TimelineService.kt new file mode 100644 index 00000000..97f83a08 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/service/post/TimelineService.kt @@ -0,0 +1,58 @@ +package dev.usbharu.hideout.service.post + +import dev.usbharu.hideout.domain.model.hideout.entity.Post +import dev.usbharu.hideout.domain.model.hideout.entity.Timeline +import dev.usbharu.hideout.domain.model.hideout.entity.Visibility +import dev.usbharu.hideout.query.FollowerQueryService +import dev.usbharu.hideout.query.UserQueryService +import dev.usbharu.hideout.repository.TimelineRepository +import org.springframework.stereotype.Service + +@Service +class TimelineService( + private val followerQueryService: FollowerQueryService, + private val userQueryService: UserQueryService, + private val timelineRepository: TimelineRepository +) { + suspend fun publishTimeline(post: Post, isLocal: Boolean) { + val findFollowersById = followerQueryService.findFollowersById(post.userId).toMutableList() + if (isLocal) { + // 自分自身も含める必要がある + val user = userQueryService.findById(post.userId) + findFollowersById.add(user) + } + val timelines = findFollowersById.map { + Timeline( + id = timelineRepository.generateId(), + userId = it.id, + timelineId = 0, + postId = post.id, + postUserId = post.userId, + createdAt = post.createdAt, + replyId = post.replyId, + repostId = post.repostId, + visibility = post.visibility, + sensitive = post.sensitive, + isLocal = isLocal + ) + }.toMutableList() + if (post.visibility == Visibility.PUBLIC) { + timelines.add( + Timeline( + id = timelineRepository.generateId(), + userId = 0, + timelineId = 0, + postId = post.id, + postUserId = post.userId, + createdAt = post.createdAt, + replyId = post.replyId, + repostId = post.repostId, + visibility = post.visibility, + sensitive = post.sensitive, + isLocal = isLocal + ) + ) + } + timelineRepository.saveAll(timelines) + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 4d052250..889d71f4 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -22,6 +22,13 @@ spring: url: "jdbc:h2:./test-dev2;MODE=POSTGRESQL" username: "" password: "" + data: + mongodb: + host: localhost + port: 27017 + database: hideout + # username: hideoutuser + # password: hideoutpass h2: console: diff --git a/src/main/resources/openapi/mastodon.yaml b/src/main/resources/openapi/mastodon.yaml index 94b34dd6..5464a28a 100644 --- a/src/main/resources/openapi/mastodon.yaml +++ b/src/main/resources/openapi/mastodon.yaml @@ -15,6 +15,8 @@ tags: description: app - name: instance description: instance + - name: timeline + description: timeline paths: /api/v2/instance: @@ -202,6 +204,94 @@ paths: 200: description: 成功 + /api/v1/timelines/public: + get: + tags: + - timeline + parameters: + - in: query + name: local + required: false + schema: + type: boolean + - in: query + name: remote + required: false + schema: + type: boolean + - in: query + name: only_media + required: false + schema: + type: boolean + - in: query + name: max_id + required: false + schema: + type: string + - in: query + name: since_id + required: false + schema: + type: string + - in: query + name: min_id + required: false + schema: + type: string + - in: query + name: limit + required: false + schema: + type: integer + responses: + 200: + description: 成功 + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Status" + + /api/v1/timelines/home: + get: + tags: + - timeline + security: + - OAuth2: + - "read:statuses" + parameters: + - in: query + name: max_id + required: false + schema: + type: string + - in: query + name: since_id + required: false + schema: + type: string + - in: query + name: min_id + required: false + schema: + type: string + - in: query + name: limit + required: false + schema: + type: integer + responses: + 200: + description: 成功 + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Status" + components: schemas: AccountsCreateRequest: diff --git a/src/test/kotlin/dev/usbharu/hideout/service/ap/APNoteServiceImplTest.kt b/src/test/kotlin/dev/usbharu/hideout/service/ap/APNoteServiceImplTest.kt index 26e1ae4e..4c40d000 100644 --- a/src/test/kotlin/dev/usbharu/hideout/service/ap/APNoteServiceImplTest.kt +++ b/src/test/kotlin/dev/usbharu/hideout/service/ap/APNoteServiceImplTest.kt @@ -80,15 +80,16 @@ class APNoteServiceImplTest { val jobQueueParentService = mock() val activityPubNoteService = APNoteServiceImpl( - mock(), - jobQueueParentService, - mock(), - mock(), - userQueryService, - followerQueryService, - mock(), + httpClient = mock(), + jobQueueParentService = jobQueueParentService, + postRepository = mock(), + apUserService = mock(), + userQueryService = userQueryService, + followerQueryService = followerQueryService, + postQueryService = mock(), objectMapper = objectMapper, - applicationConfig = testApplicationConfig + applicationConfig = testApplicationConfig, + postService = mock() ) val postEntity = Post.of( 1L, @@ -121,7 +122,8 @@ class APNoteServiceImplTest { followerQueryService = mock(), postQueryService = mock(), objectMapper = objectMapper, - applicationConfig = testApplicationConfig + applicationConfig = testApplicationConfig, + postService = mock() ) activityPubNoteService.createNoteJob( JobProps(