Merge pull request #266 from usbharu/feature/pagination

ページネーションの改善
This commit is contained in:
usbharu 2024-01-31 22:22:04 +09:00 committed by GitHub
commit 9a12c10f3a
49 changed files with 1732 additions and 590 deletions

View File

@ -0,0 +1,146 @@
package mastodon.account
import com.fasterxml.jackson.core.type.TypeReference
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import dev.usbharu.hideout.SpringApplication
import dev.usbharu.hideout.domain.mastodon.model.generated.Notification
import dev.usbharu.hideout.domain.mastodon.model.generated.Status
import org.assertj.core.api.Assertions
import org.assertj.core.api.Assertions.assertThat
import org.flywaydb.core.Flyway
import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.security.core.authority.SimpleGrantedAuthority
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors
import org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers
import org.springframework.test.context.jdbc.Sql
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.get
import org.springframework.test.web.servlet.setup.DefaultMockMvcBuilder
import org.springframework.test.web.servlet.setup.MockMvcBuilders
import org.springframework.transaction.annotation.Transactional
import org.springframework.web.context.WebApplicationContext
@SpringBootTest(classes = [SpringApplication::class])
@AutoConfigureMockMvc
@Transactional
@Sql("/sql/test-user.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_CLASS)
@Sql("/sql/test-user2.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_CLASS)
@Sql("/sql/accounts/test-accounts-statuses.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_CLASS)
class AccountApiPaginationTest {
@Autowired
private lateinit var context: WebApplicationContext
private lateinit var mockMvc: MockMvc
@Test
fun `apiV1AccountsIdStatusesGet 投稿を取得できる`() {
val content = mockMvc
.get("/api/v1/accounts/1/statuses"){
with(
SecurityMockMvcRequestPostProcessors.jwt()
.jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read"))
)
}
.asyncDispatch()
.andExpect { status { isOk() } }
.andExpect { header { string("Link","<https://example.com/api/v1/accounts/1/statuses?min_id=100>; rel=\"next\", <https://example.com/api/v1/accounts/1/statuses?max_id=81>; rel=\"prev\"") } }
.andReturn()
.response
.contentAsString
val value = jacksonObjectMapper().readValue(content, object : TypeReference<List<Status>>() {})
assertThat(value.first().id).isEqualTo("100")
assertThat(value.last().id).isEqualTo("81")
assertThat(value).size().isEqualTo(20)
}
@Test
fun `apiV1AccountsIdStatusesGet 結果が0件のときはLinkヘッダーがない`() {
val content = mockMvc
.get("/api/v1/accounts/1/statuses?min_id=100"){
with(
SecurityMockMvcRequestPostProcessors.jwt()
.jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read"))
)
}
.asyncDispatch()
.andExpect { status { isOk() } }
.andExpect { header { doesNotExist("Link") } }
.andReturn()
.response
.contentAsString
val value = jacksonObjectMapper().readValue(content, object : TypeReference<List<Status>>() {})
assertThat(value).isEmpty()
}
@Test
fun `apiV1AccountsIdStatusesGet maxIdを指定して取得`() {
val content = mockMvc
.get("/api/v1/accounts/1/statuses?max_id=100"){
with(
SecurityMockMvcRequestPostProcessors.jwt()
.jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read"))
)
}
.asyncDispatch()
.andExpect { status { isOk() } }
.andExpect { header { string("Link","<https://example.com/api/v1/accounts/1/statuses?min_id=99>; rel=\"next\", <https://example.com/api/v1/accounts/1/statuses?max_id=80>; rel=\"prev\"") } }
.andReturn()
.response
.contentAsString
val value = jacksonObjectMapper().readValue(content, object : TypeReference<List<Status>>() {})
assertThat(value.first().id).isEqualTo("99")
assertThat(value.last().id).isEqualTo("80")
assertThat(value).size().isEqualTo(20)
}
@Test
fun `apiV1AccountsIdStatusesGet minIdを指定して取得`() {
val content = mockMvc
.get("/api/v1/accounts/1/statuses?min_id=1"){
with(
SecurityMockMvcRequestPostProcessors.jwt()
.jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read"))
)
}
.asyncDispatch()
.andExpect { status { isOk() } }
.andExpect { header { string("Link","<https://example.com/api/v1/accounts/1/statuses?min_id=21>; rel=\"next\", <https://example.com/api/v1/accounts/1/statuses?max_id=2>; rel=\"prev\"") } }
.andReturn()
.response
.contentAsString
val value = jacksonObjectMapper().readValue(content, object : TypeReference<List<Status>>() {})
assertThat(value.first().id).isEqualTo("21")
assertThat(value.last().id).isEqualTo("2")
assertThat(value).size().isEqualTo(20)
}
@BeforeEach
fun setUp() {
mockMvc = MockMvcBuilders.webAppContextSetup(context)
.apply<DefaultMockMvcBuilder>(SecurityMockMvcConfigurers.springSecurity())
.build()
}
companion object {
@JvmStatic
@AfterAll
fun dropDatabase(@Autowired flyway: Flyway) {
flyway.clean()
flyway.migrate()
}
}
}

View File

@ -0,0 +1,164 @@
package mastodon.notifications
import com.fasterxml.jackson.core.type.TypeReference
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import dev.usbharu.hideout.SpringApplication
import dev.usbharu.hideout.domain.mastodon.model.generated.Notification
import kotlinx.coroutines.test.runTest
import org.assertj.core.api.Assertions.assertThat
import org.flywaydb.core.Flyway
import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.security.core.authority.SimpleGrantedAuthority
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors
import org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers
import org.springframework.test.context.jdbc.Sql
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.get
import org.springframework.test.web.servlet.setup.DefaultMockMvcBuilder
import org.springframework.test.web.servlet.setup.MockMvcBuilders
import org.springframework.transaction.annotation.Transactional
import org.springframework.web.context.WebApplicationContext
@SpringBootTest(classes = [SpringApplication::class], properties = ["hideout.use-mongodb=false"])
@AutoConfigureMockMvc
@Transactional
@Sql("/sql/test-user.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_CLASS)
@Sql("/sql/test-user2.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_CLASS)
@Sql("/sql/notification/test-notifications.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_CLASS)
@Sql("/sql/notification/test-mastodon_notifications.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_CLASS)
class ExposedNotificationsApiPaginationTest {
@Autowired
private lateinit var context: WebApplicationContext
private lateinit var mockMvc: MockMvc
@Test
fun `通知を取得できる`() = runTest {
val content = mockMvc
.get("/api/v1/notifications") {
with(
SecurityMockMvcRequestPostProcessors.jwt()
.jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read"))
)
}
.asyncDispatch()
.andExpect {
header {
string(
"Link",
"<https://example.com/api/v1/notifications?min_id=65>; rel=\"next\", <https://example.com/api/v1/notifications?max_id=26>; rel=\"prev\""
)
}
}
.andReturn()
.response
.contentAsString
val value = jacksonObjectMapper().readValue(content, object : TypeReference<List<Notification>>() {})
assertThat(value.first().id).isEqualTo("65")
assertThat(value.last().id).isEqualTo("26")
}
@Test
fun maxIdを指定して通知を取得できる() = runTest {
val content = mockMvc
.get("/api/v1/notifications?max_id=26") {
with(
SecurityMockMvcRequestPostProcessors.jwt()
.jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read"))
)
}
.asyncDispatch()
.andExpect {
header {
string(
"Link",
"<https://example.com/api/v1/notifications?min_id=25>; rel=\"next\", <https://example.com/api/v1/notifications?max_id=1>; rel=\"prev\""
)
}
}
.andReturn()
.response
.contentAsString
val value = jacksonObjectMapper().readValue(content, object : TypeReference<List<Notification>>() {})
assertThat(value.first().id).isEqualTo("25")
assertThat(value.last().id).isEqualTo("1")
}
@Test
fun minIdを指定して通知を取得できる() = runTest {
val content = mockMvc
.get("/api/v1/notifications?min_id=25") {
with(
SecurityMockMvcRequestPostProcessors.jwt()
.jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read"))
)
}
.asyncDispatch()
.andExpect {
header {
string(
"Link",
"<https://example.com/api/v1/notifications?min_id=65>; rel=\"next\", <https://example.com/api/v1/notifications?max_id=26>; rel=\"prev\""
)
}
}
.andReturn()
.response
.contentAsString
val value = jacksonObjectMapper().readValue(content, object : TypeReference<List<Notification>>() {})
assertThat(value.first().id).isEqualTo("65")
assertThat(value.last().id).isEqualTo("26")
}
@Test
fun 結果が0件のときはページネーションのヘッダーがない() = runTest {
val content = mockMvc
.get("/api/v1/notifications?max_id=1") {
with(
SecurityMockMvcRequestPostProcessors.jwt()
.jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read"))
)
}
.asyncDispatch()
.andExpect {
header {
doesNotExist("Link")
}
}
.andReturn()
.response
.contentAsString
val value = jacksonObjectMapper().readValue(content, object : TypeReference<List<Notification>>() {})
assertThat(value).size().isZero()
}
@BeforeEach
fun setUp() {
mockMvc = MockMvcBuilders.webAppContextSetup(context)
.apply<DefaultMockMvcBuilder>(SecurityMockMvcConfigurers.springSecurity())
.build()
}
companion object {
@JvmStatic
@AfterAll
fun dropDatabase(@Autowired flyway: Flyway) {
flyway.clean()
flyway.migrate()
}
}
}

View File

@ -0,0 +1,199 @@
package mastodon.notifications
import com.fasterxml.jackson.core.type.TypeReference
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import dev.usbharu.hideout.SpringApplication
import dev.usbharu.hideout.domain.mastodon.model.generated.Notification
import dev.usbharu.hideout.mastodon.domain.model.MastodonNotification
import dev.usbharu.hideout.mastodon.domain.model.NotificationType
import dev.usbharu.hideout.mastodon.infrastructure.mongorepository.MongoMastodonNotificationRepository
import kotlinx.coroutines.test.runTest
import org.assertj.core.api.Assertions
import org.flywaydb.core.Flyway
import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.data.mongodb.core.MongoTemplate
import org.springframework.security.core.authority.SimpleGrantedAuthority
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors
import org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers
import org.springframework.test.context.jdbc.Sql
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.get
import org.springframework.test.web.servlet.setup.DefaultMockMvcBuilder
import org.springframework.test.web.servlet.setup.MockMvcBuilders
import org.springframework.transaction.annotation.Transactional
import org.springframework.web.context.WebApplicationContext
import java.time.Instant
@SpringBootTest(classes = [SpringApplication::class], properties = ["hideout.use-mongodb=true"])
@AutoConfigureMockMvc
@Transactional
@Sql("/sql/test-user.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_CLASS)
@Sql("/sql/test-user2.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_CLASS)
@Sql("/sql/notification/test-notifications.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_CLASS)
class MongodbNotificationsApiPaginationTest {
@Autowired
private lateinit var context: WebApplicationContext
private lateinit var mockMvc: MockMvc
@Test
fun `通知を取得できる`() = runTest {
val content = mockMvc
.get("/api/v1/notifications") {
with(
SecurityMockMvcRequestPostProcessors.jwt()
.jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read"))
)
}
.asyncDispatch()
.andDo { print() }
.andExpect {
header {
string(
"Link",
"<https://example.com/api/v1/notifications?min_id=65>; rel=\"next\", <https://example.com/api/v1/notifications?max_id=26>; rel=\"prev\""
)
}
}
.andReturn()
.response
.contentAsString
val value = jacksonObjectMapper().readValue(content, object : TypeReference<List<Notification>>() {})
Assertions.assertThat(value.first().id).isEqualTo("65")
Assertions.assertThat(value.last().id).isEqualTo("26")
}
@Test
fun maxIdを指定して通知を取得できる() = runTest {
val content = mockMvc
.get("/api/v1/notifications?max_id=26") {
with(
SecurityMockMvcRequestPostProcessors.jwt()
.jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read"))
)
}
.asyncDispatch()
.andExpect {
header {
string(
"Link",
"<https://example.com/api/v1/notifications?min_id=25>; rel=\"next\", <https://example.com/api/v1/notifications?max_id=1>; rel=\"prev\""
)
}
}
.andReturn()
.response
.contentAsString
val value = jacksonObjectMapper().readValue(content, object : TypeReference<List<Notification>>() {})
Assertions.assertThat(value.first().id).isEqualTo("25")
Assertions.assertThat(value.last().id).isEqualTo("1")
}
@Test
fun minIdを指定して通知を取得できる() = runTest {
val content = mockMvc
.get("/api/v1/notifications?min_id=25") {
with(
SecurityMockMvcRequestPostProcessors.jwt()
.jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read"))
)
}
.asyncDispatch()
.andExpect {
header {
string(
"Link",
"<https://example.com/api/v1/notifications?min_id=65>; rel=\"next\", <https://example.com/api/v1/notifications?max_id=26>; rel=\"prev\""
)
}
}
.andReturn()
.response
.contentAsString
val value = jacksonObjectMapper().readValue(content, object : TypeReference<List<Notification>>() {})
Assertions.assertThat(value.first().id).isEqualTo("65")
Assertions.assertThat(value.last().id).isEqualTo("26")
}
@Test
fun 結果が0件のときはページネーションのヘッダーがない() = runTest {
val content = mockMvc
.get("/api/v1/notifications?max_id=1") {
with(
SecurityMockMvcRequestPostProcessors.jwt()
.jwt { it.claim("uid", "1") }.authorities(SimpleGrantedAuthority("SCOPE_read"))
)
}
.asyncDispatch()
.andExpect {
header {
doesNotExist("Link")
}
}
.andReturn()
.response
.contentAsString
val value = jacksonObjectMapper().readValue(content, object : TypeReference<List<Notification>>() {})
Assertions.assertThat(value).size().isZero()
}
@BeforeEach
fun setUp() {
mockMvc = MockMvcBuilders.webAppContextSetup(context)
.apply<DefaultMockMvcBuilder>(SecurityMockMvcConfigurers.springSecurity())
.build()
}
companion object {
@JvmStatic
@BeforeAll
fun setupMongodb(
@Autowired mongoMastodonNotificationRepository: MongoMastodonNotificationRepository
) {
mongoMastodonNotificationRepository.deleteAll()
val notifications = (1..65).map {
MastodonNotification(
it.toLong(),
1,
NotificationType.follow,
Instant.now(),
2,
null,
null,
null
)
}
mongoMastodonNotificationRepository.saveAll(notifications)
}
@JvmStatic
@AfterAll
fun dropDatabase(
@Autowired flyway: Flyway,
@Autowired mongodbMastodonNotificationRepository: MongoMastodonNotificationRepository
) {
flyway.clean()
flyway.migrate()
mongodbMastodonNotificationRepository.deleteAll()
}
}
}

View File

@ -24,7 +24,7 @@ spring:
auto-index-creation: true auto-index-creation: true
host: localhost host: localhost
port: 27017 port: 27017
database: hideout database: hideout-integration-test
h2: h2:
console: console:
enabled: true enabled: true

View File

@ -0,0 +1,202 @@
insert into posts (id, actor_id, overview, content, text, created_at, visibility, url, repost_id, reply_id, sensitive,
ap_id, deleted)
VALUES (1, 1, null, '<p>this is test</p>', 'this is test', 1706684146436, 0, 'https://example.com/users/1/posts/1',
null, null, false, 'https://example.com/users/1/posts/1', false),
(2, 1, null, '<p>this is test</p>', 'this is test', 1706684146436, 0, 'https://example.com/users/1/posts/2',
null, 1, false, 'https://example.com/users/1/posts/2', false),
(3, 1, null, '<p>this is test</p>', 'this is test', 1706684146436, 2, 'https://example.com/users/1/posts/3',
null, null, false, 'https://example.com/users/1/posts/3', false),
(4, 1, null, '<p>this is test</p>', 'this is test', 1706684146436, 0, 'https://example.com/users/1/posts/4',
null, 3, false, 'https://example.com/users/1/posts/4', false),
(5, 1, null, '<p>this is test</p>', 'this is test', 1706684146436, 0, 'https://example.com/users/1/posts/5',
null, null, false, 'https://example.com/users/1/posts/5', false),
(6, 1, null, '<p>this is test</p>', 'this is test', 1706684146436, 2, 'https://example.com/users/1/posts/6',
null, null, false, 'https://example.com/users/1/posts/6', false),
(7, 1, null, '<p>this is test</p>', 'this is test', 1706684146436, 0, 'https://example.com/users/1/posts/7',
null, null, false, 'https://example.com/users/1/posts/7', false),
(8, 1, null, '<p>this is test</p>', 'this is test', 1706684146436, 0, 'https://example.com/users/1/posts/8',
null, 7, false, 'https://example.com/users/1/posts/8', false),
(9, 1, null, '<p>this is test</p>', 'this is test', 1706684146436, 2, 'https://example.com/users/1/posts/9',
null, null, false, 'https://example.com/users/1/posts/9', false),
(10, 1, null, '<p>this is test</p>', 'this is test', 1706684146436, 0, 'https://example.com/users/1/posts/10',
null, 9, false, 'https://example.com/users/1/posts/10', false),
(11, 1, null, '<p>this is test</p>', 'this is test', 1706684146436, 0, 'https://example.com/users/1/posts/11',
null, null, false, 'https://example.com/users/1/posts/11', false),
(12, 1, null, '<p>this is test</p>', 'this is test', 1706684146436, 2, 'https://example.com/users/1/posts/12',
null, null, false, 'https://example.com/users/1/posts/12', false),
(13, 1, null, '<p>this is test</p>', 'this is test', 1706684146436, 0, 'https://example.com/users/1/posts/13',
null, null, false, 'https://example.com/users/1/posts/13', false),
(14, 1, null, '<p>this is test</p>', 'this is test', 1706684146436, 0, 'https://example.com/users/1/posts/14',
null, 13, false, 'https://example.com/users/1/posts/14', false),
(15, 1, null, '<p>this is test</p>', 'this is test', 1706684146436, 2, 'https://example.com/users/1/posts/15',
null, null, false, 'https://example.com/users/1/posts/15', false),
(16, 1, null, '<p>this is test</p>', 'this is test', 1706684146436, 0, 'https://example.com/users/1/posts/16',
null, 15, false, 'https://example.com/users/1/posts/16', false),
(17, 1, null, '<p>this is test</p>', 'this is test', 1706684146436, 0, 'https://example.com/users/1/posts/17',
null, null, false, 'https://example.com/users/1/posts/17', false),
(18, 1, null, '<p>this is test</p>', 'this is test', 1706684146436, 2, 'https://example.com/users/1/posts/18',
null, null, false, 'https://example.com/users/1/posts/18', false),
(19, 1, null, '<p>this is test</p>', 'this is test', 1706684146436, 0, 'https://example.com/users/1/posts/19',
null, null, false, 'https://example.com/users/1/posts/19', false),
(20, 1, null, '<p>this is test</p>', 'this is test', 1706684146436, 0, 'https://example.com/users/1/posts/20',
null, 19, false, 'https://example.com/users/1/posts/20', false),
(21, 1, null, '<p>this is test</p>', 'this is test', 1706684146436, 2, 'https://example.com/users/1/posts/21',
null, null, false, 'https://example.com/users/1/posts/21', false),
(22, 1, null, '<p>this is test</p>', 'this is test', 1706684146436, 0, 'https://example.com/users/1/posts/22',
null, 21, false, 'https://example.com/users/1/posts/22', false),
(23, 1, null, '<p>this is test</p>', 'this is test', 1706684146436, 0, 'https://example.com/users/1/posts/23',
null, null, false, 'https://example.com/users/1/posts/23', false),
(24, 1, null, '<p>this is test</p>', 'this is test', 1706684146436, 2, 'https://example.com/users/1/posts/24',
null, null, false, 'https://example.com/users/1/posts/24', false),
(25, 1, null, '<p>this is test</p>', 'this is test', 1706684146436, 0, 'https://example.com/users/1/posts/25',
null, null, false, 'https://example.com/users/1/posts/25', false),
(26, 1, null, '<p>this is test</p>', 'this is test', 1706684146436, 0, 'https://example.com/users/1/posts/26',
null, 25, false, 'https://example.com/users/1/posts/26', false),
(27, 1, null, '<p>this is test</p>', 'this is test', 1706684146436, 2, 'https://example.com/users/1/posts/27',
null, null, false, 'https://example.com/users/1/posts/27', false),
(28, 1, null, '<p>this is test</p>', 'this is test', 1706684146436, 0, 'https://example.com/users/1/posts/28',
null, 27, false, 'https://example.com/users/1/posts/28', false),
(29, 1, null, '<p>this is test</p>', 'this is test', 1706684146436, 0, 'https://example.com/users/1/posts/29',
null, null, false, 'https://example.com/users/1/posts/29', false),
(30, 1, null, '<p>this is test</p>', 'this is test', 1706684146436, 2, 'https://example.com/users/1/posts/30',
null, null, false, 'https://example.com/users/1/posts/30', false),
(31, 1, null, '<p>this is test</p>', 'this is test', 1706684146436, 0, 'https://example.com/users/1/posts/31',
null, null, false, 'https://example.com/users/1/posts/31', false),
(32, 1, null, '<p>this is test</p>', 'this is test', 1706684146436, 0, 'https://example.com/users/1/posts/32',
null, 31, false, 'https://example.com/users/1/posts/32', false),
(33, 1, null, '<p>this is test</p>', 'this is test', 1706684146436, 2, 'https://example.com/users/1/posts/33',
null, null, false, 'https://example.com/users/1/posts/33', false),
(34, 1, null, '<p>this is test</p>', 'this is test', 1706684146436, 0, 'https://example.com/users/1/posts/34',
null, 33, false, 'https://example.com/users/1/posts/34', false),
(35, 1, null, '<p>this is test</p>', 'this is test', 1706684146436, 0, 'https://example.com/users/1/posts/35',
null, null, false, 'https://example.com/users/1/posts/35', false),
(36, 1, null, '<p>this is test</p>', 'this is test', 1706684146436, 2, 'https://example.com/users/1/posts/36',
null, null, false, 'https://example.com/users/1/posts/36', false),
(37, 1, null, '<p>this is test</p>', 'this is test', 1706684146436, 0, 'https://example.com/users/1/posts/37',
null, null, false, 'https://example.com/users/1/posts/37', false),
(38, 1, null, '<p>this is test</p>', 'this is test', 1706684146436, 0, 'https://example.com/users/1/posts/38',
null, 37, false, 'https://example.com/users/1/posts/38', false),
(39, 1, null, '<p>this is test</p>', 'this is test', 1706684146436, 2, 'https://example.com/users/1/posts/39',
null, null, false, 'https://example.com/users/1/posts/39', false),
(40, 1, null, '<p>this is test</p>', 'this is test', 1706684146436, 0, 'https://example.com/users/1/posts/40',
null, 39, false, 'https://example.com/users/1/posts/40', false),
(41, 1, null, '<p>this is test</p>', 'this is test', 1706684146436, 0, 'https://example.com/users/1/posts/41',
null, null, false, 'https://example.com/users/1/posts/41', false),
(42, 1, null, '<p>this is test</p>', 'this is test', 1706684146436, 2, 'https://example.com/users/1/posts/42',
null, null, false, 'https://example.com/users/1/posts/42', false),
(43, 1, null, '<p>this is test</p>', 'this is test', 1706684146436, 0, 'https://example.com/users/1/posts/43',
null, null, false, 'https://example.com/users/1/posts/43', false),
(44, 1, null, '<p>this is test</p>', 'this is test', 1706684146436, 0, 'https://example.com/users/1/posts/44',
null, 43, false, 'https://example.com/users/1/posts/44', false),
(45, 1, null, '<p>this is test</p>', 'this is test', 1706684146436, 2, 'https://example.com/users/1/posts/45',
null, null, false, 'https://example.com/users/1/posts/45', false),
(46, 1, null, '<p>this is test</p>', 'this is test', 1706684146436, 0, 'https://example.com/users/1/posts/46',
null, 45, false, 'https://example.com/users/1/posts/46', false),
(47, 1, null, '<p>this is test</p>', 'this is test', 1706684146436, 0, 'https://example.com/users/1/posts/47',
null, null, false, 'https://example.com/users/1/posts/47', false),
(48, 1, null, '<p>this is test</p>', 'this is test', 1706684146436, 2, 'https://example.com/users/1/posts/48',
null, null, false, 'https://example.com/users/1/posts/48', false),
(49, 1, null, '<p>this is test</p>', 'this is test', 1706684146436, 0, 'https://example.com/users/1/posts/49',
null, null, false, 'https://example.com/users/1/posts/49', false),
(50, 1, null, '<p>this is test</p>', 'this is test', 1706684146436, 0, 'https://example.com/users/1/posts/50',
null, 49, false, 'https://example.com/users/1/posts/50', false),
(51, 1, null, '<p>this is test</p>', 'this is test', 1706684146436, 2, 'https://example.com/users/1/posts/51',
null, null, false, 'https://example.com/users/1/posts/51', false),
(52, 1, null, '<p>this is test</p>', 'this is test', 1706684146436, 0, 'https://example.com/users/1/posts/52',
null, 51, false, 'https://example.com/users/1/posts/52', false),
(53, 1, null, '<p>this is test</p>', 'this is test', 1706684146436, 0, 'https://example.com/users/1/posts/53',
null, null, false, 'https://example.com/users/1/posts/53', false),
(54, 1, null, '<p>this is test</p>', 'this is test', 1706684146436, 2, 'https://example.com/users/1/posts/54',
null, null, false, 'https://example.com/users/1/posts/54', false),
(55, 1, null, '<p>this is test</p>', 'this is test', 1706684146436, 0, 'https://example.com/users/1/posts/55',
null, null, false, 'https://example.com/users/1/posts/55', false),
(56, 1, null, '<p>this is test</p>', 'this is test', 1706684146436, 0, 'https://example.com/users/1/posts/56',
null, 55, false, 'https://example.com/users/1/posts/56', false),
(57, 1, null, '<p>this is test</p>', 'this is test', 1706684146436, 2, 'https://example.com/users/1/posts/57',
null, null, false, 'https://example.com/users/1/posts/57', false),
(58, 1, null, '<p>this is test</p>', 'this is test', 1706684146436, 0, 'https://example.com/users/1/posts/58',
null, 57, false, 'https://example.com/users/1/posts/58', false),
(59, 1, null, '<p>this is test</p>', 'this is test', 1706684146436, 0, 'https://example.com/users/1/posts/59',
null, null, false, 'https://example.com/users/1/posts/59', false),
(60, 1, null, '<p>this is test</p>', 'this is test', 1706684146436, 2, 'https://example.com/users/1/posts/60',
null, null, false, 'https://example.com/users/1/posts/60', false),
(61, 1, null, '<p>this is test</p>', 'this is test', 1706684146436, 0, 'https://example.com/users/1/posts/61',
null, null, false, 'https://example.com/users/1/posts/61', false),
(62, 1, null, '<p>this is test</p>', 'this is test', 1706684146436, 0, 'https://example.com/users/1/posts/62',
null, 61, false, 'https://example.com/users/1/posts/62', false),
(63, 1, null, '<p>this is test</p>', 'this is test', 1706684146436, 2, 'https://example.com/users/1/posts/63',
null, null, false, 'https://example.com/users/1/posts/63', false),
(64, 1, null, '<p>this is test</p>', 'this is test', 1706684146436, 0, 'https://example.com/users/1/posts/64',
null, 63, false, 'https://example.com/users/1/posts/64', false),
(65, 1, null, '<p>this is test</p>', 'this is test', 1706684146436, 0, 'https://example.com/users/1/posts/65',
null, null, false, 'https://example.com/users/1/posts/65', false),
(66, 1, null, '<p>this is test</p>', 'this is test', 1706684146436, 2, 'https://example.com/users/1/posts/66',
null, null, false, 'https://example.com/users/1/posts/66', false),
(67, 1, null, '<p>this is test</p>', 'this is test', 1706684146436, 0, 'https://example.com/users/1/posts/67',
null, null, false, 'https://example.com/users/1/posts/67', false),
(68, 1, null, '<p>this is test</p>', 'this is test', 1706684146436, 0, 'https://example.com/users/1/posts/68',
null, 67, false, 'https://example.com/users/1/posts/68', false),
(69, 1, null, '<p>this is test</p>', 'this is test', 1706684146436, 2, 'https://example.com/users/1/posts/69',
null, null, false, 'https://example.com/users/1/posts/69', false),
(70, 1, null, '<p>this is test</p>', 'this is test', 1706684146436, 0, 'https://example.com/users/1/posts/70',
null, 69, false, 'https://example.com/users/1/posts/70', false),
(71, 1, null, '<p>this is test</p>', 'this is test', 1706684146436, 0, 'https://example.com/users/1/posts/71',
null, null, false, 'https://example.com/users/1/posts/71', false),
(72, 1, null, '<p>this is test</p>', 'this is test', 1706684146436, 2, 'https://example.com/users/1/posts/72',
null, null, false, 'https://example.com/users/1/posts/72', false),
(73, 1, null, '<p>this is test</p>', 'this is test', 1706684146436, 0, 'https://example.com/users/1/posts/73',
null, null, false, 'https://example.com/users/1/posts/73', false),
(74, 1, null, '<p>this is test</p>', 'this is test', 1706684146436, 0, 'https://example.com/users/1/posts/74',
null, 73, false, 'https://example.com/users/1/posts/74', false),
(75, 1, null, '<p>this is test</p>', 'this is test', 1706684146436, 2, 'https://example.com/users/1/posts/75',
null, null, false, 'https://example.com/users/1/posts/75', false),
(76, 1, null, '<p>this is test</p>', 'this is test', 1706684146436, 0, 'https://example.com/users/1/posts/76',
null, 75, false, 'https://example.com/users/1/posts/76', false),
(77, 1, null, '<p>this is test</p>', 'this is test', 1706684146436, 0, 'https://example.com/users/1/posts/77',
null, null, false, 'https://example.com/users/1/posts/77', false),
(78, 1, null, '<p>this is test</p>', 'this is test', 1706684146436, 2, 'https://example.com/users/1/posts/78',
null, null, false, 'https://example.com/users/1/posts/78', false),
(79, 1, null, '<p>this is test</p>', 'this is test', 1706684146436, 0, 'https://example.com/users/1/posts/79',
null, null, false, 'https://example.com/users/1/posts/79', false),
(80, 1, null, '<p>this is test</p>', 'this is test', 1706684146436, 0, 'https://example.com/users/1/posts/80',
null, 79, false, 'https://example.com/users/1/posts/80', false),
(81, 1, null, '<p>this is test</p>', 'this is test', 1706684146436, 2, 'https://example.com/users/1/posts/81',
null, null, false, 'https://example.com/users/1/posts/81', false),
(82, 1, null, '<p>this is test</p>', 'this is test', 1706684146436, 0, 'https://example.com/users/1/posts/82',
null, 81, false, 'https://example.com/users/1/posts/82', false),
(83, 1, null, '<p>this is test</p>', 'this is test', 1706684146436, 0, 'https://example.com/users/1/posts/83',
null, null, false, 'https://example.com/users/1/posts/83', false),
(84, 1, null, '<p>this is test</p>', 'this is test', 1706684146436, 2, 'https://example.com/users/1/posts/84',
null, null, false, 'https://example.com/users/1/posts/84', false),
(85, 1, null, '<p>this is test</p>', 'this is test', 1706684146436, 0, 'https://example.com/users/1/posts/85',
null, null, false, 'https://example.com/users/1/posts/85', false),
(86, 1, null, '<p>this is test</p>', 'this is test', 1706684146436, 0, 'https://example.com/users/1/posts/86',
null, 85, false, 'https://example.com/users/1/posts/86', false),
(87, 1, null, '<p>this is test</p>', 'this is test', 1706684146436, 2, 'https://example.com/users/1/posts/87',
null, null, false, 'https://example.com/users/1/posts/87', false),
(88, 1, null, '<p>this is test</p>', 'this is test', 1706684146436, 0, 'https://example.com/users/1/posts/88',
null, 87, false, 'https://example.com/users/1/posts/88', false),
(89, 1, null, '<p>this is test</p>', 'this is test', 1706684146436, 0, 'https://example.com/users/1/posts/89',
null, null, false, 'https://example.com/users/1/posts/89', false),
(90, 1, null, '<p>this is test</p>', 'this is test', 1706684146436, 2, 'https://example.com/users/1/posts/90',
null, null, false, 'https://example.com/users/1/posts/90', false),
(91, 1, null, '<p>this is test</p>', 'this is test', 1706684146436, 0, 'https://example.com/users/1/posts/91',
null, null, false, 'https://example.com/users/1/posts/91', false),
(92, 1, null, '<p>this is test</p>', 'this is test', 1706684146436, 0, 'https://example.com/users/1/posts/92',
null, 91, false, 'https://example.com/users/1/posts/92', false),
(93, 1, null, '<p>this is test</p>', 'this is test', 1706684146436, 2, 'https://example.com/users/1/posts/93',
null, null, false, 'https://example.com/users/1/posts/93', false),
(94, 1, null, '<p>this is test</p>', 'this is test', 1706684146436, 0, 'https://example.com/users/1/posts/94',
null, 93, false, 'https://example.com/users/1/posts/94', false),
(95, 1, null, '<p>this is test</p>', 'this is test', 1706684146436, 0, 'https://example.com/users/1/posts/95',
null, null, false, 'https://example.com/users/1/posts/95', false),
(96, 1, null, '<p>this is test</p>', 'this is test', 1706684146436, 2, 'https://example.com/users/1/posts/96',
null, null, false, 'https://example.com/users/1/posts/96', false),
(97, 1, null, '<p>this is test</p>', 'this is test', 1706684146436, 0, 'https://example.com/users/1/posts/97',
null, null, false, 'https://example.com/users/1/posts/97', false),
(98, 1, null, '<p>this is test</p>', 'this is test', 1706684146436, 0, 'https://example.com/users/1/posts/98',
null, 97, false, 'https://example.com/users/1/posts/98', false),
(99, 1, null, '<p>this is test</p>', 'this is test', 1706684146436, 2, 'https://example.com/users/1/posts/99',
null, null, false, 'https://example.com/users/1/posts/99', false),
(100, 1, null, '<p>this is test</p>', 'this is test', 1706684146436, 0, 'https://example.com/users/1/posts/100',
null, 99, false, 'https://example.com/users/1/posts/100', false);

View File

@ -0,0 +1,66 @@
insert into mastodon_notifications (id, user_id, type, created_at, account_id, status_id, report_id, relationship_serverance_event_id)
values (1, 1, 'follow', current_timestamp, 2, null, null, null),
(2, 1, 'follow', current_timestamp, 2, null, null, null),
(3, 1, 'follow', current_timestamp, 2, null, null, null),
(4, 1, 'follow', current_timestamp, 2, null, null, null),
(5, 1, 'follow', current_timestamp, 2, null, null, null),
(6, 1, 'follow', current_timestamp, 2, null, null, null),
(7, 1, 'follow', current_timestamp, 2, null, null, null),
(8, 1, 'follow', current_timestamp, 2, null, null, null),
(9, 1, 'follow', current_timestamp, 2, null, null, null),
(10, 1, 'follow', current_timestamp, 2, null, null, null),
(11, 1, 'follow', current_timestamp, 2, null, null, null),
(12, 1, 'follow', current_timestamp, 2, null, null, null),
(13, 1, 'follow', current_timestamp, 2, null, null, null),
(14, 1, 'follow', current_timestamp, 2, null, null, null),
(15, 1, 'follow', current_timestamp, 2, null, null, null),
(16, 1, 'follow', current_timestamp, 2, null, null, null),
(17, 1, 'follow', current_timestamp, 2, null, null, null),
(18, 1, 'follow', current_timestamp, 2, null, null, null),
(19, 1, 'follow', current_timestamp, 2, null, null, null),
(20, 1, 'follow', current_timestamp, 2, null, null, null),
(21, 1, 'follow', current_timestamp, 2, null, null, null),
(22, 1, 'follow', current_timestamp, 2, null, null, null),
(23, 1, 'follow', current_timestamp, 2, null, null, null),
(24, 1, 'follow', current_timestamp, 2, null, null, null),
(25, 1, 'follow', current_timestamp, 2, null, null, null),
(26, 1, 'follow', current_timestamp, 2, null, null, null),
(27, 1, 'follow', current_timestamp, 2, null, null, null),
(28, 1, 'follow', current_timestamp, 2, null, null, null),
(29, 1, 'follow', current_timestamp, 2, null, null, null),
(30, 1, 'follow', current_timestamp, 2, null, null, null),
(31, 1, 'follow', current_timestamp, 2, null, null, null),
(32, 1, 'follow', current_timestamp, 2, null, null, null),
(33, 1, 'follow', current_timestamp, 2, null, null, null),
(34, 1, 'follow', current_timestamp, 2, null, null, null),
(35, 1, 'follow', current_timestamp, 2, null, null, null),
(36, 1, 'follow', current_timestamp, 2, null, null, null),
(37, 1, 'follow', current_timestamp, 2, null, null, null),
(38, 1, 'follow', current_timestamp, 2, null, null, null),
(39, 1, 'follow', current_timestamp, 2, null, null, null),
(40, 1, 'follow', current_timestamp, 2, null, null, null),
(41, 1, 'follow', current_timestamp, 2, null, null, null),
(42, 1, 'follow', current_timestamp, 2, null, null, null),
(43, 1, 'follow', current_timestamp, 2, null, null, null),
(44, 1, 'follow', current_timestamp, 2, null, null, null),
(45, 1, 'follow', current_timestamp, 2, null, null, null),
(46, 1, 'follow', current_timestamp, 2, null, null, null),
(47, 1, 'follow', current_timestamp, 2, null, null, null),
(48, 1, 'follow', current_timestamp, 2, null, null, null),
(49, 1, 'follow', current_timestamp, 2, null, null, null),
(50, 1, 'follow', current_timestamp, 2, null, null, null),
(51, 1, 'follow', current_timestamp, 2, null, null, null),
(52, 1, 'follow', current_timestamp, 2, null, null, null),
(53, 1, 'follow', current_timestamp, 2, null, null, null),
(54, 1, 'follow', current_timestamp, 2, null, null, null),
(55, 1, 'follow', current_timestamp, 2, null, null, null),
(56, 1, 'follow', current_timestamp, 2, null, null, null),
(57, 1, 'follow', current_timestamp, 2, null, null, null),
(58, 1, 'follow', current_timestamp, 2, null, null, null),
(59, 1, 'follow', current_timestamp, 2, null, null, null),
(60, 1, 'follow', current_timestamp, 2, null, null, null),
(61, 1, 'follow', current_timestamp, 2, null, null, null),
(62, 1, 'follow', current_timestamp, 2, null, null, null),
(63, 1, 'follow', current_timestamp, 2, null, null, null),
(64, 1, 'follow', current_timestamp, 2, null, null, null),
(65, 1, 'follow', current_timestamp, 2, null, null, null);

View File

@ -0,0 +1,66 @@
insert into notifications(id, type, user_id, source_actor_id, post_id, text, reaction_id, created_at)
VALUES (1, 'follow', 1, 2, null, null, null, current_timestamp),
(2, 'follow', 1, 2, null, null, null, current_timestamp),
(3, 'follow', 1, 2, null, null, null, current_timestamp),
(4, 'follow', 1, 2, null, null, null, current_timestamp),
(5, 'follow', 1, 2, null, null, null, current_timestamp),
(6, 'follow', 1, 2, null, null, null, current_timestamp),
(7, 'follow', 1, 2, null, null, null, current_timestamp),
(8, 'follow', 1, 2, null, null, null, current_timestamp),
(9, 'follow', 1, 2, null, null, null, current_timestamp),
(10, 'follow', 1, 2, null, null, null, current_timestamp),
(11, 'follow', 1, 2, null, null, null, current_timestamp),
(12, 'follow', 1, 2, null, null, null, current_timestamp),
(13, 'follow', 1, 2, null, null, null, current_timestamp),
(14, 'follow', 1, 2, null, null, null, current_timestamp),
(15, 'follow', 1, 2, null, null, null, current_timestamp),
(16, 'follow', 1, 2, null, null, null, current_timestamp),
(17, 'follow', 1, 2, null, null, null, current_timestamp),
(18, 'follow', 1, 2, null, null, null, current_timestamp),
(19, 'follow', 1, 2, null, null, null, current_timestamp),
(20, 'follow', 1, 2, null, null, null, current_timestamp),
(21, 'follow', 1, 2, null, null, null, current_timestamp),
(22, 'follow', 1, 2, null, null, null, current_timestamp),
(23, 'follow', 1, 2, null, null, null, current_timestamp),
(24, 'follow', 1, 2, null, null, null, current_timestamp),
(25, 'follow', 1, 2, null, null, null, current_timestamp),
(26, 'follow', 1, 2, null, null, null, current_timestamp),
(27, 'follow', 1, 2, null, null, null, current_timestamp),
(28, 'follow', 1, 2, null, null, null, current_timestamp),
(29, 'follow', 1, 2, null, null, null, current_timestamp),
(30, 'follow', 1, 2, null, null, null, current_timestamp),
(31, 'follow', 1, 2, null, null, null, current_timestamp),
(32, 'follow', 1, 2, null, null, null, current_timestamp),
(33, 'follow', 1, 2, null, null, null, current_timestamp),
(34, 'follow', 1, 2, null, null, null, current_timestamp),
(35, 'follow', 1, 2, null, null, null, current_timestamp),
(36, 'follow', 1, 2, null, null, null, current_timestamp),
(37, 'follow', 1, 2, null, null, null, current_timestamp),
(38, 'follow', 1, 2, null, null, null, current_timestamp),
(39, 'follow', 1, 2, null, null, null, current_timestamp),
(40, 'follow', 1, 2, null, null, null, current_timestamp),
(41, 'follow', 1, 2, null, null, null, current_timestamp),
(42, 'follow', 1, 2, null, null, null, current_timestamp),
(43, 'follow', 1, 2, null, null, null, current_timestamp),
(44, 'follow', 1, 2, null, null, null, current_timestamp),
(45, 'follow', 1, 2, null, null, null, current_timestamp),
(46, 'follow', 1, 2, null, null, null, current_timestamp),
(47, 'follow', 1, 2, null, null, null, current_timestamp),
(48, 'follow', 1, 2, null, null, null, current_timestamp),
(49, 'follow', 1, 2, null, null, null, current_timestamp),
(50, 'follow', 1, 2, null, null, null, current_timestamp),
(51, 'follow', 1, 2, null, null, null, current_timestamp),
(52, 'follow', 1, 2, null, null, null, current_timestamp),
(53, 'follow', 1, 2, null, null, null, current_timestamp),
(54, 'follow', 1, 2, null, null, null, current_timestamp),
(55, 'follow', 1, 2, null, null, null, current_timestamp),
(56, 'follow', 1, 2, null, null, null, current_timestamp),
(57, 'follow', 1, 2, null, null, null, current_timestamp),
(58, 'follow', 1, 2, null, null, null, current_timestamp),
(59, 'follow', 1, 2, null, null, null, current_timestamp),
(60, 'follow', 1, 2, null, null, null, current_timestamp),
(61, 'follow', 1, 2, null, null, null, current_timestamp),
(62, 'follow', 1, 2, null, null, null, current_timestamp),
(63, 'follow', 1, 2, null, null, null, current_timestamp),
(64, 'follow', 1, 2, null, null, null, current_timestamp),
(65, 'follow', 1, 2, null, null, null, current_timestamp);

View File

@ -0,0 +1,19 @@
package dev.usbharu.hideout.application.infrastructure.exposed
import org.jetbrains.exposed.sql.*
fun <S> Query.withPagination(page: Page, exp: ExpressionWithColumnType<S>): PaginationList<ResultRow, S> {
page.limit?.let { limit(it) }
val resultRows = if (page.minId != null) {
page.maxId?.let { andWhere { exp.less(it) } }
andWhere { exp.greater(page.minId!!) }
reversed()
} else {
page.maxId?.let { andWhere { exp.less(it) } }
page.sinceId?.let { andWhere { exp.greater(it) } }
orderBy(exp, SortOrder.DESC)
toList()
}
return PaginationList(resultRows, resultRows.firstOrNull()?.getOrNull(exp), resultRows.lastOrNull()?.getOrNull(exp))
}

View File

@ -0,0 +1,46 @@
package dev.usbharu.hideout.application.infrastructure.exposed
sealed class Page {
abstract val maxId: Long?
abstract val sinceId: Long?
abstract val minId: Long?
abstract val limit: Int?
data class PageByMaxId(
override val maxId: Long?,
override val sinceId: Long?,
override val limit: Int?
) : Page() {
override val minId: Long? = null
}
data class PageByMinId(
override val maxId: Long?,
override val minId: Long?,
override val limit: Int?
) : Page() {
override val sinceId: Long? = null
}
companion object {
fun of(
maxId: Long? = null,
sinceId: Long? = null,
minId: Long? = null,
limit: Int? = null
): Page =
if (minId != null) {
PageByMinId(
maxId,
minId,
limit
)
} else {
PageByMaxId(
maxId,
sinceId,
limit
)
}
}
}

View File

@ -0,0 +1,22 @@
package dev.usbharu.hideout.application.infrastructure.exposed
class PaginationList<T, ID>(list: List<T>, val next: ID?, val prev: ID?) : List<T> by list
fun <T, ID> PaginationList<T, ID>.toHttpHeader(
nextBlock: (string: String) -> String,
prevBlock: (string: String) -> String
): String? {
val mutableListOf = mutableListOf<String>()
if (next != null) {
mutableListOf.add("<${nextBlock(this.next.toString())}>; rel=\"next\"")
}
if (prev != null) {
mutableListOf.add("<${prevBlock(this.prev.toString())}>; rel=\"prev\"")
}
if (mutableListOf.isEmpty()) {
return null
}
return mutableListOf.joinToString(", ")
}

View File

@ -1,5 +1,8 @@
package dev.usbharu.hideout.core.domain.model.relationship package dev.usbharu.hideout.core.domain.model.relationship
import dev.usbharu.hideout.application.infrastructure.exposed.Page
import dev.usbharu.hideout.application.infrastructure.exposed.PaginationList
/** /**
* [Relationship]の永続化 * [Relationship]の永続化
* *
@ -33,22 +36,16 @@ interface RelationshipRepository {
suspend fun findByTargetIdAndFollowing(targetId: Long, following: Boolean): List<Relationship> suspend fun findByTargetIdAndFollowing(targetId: Long, following: Boolean): List<Relationship>
@Suppress("LongParameterList", "FunctionMaxLength")
suspend fun findByTargetIdAndFollowRequestAndIgnoreFollowRequest( suspend fun findByTargetIdAndFollowRequestAndIgnoreFollowRequest(
maxId: Long?,
sinceId: Long?,
limit: Int,
targetId: Long, targetId: Long,
followRequest: Boolean, followRequest: Boolean,
ignoreFollowRequest: Boolean ignoreFollowRequest: Boolean,
): List<Relationship> page: Page.PageByMaxId
): PaginationList<Relationship, Long>
@Suppress("FunctionMaxLength") suspend fun findByActorIdAndMuting(
suspend fun findByActorIdAntMutingAndMaxIdAndSinceId(
actorId: Long, actorId: Long,
muting: Boolean, muting: Boolean,
maxId: Long?, page: Page.PageByMaxId
sinceId: Long?, ): PaginationList<Relationship, Long>
limit: Int
): List<Relationship>
} }

View File

@ -1,5 +1,8 @@
package dev.usbharu.hideout.core.domain.model.relationship package dev.usbharu.hideout.core.domain.model.relationship
import dev.usbharu.hideout.application.infrastructure.exposed.Page
import dev.usbharu.hideout.application.infrastructure.exposed.PaginationList
import dev.usbharu.hideout.application.infrastructure.exposed.withPagination
import dev.usbharu.hideout.core.infrastructure.exposedrepository.AbstractRepository import dev.usbharu.hideout.core.infrastructure.exposedrepository.AbstractRepository
import dev.usbharu.hideout.core.infrastructure.exposedrepository.Actors import dev.usbharu.hideout.core.infrastructure.exposedrepository.Actors
import org.jetbrains.exposed.dao.id.LongIdTable import org.jetbrains.exposed.dao.id.LongIdTable
@ -74,49 +77,41 @@ class RelationshipRepositoryImpl : RelationshipRepository, AbstractRepository()
} }
override suspend fun findByTargetIdAndFollowRequestAndIgnoreFollowRequest( override suspend fun findByTargetIdAndFollowRequestAndIgnoreFollowRequest(
maxId: Long?,
sinceId: Long?,
limit: Int,
targetId: Long, targetId: Long,
followRequest: Boolean, followRequest: Boolean,
ignoreFollowRequest: Boolean ignoreFollowRequest: Boolean,
): List<Relationship> = query { page: Page.PageByMaxId
): PaginationList<Relationship, Long> = query {
val query = Relationships.select { val query = Relationships.select {
Relationships.targetActorId.eq(targetId).and(Relationships.followRequest.eq(followRequest)) Relationships.targetActorId.eq(targetId).and(Relationships.followRequest.eq(followRequest))
.and(Relationships.ignoreFollowRequestFromTarget.eq(ignoreFollowRequest)) .and(Relationships.ignoreFollowRequestFromTarget.eq(ignoreFollowRequest))
}.limit(limit)
if (maxId != null) {
query.andWhere { Relationships.id lessEq maxId }
} }
if (sinceId != null) { val resultRowList = query.withPagination(page, Relationships.id)
query.andWhere { Relationships.id greaterEq sinceId }
return@query PaginationList(
resultRowList.map { it.toRelationships() },
resultRowList.next?.value,
resultRowList.prev?.value
)
} }
return@query query.map { it.toRelationships() } override suspend fun findByActorIdAndMuting(
}
override suspend fun findByActorIdAntMutingAndMaxIdAndSinceId(
actorId: Long, actorId: Long,
muting: Boolean, muting: Boolean,
maxId: Long?, page: Page.PageByMaxId
sinceId: Long?, ): PaginationList<Relationship, Long> = query {
limit: Int
): List<Relationship> = query {
val query = Relationships.select { val query = Relationships.select {
Relationships.actorId.eq(actorId).and(Relationships.muting.eq(muting)) Relationships.actorId.eq(actorId).and(Relationships.muting.eq(muting))
}.limit(limit)
if (maxId != null) {
query.andWhere { Relationships.id lessEq maxId }
} }
if (sinceId != null) { val resultRowList = query.withPagination(page, Relationships.id)
query.andWhere { Relationships.id greaterEq sinceId }
}
return@query query.map { it.toRelationships() } return@query PaginationList(
resultRowList.map { it.toRelationships() },
resultRowList.next?.value,
resultRowList.prev?.value
)
} }
companion object { companion object {

View File

@ -1,10 +1,12 @@
package dev.usbharu.hideout.core.service.timeline package dev.usbharu.hideout.core.service.timeline
import dev.usbharu.hideout.application.infrastructure.exposed.Page
import dev.usbharu.hideout.application.infrastructure.exposed.PaginationList
import dev.usbharu.hideout.application.infrastructure.exposed.withPagination
import dev.usbharu.hideout.core.infrastructure.exposedrepository.Timelines import dev.usbharu.hideout.core.infrastructure.exposedrepository.Timelines
import dev.usbharu.hideout.domain.mastodon.model.generated.Status import dev.usbharu.hideout.domain.mastodon.model.generated.Status
import dev.usbharu.hideout.mastodon.interfaces.api.status.StatusQuery import dev.usbharu.hideout.mastodon.interfaces.api.status.StatusQuery
import dev.usbharu.hideout.mastodon.query.StatusQueryService import dev.usbharu.hideout.mastodon.query.StatusQueryService
import org.jetbrains.exposed.sql.SortOrder
import org.jetbrains.exposed.sql.andWhere import org.jetbrains.exposed.sql.andWhere
import org.jetbrains.exposed.sql.selectAll import org.jetbrains.exposed.sql.selectAll
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
@ -13,15 +15,13 @@ import org.springframework.stereotype.Service
@Service @Service
@ConditionalOnProperty("hideout.use-mongodb", havingValue = "false", matchIfMissing = true) @ConditionalOnProperty("hideout.use-mongodb", havingValue = "false", matchIfMissing = true)
class ExposedGenerateTimelineService(private val statusQueryService: StatusQueryService) : GenerateTimelineService { class ExposedGenerateTimelineService(private val statusQueryService: StatusQueryService) : GenerateTimelineService {
override suspend fun getTimeline( override suspend fun getTimeline(
forUserId: Long?, forUserId: Long?,
localOnly: Boolean, localOnly: Boolean,
mediaOnly: Boolean, mediaOnly: Boolean,
maxId: Long?, page: Page
minId: Long?, ): PaginationList<Status, Long> {
sinceId: Long?,
limit: Int
): List<Status> {
val query = Timelines.selectAll() val query = Timelines.selectAll()
if (forUserId != null) { if (forUserId != null) {
@ -30,15 +30,7 @@ class ExposedGenerateTimelineService(private val statusQueryService: StatusQuery
if (localOnly) { if (localOnly) {
query.andWhere { Timelines.isLocal eq true } query.andWhere { Timelines.isLocal eq true }
} }
if (maxId != null) { val result = query.withPagination(page, Timelines.id)
query.andWhere { Timelines.id lessEq maxId }
}
if (minId != null) {
query.andWhere { Timelines.id greaterEq minId }
}
val result = query
.limit(limit)
.orderBy(Timelines.createdAt, SortOrder.DESC)
val statusQueries = result.map { val statusQueries = result.map {
StatusQuery( StatusQuery(
@ -50,6 +42,11 @@ class ExposedGenerateTimelineService(private val statusQueryService: StatusQuery
) )
} }
return statusQueryService.findByPostIdsWithMediaIds(statusQueries) val findByPostIdsWithMediaIds = statusQueryService.findByPostIdsWithMediaIds(statusQueries)
return PaginationList(
findByPostIdsWithMediaIds,
findByPostIdsWithMediaIds.lastOrNull()?.id?.toLongOrNull(),
findByPostIdsWithMediaIds.firstOrNull()?.id?.toLongOrNull()
)
} }
} }

View File

@ -1,18 +1,18 @@
package dev.usbharu.hideout.core.service.timeline package dev.usbharu.hideout.core.service.timeline
import dev.usbharu.hideout.application.infrastructure.exposed.Page
import dev.usbharu.hideout.application.infrastructure.exposed.PaginationList
import dev.usbharu.hideout.domain.mastodon.model.generated.Status import dev.usbharu.hideout.domain.mastodon.model.generated.Status
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
@Service @Service
@Suppress("LongParameterList") @Suppress("LongParameterList")
interface GenerateTimelineService { interface GenerateTimelineService {
suspend fun getTimeline( suspend fun getTimeline(
forUserId: Long? = null, forUserId: Long? = null,
localOnly: Boolean = false, localOnly: Boolean = false,
mediaOnly: Boolean = false, mediaOnly: Boolean = false,
maxId: Long? = null, page: Page
minId: Long? = null, ): PaginationList<Status, Long>
sinceId: Long? = null,
limit: Int = 20
): List<Status>
} }

View File

@ -1,5 +1,7 @@
package dev.usbharu.hideout.core.service.timeline package dev.usbharu.hideout.core.service.timeline
import dev.usbharu.hideout.application.infrastructure.exposed.Page
import dev.usbharu.hideout.application.infrastructure.exposed.PaginationList
import dev.usbharu.hideout.core.domain.model.timeline.Timeline import dev.usbharu.hideout.core.domain.model.timeline.Timeline
import dev.usbharu.hideout.domain.mastodon.model.generated.Status import dev.usbharu.hideout.domain.mastodon.model.generated.Status
import dev.usbharu.hideout.mastodon.interfaces.api.status.StatusQuery import dev.usbharu.hideout.mastodon.interfaces.api.status.StatusQuery
@ -18,15 +20,13 @@ class MongoGenerateTimelineService(
private val mongoTemplate: MongoTemplate private val mongoTemplate: MongoTemplate
) : ) :
GenerateTimelineService { GenerateTimelineService {
override suspend fun getTimeline( override suspend fun getTimeline(
forUserId: Long?, forUserId: Long?,
localOnly: Boolean, localOnly: Boolean,
mediaOnly: Boolean, mediaOnly: Boolean,
maxId: Long?, page: Page
minId: Long?, ): PaginationList<Status, Long> {
sinceId: Long?,
limit: Int
): List<Status> {
val query = Query() val query = Query()
if (forUserId != null) { if (forUserId != null) {
@ -37,21 +37,23 @@ class MongoGenerateTimelineService(
val criteria = Criteria.where("isLocal").`is`(true) val criteria = Criteria.where("isLocal").`is`(true)
query.addCriteria(criteria) query.addCriteria(criteria)
} }
if (maxId != null) {
val criteria = Criteria.where("postId").lt(maxId) if (page.minId != null) {
query.addCriteria(criteria) page.minId?.let { query.addCriteria(Criteria.where("id").gt(it)) }
} page.maxId?.let { query.addCriteria(Criteria.where("id").lt(it)) }
if (minId != null) { } else {
val criteria = Criteria.where("postId").gt(minId) query.with(Sort.by(Sort.Direction.DESC, "createdAt"))
query.addCriteria(criteria) page.sinceId?.let { query.addCriteria(Criteria.where("id").gt(it)) }
page.maxId?.let { query.addCriteria(Criteria.where("id").lt(it)) }
} }
query.limit(limit) page.limit?.let { query.limit(it) }
query.with(Sort.by(Sort.Direction.DESC, "createdAt")) query.with(Sort.by(Sort.Direction.DESC, "createdAt"))
val timelines = mongoTemplate.find(query, Timeline::class.java) val timelines = mongoTemplate.find(query, Timeline::class.java)
return statusQueryService.findByPostIdsWithMediaIds( val statuses = statusQueryService.findByPostIdsWithMediaIds(
timelines.map { timelines.map {
StatusQuery( StatusQuery(
it.postId, it.postId,
@ -62,5 +64,10 @@ class MongoGenerateTimelineService(
) )
} }
) )
return PaginationList(
statuses,
statuses.lastOrNull()?.id?.toLongOrNull(),
statuses.firstOrNull()?.id?.toLongOrNull()
)
} }
} }

View File

@ -1,20 +1,19 @@
package dev.usbharu.hideout.mastodon.domain.model package dev.usbharu.hideout.mastodon.domain.model
import dev.usbharu.hideout.application.infrastructure.exposed.Page
import dev.usbharu.hideout.application.infrastructure.exposed.PaginationList
interface MastodonNotificationRepository { interface MastodonNotificationRepository {
suspend fun save(mastodonNotification: MastodonNotification): MastodonNotification suspend fun save(mastodonNotification: MastodonNotification): MastodonNotification
suspend fun deleteById(id: Long) suspend fun deleteById(id: Long)
suspend fun findById(id: Long): MastodonNotification? suspend fun findById(id: Long): MastodonNotification?
@Suppress("LongParameterList", "FunctionMaxLength") suspend fun findByUserIdAndInTypesAndInSourceActorId(
suspend fun findByUserIdAndMaxIdAndMinIdAndSinceIdAndInTypesAndInSourceActorId(
loginUser: Long, loginUser: Long,
maxId: Long?, types: List<NotificationType>,
minId: Long?, accountId: List<Long>,
sinceId: Long?, page: Page
limit: Int, ): PaginationList<MastodonNotification, Long>
typesTmp: MutableList<NotificationType>,
accountId: List<Long>
): List<MastodonNotification>
suspend fun deleteByUserId(userId: Long) suspend fun deleteByUserId(userId: Long)
suspend fun deleteByUserIdAndId(userId: Long, id: Long) suspend fun deleteByUserIdAndId(userId: Long, id: Long)

View File

@ -1,5 +1,8 @@
package dev.usbharu.hideout.mastodon.infrastructure.exposedquery package dev.usbharu.hideout.mastodon.infrastructure.exposedquery
import dev.usbharu.hideout.application.infrastructure.exposed.Page
import dev.usbharu.hideout.application.infrastructure.exposed.PaginationList
import dev.usbharu.hideout.application.infrastructure.exposed.withPagination
import dev.usbharu.hideout.core.domain.model.emoji.CustomEmoji import dev.usbharu.hideout.core.domain.model.emoji.CustomEmoji
import dev.usbharu.hideout.core.domain.model.media.toMediaAttachments import dev.usbharu.hideout.core.domain.model.media.toMediaAttachments
import dev.usbharu.hideout.core.infrastructure.exposedrepository.* import dev.usbharu.hideout.core.infrastructure.exposedrepository.*
@ -54,32 +57,20 @@ class StatusQueryServiceImpl : StatusQueryService {
override suspend fun accountsStatus( override suspend fun accountsStatus(
accountId: Long, accountId: Long,
maxId: Long?,
sinceId: Long?,
minId: Long?,
limit: Int,
onlyMedia: Boolean, onlyMedia: Boolean,
excludeReplies: Boolean, excludeReplies: Boolean,
excludeReblogs: Boolean, excludeReblogs: Boolean,
pinned: Boolean, pinned: Boolean,
tagged: String?, tagged: String?,
includeFollowers: Boolean includeFollowers: Boolean,
): List<Status> { page: Page
): PaginationList<Status, Long> {
val query = Posts val query = Posts
.leftJoin(PostsMedia) .leftJoin(PostsMedia)
.leftJoin(Actors) .leftJoin(Actors)
.leftJoin(Media) .leftJoin(Media)
.select { Posts.actorId eq accountId }.limit(20) .select { Posts.actorId eq accountId }
if (maxId != null) {
query.andWhere { Posts.id eq maxId }
}
if (sinceId != null) {
query.andWhere { Posts.id eq sinceId }
}
if (minId != null) {
query.andWhere { Posts.id eq minId }
}
if (onlyMedia) { if (onlyMedia) {
query.andWhere { PostsMedia.mediaId.isNotNull() } query.andWhere { PostsMedia.mediaId.isNotNull() }
} }
@ -95,7 +86,9 @@ class StatusQueryServiceImpl : StatusQueryService {
query.andWhere { Posts.visibility inList listOf(public.ordinal, unlisted.ordinal) } query.andWhere { Posts.visibility inList listOf(public.ordinal, unlisted.ordinal) }
} }
val pairs = query.groupBy { it[Posts.id] } val pairs = query
.withPagination(page, Posts.id)
.groupBy { it[Posts.id] }
.map { it.value } .map { it.value }
.map { .map {
toStatus(it.first()).copy( toStatus(it.first()).copy(
@ -105,7 +98,12 @@ class StatusQueryServiceImpl : StatusQueryService {
) to it.first()[Posts.repostId] ) to it.first()[Posts.repostId]
} }
return resolveReplyAndRepost(pairs) val statuses = resolveReplyAndRepost(pairs)
return PaginationList(
statuses,
statuses.firstOrNull()?.id?.toLongOrNull(),
statuses.lastOrNull()?.id?.toLongOrNull()
)
} }
override suspend fun findByPostId(id: Long): Status { override suspend fun findByPostId(id: Long): Status {
@ -139,7 +137,9 @@ class StatusQueryServiceImpl : StatusQueryService {
} }
.map { .map {
if (it.inReplyToId != null) { if (it.inReplyToId != null) {
it.copy(inReplyToAccountId = statuses.find { (id) -> id == it.inReplyToId }?.id) println("statuses trace: $statuses")
println("inReplyToId trace: ${it.inReplyToId}")
it.copy(inReplyToAccountId = statuses.find { (id) -> id == it.inReplyToId }?.account?.id)
} else { } else {
it it
} }

View File

@ -1,7 +1,9 @@
package dev.usbharu.hideout.mastodon.infrastructure.exposedrepository package dev.usbharu.hideout.mastodon.infrastructure.exposedrepository
import dev.usbharu.hideout.application.infrastructure.exposed.Page
import dev.usbharu.hideout.application.infrastructure.exposed.PaginationList
import dev.usbharu.hideout.application.infrastructure.exposed.withPagination
import dev.usbharu.hideout.core.infrastructure.exposedrepository.AbstractRepository import dev.usbharu.hideout.core.infrastructure.exposedrepository.AbstractRepository
import dev.usbharu.hideout.core.infrastructure.exposedrepository.Timelines
import dev.usbharu.hideout.mastodon.domain.model.MastodonNotification import dev.usbharu.hideout.mastodon.domain.model.MastodonNotification
import dev.usbharu.hideout.mastodon.domain.model.MastodonNotificationRepository import dev.usbharu.hideout.mastodon.domain.model.MastodonNotificationRepository
import dev.usbharu.hideout.mastodon.domain.model.NotificationType import dev.usbharu.hideout.mastodon.domain.model.NotificationType
@ -59,33 +61,18 @@ class ExposedMastodonNotificationRepository : MastodonNotificationRepository, Ab
MastodonNotifications.select { MastodonNotifications.id eq id }.singleOrNull()?.toMastodonNotification() MastodonNotifications.select { MastodonNotifications.id eq id }.singleOrNull()?.toMastodonNotification()
} }
override suspend fun findByUserIdAndMaxIdAndMinIdAndSinceIdAndInTypesAndInSourceActorId( override suspend fun findByUserIdAndInTypesAndInSourceActorId(
loginUser: Long, loginUser: Long,
maxId: Long?, types: List<NotificationType>,
minId: Long?, accountId: List<Long>,
sinceId: Long?, page: Page
limit: Int, ): PaginationList<MastodonNotification, Long> = query {
typesTmp: MutableList<NotificationType>,
accountId: List<Long>
): List<MastodonNotification> = query {
val query = MastodonNotifications.select { val query = MastodonNotifications.select {
MastodonNotifications.userId eq loginUser MastodonNotifications.userId eq loginUser
} }
val result = query.withPagination(page, MastodonNotifications.id)
if (maxId != null) { return@query PaginationList(result.map { it.toMastodonNotification() }, result.next, result.prev)
query.andWhere { MastodonNotifications.id lessEq maxId }
}
if (minId != null) {
query.andWhere { MastodonNotifications.id greaterEq minId }
}
if (sinceId != null) {
query.andWhere { MastodonNotifications.id greaterEq sinceId }
}
val result = query
.limit(limit)
.orderBy(Timelines.createdAt, SortOrder.DESC)
return@query result.map { it.toMastodonNotification() }
} }
override suspend fun deleteByUserId(userId: Long) { override suspend fun deleteByUserId(userId: Long) {

View File

@ -1,5 +1,7 @@
package dev.usbharu.hideout.mastodon.infrastructure.mongorepository package dev.usbharu.hideout.mastodon.infrastructure.mongorepository
import dev.usbharu.hideout.application.infrastructure.exposed.Page
import dev.usbharu.hideout.application.infrastructure.exposed.PaginationList
import dev.usbharu.hideout.mastodon.domain.model.MastodonNotification import dev.usbharu.hideout.mastodon.domain.model.MastodonNotification
import dev.usbharu.hideout.mastodon.domain.model.MastodonNotificationRepository import dev.usbharu.hideout.mastodon.domain.model.MastodonNotificationRepository
import dev.usbharu.hideout.mastodon.domain.model.NotificationType import dev.usbharu.hideout.mastodon.domain.model.NotificationType
@ -26,35 +28,33 @@ class MongoMastodonNotificationRepositoryWrapper(
override suspend fun findById(id: Long): MastodonNotification? = override suspend fun findById(id: Long): MastodonNotification? =
mongoMastodonNotificationRepository.findById(id).getOrNull() mongoMastodonNotificationRepository.findById(id).getOrNull()
override suspend fun findByUserIdAndMaxIdAndMinIdAndSinceIdAndInTypesAndInSourceActorId( override suspend fun findByUserIdAndInTypesAndInSourceActorId(
loginUser: Long, loginUser: Long,
maxId: Long?, types: List<NotificationType>,
minId: Long?, accountId: List<Long>,
sinceId: Long?, page: Page
limit: Int, ): PaginationList<MastodonNotification, Long> {
typesTmp: MutableList<NotificationType>,
accountId: List<Long>
): List<MastodonNotification> {
val query = Query() val query = Query()
if (maxId != null) { page.limit?.let { query.limit(it) }
val criteria = Criteria.where("id").lte(maxId)
query.addCriteria(criteria) val mastodonNotifications = if (page.minId != null) {
query.with(Sort.by(Sort.Direction.ASC, "id"))
page.minId?.let { query.addCriteria(Criteria.where("id").gt(it)) }
page.maxId?.let { query.addCriteria(Criteria.where("id").lt(it)) }
mongoTemplate.find(query, MastodonNotification::class.java).reversed()
} else {
query.with(Sort.by(Sort.Direction.DESC, "id"))
page.sinceId?.let { query.addCriteria(Criteria.where("id").gt(it)) }
page.maxId?.let { query.addCriteria(Criteria.where("id").lt(it)) }
mongoTemplate.find(query, MastodonNotification::class.java)
} }
if (minId != null) { return PaginationList(
val criteria = Criteria.where("id").gte(minId) mastodonNotifications,
query.addCriteria(criteria) mastodonNotifications.firstOrNull()?.id,
} mastodonNotifications.lastOrNull()?.id
if (sinceId != null) { )
val criteria = Criteria.where("id").gte(sinceId)
query.addCriteria(criteria)
}
query.limit(limit)
query.with(Sort.by(Sort.Direction.DESC, "createdAt"))
return mongoTemplate.find(query, MastodonNotification::class.java)
} }
override suspend fun deleteByUserId(userId: Long) { override suspend fun deleteByUserId(userId: Long) {

View File

@ -1,6 +1,9 @@
package dev.usbharu.hideout.mastodon.interfaces.api.account package dev.usbharu.hideout.mastodon.interfaces.api.account
import dev.usbharu.hideout.application.config.ApplicationConfig
import dev.usbharu.hideout.application.external.Transaction import dev.usbharu.hideout.application.external.Transaction
import dev.usbharu.hideout.application.infrastructure.exposed.Page
import dev.usbharu.hideout.application.infrastructure.exposed.toHttpHeader
import dev.usbharu.hideout.controller.mastodon.generated.AccountApi import dev.usbharu.hideout.controller.mastodon.generated.AccountApi
import dev.usbharu.hideout.core.infrastructure.springframework.security.LoginUserContextHolder import dev.usbharu.hideout.core.infrastructure.springframework.security.LoginUserContextHolder
import dev.usbharu.hideout.core.service.user.UserCreateDto import dev.usbharu.hideout.core.service.user.UserCreateDto
@ -19,7 +22,8 @@ import java.net.URI
class MastodonAccountApiController( class MastodonAccountApiController(
private val accountApiService: AccountApiService, private val accountApiService: AccountApiService,
private val transaction: Transaction, private val transaction: Transaction,
private val loginUserContextHolder: LoginUserContextHolder private val loginUserContextHolder: LoginUserContextHolder,
private val applicationConfig: ApplicationConfig
) : AccountApi { ) : AccountApi {
override suspend fun apiV1AccountsIdFollowPost( override suspend fun apiV1AccountsIdFollowPost(
@ -68,20 +72,31 @@ class MastodonAccountApiController(
tagged: String? tagged: String?
): ResponseEntity<Flow<Status>> = runBlocking { ): ResponseEntity<Flow<Status>> = runBlocking {
val userid = loginUserContextHolder.getLoginUserId() val userid = loginUserContextHolder.getLoginUserId()
val statusFlow = accountApiService.accountsStatuses( val statuses = accountApiService.accountsStatuses(
userid = id.toLong(), userid = id.toLong(),
maxId = maxId?.toLongOrNull(),
sinceId = sinceId?.toLongOrNull(),
minId = minId?.toLongOrNull(),
limit = limit,
onlyMedia = onlyMedia, onlyMedia = onlyMedia,
excludeReplies = excludeReplies, excludeReplies = excludeReplies,
excludeReblogs = excludeReblogs, excludeReblogs = excludeReblogs,
pinned = pinned, pinned = pinned,
tagged = tagged, tagged = tagged,
loginUser = userid loginUser = userid,
).asFlow() page = Page.of(
ResponseEntity.ok(statusFlow) maxId?.toLongOrNull(),
sinceId?.toLongOrNull(),
minId?.toLongOrNull(),
limit.coerceIn(0, 80) ?: 40
)
)
val httpHeader = statuses.toHttpHeader(
{ "${applicationConfig.url}/api/v1/accounts/$id/statuses?min_id=$it" },
{ "${applicationConfig.url}/api/v1/accounts/$id/statuses?max_id=$it" },
)
if (httpHeader != null) {
return@runBlocking ResponseEntity.ok().header("Link", httpHeader).body(statuses.asFlow())
}
ResponseEntity.ok(statuses.asFlow())
} }
override fun apiV1AccountsRelationshipsGet( override fun apiV1AccountsRelationshipsGet(
@ -128,8 +143,7 @@ class MastodonAccountApiController(
return ResponseEntity.ok(removeFromFollowers) return ResponseEntity.ok(removeFromFollowers)
} }
override suspend fun apiV1AccountsUpdateCredentialsPatch(updateCredentials: UpdateCredentials?): override suspend fun apiV1AccountsUpdateCredentialsPatch(updateCredentials: UpdateCredentials?): ResponseEntity<Account> {
ResponseEntity<Account> {
val userid = loginUserContextHolder.getLoginUserId() val userid = loginUserContextHolder.getLoginUserId()
val removeFromFollowers = accountApiService.updateProfile(userid, updateCredentials) val removeFromFollowers = accountApiService.updateProfile(userid, updateCredentials)
@ -157,10 +171,27 @@ class MastodonAccountApiController(
runBlocking { runBlocking {
val userid = loginUserContextHolder.getLoginUserId() val userid = loginUserContextHolder.getLoginUserId()
val accountFlow = val followRequests = accountApiService.followRequests(
accountApiService.followRequests(userid, maxId?.toLong(), sinceId?.toLong(), limit ?: 20, false) userid,
.asFlow() false,
ResponseEntity.ok(accountFlow) Page.PageByMaxId(
maxId?.toLongOrNull(),
sinceId?.toLongOrNull(),
limit?.coerceIn(0, 80) ?: 40
)
)
val httpHeader = followRequests.toHttpHeader(
{ "${applicationConfig.url}/api/v1/follow_requests?max_id=$it" },
{ "${applicationConfig.url}/api/v1/follow_requests?min_id=$it" },
)
if (httpHeader != null) {
return@runBlocking ResponseEntity.ok().header("Link", httpHeader).body(followRequests.asFlow())
}
ResponseEntity.ok(followRequests.asFlow())
} }
override suspend fun apiV1AccountsIdMutePost(id: String): ResponseEntity<Relationship> { override suspend fun apiV1AccountsIdMutePost(id: String): ResponseEntity<Relationship> {
@ -183,9 +214,21 @@ class MastodonAccountApiController(
runBlocking { runBlocking {
val userid = loginUserContextHolder.getLoginUserId() val userid = loginUserContextHolder.getLoginUserId()
val unmute = val mutes =
accountApiService.mutesAccount(userid, maxId?.toLong(), sinceId?.toLong(), limit ?: 20).asFlow() accountApiService.mutesAccount(
userid,
Page.PageByMaxId(maxId?.toLongOrNull(), sinceId?.toLongOrNull(), limit?.coerceIn(0, 80) ?: 40)
)
return@runBlocking ResponseEntity.ok(unmute) val httpHeader = mutes.toHttpHeader(
{ "${applicationConfig.url}/api/v1/mutes?max_id=$it" },
{ "${applicationConfig.url}/api/v1/mutes?since_id=$it" },
)
if (httpHeader != null) {
return@runBlocking ResponseEntity.ok().header("Link", httpHeader).body(mutes.asFlow())
}
return@runBlocking ResponseEntity.ok(mutes.asFlow())
} }
} }

View File

@ -1,5 +1,8 @@
package dev.usbharu.hideout.mastodon.interfaces.api.notification package dev.usbharu.hideout.mastodon.interfaces.api.notification
import dev.usbharu.hideout.application.config.ApplicationConfig
import dev.usbharu.hideout.application.infrastructure.exposed.Page
import dev.usbharu.hideout.application.infrastructure.exposed.toHttpHeader
import dev.usbharu.hideout.controller.mastodon.generated.NotificationsApi import dev.usbharu.hideout.controller.mastodon.generated.NotificationsApi
import dev.usbharu.hideout.core.infrastructure.springframework.security.LoginUserContextHolder import dev.usbharu.hideout.core.infrastructure.springframework.security.LoginUserContextHolder
import dev.usbharu.hideout.domain.mastodon.model.generated.Notification import dev.usbharu.hideout.domain.mastodon.model.generated.Notification
@ -14,7 +17,8 @@ import org.springframework.stereotype.Controller
@Controller @Controller
class MastodonNotificationApiController( class MastodonNotificationApiController(
private val loginUserContextHolder: LoginUserContextHolder, private val loginUserContextHolder: LoginUserContextHolder,
private val notificationApiService: NotificationApiService private val notificationApiService: NotificationApiService,
private val applicationConfig: ApplicationConfig
) : NotificationsApi { ) : NotificationsApi {
override suspend fun apiV1NotificationsClearPost(): ResponseEntity<Any> { override suspend fun apiV1NotificationsClearPost(): ResponseEntity<Any> {
notificationApiService.clearAll(loginUserContextHolder.getLoginUserId()) notificationApiService.clearAll(loginUserContextHolder.getLoginUserId())
@ -30,17 +34,27 @@ class MastodonNotificationApiController(
excludeTypes: List<String>?, excludeTypes: List<String>?,
accountId: List<String>? accountId: List<String>?
): ResponseEntity<Flow<Notification>> = runBlocking { ): ResponseEntity<Flow<Notification>> = runBlocking {
val notificationFlow = notificationApiService.notifications( val notifications = notificationApiService.notifications(
loginUser = loginUserContextHolder.getLoginUserId(), loginUser = loginUserContextHolder.getLoginUserId(),
maxId = maxId?.toLong(),
minId = minId?.toLong(),
sinceId = sinceId?.toLong(),
limit = limit ?: 20,
types = types.orEmpty().mapNotNull { NotificationType.parse(it) }, types = types.orEmpty().mapNotNull { NotificationType.parse(it) },
excludeTypes = excludeTypes.orEmpty().mapNotNull { NotificationType.parse(it) }, excludeTypes = excludeTypes.orEmpty().mapNotNull { NotificationType.parse(it) },
accountId = accountId.orEmpty().mapNotNull { it.toLongOrNull() } accountId = accountId.orEmpty().mapNotNull { it.toLongOrNull() },
).asFlow() page = Page.of(
ResponseEntity.ok(notificationFlow) maxId?.toLongOrNull(),
sinceId?.toLongOrNull(),
minId?.toLongOrNull(),
limit?.coerceIn(0, 80) ?: 40
)
)
val httpHeader = notifications.toHttpHeader(
{ "${applicationConfig.url}/api/v1/notifications?min_id=$it" },
{ "${applicationConfig.url}/api/v1/notifications?max_id=$it" }
) ?: return@runBlocking ResponseEntity.ok(
notifications.asFlow()
)
ResponseEntity.ok().header("Link", httpHeader).body(notifications.asFlow())
} }
override suspend fun apiV1NotificationsIdDismissPost(id: String): ResponseEntity<Any> { override suspend fun apiV1NotificationsIdDismissPost(id: String): ResponseEntity<Any> {

View File

@ -1,5 +1,8 @@
package dev.usbharu.hideout.mastodon.interfaces.api.timeline package dev.usbharu.hideout.mastodon.interfaces.api.timeline
import dev.usbharu.hideout.application.config.ApplicationConfig
import dev.usbharu.hideout.application.infrastructure.exposed.Page
import dev.usbharu.hideout.application.infrastructure.exposed.toHttpHeader
import dev.usbharu.hideout.controller.mastodon.generated.TimelineApi import dev.usbharu.hideout.controller.mastodon.generated.TimelineApi
import dev.usbharu.hideout.core.infrastructure.springframework.security.LoginUserContextHolder import dev.usbharu.hideout.core.infrastructure.springframework.security.LoginUserContextHolder
import dev.usbharu.hideout.domain.mastodon.model.generated.Status import dev.usbharu.hideout.domain.mastodon.model.generated.Status
@ -14,7 +17,8 @@ import org.springframework.stereotype.Controller
@Controller @Controller
class MastodonTimelineApiController( class MastodonTimelineApiController(
private val timelineApiService: TimelineApiService, private val timelineApiService: TimelineApiService,
private val loginUserContextHolder: LoginUserContextHolder private val loginUserContextHolder: LoginUserContextHolder,
private val applicationConfig: ApplicationConfig,
) : TimelineApi { ) : TimelineApi {
override fun apiV1TimelinesHomeGet( override fun apiV1TimelinesHomeGet(
maxId: String?, maxId: String?,
@ -24,12 +28,22 @@ class MastodonTimelineApiController(
): ResponseEntity<Flow<Status>> = runBlocking { ): ResponseEntity<Flow<Status>> = runBlocking {
val homeTimeline = timelineApiService.homeTimeline( val homeTimeline = timelineApiService.homeTimeline(
userId = loginUserContextHolder.getLoginUserId(), userId = loginUserContextHolder.getLoginUserId(),
page = Page.of(
maxId = maxId?.toLongOrNull(), maxId = maxId?.toLongOrNull(),
minId = minId?.toLongOrNull(), minId = minId?.toLongOrNull(),
sinceId = sinceId?.toLongOrNull(), sinceId = sinceId?.toLongOrNull(),
limit = limit ?: 20 limit = limit?.coerceIn(0, 80) ?: 40
) )
ResponseEntity(homeTimeline.asFlow(), HttpStatus.OK) )
val httpHeader = homeTimeline.toHttpHeader(
{ "${applicationConfig.url}/api/v1/home?max_id=$it" },
{ "${applicationConfig.url}/api/v1/home?min_id=$it" }
) ?: return@runBlocking ResponseEntity(
homeTimeline.asFlow(),
HttpStatus.OK
)
ResponseEntity.ok().header("Link", httpHeader).body(homeTimeline.asFlow())
} }
override fun apiV1TimelinesPublicGet( override fun apiV1TimelinesPublicGet(
@ -45,11 +59,21 @@ class MastodonTimelineApiController(
localOnly = local ?: false, localOnly = local ?: false,
remoteOnly = remote ?: false, remoteOnly = remote ?: false,
mediaOnly = onlyMedia ?: false, mediaOnly = onlyMedia ?: false,
page = Page.of(
maxId = maxId?.toLongOrNull(), maxId = maxId?.toLongOrNull(),
minId = minId?.toLongOrNull(), minId = minId?.toLongOrNull(),
sinceId = sinceId?.toLongOrNull(), sinceId = sinceId?.toLongOrNull(),
limit = limit ?: 20 limit = limit?.coerceIn(0, 80) ?: 40
) )
ResponseEntity(publicTimeline.asFlow(), HttpStatus.OK) )
val httpHeader = publicTimeline.toHttpHeader(
{ "${applicationConfig.url}/api/v1/public?max_id=$it" },
{ "${applicationConfig.url}/api/v1/public?min_id=$it" }
) ?: return@runBlocking ResponseEntity(
publicTimeline.asFlow(),
HttpStatus.OK
)
ResponseEntity.ok().header("Link", httpHeader).body(publicTimeline.asFlow())
} }
} }

View File

@ -1,5 +1,7 @@
package dev.usbharu.hideout.mastodon.query package dev.usbharu.hideout.mastodon.query
import dev.usbharu.hideout.application.infrastructure.exposed.Page
import dev.usbharu.hideout.application.infrastructure.exposed.PaginationList
import dev.usbharu.hideout.domain.mastodon.model.generated.Status import dev.usbharu.hideout.domain.mastodon.model.generated.Status
import dev.usbharu.hideout.mastodon.interfaces.api.status.StatusQuery import dev.usbharu.hideout.mastodon.interfaces.api.status.StatusQuery
@ -7,35 +9,17 @@ interface StatusQueryService {
suspend fun findByPostIds(ids: List<Long>): List<Status> suspend fun findByPostIds(ids: List<Long>): List<Status>
suspend fun findByPostIdsWithMediaIds(statusQueries: List<StatusQuery>): List<Status> suspend fun findByPostIdsWithMediaIds(statusQueries: List<StatusQuery>): List<Status>
/**
* アカウントの投稿一覧を取得します
*
* @param accountId 対象アカウントのid
* @param maxId 投稿の最大id
* @param sinceId 投稿の最小id
* @param minId 不明
* @param limit 投稿の最大件数
* @param onlyMedia メディア付き投稿のみ
* @param excludeReplies 返信を除外
* @param excludeReblogs リブログを除外
* @param pinned ピン止め投稿のみ
* @param tagged タグ付き?
* @param includeFollowers フォロワー限定投稿を含める
*/
@Suppress("LongParameterList") @Suppress("LongParameterList")
suspend fun accountsStatus( suspend fun accountsStatus(
accountId: Long, accountId: Long,
maxId: Long? = null,
sinceId: Long? = null,
minId: Long? = null,
limit: Int,
onlyMedia: Boolean = false, onlyMedia: Boolean = false,
excludeReplies: Boolean = false, excludeReplies: Boolean = false,
excludeReblogs: Boolean = false, excludeReblogs: Boolean = false,
pinned: Boolean = false, pinned: Boolean = false,
tagged: String? = null, tagged: String?,
includeFollowers: Boolean = false includeFollowers: Boolean = false,
): List<Status> page: Page
): PaginationList<Status, Long>
suspend fun findByPostId(id: Long): Status suspend fun findByPostId(id: Long): Status
} }

View File

@ -1,6 +1,8 @@
package dev.usbharu.hideout.mastodon.service.account package dev.usbharu.hideout.mastodon.service.account
import dev.usbharu.hideout.application.external.Transaction import dev.usbharu.hideout.application.external.Transaction
import dev.usbharu.hideout.application.infrastructure.exposed.Page
import dev.usbharu.hideout.application.infrastructure.exposed.PaginationList
import dev.usbharu.hideout.core.domain.model.relationship.RelationshipRepository import dev.usbharu.hideout.core.domain.model.relationship.RelationshipRepository
import dev.usbharu.hideout.core.service.media.MediaService import dev.usbharu.hideout.core.service.media.MediaService
import dev.usbharu.hideout.core.service.relationship.RelationshipService import dev.usbharu.hideout.core.service.relationship.RelationshipService
@ -17,20 +19,18 @@ import kotlin.math.min
@Service @Service
@Suppress("TooManyFunctions") @Suppress("TooManyFunctions")
interface AccountApiService { interface AccountApiService {
@Suppress("LongParameterList")
@Suppress("ongParameterList")
suspend fun accountsStatuses( suspend fun accountsStatuses(
userid: Long, userid: Long,
maxId: Long?,
sinceId: Long?,
minId: Long?,
limit: Int,
onlyMedia: Boolean, onlyMedia: Boolean,
excludeReplies: Boolean, excludeReplies: Boolean,
excludeReblogs: Boolean, excludeReblogs: Boolean,
pinned: Boolean, pinned: Boolean,
tagged: String?, tagged: String?,
loginUser: Long? loginUser: Long?,
): List<Status> page: Page
): PaginationList<Status, Long>
suspend fun verifyCredentials(userid: Long): CredentialAccount suspend fun verifyCredentials(userid: Long): CredentialAccount
suspend fun registerAccount(userCreateDto: UserCreateDto): Unit suspend fun registerAccount(userCreateDto: UserCreateDto): Unit
@ -50,19 +50,18 @@ interface AccountApiService {
suspend fun unfollow(userid: Long, target: Long): Relationship suspend fun unfollow(userid: Long, target: Long): Relationship
suspend fun removeFromFollowers(userid: Long, target: Long): Relationship suspend fun removeFromFollowers(userid: Long, target: Long): Relationship
suspend fun updateProfile(userid: Long, updateCredentials: UpdateCredentials?): Account suspend fun updateProfile(userid: Long, updateCredentials: UpdateCredentials?): Account
suspend fun followRequests( suspend fun followRequests(
loginUser: Long, loginUser: Long,
maxId: Long?, withIgnore: Boolean,
sinceId: Long?, pageByMaxId: Page.PageByMaxId
limit: Int = 20, ): PaginationList<Account, Long>
withIgnore: Boolean
): List<Account>
suspend fun acceptFollowRequest(loginUser: Long, target: Long): Relationship suspend fun acceptFollowRequest(loginUser: Long, target: Long): Relationship
suspend fun rejectFollowRequest(loginUser: Long, target: Long): Relationship suspend fun rejectFollowRequest(loginUser: Long, target: Long): Relationship
suspend fun mute(userid: Long, target: Long): Relationship suspend fun mute(userid: Long, target: Long): Relationship
suspend fun unmute(userid: Long, target: Long): Relationship suspend fun unmute(userid: Long, target: Long): Relationship
suspend fun mutesAccount(userid: Long, maxId: Long?, sinceId: Long?, limit: Int): List<Account> suspend fun mutesAccount(userid: Long, pageByMaxId: Page.PageByMaxId): PaginationList<Account, Long>
} }
@Service @Service
@ -76,21 +75,21 @@ class AccountApiServiceImpl(
private val mediaService: MediaService private val mediaService: MediaService
) : ) :
AccountApiService { AccountApiService {
override suspend fun accountsStatuses( override suspend fun accountsStatuses(
userid: Long, userid: Long,
maxId: Long?,
sinceId: Long?,
minId: Long?,
limit: Int,
onlyMedia: Boolean, onlyMedia: Boolean,
excludeReplies: Boolean, excludeReplies: Boolean,
excludeReblogs: Boolean, excludeReblogs: Boolean,
pinned: Boolean, pinned: Boolean,
tagged: String?, tagged: String?,
loginUser: Long? loginUser: Long?,
): List<Status> { page: Page
): PaginationList<Status, Long> {
val canViewFollowers = if (loginUser == null) { val canViewFollowers = if (loginUser == null) {
false false
} else if (loginUser == userid) {
true
} else { } else {
transaction.transaction { transaction.transaction {
isFollowing(loginUser, userid) isFollowing(loginUser, userid)
@ -100,16 +99,13 @@ class AccountApiServiceImpl(
return transaction.transaction { return transaction.transaction {
statusQueryService.accountsStatus( statusQueryService.accountsStatus(
accountId = userid, accountId = userid,
maxId = maxId,
sinceId = sinceId,
minId = minId,
limit = limit,
onlyMedia = onlyMedia, onlyMedia = onlyMedia,
excludeReplies = excludeReplies, excludeReplies = excludeReplies,
excludeReblogs = excludeReblogs, excludeReblogs = excludeReblogs,
pinned = pinned, pinned = pinned,
tagged = tagged, tagged = tagged,
includeFollowers = canViewFollowers includeFollowers = canViewFollowers,
page = page
) )
} }
} }
@ -217,23 +213,19 @@ class AccountApiServiceImpl(
override suspend fun followRequests( override suspend fun followRequests(
loginUser: Long, loginUser: Long,
maxId: Long?, withIgnore: Boolean,
sinceId: Long?, pageByMaxId: Page.PageByMaxId
limit: Int, ): PaginationList<Account, Long> = transaction.transaction {
withIgnore: Boolean val request =
): List<Account> = transaction.transaction { relationshipRepository.findByTargetIdAndFollowRequestAndIgnoreFollowRequest(
val actorIdList = relationshipRepository loginUser,
.findByTargetIdAndFollowRequestAndIgnoreFollowRequest( true,
maxId = maxId, withIgnore,
sinceId = sinceId, pageByMaxId
limit = limit,
targetId = loginUser,
followRequest = true,
ignoreFollowRequest = withIgnore
) )
.map { it.actorId } val actorIds = request.map { it.actorId }
return@transaction accountService.findByIds(actorIdList) return@transaction PaginationList(accountService.findByIds(actorIds), request.next, request.prev)
} }
override suspend fun acceptFollowRequest(loginUser: Long, target: Long): Relationship = transaction.transaction { override suspend fun acceptFollowRequest(loginUser: Long, target: Long): Relationship = transaction.transaction {
@ -260,11 +252,14 @@ class AccountApiServiceImpl(
return@transaction fetchRelationship(userid, target) return@transaction fetchRelationship(userid, target)
} }
override suspend fun mutesAccount(userid: Long, maxId: Long?, sinceId: Long?, limit: Int): List<Account> { override suspend fun mutesAccount(userid: Long, pageByMaxId: Page.PageByMaxId): PaginationList<Account, Long> {
val mutedAccounts = val mutedAccounts = relationshipRepository.findByActorIdAndMuting(userid, true, pageByMaxId)
relationshipRepository.findByActorIdAntMutingAndMaxIdAndSinceId(userid, true, maxId, sinceId, limit)
return accountService.findByIds(mutedAccounts.map { it.targetActorId }) return PaginationList(
accountService.findByIds(mutedAccounts.map { it.targetActorId }),
mutedAccounts.next,
mutedAccounts.prev
)
} }
private fun from(account: Account): CredentialAccount { private fun from(account: Account): CredentialAccount {

View File

@ -1,20 +1,19 @@
package dev.usbharu.hideout.mastodon.service.notification package dev.usbharu.hideout.mastodon.service.notification
import dev.usbharu.hideout.application.infrastructure.exposed.Page
import dev.usbharu.hideout.application.infrastructure.exposed.PaginationList
import dev.usbharu.hideout.domain.mastodon.model.generated.Notification import dev.usbharu.hideout.domain.mastodon.model.generated.Notification
import dev.usbharu.hideout.mastodon.domain.model.NotificationType import dev.usbharu.hideout.mastodon.domain.model.NotificationType
interface NotificationApiService { interface NotificationApiService {
@Suppress("LongParameterList")
suspend fun notifications( suspend fun notifications(
loginUser: Long, loginUser: Long,
maxId: Long?,
minId: Long?,
sinceId: Long?,
limit: Int,
types: List<NotificationType>, types: List<NotificationType>,
excludeTypes: List<NotificationType>, excludeTypes: List<NotificationType>,
accountId: List<Long> accountId: List<Long>,
): List<Notification> page: Page
): PaginationList<Notification, Long>
suspend fun fingById(loginUser: Long, notificationId: Long): Notification? suspend fun fingById(loginUser: Long, notificationId: Long): Notification?

View File

@ -1,6 +1,8 @@
package dev.usbharu.hideout.mastodon.service.notification package dev.usbharu.hideout.mastodon.service.notification
import dev.usbharu.hideout.application.external.Transaction import dev.usbharu.hideout.application.external.Transaction
import dev.usbharu.hideout.application.infrastructure.exposed.Page
import dev.usbharu.hideout.application.infrastructure.exposed.PaginationList
import dev.usbharu.hideout.domain.mastodon.model.generated.Notification import dev.usbharu.hideout.domain.mastodon.model.generated.Notification
import dev.usbharu.hideout.mastodon.domain.model.MastodonNotificationRepository import dev.usbharu.hideout.mastodon.domain.model.MastodonNotificationRepository
import dev.usbharu.hideout.mastodon.domain.model.NotificationType import dev.usbharu.hideout.mastodon.domain.model.NotificationType
@ -17,30 +19,25 @@ class NotificationApiServiceImpl(
private val statusQueryService: StatusQueryService private val statusQueryService: StatusQueryService
) : ) :
NotificationApiService { NotificationApiService {
override suspend fun notifications( override suspend fun notifications(
loginUser: Long, loginUser: Long,
maxId: Long?,
minId: Long?,
sinceId: Long?,
limit: Int,
types: List<NotificationType>, types: List<NotificationType>,
excludeTypes: List<NotificationType>, excludeTypes: List<NotificationType>,
accountId: List<Long> accountId: List<Long>,
): List<Notification> = transaction.transaction { page: Page
): PaginationList<Notification, Long> = transaction.transaction {
val typesTmp = mutableListOf<NotificationType>() val typesTmp = mutableListOf<NotificationType>()
typesTmp.addAll(types) typesTmp.addAll(types)
typesTmp.removeAll(excludeTypes) typesTmp.removeAll(excludeTypes)
val mastodonNotifications = val mastodonNotifications =
mastodonNotificationRepository.findByUserIdAndMaxIdAndMinIdAndSinceIdAndInTypesAndInSourceActorId( mastodonNotificationRepository.findByUserIdAndInTypesAndInSourceActorId(
loginUser, loginUser,
maxId,
minId,
sinceId,
limit,
typesTmp, typesTmp,
accountId accountId,
page
) )
val accounts = accountService.findByIds( val accounts = accountService.findByIds(
@ -52,7 +49,7 @@ class NotificationApiServiceImpl(
val statuses = statusQueryService.findByPostIds(mastodonNotifications.mapNotNull { it.statusId }) val statuses = statusQueryService.findByPostIds(mastodonNotifications.mapNotNull { it.statusId })
.associateBy { it.id.toLong() } .associateBy { it.id.toLong() }
mastodonNotifications.map { val notifications = mastodonNotifications.map {
Notification( Notification(
id = it.id.toString(), id = it.id.toString(),
type = convertNotificationType(it.type), type = convertNotificationType(it.type),
@ -63,6 +60,8 @@ class NotificationApiServiceImpl(
relationshipSeveranceEvent = null relationshipSeveranceEvent = null
) )
} }
return@transaction PaginationList(notifications, mastodonNotifications.next, mastodonNotifications.prev)
} }
override suspend fun fingById(loginUser: Long, notificationId: Long): Notification? { override suspend fun fingById(loginUser: Long, notificationId: Long): Notification? {

View File

@ -1,29 +1,26 @@
package dev.usbharu.hideout.mastodon.service.timeline package dev.usbharu.hideout.mastodon.service.timeline
import dev.usbharu.hideout.application.external.Transaction import dev.usbharu.hideout.application.external.Transaction
import dev.usbharu.hideout.application.infrastructure.exposed.Page
import dev.usbharu.hideout.application.infrastructure.exposed.PaginationList
import dev.usbharu.hideout.core.service.timeline.GenerateTimelineService import dev.usbharu.hideout.core.service.timeline.GenerateTimelineService
import dev.usbharu.hideout.domain.mastodon.model.generated.Status import dev.usbharu.hideout.domain.mastodon.model.generated.Status
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
@Suppress("LongParameterList") @Suppress("LongParameterList")
interface TimelineApiService { interface TimelineApiService {
suspend fun publicTimeline( suspend fun publicTimeline(
localOnly: Boolean = false, localOnly: Boolean = false,
remoteOnly: Boolean = false, remoteOnly: Boolean = false,
mediaOnly: Boolean = false, mediaOnly: Boolean = false,
maxId: Long?, page: Page
minId: Long?, ): PaginationList<Status, Long>
sinceId: Long?,
limit: Int = 20
): List<Status>
suspend fun homeTimeline( suspend fun homeTimeline(
userId: Long, userId: Long,
maxId: Long?, page: Page
minId: Long?, ): PaginationList<Status, Long>
sinceId: Long?,
limit: Int = 20
): List<Status>
} }
@Service @Service
@ -31,39 +28,18 @@ class TimelineApiServiceImpl(
private val generateTimelineService: GenerateTimelineService, private val generateTimelineService: GenerateTimelineService,
private val transaction: Transaction private val transaction: Transaction
) : TimelineApiService { ) : TimelineApiService {
override suspend fun publicTimeline( override suspend fun publicTimeline(
localOnly: Boolean, localOnly: Boolean,
remoteOnly: Boolean, remoteOnly: Boolean,
mediaOnly: Boolean, mediaOnly: Boolean,
maxId: Long?, page: Page
minId: Long?, ): PaginationList<Status, Long> = transaction.transaction {
sinceId: Long?, return@transaction generateTimelineService.getTimeline(forUserId = 0, localOnly, mediaOnly, page)
limit: Int
): List<Status> = transaction.transaction {
generateTimelineService.getTimeline(
forUserId = 0,
localOnly = localOnly,
mediaOnly = mediaOnly,
maxId = maxId,
minId = minId,
sinceId = sinceId,
limit = limit
)
} }
override suspend fun homeTimeline( override suspend fun homeTimeline(userId: Long, page: Page): PaginationList<Status, Long> =
userId: Long, transaction.transaction {
maxId: Long?, return@transaction generateTimelineService.getTimeline(forUserId = userId, page = page)
minId: Long?,
sinceId: Long?,
limit: Int
): List<Status> = transaction.transaction {
generateTimelineService.getTimeline(
forUserId = userId,
maxId = maxId,
minId = minId,
sinceId = sinceId,
limit = limit
)
} }
} }

View File

@ -573,6 +573,7 @@ paths:
required: false required: false
schema: schema:
type: integer type: integer
nullable: true
default: 20 default: 20
- in: query - in: query
name: only_media name: only_media
@ -1051,7 +1052,6 @@ components:
- group - group
- discoverable - discoverable
- created_at - created_at
- last_status_at
- statuses_count - statuses_count
- followers_count - followers_count
- followers_count - followers_count
@ -1279,6 +1279,7 @@ components:
language: language:
type: string type: string
nullable: true nullable: true
default: null
text: text:
type: string type: string
nullable: true nullable: true
@ -1316,11 +1317,7 @@ components:
- favourites_count - favourites_count
- replies_count - replies_count
- url - url
- in_reply_to_id
- in_reply_to_account_id
- language
- text - text
- edited_at
MediaAttachment: MediaAttachment:
type: object type: object

View File

@ -0,0 +1,137 @@
package dev.usbharu.hideout.application.infrastructure.exposed
import org.assertj.core.api.Assertions.assertThat
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.transactions.transaction
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
class ExposedPaginationExtensionKtTest {
@BeforeEach
fun setUp(): Unit = transaction {
val map = (1..100).map { it to it.toString() }
ExposePaginationTestTable.batchInsert(map){
this[ExposePaginationTestTable.id] = it.first.toLong()
this[ExposePaginationTestTable.name] = it.second
}
}
@AfterEach
fun tearDown():Unit = transaction {
ExposePaginationTestTable.deleteAll()
}
@Test
fun パラメーター無しでの取得(): Unit = transaction {
val pagination: PaginationList<ResultRow,Long> = ExposePaginationTestTable.selectAll().limit(20).withPagination(Page.of(), ExposePaginationTestTable.id)
assertThat(pagination.next).isEqualTo(100)
assertThat(pagination.prev).isEqualTo(81)
assertThat(pagination.first()[ExposePaginationTestTable.id]).isEqualTo(100)
assertThat(pagination.last()[ExposePaginationTestTable.id]).isEqualTo(81)
assertThat(pagination).size().isEqualTo(20)
}
@Test
fun maxIdを指定して取得(): Unit = transaction {
val pagination: PaginationList<ResultRow,Long> = ExposePaginationTestTable.selectAll().limit(20).withPagination(Page.of(maxId = 100), ExposePaginationTestTable.id)
assertThat(pagination.next).isEqualTo(99)
assertThat(pagination.prev).isEqualTo(80)
assertThat(pagination.first()[ExposePaginationTestTable.id]).isEqualTo(99)
assertThat(pagination.last()[ExposePaginationTestTable.id]).isEqualTo(80)
assertThat(pagination).size().isEqualTo(20)
}
@Test
fun sinceIdを指定して取得(): Unit = transaction {
val pagination: PaginationList<ResultRow,Long> = ExposePaginationTestTable.selectAll().limit(20).withPagination(Page.of(sinceId = 15), ExposePaginationTestTable.id)
assertThat(pagination.next).isEqualTo(100)
assertThat(pagination.prev).isEqualTo(81)
assertThat(pagination.first()[ExposePaginationTestTable.id]).isEqualTo(100)
assertThat(pagination.last()[ExposePaginationTestTable.id]).isEqualTo(81)
assertThat(pagination).size().isEqualTo(20)
}
@Test
fun minIdを指定して取得():Unit = transaction {
val pagination: PaginationList<ResultRow,Long> = ExposePaginationTestTable.selectAll().limit(20).withPagination(Page.of(minId = 45), ExposePaginationTestTable.id)
assertThat(pagination.next).isEqualTo(65)
assertThat(pagination.prev).isEqualTo(46)
assertThat(pagination.first()[ExposePaginationTestTable.id]).isEqualTo(65)
assertThat(pagination.last()[ExposePaginationTestTable.id]).isEqualTo(46)
assertThat(pagination).size().isEqualTo(20)
}
@Test
fun maxIdとsinceIdを指定して取得(): Unit = transaction {
val pagination: PaginationList<ResultRow,Long> = ExposePaginationTestTable.selectAll().limit(20).withPagination(Page.of(maxId = 45, sinceId = 34), ExposePaginationTestTable.id)
assertThat(pagination.next).isEqualTo(44)
assertThat(pagination.prev).isEqualTo(35)
assertThat(pagination.first()[ExposePaginationTestTable.id]).isEqualTo(44)
assertThat(pagination.last()[ExposePaginationTestTable.id]).isEqualTo(35)
assertThat(pagination).size().isEqualTo(10)
}
@Test
fun maxIdとminIdを指定して取得():Unit = transaction {
val pagination: PaginationList<ResultRow,Long> = ExposePaginationTestTable.selectAll().limit(20).withPagination(Page.of(maxId = 54, minId = 45), ExposePaginationTestTable.id)
assertThat(pagination.next).isEqualTo(53)
assertThat(pagination.prev).isEqualTo(46)
assertThat(pagination.first()[ExposePaginationTestTable.id]).isEqualTo(53)
assertThat(pagination.last()[ExposePaginationTestTable.id]).isEqualTo(46)
assertThat(pagination).size().isEqualTo(8)
}
@Test
fun limitを指定して取得():Unit = transaction {
val pagination: PaginationList<ResultRow,Long> = ExposePaginationTestTable.selectAll().withPagination(Page.of(limit = 30), ExposePaginationTestTable.id)
assertThat(pagination).size().isEqualTo(30)
}
@Test
fun 結果が0件の場合はprevとnextがnullになる():Unit = transaction {
val pagination = ExposePaginationTestTable.select { ExposePaginationTestTable.id.isNull() }
.withPagination(Page.of(), ExposePaginationTestTable.id)
assertThat(pagination).isEmpty()
assertThat(pagination.next).isNull()
assertThat(pagination.prev).isNull()
}
object ExposePaginationTestTable : Table(){
val id = long("id")
val name = varchar("name",100)
override val primaryKey: PrimaryKey?
get() = PrimaryKey(id)
}
companion object {
private lateinit var database: Database
@JvmStatic
@BeforeAll
fun beforeAll(): Unit {
database = Database.connect(
url = "jdbc:h2:mem:test;MODE=POSTGRESQL;DB_CLOSE_DELAY=-1;CASE_INSENSITIVE_IDENTIFIERS=true;TRACE_LEVEL_FILE=4;",
driver = "org.h2.Driver",
user = "sa",
password = ""
)
transaction(database) {
SchemaUtils.create(ExposePaginationTestTable)
SchemaUtils.createMissingTablesAndColumns(ExposePaginationTestTable)
}
}
}
}

View File

@ -0,0 +1,27 @@
package dev.usbharu.hideout.application.infrastructure.exposed
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.Test
class PageTest {
@Test
fun minIdが指定されているとsinceIdは無視される() {
val page = Page.of(1, 2, 3, 4)
assertThat(page.maxId).isEqualTo(1)
assertThat(page.sinceId).isNull()
assertThat(page.minId).isEqualTo(3)
assertThat(page.limit).isEqualTo(4)
}
@Test
fun minIdがnullのときはsinceIdが使われる() {
val page = Page.of(1, 2, null, 4)
assertThat(page.maxId).isEqualTo(1)
assertThat(page.minId).isNull()
assertThat(page.sinceId).isEqualTo(2)
assertThat(page.limit).isEqualTo(4)
}
}

View File

@ -0,0 +1,48 @@
package dev.usbharu.hideout.application.infrastructure.exposed
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.Test
class PaginationListKtTest {
@Test
fun `toHttpHeader nextとprevがnullでない場合両方作成される`() {
val paginationList = PaginationList<String, Long>(emptyList(), 1, 2)
val httpHeader =
paginationList.toHttpHeader({ "https://example.com?max_id=$it" }, { "https://example.com?min_id=$it" })
assertThat(httpHeader).isEqualTo("<https://example.com?max_id=1>; rel=\"next\", <https://example.com?min_id=2>; rel=\"prev\"")
}
@Test
fun `toHttpHeader nextがnullなら片方だけ作成される`() {
val paginationList = PaginationList<String, Long>(emptyList(), 1,null)
val httpHeader =
paginationList.toHttpHeader({ "https://example.com?max_id=$it" }, { "https://example.com?min_id=$it" })
assertThat(httpHeader).isEqualTo("<https://example.com?max_id=1>; rel=\"next\"")
}
@Test
fun `toHttpHeader prevがnullなら片方だけ作成される`() {
val paginationList = PaginationList<String, Long>(emptyList(), null,2)
val httpHeader =
paginationList.toHttpHeader({ "https://example.com?max_id=$it" }, { "https://example.com?min_id=$it" })
assertThat(httpHeader).isEqualTo("<https://example.com?min_id=2>; rel=\"prev\"")
}
@Test
fun `toHttpHeader 両方nullならnullが返ってくる`() {
val paginationList = PaginationList<String, Long>(emptyList(), null, null)
val httpHeader =
paginationList.toHttpHeader({ "https://example.com?max_id=$it" }, { "https://example.com?min_id=$it" })
assertThat(httpHeader).isNull()
}
}

View File

@ -1,6 +1,7 @@
package dev.usbharu.hideout.mastodon.interfaces.api.account package dev.usbharu.hideout.mastodon.interfaces.api.account
import dev.usbharu.hideout.application.config.ActivityPubConfig import dev.usbharu.hideout.application.config.ActivityPubConfig
import dev.usbharu.hideout.application.config.ApplicationConfig
import dev.usbharu.hideout.core.infrastructure.springframework.security.OAuth2JwtLoginUserContextHolder import dev.usbharu.hideout.core.infrastructure.springframework.security.OAuth2JwtLoginUserContextHolder
import dev.usbharu.hideout.domain.mastodon.model.generated.AccountSource import dev.usbharu.hideout.domain.mastodon.model.generated.AccountSource
import dev.usbharu.hideout.domain.mastodon.model.generated.CredentialAccount import dev.usbharu.hideout.domain.mastodon.model.generated.CredentialAccount
@ -26,6 +27,7 @@ import org.springframework.test.web.servlet.get
import org.springframework.test.web.servlet.post import org.springframework.test.web.servlet.post
import org.springframework.test.web.servlet.setup.MockMvcBuilders import org.springframework.test.web.servlet.setup.MockMvcBuilders
import utils.TestTransaction import utils.TestTransaction
import java.net.URL
@ExtendWith(MockitoExtension::class) @ExtendWith(MockitoExtension::class)
class MastodonAccountApiControllerTest { class MastodonAccountApiControllerTest {
@ -41,6 +43,9 @@ class MastodonAccountApiControllerTest {
@Mock @Mock
private lateinit var accountApiService: AccountApiService private lateinit var accountApiService: AccountApiService
@Spy
private val applicationConfig: ApplicationConfig = ApplicationConfig(URL("https://example.com"))
@InjectMocks @InjectMocks
private lateinit var mastodonAccountApiController: MastodonAccountApiController private lateinit var mastodonAccountApiController: MastodonAccountApiController

View File

@ -1,6 +1,8 @@
package dev.usbharu.hideout.mastodon.interfaces.api.timeline package dev.usbharu.hideout.mastodon.interfaces.api.timeline
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import dev.usbharu.hideout.application.config.ApplicationConfig
import dev.usbharu.hideout.application.infrastructure.exposed.PaginationList
import dev.usbharu.hideout.core.infrastructure.springframework.security.OAuth2JwtLoginUserContextHolder import dev.usbharu.hideout.core.infrastructure.springframework.security.OAuth2JwtLoginUserContextHolder
import dev.usbharu.hideout.domain.mastodon.model.generated.Account import dev.usbharu.hideout.domain.mastodon.model.generated.Account
import dev.usbharu.hideout.domain.mastodon.model.generated.Status import dev.usbharu.hideout.domain.mastodon.model.generated.Status
@ -21,6 +23,7 @@ import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.get import org.springframework.test.web.servlet.get
import org.springframework.test.web.servlet.post import org.springframework.test.web.servlet.post
import org.springframework.test.web.servlet.setup.MockMvcBuilders import org.springframework.test.web.servlet.setup.MockMvcBuilders
import java.net.URL
@ExtendWith(MockitoExtension::class) @ExtendWith(MockitoExtension::class)
class MastodonTimelineApiControllerTest { class MastodonTimelineApiControllerTest {
@ -31,6 +34,9 @@ class MastodonTimelineApiControllerTest {
@Mock @Mock
private lateinit var timelineApiService: TimelineApiService private lateinit var timelineApiService: TimelineApiService
@Spy
private val applicationConfig: ApplicationConfig = ApplicationConfig(URL("https://example.com"))
@InjectMocks @InjectMocks
private lateinit var mastodonTimelineApiController: MastodonTimelineApiController private lateinit var mastodonTimelineApiController: MastodonTimelineApiController
@ -41,7 +47,8 @@ class MastodonTimelineApiControllerTest {
mockMvc = MockMvcBuilders.standaloneSetup(mastodonTimelineApiController).build() mockMvc = MockMvcBuilders.standaloneSetup(mastodonTimelineApiController).build()
} }
val statusList = listOf<Status>( val statusList = PaginationList<Status, Long>(
listOf<Status>(
Status( Status(
id = "", id = "",
uri = "", uri = "",
@ -142,6 +149,7 @@ class MastodonTimelineApiControllerTest {
editedAt = null editedAt = null
) )
), null, null
) )
@Test @Test
@ -156,10 +164,7 @@ class MastodonTimelineApiControllerTest {
whenever( whenever(
timelineApiService.homeTimeline( timelineApiService.homeTimeline(
eq(1234), eq(1234),
eq(123456), any()
eq(54321),
eq(1234567),
eq(20)
) )
).doReturn(statusList) ).doReturn(statusList)
@ -183,10 +188,7 @@ class MastodonTimelineApiControllerTest {
whenever( whenever(
timelineApiService.homeTimeline( timelineApiService.homeTimeline(
eq(1234), eq(1234),
isNull(), any()
isNull(),
isNull(),
eq(20)
) )
).doReturn(statusList) ).doReturn(statusList)
@ -213,10 +215,7 @@ class MastodonTimelineApiControllerTest {
localOnly = eq(false), localOnly = eq(false),
remoteOnly = eq(true), remoteOnly = eq(true),
mediaOnly = eq(false), mediaOnly = eq(false),
maxId = eq(1234), any()
minId = eq(4321),
sinceId = eq(12345),
limit = eq(20)
) )
).doAnswer { ).doAnswer {
println(it.arguments.joinToString()) println(it.arguments.joinToString())
@ -245,10 +244,7 @@ class MastodonTimelineApiControllerTest {
localOnly = eq(false), localOnly = eq(false),
remoteOnly = eq(false), remoteOnly = eq(false),
mediaOnly = eq(false), mediaOnly = eq(false),
maxId = isNull(), any()
minId = isNull(),
sinceId = isNull(),
limit = eq(20)
) )
).doAnswer { ).doAnswer {
println(it.arguments.joinToString()) println(it.arguments.joinToString())

View File

@ -1,6 +1,8 @@
package dev.usbharu.hideout.mastodon.service.account package dev.usbharu.hideout.mastodon.service.account
import dev.usbharu.hideout.application.external.Transaction import dev.usbharu.hideout.application.external.Transaction
import dev.usbharu.hideout.application.infrastructure.exposed.Page
import dev.usbharu.hideout.application.infrastructure.exposed.PaginationList
import dev.usbharu.hideout.core.domain.model.actor.ActorRepository import dev.usbharu.hideout.core.domain.model.actor.ActorRepository
import dev.usbharu.hideout.core.domain.model.relationship.RelationshipRepository import dev.usbharu.hideout.core.domain.model.relationship.RelationshipRepository
import dev.usbharu.hideout.core.query.FollowerQueryService import dev.usbharu.hideout.core.query.FollowerQueryService
@ -55,7 +57,8 @@ class AccountApiServiceImplTest {
@InjectMocks @InjectMocks
private lateinit var accountApiServiceImpl: AccountApiServiceImpl private lateinit var accountApiServiceImpl: AccountApiServiceImpl
private val statusList = listOf( private val statusList = PaginationList<Status, Long>(
listOf(
Status( Status(
id = "", id = "",
uri = "", uri = "",
@ -105,6 +108,7 @@ class AccountApiServiceImplTest {
text = "Test", text = "Test",
editedAt = null editedAt = null
) )
), null, null
) )
@Test @Test
@ -114,16 +118,13 @@ class AccountApiServiceImplTest {
whenever( whenever(
statusQueryService.accountsStatus( statusQueryService.accountsStatus(
accountId = eq(userId), accountId = eq(userId),
maxId = isNull(),
sinceId = isNull(),
minId = isNull(),
limit = eq(20),
onlyMedia = eq(false), onlyMedia = eq(false),
excludeReplies = eq(false), excludeReplies = eq(false),
excludeReblogs = eq(false), excludeReblogs = eq(false),
pinned = eq(false), pinned = eq(false),
tagged = isNull(), tagged = isNull(),
includeFollowers = eq(false) includeFollowers = eq(false),
page = any()
) )
).doReturn( ).doReturn(
statusList statusList
@ -132,16 +133,13 @@ class AccountApiServiceImplTest {
val accountsStatuses = accountApiServiceImpl.accountsStatuses( val accountsStatuses = accountApiServiceImpl.accountsStatuses(
userid = userId, userid = userId,
maxId = null,
sinceId = null,
minId = null,
limit = 20,
onlyMedia = false, onlyMedia = false,
excludeReplies = false, excludeReplies = false,
excludeReblogs = false, excludeReblogs = false,
pinned = false, pinned = false,
tagged = null, tagged = null,
loginUser = null loginUser = null,
Page.of()
) )
assertThat(accountsStatuses).hasSize(1) assertThat(accountsStatuses).hasSize(1)
@ -156,31 +154,25 @@ class AccountApiServiceImplTest {
whenever( whenever(
statusQueryService.accountsStatus( statusQueryService.accountsStatus(
accountId = eq(userId), accountId = eq(userId),
maxId = isNull(),
sinceId = isNull(),
minId = isNull(),
limit = eq(20),
onlyMedia = eq(false), onlyMedia = eq(false),
excludeReplies = eq(false), excludeReplies = eq(false),
excludeReblogs = eq(false), excludeReblogs = eq(false),
pinned = eq(false), pinned = eq(false),
tagged = isNull(), tagged = isNull(),
includeFollowers = eq(false) includeFollowers = eq(false),
page = any()
) )
).doReturn(statusList) ).doReturn(statusList)
val accountsStatuses = accountApiServiceImpl.accountsStatuses( val accountsStatuses = accountApiServiceImpl.accountsStatuses(
userid = userId, userid = userId,
maxId = null,
sinceId = null,
minId = null,
limit = 20,
onlyMedia = false, onlyMedia = false,
excludeReplies = false, excludeReplies = false,
excludeReblogs = false, excludeReblogs = false,
pinned = false, pinned = false,
tagged = null, tagged = null,
loginUser = loginUser loginUser = loginUser,
Page.of()
) )
assertThat(accountsStatuses).hasSize(1) assertThat(accountsStatuses).hasSize(1)
@ -193,16 +185,13 @@ class AccountApiServiceImplTest {
whenever( whenever(
statusQueryService.accountsStatus( statusQueryService.accountsStatus(
accountId = eq(userId), accountId = eq(userId),
maxId = isNull(),
sinceId = isNull(),
minId = isNull(),
limit = eq(20),
onlyMedia = eq(false), onlyMedia = eq(false),
excludeReplies = eq(false), excludeReplies = eq(false),
excludeReblogs = eq(false), excludeReblogs = eq(false),
pinned = eq(false), pinned = eq(false),
tagged = isNull(), tagged = isNull(),
includeFollowers = eq(true) includeFollowers = eq(true),
page = any()
) )
).doReturn(statusList) ).doReturn(statusList)
@ -221,16 +210,13 @@ class AccountApiServiceImplTest {
val accountsStatuses = accountApiServiceImpl.accountsStatuses( val accountsStatuses = accountApiServiceImpl.accountsStatuses(
userid = userId, userid = userId,
maxId = null,
sinceId = null,
minId = null,
limit = 20,
onlyMedia = false, onlyMedia = false,
excludeReplies = false, excludeReplies = false,
excludeReblogs = false, excludeReblogs = false,
pinned = false, pinned = false,
tagged = null, tagged = null,
loginUser = loginUser loginUser = loginUser,
Page.of()
) )
assertThat(accountsStatuses).hasSize(1) assertThat(accountsStatuses).hasSize(1)