mirror of https://github.com/usbharu/Hideout.git
Merge pull request #246 from usbharu/feature/post-decoration
Feature/post decoration
This commit is contained in:
commit
99081d6483
|
@ -215,6 +215,8 @@ dependencies {
|
||||||
implementation("org.flywaydb:flyway-core")
|
implementation("org.flywaydb:flyway-core")
|
||||||
|
|
||||||
implementation("dev.usbharu:emoji-kt:2.0.0")
|
implementation("dev.usbharu:emoji-kt:2.0.0")
|
||||||
|
implementation("org.jsoup:jsoup:1.17.2")
|
||||||
|
implementation("com.googlecode.owasp-java-html-sanitizer:owasp-java-html-sanitizer:20220608.1")
|
||||||
|
|
||||||
implementation("io.ktor:ktor-client-logging-jvm:$ktor_version")
|
implementation("io.ktor:ktor-client-logging-jvm:$ktor_version")
|
||||||
|
|
||||||
|
|
|
@ -21,6 +21,8 @@ insert into relationships (actor_id, target_actor_id, following, blocking, mutin
|
||||||
ignore_follow_request)
|
ignore_follow_request)
|
||||||
VALUES (9, 8, true, false, false, false, false);
|
VALUES (9, 8, true, false, false, false, false);
|
||||||
|
|
||||||
insert into POSTS (ID, ACTOR_ID, OVERVIEW, TEXT, CREATED_AT, VISIBILITY, URL, REPLY_ID, REPOST_ID, SENSITIVE, AP_ID)
|
insert into POSTS (ID, ACTOR_ID, OVERVIEW, CONTENT, TEXT, CREATED_AT, VISIBILITY, URL, REPLY_ID, REPOST_ID, SENSITIVE,
|
||||||
VALUES (1239, 8, null, 'test post', 12345680, 2, 'https://example.com/users/test-user8/posts/1239', null, null, false,
|
AP_ID)
|
||||||
|
VALUES (1239, 8, null, '<p>test post</p>', 'test post', 12345680, 2, 'https://example.com/users/test-user8/posts/1239',
|
||||||
|
null, null, false,
|
||||||
'https://example.com/users/test-user8/posts/1239');
|
'https://example.com/users/test-user8/posts/1239');
|
||||||
|
|
|
@ -21,7 +21,9 @@ insert into relationships (actor_id, target_actor_id, following, blocking, mutin
|
||||||
ignore_follow_request)
|
ignore_follow_request)
|
||||||
VALUES (5, 4, true, false, false, false, false);
|
VALUES (5, 4, true, false, false, false, false);
|
||||||
|
|
||||||
insert into POSTS (ID, "actor_id", OVERVIEW, TEXT, "created_at", VISIBILITY, URL, "repost_id", "reply_id", SENSITIVE,
|
insert into POSTS (ID, "actor_id", OVERVIEW, CONTENT, TEXT, "created_at", VISIBILITY, URL, "repost_id", "reply_id",
|
||||||
|
SENSITIVE,
|
||||||
AP_ID)
|
AP_ID)
|
||||||
VALUES (1237, 4, null, 'test post', 12345680, 0, 'https://example.com/users/test-user4/posts/1237', null, null, false,
|
VALUES (1237, 4, null, '<p>test post</p>', 'test post', 12345680, 0, 'https://example.com/users/test-user4/posts/1237',
|
||||||
|
null, null, false,
|
||||||
'https://example.com/users/test-user4/posts/1237');
|
'https://example.com/users/test-user4/posts/1237');
|
||||||
|
|
|
@ -21,7 +21,9 @@ insert into relationships (actor_id, target_actor_id, following, blocking, mutin
|
||||||
ignore_follow_request)
|
ignore_follow_request)
|
||||||
VALUES (7, 6, true, false, false, false, false);
|
VALUES (7, 6, true, false, false, false, false);
|
||||||
|
|
||||||
insert into POSTS (ID, "actor_ID", OVERVIEW, TEXT, "CREATED_AT", VISIBILITY, URL, "REPOST_ID", "REPLY_ID", SENSITIVE,
|
insert into POSTS (ID, "actor_ID", OVERVIEW, CONTENT, TEXT, "CREATED_AT", VISIBILITY, URL, "REPOST_ID", "REPLY_ID",
|
||||||
|
SENSITIVE,
|
||||||
AP_ID)
|
AP_ID)
|
||||||
VALUES (1238, 6, null, 'test post', 12345680, 1, 'https://example.com/users/test-user6/posts/1238', null, null, false,
|
VALUES (1238, 6, null, '<p>test post</p>', 'test post', 12345680, 1, 'https://example.com/users/test-user6/posts/1238',
|
||||||
|
null, null, false,
|
||||||
'https://example.com/users/test-user6/posts/1238');
|
'https://example.com/users/test-user6/posts/1238');
|
||||||
|
|
|
@ -9,9 +9,11 @@ VALUES (11, 'test-user11', 'example.com', 'Im test-user11.', 'THis account is te
|
||||||
'https://example.com/users/test-user11#pubkey', 'https://example.com/users/test-user11/following',
|
'https://example.com/users/test-user11#pubkey', 'https://example.com/users/test-user11/following',
|
||||||
'https://example.com/users/test-user11/followers', null, false, 0, 0, 0, null);
|
'https://example.com/users/test-user11/followers', null, false, 0, 0, 0, null);
|
||||||
|
|
||||||
insert into POSTS (id, actor_id, overview, text, created_at, visibility, url, repost_id, reply_id, sensitive, ap_id,
|
insert into POSTS (id, actor_id, overview, content, text, created_at, visibility, url, repost_id, reply_id, sensitive,
|
||||||
|
ap_id,
|
||||||
deleted)
|
deleted)
|
||||||
VALUES (1242, 11, null, 'test post', 12345680, 0, 'https://example.com/users/test-user11/posts/1242', null, null, false,
|
VALUES (1242, 11, null, '<p>test post</p>', 'test post', 12345680, 0,
|
||||||
|
'https://example.com/users/test-user11/posts/1242', null, null, false,
|
||||||
'https://example.com/users/test-user11/posts/1242', false);
|
'https://example.com/users/test-user11/posts/1242', false);
|
||||||
|
|
||||||
insert into MEDIA (ID, NAME, URL, REMOTE_URL, THUMBNAIL_URL, TYPE, BLURHASH, MIME_TYPE, DESCRIPTION)
|
insert into MEDIA (ID, NAME, URL, REMOTE_URL, THUMBNAIL_URL, TYPE, BLURHASH, MIME_TYPE, DESCRIPTION)
|
||||||
|
|
|
@ -9,9 +9,12 @@ VALUES (10, 'test-user10', 'example.com', 'Im test-user10.', 'THis account is te
|
||||||
'https://example.com/users/test-user10#pubkey', 'https://example.com/users/test-user10/following',
|
'https://example.com/users/test-user10#pubkey', 'https://example.com/users/test-user10/following',
|
||||||
'https://example.com/users/test-user10/followers', null, false, 0, 0, 0, null);
|
'https://example.com/users/test-user10/followers', null, false, 0, 0, 0, null);
|
||||||
|
|
||||||
insert into POSTS (id, actor_id, overview, text, created_at, visibility, url, repost_id, reply_id, sensitive, ap_id,
|
insert into POSTS (id, actor_id, overview, content, text, created_at, visibility, url, repost_id, reply_id, sensitive,
|
||||||
|
ap_id,
|
||||||
deleted)
|
deleted)
|
||||||
VALUES (1240, 10, null, 'test post', 12345680, 0, 'https://example.com/users/test-user10/posts/1240', null, null, false,
|
VALUES (1240, 10, null, '<p>test post</p>', 'test post', 12345680, 0,
|
||||||
|
'https://example.com/users/test-user10/posts/1240', null, null, false,
|
||||||
'https://example.com/users/test-user10/posts/1240', false),
|
'https://example.com/users/test-user10/posts/1240', false),
|
||||||
(1241, 10, null, 'test post', 12345680, 0, 'https://example.com/users/test-user10/posts/1241', null, 1240, false,
|
(1241, 10, null, '<p>test post</p>', 'test post', 12345680, 0,
|
||||||
|
'https://example.com/users/test-user10/posts/1241', null, 1240, false,
|
||||||
'https://example.com/users/test-user10/posts/1241', false);
|
'https://example.com/users/test-user10/posts/1241', false);
|
||||||
|
|
|
@ -9,7 +9,9 @@ VALUES (3, 'test-user3', 'example.com', 'Im test user3.', 'THis account is test
|
||||||
'https://example.com/users/test-user3#pubkey', 'https://example.com/users/test-user3/following',
|
'https://example.com/users/test-user3#pubkey', 'https://example.com/users/test-user3/following',
|
||||||
'https://example.com/users/test-user3/followers', null, false, 0, 0, 0, null);
|
'https://example.com/users/test-user3/followers', null, false, 0, 0, 0, null);
|
||||||
|
|
||||||
insert into POSTS (id, actor_id, overview, text, created_at, visibility, url, repost_id, reply_id, sensitive, ap_id,
|
insert into POSTS (id, actor_id, overview, content, text, created_at, visibility, url, repost_id, reply_id, sensitive,
|
||||||
|
ap_id,
|
||||||
deleted)
|
deleted)
|
||||||
VALUES (1236, 3, null, 'test post', 12345680, 2, 'https://example.com/users/test-user3/posts/1236', null, null, false,
|
VALUES (1236, 3, null, '<p>test post</p>', 'test post', 12345680, 2, 'https://example.com/users/test-user3/posts/1236',
|
||||||
|
null, null, false,
|
||||||
'https://example.com/users/test-user3/posts/1236', false)
|
'https://example.com/users/test-user3/posts/1236', false)
|
||||||
|
|
|
@ -9,7 +9,9 @@ VALUES (1, 'test-user', 'example.com', 'Im test user.', 'THis account is test us
|
||||||
'https://example.com/users/test-user#pubkey', 'https://example.com/users/test-user/following',
|
'https://example.com/users/test-user#pubkey', 'https://example.com/users/test-user/following',
|
||||||
'https://example.com/users/test-users/followers', null, false, 0, 0, 0, null);
|
'https://example.com/users/test-users/followers', null, false, 0, 0, 0, null);
|
||||||
|
|
||||||
insert into POSTS (id, actor_id, overview, text, created_at, visibility, url, repost_id, reply_id, sensitive, ap_id,
|
insert into POSTS (id, actor_id, overview, content, text, created_at, visibility, url, repost_id, reply_id, sensitive,
|
||||||
|
ap_id,
|
||||||
deleted)
|
deleted)
|
||||||
VALUES (1234, 1, null, 'test post', 12345680, 0, 'https://example.com/users/test-user/posts/1234', null, null, false,
|
VALUES (1234, 1, null, '<p>test post</p>', 'test post', 12345680, 0, 'https://example.com/users/test-user/posts/1234',
|
||||||
|
null, null, false,
|
||||||
'https://example.com/users/test-user/posts/1234', false)
|
'https://example.com/users/test-user/posts/1234', false)
|
||||||
|
|
|
@ -9,7 +9,9 @@ VALUES (2, 'test-user2', 'example.com', 'Im test user2.', 'THis account is test
|
||||||
'https://example.com/users/test-user2#pubkey', 'https://example.com/users/test-user2/following',
|
'https://example.com/users/test-user2#pubkey', 'https://example.com/users/test-user2/following',
|
||||||
'https://example.com/users/test-user2/followers', null, false, 0, 0, 0, null);
|
'https://example.com/users/test-user2/followers', null, false, 0, 0, 0, null);
|
||||||
|
|
||||||
insert into POSTS (id, actor_id, overview, text, created_at, visibility, url, repost_id, reply_id, sensitive, ap_id,
|
insert into POSTS (id, actor_id, overview, content, text, created_at, visibility, url, repost_id, reply_id, sensitive,
|
||||||
|
ap_id,
|
||||||
deleted)
|
deleted)
|
||||||
VALUES (1235, 2, null, 'test post', 12345680, 1, 'https://example.com/users/test-user2/posts/1235', null, null, false,
|
VALUES (1235, 2, null, '<p>test post</p>', 'test post', 12345680, 1, 'https://example.com/users/test-user2/posts/1235',
|
||||||
|
null, null, false,
|
||||||
'https://example.com/users/test-user2/posts/1235', false)
|
'https://example.com/users/test-user2/posts/1235', false)
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
insert into posts (id, actor_id, overview, text, created_at, visibility, url, repost_id, reply_id, sensitive, ap_id)
|
insert into posts (id, actor_id, overview, content, text, created_at, visibility, url, repost_id, reply_id, sensitive,
|
||||||
VALUES (1, 1, null, 'hello', 1234455, 0, 'https://localhost/users/1/posts/1', null, null, false,
|
ap_id)
|
||||||
|
VALUES (1, 1, null, '<p>test post</p>', 'hello', 1234455, 0, 'https://localhost/users/1/posts/1', null, null, false,
|
||||||
'https://users/1/posts/1');
|
'https://users/1/posts/1');
|
||||||
|
|
|
@ -128,7 +128,7 @@ class APNoteServiceImpl(
|
||||||
postBuilder.of(
|
postBuilder.of(
|
||||||
id = postRepository.generateId(),
|
id = postRepository.generateId(),
|
||||||
actorId = person.second.id,
|
actorId = person.second.id,
|
||||||
text = note.content,
|
content = note.content,
|
||||||
createdAt = Instant.parse(note.published).toEpochMilli(),
|
createdAt = Instant.parse(note.published).toEpochMilli(),
|
||||||
visibility = visibility,
|
visibility = visibility,
|
||||||
url = note.id,
|
url = note.id,
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
package dev.usbharu.hideout.application.config
|
||||||
|
|
||||||
|
import org.owasp.html.HtmlPolicyBuilder
|
||||||
|
import org.owasp.html.PolicyFactory
|
||||||
|
import org.springframework.context.annotation.Bean
|
||||||
|
import org.springframework.context.annotation.Configuration
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
class HtmlSanitizeConfig {
|
||||||
|
@Bean
|
||||||
|
fun policy(): PolicyFactory {
|
||||||
|
return HtmlPolicyBuilder()
|
||||||
|
.allowElements("p")
|
||||||
|
.allowElements("a")
|
||||||
|
.allowElements("br")
|
||||||
|
.allowAttributes("href").onElements("a")
|
||||||
|
.allowUrlProtocols("http", "https")
|
||||||
|
.allowElements({ _, _ -> return@allowElements "p" }, "h1", "h2", "h3", "h4", "h5", "h6")
|
||||||
|
.toFactory()
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
package dev.usbharu.hideout.core.domain.model.post
|
package dev.usbharu.hideout.core.domain.model.post
|
||||||
|
|
||||||
import dev.usbharu.hideout.application.config.CharacterLimit
|
import dev.usbharu.hideout.application.config.CharacterLimit
|
||||||
|
import dev.usbharu.hideout.core.service.post.PostContentFormatter
|
||||||
import org.springframework.stereotype.Component
|
import org.springframework.stereotype.Component
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
|
|
||||||
|
@ -8,6 +9,7 @@ data class Post private constructor(
|
||||||
val id: Long,
|
val id: Long,
|
||||||
val actorId: Long,
|
val actorId: Long,
|
||||||
val overview: String? = null,
|
val overview: String? = null,
|
||||||
|
val content: String,
|
||||||
val text: String,
|
val text: String,
|
||||||
val createdAt: Long,
|
val createdAt: Long,
|
||||||
val visibility: Visibility,
|
val visibility: Visibility,
|
||||||
|
@ -22,13 +24,16 @@ data class Post private constructor(
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
class PostBuilder(private val characterLimit: CharacterLimit) {
|
class PostBuilder(
|
||||||
|
private val characterLimit: CharacterLimit,
|
||||||
|
private val postContentFormatter: PostContentFormatter
|
||||||
|
) {
|
||||||
@Suppress("FunctionMinLength", "LongParameterList")
|
@Suppress("FunctionMinLength", "LongParameterList")
|
||||||
fun of(
|
fun of(
|
||||||
id: Long,
|
id: Long,
|
||||||
actorId: Long,
|
actorId: Long,
|
||||||
overview: String? = null,
|
overview: String? = null,
|
||||||
text: String,
|
content: String,
|
||||||
createdAt: Long,
|
createdAt: Long,
|
||||||
visibility: Visibility,
|
visibility: Visibility,
|
||||||
url: String,
|
url: String,
|
||||||
|
@ -49,12 +54,14 @@ data class Post private constructor(
|
||||||
overview
|
overview
|
||||||
}
|
}
|
||||||
|
|
||||||
val limitedText = if (text.length >= characterLimit.post.text) {
|
val limitedText = if (content.length >= characterLimit.post.text) {
|
||||||
text.substring(0, characterLimit.post.text)
|
content.substring(0, characterLimit.post.text)
|
||||||
} else {
|
} else {
|
||||||
text
|
content
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val (html, content1) = postContentFormatter.format(limitedText)
|
||||||
|
|
||||||
require(url.isNotBlank()) { "url must contain non-blank characters" }
|
require(url.isNotBlank()) { "url must contain non-blank characters" }
|
||||||
require(url.length <= characterLimit.general.url) {
|
require(url.length <= characterLimit.general.url) {
|
||||||
"url must not exceed ${characterLimit.general.url} characters."
|
"url must not exceed ${characterLimit.general.url} characters."
|
||||||
|
@ -67,7 +74,8 @@ data class Post private constructor(
|
||||||
id = id,
|
id = id,
|
||||||
actorId = actorId,
|
actorId = actorId,
|
||||||
overview = limitedOverview,
|
overview = limitedOverview,
|
||||||
text = limitedText,
|
content = html,
|
||||||
|
text = content1,
|
||||||
createdAt = createdAt,
|
createdAt = createdAt,
|
||||||
visibility = visibility,
|
visibility = visibility,
|
||||||
url = url,
|
url = url,
|
||||||
|
@ -94,6 +102,7 @@ data class Post private constructor(
|
||||||
id = id,
|
id = id,
|
||||||
actorId = 0,
|
actorId = 0,
|
||||||
overview = null,
|
overview = null,
|
||||||
|
content = "",
|
||||||
text = "",
|
text = "",
|
||||||
createdAt = Instant.EPOCH.toEpochMilli(),
|
createdAt = Instant.EPOCH.toEpochMilli(),
|
||||||
visibility = visibility,
|
visibility = visibility,
|
||||||
|
@ -113,6 +122,7 @@ data class Post private constructor(
|
||||||
id = this.id,
|
id = this.id,
|
||||||
actorId = 0,
|
actorId = 0,
|
||||||
overview = null,
|
overview = null,
|
||||||
|
content = "",
|
||||||
text = "",
|
text = "",
|
||||||
createdAt = Instant.EPOCH.toEpochMilli(),
|
createdAt = Instant.EPOCH.toEpochMilli(),
|
||||||
visibility = visibility,
|
visibility = visibility,
|
||||||
|
|
|
@ -25,7 +25,7 @@ class PostResultRowMapper(private val postBuilder: Post.PostBuilder) : ResultRow
|
||||||
id = resultRow[Posts.id],
|
id = resultRow[Posts.id],
|
||||||
actorId = resultRow[Posts.actorId],
|
actorId = resultRow[Posts.actorId],
|
||||||
overview = resultRow[Posts.overview],
|
overview = resultRow[Posts.overview],
|
||||||
text = resultRow[Posts.text],
|
content = resultRow[Posts.text],
|
||||||
createdAt = resultRow[Posts.createdAt],
|
createdAt = resultRow[Posts.createdAt],
|
||||||
visibility = Visibility.values().first { visibility -> visibility.ordinal == resultRow[Posts.visibility] },
|
visibility = Visibility.values().first { visibility -> visibility.ordinal == resultRow[Posts.visibility] },
|
||||||
url = resultRow[Posts.url],
|
url = resultRow[Posts.url],
|
||||||
|
|
|
@ -27,6 +27,7 @@ class PostRepositoryImpl(
|
||||||
it[id] = post.id
|
it[id] = post.id
|
||||||
it[actorId] = post.actorId
|
it[actorId] = post.actorId
|
||||||
it[overview] = post.overview
|
it[overview] = post.overview
|
||||||
|
it[content] = post.content
|
||||||
it[text] = post.text
|
it[text] = post.text
|
||||||
it[createdAt] = post.createdAt
|
it[createdAt] = post.createdAt
|
||||||
it[visibility] = post.visibility.ordinal
|
it[visibility] = post.visibility.ordinal
|
||||||
|
@ -63,6 +64,7 @@ class PostRepositoryImpl(
|
||||||
Posts.update({ Posts.id eq post.id }) {
|
Posts.update({ Posts.id eq post.id }) {
|
||||||
it[actorId] = post.actorId
|
it[actorId] = post.actorId
|
||||||
it[overview] = post.overview
|
it[overview] = post.overview
|
||||||
|
it[content] = post.content
|
||||||
it[text] = post.text
|
it[text] = post.text
|
||||||
it[createdAt] = post.createdAt
|
it[createdAt] = post.createdAt
|
||||||
it[visibility] = post.visibility.ordinal
|
it[visibility] = post.visibility.ordinal
|
||||||
|
@ -128,6 +130,7 @@ object Posts : Table() {
|
||||||
val id: Column<Long> = long("id")
|
val id: Column<Long> = long("id")
|
||||||
val actorId: Column<Long> = long("actor_id").references(Actors.id)
|
val actorId: Column<Long> = long("actor_id").references(Actors.id)
|
||||||
val overview: Column<String?> = varchar("overview", 100).nullable()
|
val overview: Column<String?> = varchar("overview", 100).nullable()
|
||||||
|
val content = varchar("content", 5000)
|
||||||
val text: Column<String> = varchar("text", 3000)
|
val text: Column<String> = varchar("text", 3000)
|
||||||
val createdAt: Column<Long> = long("created_at")
|
val createdAt: Column<Long> = long("created_at")
|
||||||
val visibility: Column<Int> = integer("visibility").default(0)
|
val visibility: Column<Int> = integer("visibility").default(0)
|
||||||
|
|
|
@ -0,0 +1,83 @@
|
||||||
|
package dev.usbharu.hideout.core.service.post
|
||||||
|
|
||||||
|
import org.jsoup.Jsoup
|
||||||
|
import org.jsoup.nodes.Document
|
||||||
|
import org.jsoup.nodes.Element
|
||||||
|
import org.jsoup.nodes.TextNode
|
||||||
|
import org.jsoup.select.Elements
|
||||||
|
import org.owasp.html.PolicyFactory
|
||||||
|
import org.springframework.stereotype.Service
|
||||||
|
|
||||||
|
@Service
|
||||||
|
class DefaultPostContentFormatter(private val policyFactory: PolicyFactory) : PostContentFormatter {
|
||||||
|
override fun format(content: String): FormattedPostContent {
|
||||||
|
// まず不正なHTMLを整形する
|
||||||
|
val document = Jsoup.parseBodyFragment(content)
|
||||||
|
val outputSettings = Document.OutputSettings()
|
||||||
|
outputSettings.prettyPrint(false)
|
||||||
|
|
||||||
|
document.outputSettings(outputSettings)
|
||||||
|
|
||||||
|
val unsafeElement = document.getElementsByTag("body").first() ?: return FormattedPostContent(
|
||||||
|
"",
|
||||||
|
""
|
||||||
|
)
|
||||||
|
|
||||||
|
// 文字だけのHTMLなどはここでpタグで囲む
|
||||||
|
val flattenHtml = unsafeElement.childNodes().mapNotNull {
|
||||||
|
if (it is Element) {
|
||||||
|
it
|
||||||
|
} else if (it is TextNode) {
|
||||||
|
Element("p").appendText(it.text())
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}.filter { it.text().isNotBlank() }
|
||||||
|
|
||||||
|
// HTMLのサニタイズをする
|
||||||
|
val unsafeHtml = Elements(flattenHtml).outerHtml()
|
||||||
|
|
||||||
|
val safeHtml = policyFactory.sanitize(unsafeHtml)
|
||||||
|
|
||||||
|
val safeDocument =
|
||||||
|
Jsoup.parseBodyFragment(safeHtml).getElementsByTag("body").first() ?: return FormattedPostContent("", "")
|
||||||
|
|
||||||
|
val formattedHtml = mutableListOf<Element>()
|
||||||
|
|
||||||
|
// 連続するbrタグを段落に変換する
|
||||||
|
for (element in safeDocument.children()) {
|
||||||
|
var brCount = 0
|
||||||
|
var prevIndex = 0
|
||||||
|
val childNodes = element.childNodes()
|
||||||
|
for ((index, childNode) in childNodes.withIndex()) {
|
||||||
|
if (childNode is Element && childNode.tagName() == "br") {
|
||||||
|
brCount++
|
||||||
|
} else if (brCount >= 2) {
|
||||||
|
formattedHtml.add(Element("p").appendChildren(childNodes.subList(prevIndex, index - brCount)))
|
||||||
|
prevIndex = index
|
||||||
|
}
|
||||||
|
}
|
||||||
|
formattedHtml.add(Element("p").appendChildren(childNodes.subList(prevIndex, childNodes.size)))
|
||||||
|
}
|
||||||
|
|
||||||
|
val elements = Elements(formattedHtml)
|
||||||
|
|
||||||
|
return FormattedPostContent(elements.outerHtml().replace("\n", ""), printHtml(elements))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun printHtml(element: Elements): String {
|
||||||
|
return element.joinToString("\n\n") {
|
||||||
|
it.childNodes().joinToString("") {
|
||||||
|
if (it is Element && it.tagName() == "br") {
|
||||||
|
"\n"
|
||||||
|
} else if (it is Element) {
|
||||||
|
it.text()
|
||||||
|
} else if (it is TextNode) {
|
||||||
|
it.text()
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
package dev.usbharu.hideout.core.service.post
|
||||||
|
|
||||||
|
data class FormattedPostContent(
|
||||||
|
val html: String,
|
||||||
|
val content: String
|
||||||
|
)
|
|
@ -0,0 +1,5 @@
|
||||||
|
package dev.usbharu.hideout.core.service.post
|
||||||
|
|
||||||
|
interface PostContentFormatter {
|
||||||
|
fun format(content: String): FormattedPostContent
|
||||||
|
}
|
|
@ -97,7 +97,7 @@ class PostServiceImpl(
|
||||||
id = id,
|
id = id,
|
||||||
actorId = post.userId,
|
actorId = post.userId,
|
||||||
overview = post.overview,
|
overview = post.overview,
|
||||||
text = post.text,
|
content = post.text,
|
||||||
createdAt = Instant.now().toEpochMilli(),
|
createdAt = Instant.now().toEpochMilli(),
|
||||||
visibility = post.visibility,
|
visibility = post.visibility,
|
||||||
url = "${user.url}/posts/$id",
|
url = "${user.url}/posts/$id",
|
||||||
|
|
|
@ -90,6 +90,7 @@ create table if not exists posts
|
||||||
id bigint primary key,
|
id bigint primary key,
|
||||||
actor_id bigint not null,
|
actor_id bigint not null,
|
||||||
overview varchar(100) null,
|
overview varchar(100) null,
|
||||||
|
content varchar(5000) not null,
|
||||||
text varchar(3000) not null,
|
text varchar(3000) not null,
|
||||||
created_at bigint not null,
|
created_at bigint not null,
|
||||||
visibility int default 0 not null,
|
visibility int default 0 not null,
|
||||||
|
|
|
@ -12,10 +12,12 @@ import dev.usbharu.hideout.activitypub.service.common.APResourceResolveService
|
||||||
import dev.usbharu.hideout.activitypub.service.objects.note.APNoteServiceImpl.Companion.public
|
import dev.usbharu.hideout.activitypub.service.objects.note.APNoteServiceImpl.Companion.public
|
||||||
import dev.usbharu.hideout.activitypub.service.objects.user.APUserService
|
import dev.usbharu.hideout.activitypub.service.objects.user.APUserService
|
||||||
import dev.usbharu.hideout.application.config.CharacterLimit
|
import dev.usbharu.hideout.application.config.CharacterLimit
|
||||||
|
import dev.usbharu.hideout.application.config.HtmlSanitizeConfig
|
||||||
import dev.usbharu.hideout.application.service.id.TwitterSnowflakeIdGenerateService
|
import dev.usbharu.hideout.application.service.id.TwitterSnowflakeIdGenerateService
|
||||||
import dev.usbharu.hideout.core.domain.model.actor.ActorRepository
|
import dev.usbharu.hideout.core.domain.model.actor.ActorRepository
|
||||||
import dev.usbharu.hideout.core.domain.model.post.Post
|
import dev.usbharu.hideout.core.domain.model.post.Post
|
||||||
import dev.usbharu.hideout.core.domain.model.post.PostRepository
|
import dev.usbharu.hideout.core.domain.model.post.PostRepository
|
||||||
|
import dev.usbharu.hideout.core.service.post.DefaultPostContentFormatter
|
||||||
import dev.usbharu.hideout.core.service.post.PostService
|
import dev.usbharu.hideout.core.service.post.PostService
|
||||||
import io.ktor.client.*
|
import io.ktor.client.*
|
||||||
import io.ktor.client.call.*
|
import io.ktor.client.call.*
|
||||||
|
@ -42,7 +44,7 @@ import java.time.Instant
|
||||||
|
|
||||||
class APNoteServiceImplTest {
|
class APNoteServiceImplTest {
|
||||||
|
|
||||||
val postBuilder = Post.PostBuilder(CharacterLimit())
|
val postBuilder = Post.PostBuilder(CharacterLimit(), DefaultPostContentFormatter(HtmlSanitizeConfig().policy()))
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `fetchNote(String,String) ノートが既に存在する場合はDBから取得したものを返す`() = runTest {
|
fun `fetchNote(String,String) ノートが既に存在する場合はDBから取得したものを返す`() = runTest {
|
||||||
|
@ -71,7 +73,10 @@ class APNoteServiceImplTest {
|
||||||
apUserService = mock(),
|
apUserService = mock(),
|
||||||
postService = mock(),
|
postService = mock(),
|
||||||
apResourceResolveService = mock(),
|
apResourceResolveService = mock(),
|
||||||
postBuilder = Post.PostBuilder(CharacterLimit()),
|
postBuilder = Post.PostBuilder(
|
||||||
|
CharacterLimit(),
|
||||||
|
DefaultPostContentFormatter(HtmlSanitizeConfig().policy())
|
||||||
|
),
|
||||||
noteQueryService = noteQueryService,
|
noteQueryService = noteQueryService,
|
||||||
mock(),
|
mock(),
|
||||||
mock()
|
mock()
|
||||||
|
@ -141,7 +146,10 @@ class APNoteServiceImplTest {
|
||||||
apUserService = apUserService,
|
apUserService = apUserService,
|
||||||
postService = mock(),
|
postService = mock(),
|
||||||
apResourceResolveService = apResourceResolveService,
|
apResourceResolveService = apResourceResolveService,
|
||||||
postBuilder = Post.PostBuilder(CharacterLimit()),
|
postBuilder = Post.PostBuilder(
|
||||||
|
CharacterLimit(),
|
||||||
|
DefaultPostContentFormatter(HtmlSanitizeConfig().policy())
|
||||||
|
),
|
||||||
noteQueryService = noteQueryService,
|
noteQueryService = noteQueryService,
|
||||||
mock(),
|
mock(),
|
||||||
mock { }
|
mock { }
|
||||||
|
@ -190,7 +198,10 @@ class APNoteServiceImplTest {
|
||||||
apUserService = mock(),
|
apUserService = mock(),
|
||||||
postService = mock(),
|
postService = mock(),
|
||||||
apResourceResolveService = apResourceResolveService,
|
apResourceResolveService = apResourceResolveService,
|
||||||
postBuilder = Post.PostBuilder(CharacterLimit()),
|
postBuilder = Post.PostBuilder(
|
||||||
|
CharacterLimit(),
|
||||||
|
DefaultPostContentFormatter(HtmlSanitizeConfig().policy())
|
||||||
|
),
|
||||||
noteQueryService = noteQueryService,
|
noteQueryService = noteQueryService,
|
||||||
mock(),
|
mock(),
|
||||||
mock()
|
mock()
|
||||||
|
|
|
@ -0,0 +1,135 @@
|
||||||
|
package dev.usbharu.hideout.core.service.post
|
||||||
|
|
||||||
|
import dev.usbharu.hideout.application.config.HtmlSanitizeConfig
|
||||||
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
|
||||||
|
class DefaultPostContentFormatterTest {
|
||||||
|
val defaultPostContentFormatter = DefaultPostContentFormatter(HtmlSanitizeConfig().policy())
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun pタグがpタグになる() {
|
||||||
|
//language=HTML
|
||||||
|
val html = """<p>hoge</p>"""
|
||||||
|
|
||||||
|
val actual = defaultPostContentFormatter.format(html)
|
||||||
|
|
||||||
|
assertThat(actual).isEqualTo(FormattedPostContent("<p>hoge</p>", "hoge"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun hタグがpタグになる() {
|
||||||
|
//language=HTML
|
||||||
|
val html = """<h1>hoge</h1>"""
|
||||||
|
|
||||||
|
val actual = defaultPostContentFormatter.format(html)
|
||||||
|
|
||||||
|
assertThat(actual).isEqualTo(FormattedPostContent("<p>hoge</p>", "hoge"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun pタグのネストは破棄される() {
|
||||||
|
//language=HTML
|
||||||
|
val html = """<p>hoge<p>fuga</p>piyo</p>"""
|
||||||
|
|
||||||
|
val actual = defaultPostContentFormatter.format(html)
|
||||||
|
|
||||||
|
assertThat(actual).isEqualTo(FormattedPostContent("<p>hoge</p><p>fuga</p><p>piyo</p>", "hoge\n\nfuga\n\npiyo"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun spanタグは無視される() {
|
||||||
|
//language=HTML
|
||||||
|
val html = """<p><span>hoge</span></p>"""
|
||||||
|
|
||||||
|
val actual = defaultPostContentFormatter.format(html)
|
||||||
|
|
||||||
|
assertThat(actual).isEqualTo(FormattedPostContent("<p>hoge</p>", "hoge"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `2連続改行は段落に変換される`() {
|
||||||
|
//language=HTML
|
||||||
|
val html = """<p>hoge<br><br>fuga</p>"""
|
||||||
|
|
||||||
|
val actual = defaultPostContentFormatter.format(html)
|
||||||
|
|
||||||
|
assertThat(actual).isEqualTo(FormattedPostContent("<p>hoge</p><p>fuga</p>", "hoge\n\nfuga"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun iタグは無視される() {
|
||||||
|
//language=HTML
|
||||||
|
val html = """<p><i>hoge</i></p>"""
|
||||||
|
|
||||||
|
val actual = defaultPostContentFormatter.format(html)
|
||||||
|
|
||||||
|
assertThat(actual).isEqualTo(FormattedPostContent("<p>hoge</p>", "hoge"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun aタグはhrefの中身のみ引き継がれる() {
|
||||||
|
//language=HTML
|
||||||
|
val html = """<p><a href='https://example.com' class='u-url' target='_blank'>hoge</a></p>"""
|
||||||
|
|
||||||
|
val actual = defaultPostContentFormatter.format(html)
|
||||||
|
|
||||||
|
assertThat(actual).isEqualTo(FormattedPostContent("<p><a href=\"https://example.com\">hoge</a></p>", "hoge"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun aタグの中のspanは無視される() {
|
||||||
|
//language=HTML
|
||||||
|
val html = """<p><a href='https://example.com'><span>hoge</span></a></p>"""
|
||||||
|
|
||||||
|
val actual = defaultPostContentFormatter.format(html)
|
||||||
|
|
||||||
|
assertThat(actual).isEqualTo(FormattedPostContent("<p><a href=\"https://example.com\">hoge</a></p>", "hoge"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun brタグのコンテンツは改行になる() {
|
||||||
|
//language=HTML
|
||||||
|
val html = """<p>hoge<br>fuga</p>"""
|
||||||
|
|
||||||
|
val actual = defaultPostContentFormatter.format(html)
|
||||||
|
|
||||||
|
assertThat(actual).isEqualTo(FormattedPostContent("<p>hoge<br> fuga</p>", "hoge\nfuga"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun いきなりテキストが来たらpタグで囲む() {
|
||||||
|
//language=HTML
|
||||||
|
val html = """hoge"""
|
||||||
|
|
||||||
|
val actual = defaultPostContentFormatter.format(html)
|
||||||
|
|
||||||
|
assertThat(actual).isEqualTo(FormattedPostContent("<p>hoge</p>", "hoge"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun bodyタグが含まれていた場合消す() {
|
||||||
|
//language=HTML
|
||||||
|
val html = """</body><p>hoge</p>"""
|
||||||
|
|
||||||
|
val actual = defaultPostContentFormatter.format(html)
|
||||||
|
|
||||||
|
assertThat(actual).isEqualTo(FormattedPostContent("<p>hoge</p>", "hoge"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun pタグの中のspanは無視される() {
|
||||||
|
//language=HTML
|
||||||
|
val html =
|
||||||
|
"""<p><span class="h-card" translate="no"><a href="https://test-hideout.usbharu.dev/users/testuser14" class="u-url mention">@<span>testuser14</span></a></span> tes</p>"""
|
||||||
|
|
||||||
|
val actual = defaultPostContentFormatter.format(html)
|
||||||
|
|
||||||
|
assertThat(actual).isEqualTo(
|
||||||
|
FormattedPostContent(
|
||||||
|
"<p><a href=\"https://test-hideout.usbharu.dev/users/testuser14\">@testuser14</a> tes</p>",
|
||||||
|
"@testuser14 tes"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,6 +3,7 @@ package dev.usbharu.hideout.core.service.post
|
||||||
import dev.usbharu.hideout.activitypub.service.activity.create.ApSendCreateService
|
import dev.usbharu.hideout.activitypub.service.activity.create.ApSendCreateService
|
||||||
import dev.usbharu.hideout.activitypub.service.activity.delete.APSendDeleteService
|
import dev.usbharu.hideout.activitypub.service.activity.delete.APSendDeleteService
|
||||||
import dev.usbharu.hideout.application.config.CharacterLimit
|
import dev.usbharu.hideout.application.config.CharacterLimit
|
||||||
|
import dev.usbharu.hideout.application.config.HtmlSanitizeConfig
|
||||||
import dev.usbharu.hideout.core.domain.exception.resource.DuplicateException
|
import dev.usbharu.hideout.core.domain.exception.resource.DuplicateException
|
||||||
import dev.usbharu.hideout.core.domain.model.actor.ActorRepository
|
import dev.usbharu.hideout.core.domain.model.actor.ActorRepository
|
||||||
import dev.usbharu.hideout.core.domain.model.post.Post
|
import dev.usbharu.hideout.core.domain.model.post.Post
|
||||||
|
@ -36,7 +37,11 @@ class PostServiceImplTest {
|
||||||
@Mock
|
@Mock
|
||||||
private lateinit var timelineService: TimelineService
|
private lateinit var timelineService: TimelineService
|
||||||
@Spy
|
@Spy
|
||||||
private var postBuilder: Post.PostBuilder = Post.PostBuilder(CharacterLimit())
|
private var postBuilder: Post.PostBuilder = Post.PostBuilder(
|
||||||
|
CharacterLimit(), DefaultPostContentFormatter(
|
||||||
|
HtmlSanitizeConfig().policy()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
private lateinit var apSendCreateService: ApSendCreateService
|
private lateinit var apSendCreateService: ApSendCreateService
|
||||||
|
|
|
@ -4,9 +4,11 @@ package dev.usbharu.hideout.core.service.user
|
||||||
|
|
||||||
import dev.usbharu.hideout.application.config.ApplicationConfig
|
import dev.usbharu.hideout.application.config.ApplicationConfig
|
||||||
import dev.usbharu.hideout.application.config.CharacterLimit
|
import dev.usbharu.hideout.application.config.CharacterLimit
|
||||||
|
import dev.usbharu.hideout.application.config.HtmlSanitizeConfig
|
||||||
import dev.usbharu.hideout.core.domain.model.actor.Actor
|
import dev.usbharu.hideout.core.domain.model.actor.Actor
|
||||||
import dev.usbharu.hideout.core.domain.model.actor.ActorRepository
|
import dev.usbharu.hideout.core.domain.model.actor.ActorRepository
|
||||||
import dev.usbharu.hideout.core.domain.model.post.Post
|
import dev.usbharu.hideout.core.domain.model.post.Post
|
||||||
|
import dev.usbharu.hideout.core.service.post.DefaultPostContentFormatter
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
|
@ -20,7 +22,7 @@ import kotlin.test.assertNull
|
||||||
|
|
||||||
class ActorServiceTest {
|
class ActorServiceTest {
|
||||||
val actorBuilder = Actor.UserBuilder(CharacterLimit(), ApplicationConfig(URL("https://example.com")))
|
val actorBuilder = Actor.UserBuilder(CharacterLimit(), ApplicationConfig(URL("https://example.com")))
|
||||||
val postBuilder = Post.PostBuilder(CharacterLimit())
|
val postBuilder = Post.PostBuilder(CharacterLimit(), DefaultPostContentFormatter(HtmlSanitizeConfig().policy()))
|
||||||
@Test
|
@Test
|
||||||
fun `createLocalUser ローカルユーザーを作成できる`() = runTest {
|
fun `createLocalUser ローカルユーザーを作成できる`() = runTest {
|
||||||
|
|
||||||
|
|
|
@ -1,15 +1,18 @@
|
||||||
package utils
|
package utils
|
||||||
|
|
||||||
import dev.usbharu.hideout.application.config.CharacterLimit
|
import dev.usbharu.hideout.application.config.CharacterLimit
|
||||||
|
import dev.usbharu.hideout.application.config.HtmlSanitizeConfig
|
||||||
import dev.usbharu.hideout.application.service.id.TwitterSnowflakeIdGenerateService
|
import dev.usbharu.hideout.application.service.id.TwitterSnowflakeIdGenerateService
|
||||||
import dev.usbharu.hideout.core.domain.model.post.Post
|
import dev.usbharu.hideout.core.domain.model.post.Post
|
||||||
import dev.usbharu.hideout.core.domain.model.post.Visibility
|
import dev.usbharu.hideout.core.domain.model.post.Visibility
|
||||||
|
import dev.usbharu.hideout.core.service.post.DefaultPostContentFormatter
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
|
|
||||||
object PostBuilder {
|
object PostBuilder {
|
||||||
|
|
||||||
private val postBuilder = Post.PostBuilder(CharacterLimit())
|
private val postBuilder =
|
||||||
|
Post.PostBuilder(CharacterLimit(), DefaultPostContentFormatter(HtmlSanitizeConfig().policy()))
|
||||||
|
|
||||||
private val idGenerator = TwitterSnowflakeIdGenerateService
|
private val idGenerator = TwitterSnowflakeIdGenerateService
|
||||||
|
|
||||||
|
@ -26,7 +29,7 @@ object PostBuilder {
|
||||||
id = id,
|
id = id,
|
||||||
actorId = userId,
|
actorId = userId,
|
||||||
overview = overview,
|
overview = overview,
|
||||||
text = text,
|
content = text,
|
||||||
createdAt = createdAt,
|
createdAt = createdAt,
|
||||||
visibility = visibility,
|
visibility = visibility,
|
||||||
url = url,
|
url = url,
|
||||||
|
|
Loading…
Reference in New Issue