diff --git a/.gitignore b/.gitignore index 10946754..a0062d99 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,5 @@ data/ logs/ config.yml + +store-test/ diff --git a/src/main/kotlin/com/jaoafa/vcspeaker/tts/TextProcessor.kt b/src/main/kotlin/com/jaoafa/vcspeaker/tts/TextProcessor.kt index bc7183fc..e26386cf 100644 --- a/src/main/kotlin/com/jaoafa/vcspeaker/tts/TextProcessor.kt +++ b/src/main/kotlin/com/jaoafa/vcspeaker/tts/TextProcessor.kt @@ -28,9 +28,9 @@ object TextProcessor { suspend fun processText(guildId: Snowflake, text: String): String? { if (text.shouldIgnoreOn(guildId)) return null - val replacedText = replacers.fold(text) { replacedText, replacer -> - replacer.replace(replacedText, guildId) - }.replaceEmojiToName() + val replacedText = replacers.fold(mutableListOf(Token(text))) { tokens, replacer -> + replacer.replace(tokens, guildId) + }.joinToString("") { it.text }.replaceEmojiToName() if (replacedText.shouldIgnoreOn(guildId)) return null diff --git a/src/main/kotlin/com/jaoafa/vcspeaker/tts/Token.kt b/src/main/kotlin/com/jaoafa/vcspeaker/tts/Token.kt new file mode 100644 index 00000000..c23a0128 --- /dev/null +++ b/src/main/kotlin/com/jaoafa/vcspeaker/tts/Token.kt @@ -0,0 +1,3 @@ +package com.jaoafa.vcspeaker.tts + +data class Token(val text: String, val replaced: Boolean = false) diff --git a/src/main/kotlin/com/jaoafa/vcspeaker/tts/replacers/AliasReplacer.kt b/src/main/kotlin/com/jaoafa/vcspeaker/tts/replacers/AliasReplacer.kt index 2701823b..6252af7a 100644 --- a/src/main/kotlin/com/jaoafa/vcspeaker/tts/replacers/AliasReplacer.kt +++ b/src/main/kotlin/com/jaoafa/vcspeaker/tts/replacers/AliasReplacer.kt @@ -1,16 +1,34 @@ package com.jaoafa.vcspeaker.tts.replacers import com.jaoafa.vcspeaker.stores.AliasType +import com.jaoafa.vcspeaker.tts.Token import dev.kord.common.entity.Snowflake /** * エイリアスを置換するクラス */ object AliasReplacer : BaseReplacer { - override val priority = ReplacerPriority.Normal + override val priority = ReplacerPriority.Low - override suspend fun replace(text: String, guildId: Snowflake) = - replaceText(text, guildId, AliasType.Text) { alias, replacedText -> - replacedText.replace(alias.search, alias.replace) + override suspend fun replace(tokens: MutableList, guildId: Snowflake) = + replaceText(tokens, guildId, AliasType.Text) { alias, replacedTokens -> + buildList { + for (replacedToken in replacedTokens) { + val text = replacedToken.text + + if (replacedToken.replaced || !text.contains(alias.search)) { + add(replacedToken) + continue + } + + val splitTexts = text.split(alias.search) + + val additions = splitTexts.mixin { + Token(alias.replace, true) + } + + addAll(additions) + } + }.toMutableList() } } \ No newline at end of file diff --git a/src/main/kotlin/com/jaoafa/vcspeaker/tts/replacers/BaseReplacer.kt b/src/main/kotlin/com/jaoafa/vcspeaker/tts/replacers/BaseReplacer.kt index 1de362e5..29b172b8 100644 --- a/src/main/kotlin/com/jaoafa/vcspeaker/tts/replacers/BaseReplacer.kt +++ b/src/main/kotlin/com/jaoafa/vcspeaker/tts/replacers/BaseReplacer.kt @@ -4,8 +4,10 @@ import com.jaoafa.vcspeaker.VCSpeaker import com.jaoafa.vcspeaker.stores.AliasData import com.jaoafa.vcspeaker.stores.AliasStore import com.jaoafa.vcspeaker.stores.AliasType +import com.jaoafa.vcspeaker.tts.Token import dev.kord.common.entity.Snowflake import dev.kord.core.Kord +import kotlinx.coroutines.runBlocking /** * テキストを置換する基底クラス @@ -13,37 +15,64 @@ import dev.kord.core.Kord interface BaseReplacer { val priority: ReplacerPriority - suspend fun replace(text: String, guildId: Snowflake): String + suspend fun replace(tokens: MutableList, guildId: Snowflake): MutableList fun replaceText( - text: String, + tokens: MutableList, guildId: Snowflake, type: AliasType, - transform: (AliasData, String) -> String - ): String { + transform: (AliasData, MutableList) -> MutableList + ): MutableList { val aliases = AliasStore.filter(guildId).filter { it.type == type } - val replacedText = aliases.fold(text) { replacedText, alias -> - transform(alias, replacedText) + val replacedText = aliases.fold(tokens) { replacedTokens, alias -> + transform(alias, replacedTokens) } return replacedText } suspend fun replaceMentionable( - text: String, + tokens: MutableList, regex: Regex, nameSupplier: suspend (Kord, Snowflake) -> String - ): String { - val matches = regex.findAll(text) + ): MutableList { + val newTokens = mutableListOf() - val replacedText = matches.fold(text) { replacedText, match -> - val id = Snowflake(match.groupValues[1]) // 0 is for whole match - val name = nameSupplier(VCSpeaker.kord, id) + for (token in tokens) { + val text = token.text - replacedText.replace(match.value, name) + if (token.replaced || !text.partialMatch(regex)) { + newTokens.add(token) + continue + } + + val matches = regex.findAll(text).toList() + + val splitTexts = text.split(regex) + + val additions = splitTexts.mixin { index -> + val match = matches[index] + val id = Snowflake(match.groupValues[1]) // 0 is for whole match + val name = nameSupplier(VCSpeaker.kord, id) + + Token(name, true) + } + + newTokens.addAll(additions) } - return replacedText + return newTokens } + + + fun List.mixin(provider: suspend (Int) -> Token) = buildList { + // ["Token1", "Token2", "Token3"] -> ["Token1", provider(1), "Token2", provider(2), "Token3"] + for (index in 0..(this@mixin.size * 2 - 2)) { + if (index % 2 == 0) add(Token(this@mixin[index / 2])) + else add(runBlocking { provider((index + 1) / 2) }) + } + } + + fun String.partialMatch(regex: Regex) = regex.findAll(this).toList().isNotEmpty() } \ No newline at end of file diff --git a/src/main/kotlin/com/jaoafa/vcspeaker/tts/replacers/ChannelMentionReplacer.kt b/src/main/kotlin/com/jaoafa/vcspeaker/tts/replacers/ChannelMentionReplacer.kt index 4137aa77..9d842097 100644 --- a/src/main/kotlin/com/jaoafa/vcspeaker/tts/replacers/ChannelMentionReplacer.kt +++ b/src/main/kotlin/com/jaoafa/vcspeaker/tts/replacers/ChannelMentionReplacer.kt @@ -1,15 +1,16 @@ package com.jaoafa.vcspeaker.tts.replacers +import com.jaoafa.vcspeaker.tts.Token import dev.kord.common.entity.Snowflake /** * チャンネルメンションを置換するクラス */ object ChannelMentionReplacer : BaseReplacer { - override val priority = ReplacerPriority.High + override val priority = ReplacerPriority.Normal - override suspend fun replace(text: String, guildId: Snowflake) = - replaceMentionable(text, Regex("<#(\\d+)>")) { kord, id -> + override suspend fun replace(tokens: MutableList, guildId: Snowflake) = + replaceMentionable(tokens, Regex("<#(\\d+)>")) { kord, id -> kord.getChannel(id)?.data?.name?.value ?: "不明なチャンネル" } } \ No newline at end of file diff --git a/src/main/kotlin/com/jaoafa/vcspeaker/tts/replacers/EmojiReplacer.kt b/src/main/kotlin/com/jaoafa/vcspeaker/tts/replacers/EmojiReplacer.kt index cdfdcade..dd6f39f9 100644 --- a/src/main/kotlin/com/jaoafa/vcspeaker/tts/replacers/EmojiReplacer.kt +++ b/src/main/kotlin/com/jaoafa/vcspeaker/tts/replacers/EmojiReplacer.kt @@ -1,16 +1,34 @@ package com.jaoafa.vcspeaker.tts.replacers import com.jaoafa.vcspeaker.stores.AliasType +import com.jaoafa.vcspeaker.tts.Token import dev.kord.common.entity.Snowflake /** * 絵文字エイリアスを置換するクラス */ object EmojiReplacer : BaseReplacer { - override val priority = ReplacerPriority.High + override val priority = ReplacerPriority.Normal - override suspend fun replace(text: String, guildId: Snowflake) = - replaceText(text, guildId, AliasType.Emoji) { alias, replacedText -> - replacedText.replace(alias.search, alias.replace) + override suspend fun replace(tokens: MutableList, guildId: Snowflake) = + replaceText(tokens, guildId, AliasType.Emoji) { alias, replacedTokens -> + buildList { + for (token in replacedTokens) { + val text = token.text + + if (token.replaced || !text.contains(alias.search)) { + add(token) + continue + } + + val splitTexts = text.split(alias.search) + + val additions = splitTexts.mixin { + Token(alias.replace, true) + } + + addAll(additions) + } + }.toMutableList() } } \ No newline at end of file diff --git a/src/main/kotlin/com/jaoafa/vcspeaker/tts/replacers/GuildEmojiReplacer.kt b/src/main/kotlin/com/jaoafa/vcspeaker/tts/replacers/GuildEmojiReplacer.kt index 395c84f3..750718b6 100644 --- a/src/main/kotlin/com/jaoafa/vcspeaker/tts/replacers/GuildEmojiReplacer.kt +++ b/src/main/kotlin/com/jaoafa/vcspeaker/tts/replacers/GuildEmojiReplacer.kt @@ -1,22 +1,36 @@ package com.jaoafa.vcspeaker.tts.replacers +import com.jaoafa.vcspeaker.tts.Token import dev.kord.common.entity.Snowflake /** * Guildの絵文字を置換するクラス */ object GuildEmojiReplacer : BaseReplacer { - override val priority = ReplacerPriority.High + override val priority = ReplacerPriority.Normal - override suspend fun replace(text: String, guildId: Snowflake): String { - val matches = Regex("").findAll(text) + override suspend fun replace(tokens: MutableList, guildId: Snowflake) = buildList { + val regex = Regex("") + for (token in tokens) { + val text = token.text - val replacedText = matches.fold(text) { replacedText, match -> - val emojiName = match.groupValues[1] + if (token.replaced || !text.partialMatch(regex)) { + add(token) + continue + } - replacedText.replace(match.value, emojiName) - } + val matches = regex.findAll(text).toList() + + val splitTexts = text.split(regex) + + val additions = splitTexts.mixin { index -> + val match = matches[index] + val emojiName = match.groupValues[1] - return replacedText - } + Token(emojiName, true) + } + + addAll(additions) + } + }.toMutableList() } \ No newline at end of file diff --git a/src/main/kotlin/com/jaoafa/vcspeaker/tts/replacers/RegexReplacer.kt b/src/main/kotlin/com/jaoafa/vcspeaker/tts/replacers/RegexReplacer.kt index 0986a138..4b6747e6 100644 --- a/src/main/kotlin/com/jaoafa/vcspeaker/tts/replacers/RegexReplacer.kt +++ b/src/main/kotlin/com/jaoafa/vcspeaker/tts/replacers/RegexReplacer.kt @@ -1,16 +1,36 @@ package com.jaoafa.vcspeaker.tts.replacers import com.jaoafa.vcspeaker.stores.AliasType +import com.jaoafa.vcspeaker.tts.Token import dev.kord.common.entity.Snowflake /** * 正規表現エイリアスを置換するクラス */ object RegexReplacer : BaseReplacer { - override val priority = ReplacerPriority.Normal + override val priority = ReplacerPriority.Low - override suspend fun replace(text: String, guildId: Snowflake) = - replaceText(text, guildId, AliasType.Regex) { alias, replacedText -> - replacedText.replace(Regex(alias.search), alias.replace) + override suspend fun replace(tokens: MutableList, guildId: Snowflake) = + replaceText(tokens, guildId, AliasType.Regex) { alias, replacedTokens -> + buildList { + val regex = Regex(alias.search) + + for (replacedToken in replacedTokens) { + val text = replacedToken.text + + if (replacedToken.replaced || !text.partialMatch(regex)) { + add(replacedToken) + continue + } + + val splitTexts = text.split(regex) + + val additions = splitTexts.mixin { + Token(alias.replace, true) + } + + addAll(additions) + } + }.toMutableList() } } \ No newline at end of file diff --git a/src/main/kotlin/com/jaoafa/vcspeaker/tts/replacers/RoleMentionReplacer.kt b/src/main/kotlin/com/jaoafa/vcspeaker/tts/replacers/RoleMentionReplacer.kt index 082bacbe..3b05589a 100644 --- a/src/main/kotlin/com/jaoafa/vcspeaker/tts/replacers/RoleMentionReplacer.kt +++ b/src/main/kotlin/com/jaoafa/vcspeaker/tts/replacers/RoleMentionReplacer.kt @@ -1,15 +1,16 @@ package com.jaoafa.vcspeaker.tts.replacers +import com.jaoafa.vcspeaker.tts.Token import dev.kord.common.entity.Snowflake /** * ロールメンションを置換するクラス */ object RoleMentionReplacer : BaseReplacer { - override val priority = ReplacerPriority.High + override val priority = ReplacerPriority.Normal - override suspend fun replace(text: String, guildId: Snowflake) = - replaceMentionable(text, Regex("<@&(\\d+)>")) { kord, id -> + override suspend fun replace(tokens: MutableList, guildId: Snowflake) = + replaceMentionable(tokens, Regex("<@&(\\d+)>")) { kord, id -> kord.getGuildOrNull(guildId)?.getRole(id)?.data?.name ?: "不明なロール" } } \ No newline at end of file diff --git a/src/main/kotlin/com/jaoafa/vcspeaker/tts/replacers/UrlReplacer.kt b/src/main/kotlin/com/jaoafa/vcspeaker/tts/replacers/UrlReplacer.kt index 91bc3d9b..a68718ad 100644 --- a/src/main/kotlin/com/jaoafa/vcspeaker/tts/replacers/UrlReplacer.kt +++ b/src/main/kotlin/com/jaoafa/vcspeaker/tts/replacers/UrlReplacer.kt @@ -8,6 +8,7 @@ import com.jaoafa.vcspeaker.tools.Steam import com.jaoafa.vcspeaker.tools.Twitter import com.jaoafa.vcspeaker.tools.YouTube import com.jaoafa.vcspeaker.tools.discord.DiscordExtensions.isThread +import com.jaoafa.vcspeaker.tts.Token import dev.kord.common.entity.ChannelType import dev.kord.common.entity.Snowflake import dev.kord.core.behavior.channel.asChannelOf @@ -34,24 +35,28 @@ import kotlin.text.String object UrlReplacer : BaseReplacer { override val priority = ReplacerPriority.High - override suspend fun replace(text: String, guildId: Snowflake): String { + override suspend fun replace(tokens: MutableList, guildId: Snowflake): MutableList { suspend fun replaceUrl(vararg replacers: suspend (String, Snowflake) -> String) = - replacers.fold(text) { replacedText, replacer -> + replacers.fold(tokens.joinToString("") { it.text }) { replacedText, replacer -> replacer(replacedText, guildId) } - return replaceUrl( - ::replaceMessageUrl, - ::replaceChannelUrl, - ::replaceEventDirectUrl, - ::replaceEventInviteUrl, - ::replaceTweetUrl, - ::replaceInviteUrl, - ::replaceSteamAppUrl, - ::replaceYouTubeUrl, - ::replaceYouTubePlaylistUrl, - ::replaceUrlToTitle, - ::replaceUrl, + return mutableListOf( + Token( + replaceUrl( + ::replaceMessageUrl, + ::replaceChannelUrl, + ::replaceEventDirectUrl, + ::replaceEventInviteUrl, + ::replaceTweetUrl, + ::replaceInviteUrl, + ::replaceSteamAppUrl, + ::replaceYouTubeUrl, + ::replaceYouTubePlaylistUrl, + ::replaceUrlToTitle, + ::replaceUrl, + ) + ) ) } @@ -523,11 +528,9 @@ object UrlReplacer : BaseReplacer { ) // 動画タイトルが20文字を超える場合は、20文字に短縮して「以下略」を付ける - val videoTitle = video.title.substring(0, 15.coerceAtMost(video.title.length)) + - if (video.title.length > 15) " 以下略" else "" + val videoTitle = video.title.shorten(20) // 投稿者名が15文字を超える場合は、15文字に短縮して「以下略」を付ける - val authorName = video.authorName.substring(0, 13.coerceAtMost(video.authorName.length)) + - if (video.authorName.length > 15) " 以下略" else "" + val authorName = video.authorName.shorten(15) // URLからアイテムの種別を断定できる場合は、それに応じたテンプレートを使用する val replaceTo = when (videoType) { diff --git a/src/main/kotlin/com/jaoafa/vcspeaker/tts/replacers/UserMentionReplacer.kt b/src/main/kotlin/com/jaoafa/vcspeaker/tts/replacers/UserMentionReplacer.kt index e0383b33..b7619be1 100644 --- a/src/main/kotlin/com/jaoafa/vcspeaker/tts/replacers/UserMentionReplacer.kt +++ b/src/main/kotlin/com/jaoafa/vcspeaker/tts/replacers/UserMentionReplacer.kt @@ -1,15 +1,16 @@ package com.jaoafa.vcspeaker.tts.replacers +import com.jaoafa.vcspeaker.tts.Token import dev.kord.common.entity.Snowflake /** * ユーザーメンションを置換するクラス */ object UserMentionReplacer : BaseReplacer { - override val priority = ReplacerPriority.High + override val priority = ReplacerPriority.Normal - override suspend fun replace(text: String, guildId: Snowflake) = - replaceMentionable(text, Regex("<@!?(\\d+)>")) { kord, id -> + override suspend fun replace(tokens: MutableList, guildId: Snowflake) = + replaceMentionable(tokens, Regex("<@!?(\\d+)>")) { kord, id -> val effectiveName = kord.getGuildOrNull(guildId)?.getMember(id)?.effectiveName effectiveName ?: "不明なユーザー" } diff --git a/src/test/kotlin/TextProcessorTest.kt b/src/test/kotlin/TextProcessorTest.kt index a07c8ce8..3c0e2626 100644 --- a/src/test/kotlin/TextProcessorTest.kt +++ b/src/test/kotlin/TextProcessorTest.kt @@ -1,3 +1,4 @@ + import com.jaoafa.vcspeaker.VCSpeaker import com.jaoafa.vcspeaker.stores.* import com.jaoafa.vcspeaker.tts.TextProcessor @@ -283,7 +284,7 @@ class TextProcessorTest : FunSpec({ processed shouldBe "Bonjour, Kotlin!" } - test("processText - alias - recursive") { + test("processText - alias - non recursive") { AliasStore.create( AliasData( guildId = Snowflake(0), @@ -294,6 +295,7 @@ class TextProcessorTest : FunSpec({ ) ) + // should be skipped AliasStore.create( AliasData( guildId = Snowflake(0), @@ -304,8 +306,18 @@ class TextProcessorTest : FunSpec({ ) ) + AliasStore.create( + AliasData( + guildId = Snowflake(0), + userId = Snowflake(0), + type = AliasType.Regex, + search = "w.+d", + replace = "Kotlin" + ) + ) + val processed = TextProcessor.processText(Snowflake(0), "Hello, world!") - processed shouldBe "你好,Kotlin!" + processed shouldBe "Bonjour, Kotlin!" } } }) \ No newline at end of file