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
host: localhost
port: 27017
database: hideout
database: hideout-integration-test
h2:
console:
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

@ -52,11 +52,11 @@ open class Delete : Object, HasId, HasActor {
override fun toString(): String {
return "Delete(" +
"apObject=$apObject, " +
"published='$published', " +
"actor='$actor', " +
"id='$id'" +
")" +
" ${super.toString()}"
"apObject=$apObject, " +
"published='$published', " +
"actor='$actor', " +
"id='$id'" +
")" +
" ${super.toString()}"
}
}

View File

@ -16,12 +16,12 @@ open class Emoji(
override fun toString(): String {
return "Emoji(" +
"name='$name', " +
"id='$id', " +
"updated='$updated', " +
"icon=$icon" +
")" +
" ${super.toString()}"
"name='$name', " +
"id='$id', " +
"updated='$updated', " +
"icon=$icon" +
")" +
" ${super.toString()}"
}
override fun equals(other: Any?): Boolean {

View File

@ -36,10 +36,10 @@ open class Follow(
override fun toString(): String {
return "Follow(" +
"apObject='$apObject', " +
"actor='$actor', " +
"id=$id" +
")" +
" ${super.toString()}"
"apObject='$apObject', " +
"actor='$actor', " +
"id=$id" +
")" +
" ${super.toString()}"
}
}

View File

@ -62,17 +62,17 @@ constructor(
override fun toString(): String {
return "Note(" +
"id='$id', " +
"attributedTo='$attributedTo', " +
"content='$content', " +
"published='$published', " +
"to=$to, " +
"cc=$cc, " +
"sensitive=$sensitive, " +
"inReplyTo=$inReplyTo, " +
"attachment=$attachment, " +
"tag=$tag" +
")" +
" ${super.toString()}"
"id='$id', " +
"attributedTo='$attributedTo', " +
"content='$content', " +
"published='$published', " +
"to=$to, " +
"cc=$cc, " +
"sensitive=$sensitive, " +
"inReplyTo=$inReplyTo, " +
"attachment=$attachment, " +
"tag=$tag" +
")" +
" ${super.toString()}"
}
}

View File

@ -40,11 +40,11 @@ open class Undo(
override fun toString(): String {
return "Undo(" +
"actor='$actor', " +
"id='$id', " +
"apObject=$apObject, " +
"published=$published" +
")" +
" ${super.toString()}"
"actor='$actor', " +
"id='$id', " +
"apObject=$apObject, " +
"published=$published" +
")" +
" ${super.toString()}"
}
}

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

@ -212,27 +212,27 @@ data class Actor private constructor(
fun withLastPostAt(lastPostDate: Instant): Actor = this.copy(lastPostDate = lastPostDate)
override fun toString(): String {
return "Actor(" +
"id=$id, " +
"name='$name', " +
"domain='$domain', " +
"screenName='$screenName', " +
"description='$description', " +
"inbox='$inbox', " +
"outbox='$outbox', " +
"url='$url', " +
"publicKey='$publicKey', " +
"privateKey=$privateKey, " +
"createdAt=$createdAt, " +
"keyId='$keyId', " +
"followers=$followers, " +
"following=$following, " +
"instance=$instance, " +
"locked=$locked, " +
"followersCount=$followersCount, " +
"followingCount=$followingCount, " +
"postsCount=$postsCount, " +
"lastPostDate=$lastPostDate, " +
"emojis=$emojis" +
")"
"id=$id, " +
"name='$name', " +
"domain='$domain', " +
"screenName='$screenName', " +
"description='$description', " +
"inbox='$inbox', " +
"outbox='$outbox', " +
"url='$url', " +
"publicKey='$publicKey', " +
"privateKey=$privateKey, " +
"createdAt=$createdAt, " +
"keyId='$keyId', " +
"followers=$followers, " +
"following=$following, " +
"instance=$instance, " +
"locked=$locked, " +
"followersCount=$followersCount, " +
"followingCount=$followingCount, " +
"postsCount=$postsCount, " +
"lastPostDate=$lastPostDate, " +
"emojis=$emojis" +
")"
}
}

View File

@ -10,9 +10,9 @@ sealed class Emoji {
abstract fun id(): String
override fun toString(): String {
return "Emoji(" +
"domain='$domain', " +
"name='$name'" +
")"
"domain='$domain', " +
"name='$name'" +
")"
}
}

View File

@ -1,5 +1,8 @@
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]の永続化
*
@ -33,22 +36,16 @@ interface RelationshipRepository {
suspend fun findByTargetIdAndFollowing(targetId: Long, following: Boolean): List<Relationship>
@Suppress("LongParameterList", "FunctionMaxLength")
suspend fun findByTargetIdAndFollowRequestAndIgnoreFollowRequest(
maxId: Long?,
sinceId: Long?,
limit: Int,
targetId: Long,
followRequest: Boolean,
ignoreFollowRequest: Boolean
): List<Relationship>
ignoreFollowRequest: Boolean,
page: Page.PageByMaxId
): PaginationList<Relationship, Long>
@Suppress("FunctionMaxLength")
suspend fun findByActorIdAntMutingAndMaxIdAndSinceId(
suspend fun findByActorIdAndMuting(
actorId: Long,
muting: Boolean,
maxId: Long?,
sinceId: Long?,
limit: Int
): List<Relationship>
page: Page.PageByMaxId
): PaginationList<Relationship, Long>
}

View File

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

View File

@ -41,10 +41,10 @@ class HttpSignatureUser(
override fun toString(): String {
return "HttpSignatureUser(" +
"domain='$domain', " +
"id=$id" +
")" +
" ${super.toString()}"
"domain='$domain', " +
"id=$id" +
")" +
" ${super.toString()}"
}
companion object {

View File

@ -273,7 +273,7 @@ class ExposedOAuth2AuthorizationService(
oidcTokenIssuedAt,
oidcTokenExpiresAt,
oidcTokenMetadata.getValue(OAuth2Authorization.Token.CLAIMS_METADATA_NAME)
as MutableMap<String, Any>?
as MutableMap<String, Any>?
)
builder.token(oidcIdToken) { it.putAll(oidcTokenMetadata) }

View File

@ -28,9 +28,9 @@ class UserDetailsImpl(
) : User(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities) {
override fun toString(): String {
return "UserDetailsImpl(" +
"id=$id" +
")" +
" ${super.toString()}"
"id=$id" +
")" +
" ${super.toString()}"
}
override fun equals(other: Any?): Boolean {

View File

@ -29,7 +29,7 @@ class MediaServiceImpl(
val fileName = mediaRequest.file.name
logger.info(
"Media upload. filename:$fileName " +
"contentType:${mediaRequest.file.contentType}"
"contentType:${mediaRequest.file.contentType}"
)
val tempFile = Files.createTempFile("hideout-tmp-file", ".tmp")

View File

@ -8,7 +8,7 @@ data class ImageMediaProcessorConfiguration(
val thubnail: ImageMediaProcessorThumbnailConfiguration?,
val supportedType: List<String>?,
)
)
data class ImageMediaProcessorThumbnailConfiguration(
val generate: Boolean,

View File

@ -1,10 +1,12 @@
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.domain.mastodon.model.generated.Status
import dev.usbharu.hideout.mastodon.interfaces.api.status.StatusQuery
import dev.usbharu.hideout.mastodon.query.StatusQueryService
import org.jetbrains.exposed.sql.SortOrder
import org.jetbrains.exposed.sql.andWhere
import org.jetbrains.exposed.sql.selectAll
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
@ -13,15 +15,13 @@ import org.springframework.stereotype.Service
@Service
@ConditionalOnProperty("hideout.use-mongodb", havingValue = "false", matchIfMissing = true)
class ExposedGenerateTimelineService(private val statusQueryService: StatusQueryService) : GenerateTimelineService {
override suspend fun getTimeline(
forUserId: Long?,
localOnly: Boolean,
mediaOnly: Boolean,
maxId: Long?,
minId: Long?,
sinceId: Long?,
limit: Int
): List<Status> {
page: Page
): PaginationList<Status, Long> {
val query = Timelines.selectAll()
if (forUserId != null) {
@ -30,15 +30,7 @@ class ExposedGenerateTimelineService(private val statusQueryService: StatusQuery
if (localOnly) {
query.andWhere { Timelines.isLocal eq true }
}
if (maxId != null) {
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 result = query.withPagination(page, Timelines.id)
val statusQueries = result.map {
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
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 org.springframework.stereotype.Service
@Service
@Suppress("LongParameterList")
interface GenerateTimelineService {
suspend fun getTimeline(
forUserId: Long? = null,
localOnly: Boolean = false,
mediaOnly: Boolean = false,
maxId: Long? = null,
minId: Long? = null,
sinceId: Long? = null,
limit: Int = 20
): List<Status>
page: Page
): PaginationList<Status, Long>
}

View File

@ -1,5 +1,7 @@
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.domain.mastodon.model.generated.Status
import dev.usbharu.hideout.mastodon.interfaces.api.status.StatusQuery
@ -18,15 +20,13 @@ class MongoGenerateTimelineService(
private val mongoTemplate: MongoTemplate
) :
GenerateTimelineService {
override suspend fun getTimeline(
forUserId: Long?,
localOnly: Boolean,
mediaOnly: Boolean,
maxId: Long?,
minId: Long?,
sinceId: Long?,
limit: Int
): List<Status> {
page: Page
): PaginationList<Status, Long> {
val query = Query()
if (forUserId != null) {
@ -37,21 +37,23 @@ class MongoGenerateTimelineService(
val criteria = Criteria.where("isLocal").`is`(true)
query.addCriteria(criteria)
}
if (maxId != null) {
val criteria = Criteria.where("postId").lt(maxId)
query.addCriteria(criteria)
}
if (minId != null) {
val criteria = Criteria.where("postId").gt(minId)
query.addCriteria(criteria)
if (page.minId != null) {
page.minId?.let { query.addCriteria(Criteria.where("id").gt(it)) }
page.maxId?.let { query.addCriteria(Criteria.where("id").lt(it)) }
} else {
query.with(Sort.by(Sort.Direction.DESC, "createdAt"))
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"))
val timelines = mongoTemplate.find(query, Timeline::class.java)
return statusQueryService.findByPostIdsWithMediaIds(
val statuses = statusQueryService.findByPostIdsWithMediaIds(
timelines.map {
StatusQuery(
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
import dev.usbharu.hideout.application.infrastructure.exposed.Page
import dev.usbharu.hideout.application.infrastructure.exposed.PaginationList
interface MastodonNotificationRepository {
suspend fun save(mastodonNotification: MastodonNotification): MastodonNotification
suspend fun deleteById(id: Long)
suspend fun findById(id: Long): MastodonNotification?
@Suppress("LongParameterList", "FunctionMaxLength")
suspend fun findByUserIdAndMaxIdAndMinIdAndSinceIdAndInTypesAndInSourceActorId(
suspend fun findByUserIdAndInTypesAndInSourceActorId(
loginUser: Long,
maxId: Long?,
minId: Long?,
sinceId: Long?,
limit: Int,
typesTmp: MutableList<NotificationType>,
accountId: List<Long>
): List<MastodonNotification>
types: List<NotificationType>,
accountId: List<Long>,
page: Page
): PaginationList<MastodonNotification, Long>
suspend fun deleteByUserId(userId: Long)
suspend fun deleteByUserIdAndId(userId: Long, id: Long)

View File

@ -1,5 +1,8 @@
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.media.toMediaAttachments
import dev.usbharu.hideout.core.infrastructure.exposedrepository.*
@ -54,32 +57,20 @@ class StatusQueryServiceImpl : StatusQueryService {
override suspend fun accountsStatus(
accountId: Long,
maxId: Long?,
sinceId: Long?,
minId: Long?,
limit: Int,
onlyMedia: Boolean,
excludeReplies: Boolean,
excludeReblogs: Boolean,
pinned: Boolean,
tagged: String?,
includeFollowers: Boolean
): List<Status> {
includeFollowers: Boolean,
page: Page
): PaginationList<Status, Long> {
val query = Posts
.leftJoin(PostsMedia)
.leftJoin(Actors)
.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) {
query.andWhere { PostsMedia.mediaId.isNotNull() }
}
@ -95,7 +86,9 @@ class StatusQueryServiceImpl : StatusQueryService {
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 {
toStatus(it.first()).copy(
@ -105,7 +98,12 @@ class StatusQueryServiceImpl : StatusQueryService {
) 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 {
@ -139,7 +137,9 @@ class StatusQueryServiceImpl : StatusQueryService {
}
.map {
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 {
it
}

View File

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

View File

@ -1,5 +1,7 @@
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.MastodonNotificationRepository
import dev.usbharu.hideout.mastodon.domain.model.NotificationType
@ -26,35 +28,33 @@ class MongoMastodonNotificationRepositoryWrapper(
override suspend fun findById(id: Long): MastodonNotification? =
mongoMastodonNotificationRepository.findById(id).getOrNull()
override suspend fun findByUserIdAndMaxIdAndMinIdAndSinceIdAndInTypesAndInSourceActorId(
override suspend fun findByUserIdAndInTypesAndInSourceActorId(
loginUser: Long,
maxId: Long?,
minId: Long?,
sinceId: Long?,
limit: Int,
typesTmp: MutableList<NotificationType>,
accountId: List<Long>
): List<MastodonNotification> {
types: List<NotificationType>,
accountId: List<Long>,
page: Page
): PaginationList<MastodonNotification, Long> {
val query = Query()
if (maxId != null) {
val criteria = Criteria.where("id").lte(maxId)
query.addCriteria(criteria)
page.limit?.let { query.limit(it) }
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) {
val criteria = Criteria.where("id").gte(minId)
query.addCriteria(criteria)
}
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)
return PaginationList(
mastodonNotifications,
mastodonNotifications.firstOrNull()?.id,
mastodonNotifications.lastOrNull()?.id
)
}
override suspend fun deleteByUserId(userId: Long) {

View File

@ -1,6 +1,9 @@
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.infrastructure.exposed.Page
import dev.usbharu.hideout.application.infrastructure.exposed.toHttpHeader
import dev.usbharu.hideout.controller.mastodon.generated.AccountApi
import dev.usbharu.hideout.core.infrastructure.springframework.security.LoginUserContextHolder
import dev.usbharu.hideout.core.service.user.UserCreateDto
@ -19,7 +22,8 @@ import java.net.URI
class MastodonAccountApiController(
private val accountApiService: AccountApiService,
private val transaction: Transaction,
private val loginUserContextHolder: LoginUserContextHolder
private val loginUserContextHolder: LoginUserContextHolder,
private val applicationConfig: ApplicationConfig
) : AccountApi {
override suspend fun apiV1AccountsIdFollowPost(
@ -68,20 +72,31 @@ class MastodonAccountApiController(
tagged: String?
): ResponseEntity<Flow<Status>> = runBlocking {
val userid = loginUserContextHolder.getLoginUserId()
val statusFlow = accountApiService.accountsStatuses(
val statuses = accountApiService.accountsStatuses(
userid = id.toLong(),
maxId = maxId?.toLongOrNull(),
sinceId = sinceId?.toLongOrNull(),
minId = minId?.toLongOrNull(),
limit = limit,
onlyMedia = onlyMedia,
excludeReplies = excludeReplies,
excludeReblogs = excludeReblogs,
pinned = pinned,
tagged = tagged,
loginUser = userid
).asFlow()
ResponseEntity.ok(statusFlow)
loginUser = userid,
page = Page.of(
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(
@ -128,8 +143,7 @@ class MastodonAccountApiController(
return ResponseEntity.ok(removeFromFollowers)
}
override suspend fun apiV1AccountsUpdateCredentialsPatch(updateCredentials: UpdateCredentials?):
ResponseEntity<Account> {
override suspend fun apiV1AccountsUpdateCredentialsPatch(updateCredentials: UpdateCredentials?): ResponseEntity<Account> {
val userid = loginUserContextHolder.getLoginUserId()
val removeFromFollowers = accountApiService.updateProfile(userid, updateCredentials)
@ -157,10 +171,27 @@ class MastodonAccountApiController(
runBlocking {
val userid = loginUserContextHolder.getLoginUserId()
val accountFlow =
accountApiService.followRequests(userid, maxId?.toLong(), sinceId?.toLong(), limit ?: 20, false)
.asFlow()
ResponseEntity.ok(accountFlow)
val followRequests = accountApiService.followRequests(
userid,
false,
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> {
@ -183,9 +214,21 @@ class MastodonAccountApiController(
runBlocking {
val userid = loginUserContextHolder.getLoginUserId()
val unmute =
accountApiService.mutesAccount(userid, maxId?.toLong(), sinceId?.toLong(), limit ?: 20).asFlow()
val mutes =
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
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.core.infrastructure.springframework.security.LoginUserContextHolder
import dev.usbharu.hideout.domain.mastodon.model.generated.Notification
@ -14,7 +17,8 @@ import org.springframework.stereotype.Controller
@Controller
class MastodonNotificationApiController(
private val loginUserContextHolder: LoginUserContextHolder,
private val notificationApiService: NotificationApiService
private val notificationApiService: NotificationApiService,
private val applicationConfig: ApplicationConfig
) : NotificationsApi {
override suspend fun apiV1NotificationsClearPost(): ResponseEntity<Any> {
notificationApiService.clearAll(loginUserContextHolder.getLoginUserId())
@ -30,17 +34,27 @@ class MastodonNotificationApiController(
excludeTypes: List<String>?,
accountId: List<String>?
): ResponseEntity<Flow<Notification>> = runBlocking {
val notificationFlow = notificationApiService.notifications(
val notifications = notificationApiService.notifications(
loginUser = loginUserContextHolder.getLoginUserId(),
maxId = maxId?.toLong(),
minId = minId?.toLong(),
sinceId = sinceId?.toLong(),
limit = limit ?: 20,
types = types.orEmpty().mapNotNull { NotificationType.parse(it) },
excludeTypes = excludeTypes.orEmpty().mapNotNull { NotificationType.parse(it) },
accountId = accountId.orEmpty().mapNotNull { it.toLongOrNull() }
).asFlow()
ResponseEntity.ok(notificationFlow)
accountId = accountId.orEmpty().mapNotNull { it.toLongOrNull() },
page = Page.of(
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> {

View File

@ -66,16 +66,16 @@ class StatusesRequest {
override fun toString(): String {
return "StatusesRequest(" +
"status=$status, " +
"media_ids=$media_ids, " +
"poll=$poll, " +
"in_reply_to_id=$in_reply_to_id, " +
"sensitive=$sensitive, " +
"spoiler_text=$spoiler_text, " +
"visibility=$visibility, " +
"language=$language, " +
"scheduled_at=$scheduled_at" +
")"
"status=$status, " +
"media_ids=$media_ids, " +
"poll=$poll, " +
"in_reply_to_id=$in_reply_to_id, " +
"sensitive=$sensitive, " +
"spoiler_text=$spoiler_text, " +
"visibility=$visibility, " +
"language=$language, " +
"scheduled_at=$scheduled_at" +
")"
}
@Suppress("EnumNaming", "EnumEntryNameCase")

View File

@ -1,5 +1,8 @@
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.core.infrastructure.springframework.security.LoginUserContextHolder
import dev.usbharu.hideout.domain.mastodon.model.generated.Status
@ -14,7 +17,8 @@ import org.springframework.stereotype.Controller
@Controller
class MastodonTimelineApiController(
private val timelineApiService: TimelineApiService,
private val loginUserContextHolder: LoginUserContextHolder
private val loginUserContextHolder: LoginUserContextHolder,
private val applicationConfig: ApplicationConfig,
) : TimelineApi {
override fun apiV1TimelinesHomeGet(
maxId: String?,
@ -24,12 +28,22 @@ class MastodonTimelineApiController(
): ResponseEntity<Flow<Status>> = runBlocking {
val homeTimeline = timelineApiService.homeTimeline(
userId = loginUserContextHolder.getLoginUserId(),
maxId = maxId?.toLongOrNull(),
minId = minId?.toLongOrNull(),
sinceId = sinceId?.toLongOrNull(),
limit = limit ?: 20
page = Page.of(
maxId = maxId?.toLongOrNull(),
minId = minId?.toLongOrNull(),
sinceId = sinceId?.toLongOrNull(),
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(
@ -45,11 +59,21 @@ class MastodonTimelineApiController(
localOnly = local ?: false,
remoteOnly = remote ?: false,
mediaOnly = onlyMedia ?: false,
maxId = maxId?.toLongOrNull(),
minId = minId?.toLongOrNull(),
sinceId = sinceId?.toLongOrNull(),
limit = limit ?: 20
page = Page.of(
maxId = maxId?.toLongOrNull(),
minId = minId?.toLongOrNull(),
sinceId = sinceId?.toLongOrNull(),
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
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.mastodon.interfaces.api.status.StatusQuery
@ -7,35 +9,17 @@ interface StatusQueryService {
suspend fun findByPostIds(ids: List<Long>): 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")
suspend fun accountsStatus(
accountId: Long,
maxId: Long? = null,
sinceId: Long? = null,
minId: Long? = null,
limit: Int,
onlyMedia: Boolean = false,
excludeReplies: Boolean = false,
excludeReblogs: Boolean = false,
pinned: Boolean = false,
tagged: String? = null,
includeFollowers: Boolean = false
): List<Status>
tagged: String?,
includeFollowers: Boolean = false,
page: Page
): PaginationList<Status, Long>
suspend fun findByPostId(id: Long): Status
}

View File

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

View File

@ -19,7 +19,7 @@ class InstanceApiServiceImpl(private val applicationConfig: ApplicationConfig) :
title = "Hideout Server",
shortDescription = "Hideout test server",
description = "This server is operated for testing of Hideout." +
" We are not responsible for any events that occur when associating with this server",
" We are not responsible for any events that occur when associating with this server",
email = "i@usbharu.dev",
version = "0.0.1",
urls = V1InstanceUrls("wss://${url.host}"),

View File

@ -1,20 +1,19 @@
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.mastodon.domain.model.NotificationType
interface NotificationApiService {
@Suppress("LongParameterList")
suspend fun notifications(
loginUser: Long,
maxId: Long?,
minId: Long?,
sinceId: Long?,
limit: Int,
types: List<NotificationType>,
excludeTypes: List<NotificationType>,
accountId: List<Long>
): List<Notification>
accountId: List<Long>,
page: Page
): PaginationList<Notification, Long>
suspend fun fingById(loginUser: Long, notificationId: Long): Notification?

View File

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

View File

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

View File

@ -7,9 +7,9 @@ class LruCache<K, V>(private val maxSize: Int) : LinkedHashMap<K, V>(15, 0.75f,
override fun removeEldestEntry(eldest: MutableMap.MutableEntry<K, V>?): Boolean = size > maxSize
override fun toString(): String {
return "LruCache(" +
"maxSize=$maxSize" +
")" +
" ${super.toString()}"
"maxSize=$maxSize" +
")" +
" ${super.toString()}"
}
companion object {

View File

@ -573,6 +573,7 @@ paths:
required: false
schema:
type: integer
nullable: true
default: 20
- in: query
name: only_media
@ -1051,7 +1052,6 @@ components:
- group
- discoverable
- created_at
- last_status_at
- statuses_count
- followers_count
- followers_count
@ -1279,6 +1279,7 @@ components:
language:
type: string
nullable: true
default: null
text:
type: string
nullable: true
@ -1316,11 +1317,7 @@ components:
- favourites_count
- replies_count
- url
- in_reply_to_id
- in_reply_to_account_id
- language
- text
- edited_at
MediaAttachment:
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
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.domain.mastodon.model.generated.AccountSource
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.setup.MockMvcBuilders
import utils.TestTransaction
import java.net.URL
@ExtendWith(MockitoExtension::class)
class MastodonAccountApiControllerTest {
@ -41,6 +43,9 @@ class MastodonAccountApiControllerTest {
@Mock
private lateinit var accountApiService: AccountApiService
@Spy
private val applicationConfig: ApplicationConfig = ApplicationConfig(URL("https://example.com"))
@InjectMocks
private lateinit var mastodonAccountApiController: MastodonAccountApiController

View File

@ -1,6 +1,8 @@
package dev.usbharu.hideout.mastodon.interfaces.api.timeline
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.domain.mastodon.model.generated.Account
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.post
import org.springframework.test.web.servlet.setup.MockMvcBuilders
import java.net.URL
@ExtendWith(MockitoExtension::class)
class MastodonTimelineApiControllerTest {
@ -31,6 +34,9 @@ class MastodonTimelineApiControllerTest {
@Mock
private lateinit var timelineApiService: TimelineApiService
@Spy
private val applicationConfig: ApplicationConfig = ApplicationConfig(URL("https://example.com"))
@InjectMocks
private lateinit var mastodonTimelineApiController: MastodonTimelineApiController
@ -41,107 +47,109 @@ class MastodonTimelineApiControllerTest {
mockMvc = MockMvcBuilders.standaloneSetup(mastodonTimelineApiController).build()
}
val statusList = listOf<Status>(
Status(
id = "",
uri = "",
createdAt = "",
account = Account(
val statusList = PaginationList<Status, Long>(
listOf<Status>(
Status(
id = "",
username = "",
acct = "",
url = "",
displayName = "",
note = "",
avatar = "",
avatarStatic = "",
header = "",
headerStatic = "",
locked = false,
fields = emptyList(),
emojis = emptyList(),
bot = false,
group = false,
discoverable = true,
uri = "",
createdAt = "",
lastStatusAt = "",
statusesCount = 0,
followersCount = 0,
noindex = false,
moved = false,
suspendex = false,
limited = false,
followingCount = 0
),
content = "",
visibility = Status.Visibility.public,
sensitive = false,
spoilerText = "",
mediaAttachments = emptyList(),
mentions = emptyList(),
tags = emptyList(),
emojis = emptyList(),
reblogsCount = 0,
favouritesCount = 0,
repliesCount = 0,
url = "https://example.com",
inReplyToId = null,
inReplyToAccountId = null,
language = "ja_JP",
text = "Test",
editedAt = null
account = Account(
id = "",
username = "",
acct = "",
url = "",
displayName = "",
note = "",
avatar = "",
avatarStatic = "",
header = "",
headerStatic = "",
locked = false,
fields = emptyList(),
emojis = emptyList(),
bot = false,
group = false,
discoverable = true,
createdAt = "",
lastStatusAt = "",
statusesCount = 0,
followersCount = 0,
noindex = false,
moved = false,
suspendex = false,
limited = false,
followingCount = 0
),
content = "",
visibility = Status.Visibility.public,
sensitive = false,
spoilerText = "",
mediaAttachments = emptyList(),
mentions = emptyList(),
tags = emptyList(),
emojis = emptyList(),
reblogsCount = 0,
favouritesCount = 0,
repliesCount = 0,
url = "https://example.com",
inReplyToId = null,
inReplyToAccountId = null,
language = "ja_JP",
text = "Test",
editedAt = null
),
Status(
id = "",
uri = "",
createdAt = "",
account = Account(
),
Status(
id = "",
username = "",
acct = "",
url = "",
displayName = "",
note = "",
avatar = "",
avatarStatic = "",
header = "",
headerStatic = "",
locked = false,
fields = emptyList(),
emojis = emptyList(),
bot = false,
group = false,
discoverable = true,
uri = "",
createdAt = "",
lastStatusAt = "",
statusesCount = 0,
followersCount = 0,
noindex = false,
moved = false,
suspendex = false,
limited = false,
followingCount = 0
),
content = "",
visibility = Status.Visibility.public,
sensitive = false,
spoilerText = "",
mediaAttachments = emptyList(),
mentions = emptyList(),
tags = emptyList(),
emojis = emptyList(),
reblogsCount = 0,
favouritesCount = 0,
repliesCount = 0,
url = "https://example.com",
inReplyToId = null,
inReplyToAccountId = null,
language = "ja_JP",
text = "Test",
editedAt = null
account = Account(
id = "",
username = "",
acct = "",
url = "",
displayName = "",
note = "",
avatar = "",
avatarStatic = "",
header = "",
headerStatic = "",
locked = false,
fields = emptyList(),
emojis = emptyList(),
bot = false,
group = false,
discoverable = true,
createdAt = "",
lastStatusAt = "",
statusesCount = 0,
followersCount = 0,
noindex = false,
moved = false,
suspendex = false,
limited = false,
followingCount = 0
),
content = "",
visibility = Status.Visibility.public,
sensitive = false,
spoilerText = "",
mediaAttachments = emptyList(),
mentions = emptyList(),
tags = emptyList(),
emojis = emptyList(),
reblogsCount = 0,
favouritesCount = 0,
repliesCount = 0,
url = "https://example.com",
inReplyToId = null,
inReplyToAccountId = null,
language = "ja_JP",
text = "Test",
editedAt = null
)
)
), null, null
)
@Test
@ -156,10 +164,7 @@ class MastodonTimelineApiControllerTest {
whenever(
timelineApiService.homeTimeline(
eq(1234),
eq(123456),
eq(54321),
eq(1234567),
eq(20)
any()
)
).doReturn(statusList)
@ -183,10 +188,7 @@ class MastodonTimelineApiControllerTest {
whenever(
timelineApiService.homeTimeline(
eq(1234),
isNull(),
isNull(),
isNull(),
eq(20)
any()
)
).doReturn(statusList)
@ -213,10 +215,7 @@ class MastodonTimelineApiControllerTest {
localOnly = eq(false),
remoteOnly = eq(true),
mediaOnly = eq(false),
maxId = eq(1234),
minId = eq(4321),
sinceId = eq(12345),
limit = eq(20)
any()
)
).doAnswer {
println(it.arguments.joinToString())
@ -245,10 +244,7 @@ class MastodonTimelineApiControllerTest {
localOnly = eq(false),
remoteOnly = eq(false),
mediaOnly = eq(false),
maxId = isNull(),
minId = isNull(),
sinceId = isNull(),
limit = eq(20)
any()
)
).doAnswer {
println(it.arguments.joinToString())

View File

@ -1,6 +1,8 @@
package dev.usbharu.hideout.mastodon.service.account
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.relationship.RelationshipRepository
import dev.usbharu.hideout.core.query.FollowerQueryService
@ -48,63 +50,65 @@ class AccountApiServiceImplTest {
@Mock
private lateinit var relationshipRepository: RelationshipRepository
@Mock
private lateinit var mediaService: MediaService
@InjectMocks
private lateinit var accountApiServiceImpl: AccountApiServiceImpl
private val statusList = listOf(
Status(
id = "",
uri = "",
createdAt = "",
account = Account(
private val statusList = PaginationList<Status, Long>(
listOf(
Status(
id = "",
username = "",
acct = "",
url = "",
displayName = "",
note = "",
avatar = "",
avatarStatic = "",
header = "",
headerStatic = "",
locked = false,
fields = emptyList(),
emojis = emptyList(),
bot = false,
group = false,
discoverable = true,
uri = "",
createdAt = "",
lastStatusAt = "",
statusesCount = 0,
followersCount = 0,
noindex = false,
moved = false,
suspendex = false,
limited = false,
followingCount = 0
),
content = "",
visibility = Status.Visibility.public,
sensitive = false,
spoilerText = "",
mediaAttachments = emptyList(),
mentions = emptyList(),
tags = emptyList(),
emojis = emptyList(),
reblogsCount = 0,
favouritesCount = 0,
repliesCount = 0,
url = "https://example.com",
inReplyToId = null,
inReplyToAccountId = null,
language = "ja_JP",
text = "Test",
editedAt = null
)
account = Account(
id = "",
username = "",
acct = "",
url = "",
displayName = "",
note = "",
avatar = "",
avatarStatic = "",
header = "",
headerStatic = "",
locked = false,
fields = emptyList(),
emojis = emptyList(),
bot = false,
group = false,
discoverable = true,
createdAt = "",
lastStatusAt = "",
statusesCount = 0,
followersCount = 0,
noindex = false,
moved = false,
suspendex = false,
limited = false,
followingCount = 0
),
content = "",
visibility = Status.Visibility.public,
sensitive = false,
spoilerText = "",
mediaAttachments = emptyList(),
mentions = emptyList(),
tags = emptyList(),
emojis = emptyList(),
reblogsCount = 0,
favouritesCount = 0,
repliesCount = 0,
url = "https://example.com",
inReplyToId = null,
inReplyToAccountId = null,
language = "ja_JP",
text = "Test",
editedAt = null
)
), null, null
)
@Test
@ -114,16 +118,13 @@ class AccountApiServiceImplTest {
whenever(
statusQueryService.accountsStatus(
accountId = eq(userId),
maxId = isNull(),
sinceId = isNull(),
minId = isNull(),
limit = eq(20),
onlyMedia = eq(false),
excludeReplies = eq(false),
excludeReblogs = eq(false),
pinned = eq(false),
tagged = isNull(),
includeFollowers = eq(false)
includeFollowers = eq(false),
page = any()
)
).doReturn(
statusList
@ -132,16 +133,13 @@ class AccountApiServiceImplTest {
val accountsStatuses = accountApiServiceImpl.accountsStatuses(
userid = userId,
maxId = null,
sinceId = null,
minId = null,
limit = 20,
onlyMedia = false,
excludeReplies = false,
excludeReblogs = false,
pinned = false,
tagged = null,
loginUser = null
loginUser = null,
Page.of()
)
assertThat(accountsStatuses).hasSize(1)
@ -156,31 +154,25 @@ class AccountApiServiceImplTest {
whenever(
statusQueryService.accountsStatus(
accountId = eq(userId),
maxId = isNull(),
sinceId = isNull(),
minId = isNull(),
limit = eq(20),
onlyMedia = eq(false),
excludeReplies = eq(false),
excludeReblogs = eq(false),
pinned = eq(false),
tagged = isNull(),
includeFollowers = eq(false)
includeFollowers = eq(false),
page = any()
)
).doReturn(statusList)
val accountsStatuses = accountApiServiceImpl.accountsStatuses(
userid = userId,
maxId = null,
sinceId = null,
minId = null,
limit = 20,
onlyMedia = false,
excludeReplies = false,
excludeReblogs = false,
pinned = false,
tagged = null,
loginUser = loginUser
loginUser = loginUser,
Page.of()
)
assertThat(accountsStatuses).hasSize(1)
@ -193,16 +185,13 @@ class AccountApiServiceImplTest {
whenever(
statusQueryService.accountsStatus(
accountId = eq(userId),
maxId = isNull(),
sinceId = isNull(),
minId = isNull(),
limit = eq(20),
onlyMedia = eq(false),
excludeReplies = eq(false),
excludeReblogs = eq(false),
pinned = eq(false),
tagged = isNull(),
includeFollowers = eq(true)
includeFollowers = eq(true),
page = any()
)
).doReturn(statusList)
@ -221,16 +210,13 @@ class AccountApiServiceImplTest {
val accountsStatuses = accountApiServiceImpl.accountsStatuses(
userid = userId,
maxId = null,
sinceId = null,
minId = null,
limit = 20,
onlyMedia = false,
excludeReplies = false,
excludeReblogs = false,
pinned = false,
tagged = null,
loginUser = loginUser
loginUser = loginUser,
Page.of()
)
assertThat(accountsStatuses).hasSize(1)