From 1b29ef12b6aa6080cbe4f315c8c2761bbec5ac18 Mon Sep 17 00:00:00 2001 From: yuuaHP Date: Sun, 9 Jun 2024 19:13:40 +0900 Subject: [PATCH] feat: non recursive replacers (#109) * feat: non recursive replacers * fix: wrong index * fix: accurate comment * feat: parse command * feat: allow alias search update * feat: distinguish system messages --- .gitignore | 2 + src/main/kotlin/com/jaoafa/vcspeaker/Main.kt | 2 + .../jaoafa/vcspeaker/commands/AliasCommand.kt | 36 +++- .../vcspeaker/commands/IgnoreCommand.kt | 6 +- .../jaoafa/vcspeaker/commands/ParseCommand.kt | 176 ++++++++++++++++++ .../com/jaoafa/vcspeaker/features/Alias.kt | 8 +- .../com/jaoafa/vcspeaker/stores/AliasStore.kt | 7 +- .../jaoafa/vcspeaker/stores/IgnoreStore.kt | 10 +- .../com/jaoafa/vcspeaker/tools/Emoji.kt | 16 +- .../com/jaoafa/vcspeaker/tts/TextProcessor.kt | 14 +- .../kotlin/com/jaoafa/vcspeaker/tts/Token.kt | 5 + .../vcspeaker/tts/replacers/AliasReplacer.kt | 27 ++- .../vcspeaker/tts/replacers/BaseReplacer.kt | 57 ++++-- .../tts/replacers/ChannelMentionReplacer.kt | 7 +- .../vcspeaker/tts/replacers/EmojiReplacer.kt | 26 ++- .../tts/replacers/GuildEmojiReplacer.kt | 32 +++- .../vcspeaker/tts/replacers/RegexReplacer.kt | 28 ++- .../tts/replacers/RoleMentionReplacer.kt | 7 +- .../vcspeaker/tts/replacers/UrlReplacer.kt | 39 ++-- .../tts/replacers/UserMentionReplacer.kt | 7 +- src/test/kotlin/TextProcessorTest.kt | 32 +++- 21 files changed, 437 insertions(+), 107 deletions(-) create mode 100644 src/main/kotlin/com/jaoafa/vcspeaker/commands/ParseCommand.kt create mode 100644 src/main/kotlin/com/jaoafa/vcspeaker/tts/Token.kt 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/Main.kt b/src/main/kotlin/com/jaoafa/vcspeaker/Main.kt index 4d1cd5ef..593eb74b 100644 --- a/src/main/kotlin/com/jaoafa/vcspeaker/Main.kt +++ b/src/main/kotlin/com/jaoafa/vcspeaker/Main.kt @@ -81,6 +81,8 @@ class Main : CliktCommand() { override fun run() { logger.info { "Starting VCSpeaker..." } + logger.info { "Reading config: $configPath" } + // Options > Config > Default val config = Config { addSpec(TokenSpec) diff --git a/src/main/kotlin/com/jaoafa/vcspeaker/commands/AliasCommand.kt b/src/main/kotlin/com/jaoafa/vcspeaker/commands/AliasCommand.kt index a23932d1..5eb3c00f 100644 --- a/src/main/kotlin/com/jaoafa/vcspeaker/commands/AliasCommand.kt +++ b/src/main/kotlin/com/jaoafa/vcspeaker/commands/AliasCommand.kt @@ -47,7 +47,7 @@ class AliasCommand : Extension() { } inner class UpdateOptions : Options() { - val search by string { + val alias by string { name = "alias" description = "更新するエイリアス" @@ -61,6 +61,11 @@ class AliasCommand : Extension() { choice(aliasType.displayName, aliasType.name) } + val search by optionalString { + name = "search" + description = "置き換える条件" + } + val replace by optionalString { name = "replace" description = "置き換える文字列" @@ -121,11 +126,12 @@ class AliasCommand : Extension() { publicSubCommand("update", "エイリアスを更新します。", ::UpdateOptions) { action { - val aliasData = AliasStore.find(guild!!.id, arguments.search) + val aliasData = AliasStore.find(guild!!.id, arguments.alias) if (aliasData != null) { val (_, _, type, search, replace) = aliasData val updatedType = arguments.type?.let { typeString -> AliasType.valueOf(typeString) } ?: type + val updatedSearch = arguments.search ?: search val updatedReplace = arguments.replace ?: replace AliasStore.remove(aliasData) @@ -133,20 +139,31 @@ class AliasCommand : Extension() { aliasData.copy( userId = user.id, type = updatedType, + search = updatedSearch, replace = updatedReplace ) ) respondEmbed( ":repeat: Alias Updated", - "${type.displayName}のエイリアスを更新しました。" + "エイリアスを更新しました。" ) { authorOf(user) - fieldAliasFrom(updatedType, search) + fun searchDisplay(type: AliasType, search: String) = when (type) { + AliasType.Text -> search + AliasType.Regex -> "`$search`" + AliasType.Emoji -> "$search `$search`" + } + + field("${updatedType.emoji} ${updatedType.displayName}", true) { + searchDisplay(type, search) + if (replace != updatedReplace) + " → **${searchDisplay(updatedType, updatedSearch)}**" + else "" + } field(":arrows_counterclockwise: 置き換える文字列", true) { - "$replace → **${updatedReplace}**" + if (replace != updatedReplace) "「$replace」→「**$updatedReplace**」" else "「$replace」" } successColor() @@ -158,14 +175,14 @@ class AliasCommand : Extension() { } else { respondEmbed( ":question: Alias Not Found", - "置き換え条件が「${arguments.search}」のエイリアスは見つかりませんでした。" + "置き換え条件が「${arguments.alias}」のエイリアスは見つかりませんでした。" ) { authorOf(user) errorColor() } log(logger) { guild, user -> - "[${guild.name}] Alias Not Found: @${user.username} searched for alias contains \"${arguments.search}\" but not found" + "[${guild.name}] Alias Not Found: @${user.username} searched for alias contains \"${arguments.alias}\" but not found" } } } @@ -238,9 +255,8 @@ class AliasCommand : Extension() { title = ":information_source: Aliases" - description = chunkedAliases.joinToString("\n") { (_, userId, type, from, to) -> - val fromDisplay = if (type == AliasType.Regex) "`$from`" else from - "${type.emoji} ${type.displayName} | 「$fromDisplay → $to」 | <@${userId}>" + description = chunkedAliases.joinToString("\n") { + it.toDisplayWithEmoji() } successColor() diff --git a/src/main/kotlin/com/jaoafa/vcspeaker/commands/IgnoreCommand.kt b/src/main/kotlin/com/jaoafa/vcspeaker/commands/IgnoreCommand.kt index 5524ce82..3887cbd3 100644 --- a/src/main/kotlin/com/jaoafa/vcspeaker/commands/IgnoreCommand.kt +++ b/src/main/kotlin/com/jaoafa/vcspeaker/commands/IgnoreCommand.kt @@ -47,7 +47,7 @@ class IgnoreCommand : Extension() { val guildId = event.interaction.getChannel().data.guildId.value suggestStringCollection( - IgnoreStore.filter(guildId).map { it.text }, + IgnoreStore.filter(guildId).map { it.search }, FilterStrategy.Contains ) } @@ -144,8 +144,8 @@ class IgnoreCommand : Extension() { title = ":information_source: Ignores" - description = chunkedIgnores.joinToString("\n") { (_, userId, type, text) -> - "${type.emoji} ${type.displayName} | 「$text」 | <@${userId}>" + description = chunkedIgnores.joinToString("\n") { + it.toDisplayWithEmoji() } successColor() diff --git a/src/main/kotlin/com/jaoafa/vcspeaker/commands/ParseCommand.kt b/src/main/kotlin/com/jaoafa/vcspeaker/commands/ParseCommand.kt new file mode 100644 index 00000000..1be8eafe --- /dev/null +++ b/src/main/kotlin/com/jaoafa/vcspeaker/commands/ParseCommand.kt @@ -0,0 +1,176 @@ +package com.jaoafa.vcspeaker.commands + +import com.jaoafa.vcspeaker.stores.IgnoreStore +import com.jaoafa.vcspeaker.stores.IgnoreType +import com.jaoafa.vcspeaker.tools.Emoji.containsEmojis +import com.jaoafa.vcspeaker.tools.Emoji.getEmojis +import com.jaoafa.vcspeaker.tools.discord.DiscordExtensions.errorColor +import com.jaoafa.vcspeaker.tools.discord.DiscordExtensions.respondEmbed +import com.jaoafa.vcspeaker.tools.discord.DiscordExtensions.successColor +import com.jaoafa.vcspeaker.tools.discord.Options +import com.jaoafa.vcspeaker.tools.discord.SlashCommandExtensions.publicSlashCommand +import com.jaoafa.vcspeaker.tts.TextProcessor +import com.jaoafa.vcspeaker.tts.Token +import com.kotlindiscord.kord.extensions.checks.anyGuild +import com.kotlindiscord.kord.extensions.commands.converters.impl.string +import com.kotlindiscord.kord.extensions.extensions.Extension + +class ParseCommand : Extension() { + override val name = this::class.simpleName!! + + inner class ParseOptions : Options() { + val text by string { + name = "message" + description = "試すメッセージ" + } + } + + override suspend fun setup() { + publicSlashCommand("parse", "読み上げる文章の処理をテストします", ::ParseOptions) { + check { anyGuild() } + action { + val guildId = guild!!.id + val text = arguments.text + + fun effectiveIgnores(text: String) = IgnoreStore.filter(guildId).filter { + when (it.type) { + IgnoreType.Equals -> text == it.search + IgnoreType.Contains -> text.contains(it.search) + } + } + + val effectiveIgnores = effectiveIgnores(text) + + suspend fun respondStepEmbed( + checkIgnore: String? = null, + applyAlias: String? = null, + recheckIgnore: String? = null, + replaceEmoji: String? = null, + result: String, + ignored: Boolean + ) = respondEmbed( + ":alembic: Text Parsed" + ) { + field(":a: 入力") { + "「$text」" + } + + fun stepField(step: Int, title: String, text: String?) { + val emojis = listOf(":one:", ":two:", ":three:", ":four:") + val emoji = if (text != null) emojis.getOrNull(step - 1) else ":white_large_square:" + + field("$emoji $title") { + text ?: "*スキップされました。" + } + } + + stepField( + step = 1, + title = "無視するか確認", + text = checkIgnore + ) + + stepField( + step = 2, + title = "エイリアスを適用", + text = applyAlias + ) + + stepField( + step = 3, + title = "無視するか再確認", + text = recheckIgnore + ) + + stepField( + step = 4, + title = "Unicode 絵文字を置き換え", + text = replaceEmoji + ) + + field(":white_check_mark: 結果") { + result + } + + if (ignored) errorColor() else successColor() + } + + // step 1: check ignore + if (effectiveIgnores.isNotEmpty()) { + respondStepEmbed( + checkIgnore = effectiveIgnores.joinToString("\n") { + it.toDisplay() + }, + result = "*無視されました。", + ignored = true + ) + + return@action + } + + // step 2: apply alias + val tokens = TextProcessor.replacers.fold(mutableListOf(Token(text))) { tokens, replacer -> + replacer.replace(tokens, guildId) + } + + // annotate text with alias index + var aliasIndex = 1 + val annotatedText = tokens.joinToString("") { + it.text + if (it.replaced()) { + val annotation = " `[$aliasIndex]` " + aliasIndex++ + annotation + } else "" + } + + // step 3: recheck ignore + val annotatedEffectiveIgnores = effectiveIgnores(annotatedText) + + val replacedTokens = tokens.filter { it.replaced() } + + val replaceResult = "「$annotatedText」\n" + replacedTokens.withIndex() + .joinToString("\n") { (i, token) -> "$i. ${token.replacer}" } + + if (annotatedEffectiveIgnores.isNotEmpty()) { + respondStepEmbed( + checkIgnore = "*無視されませんでした。", + applyAlias = replaceResult, + recheckIgnore = annotatedEffectiveIgnores.joinToString("\n") { + it.toDisplay() + }, + result = "*無視されました。", + ignored = true + ) + + return@action + } + + // step 4: replace emoji + val appliedText = tokens.joinToString("") { it.text } + val emojis = appliedText.getEmojis() + + var emojiIndex = 1 + val (annotatedAppliedText, result) = emojis.fold(appliedText to appliedText) { (annotated, result), emoji -> + val newAnnotated = annotated.replace(emoji.unicode, "${emoji.name} `[$emojiIndex]`") + val newResult = result.replace(emoji.unicode, emoji.name) + + emojiIndex++ + + newAnnotated to newResult + } + + val emojiReplaceResult = "「$annotatedAppliedText」\n" + emojis + .withIndex().joinToString("\n") { (i, emoji) -> "$i. ${emoji.unicode} → ${emoji.name}" } + + respondStepEmbed( + checkIgnore = "*無視されませんでした。", + applyAlias = if (replacedTokens.isNotEmpty()) replaceResult else "*置き換えられませんでした。", + recheckIgnore = "*無視されませんでした。", + replaceEmoji = if (appliedText.containsEmojis()) emojiReplaceResult else "*絵文字は含まれていませんでした。", + result = "「$result」", + ignored = false + ) + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/jaoafa/vcspeaker/features/Alias.kt b/src/main/kotlin/com/jaoafa/vcspeaker/features/Alias.kt index 061793bc..44c05cb6 100644 --- a/src/main/kotlin/com/jaoafa/vcspeaker/features/Alias.kt +++ b/src/main/kotlin/com/jaoafa/vcspeaker/features/Alias.kt @@ -19,12 +19,12 @@ object Alias { ) } - fun EmbedBuilder.fieldAliasFrom(type: AliasType, from: String) = + fun EmbedBuilder.fieldAliasFrom(type: AliasType, search: String) = this.field("${type.emoji} ${type.displayName}", true) { when (type) { - AliasType.Text -> from - AliasType.Regex -> "`$from`" - AliasType.Emoji -> "$from `$from`" + AliasType.Text -> search + AliasType.Regex -> "`$search`" + AliasType.Emoji -> "$search `$search`" } } } \ No newline at end of file diff --git a/src/main/kotlin/com/jaoafa/vcspeaker/stores/AliasStore.kt b/src/main/kotlin/com/jaoafa/vcspeaker/stores/AliasStore.kt index 35af43ca..45f25956 100644 --- a/src/main/kotlin/com/jaoafa/vcspeaker/stores/AliasStore.kt +++ b/src/main/kotlin/com/jaoafa/vcspeaker/stores/AliasStore.kt @@ -22,7 +22,12 @@ data class AliasData( val type: AliasType, val search: String, val replace: String -) +) { + private val searchDisplay = if (type == AliasType.Regex) " `$search` " else "「$search」" + fun toDisplay() = "${type.displayName}${searchDisplay}→「$replace」<@$userId>" + + fun toDisplayWithEmoji() = "${type.emoji} ${toDisplay()}" +} object AliasStore : StoreStruct( VCSpeaker.Files.aliases.path, diff --git a/src/main/kotlin/com/jaoafa/vcspeaker/stores/IgnoreStore.kt b/src/main/kotlin/com/jaoafa/vcspeaker/stores/IgnoreStore.kt index 9e326be0..20ddcfe7 100644 --- a/src/main/kotlin/com/jaoafa/vcspeaker/stores/IgnoreStore.kt +++ b/src/main/kotlin/com/jaoafa/vcspeaker/stores/IgnoreStore.kt @@ -19,15 +19,19 @@ data class IgnoreData( val guildId: Snowflake, val userId: Snowflake, val type: IgnoreType, - val text: String -) + val search: String +) { + fun toDisplay() = "${type.displayName}「$search」<@$userId>" + + fun toDisplayWithEmoji() = "${type.emoji} ${toDisplay()}" +} object IgnoreStore : StoreStruct( VCSpeaker.Files.ignores.path, IgnoreData.serializer(), { Json.decodeFromString(this) } ) { - fun find(guildId: Snowflake, text: String) = data.find { it.guildId == guildId && it.text == text } + fun find(guildId: Snowflake, text: String) = data.find { it.guildId == guildId && it.search == text } fun filter(guildId: Snowflake?) = data.filter { it.guildId == guildId } } \ No newline at end of file diff --git a/src/main/kotlin/com/jaoafa/vcspeaker/tools/Emoji.kt b/src/main/kotlin/com/jaoafa/vcspeaker/tools/Emoji.kt index 92746791..c3ae13c5 100644 --- a/src/main/kotlin/com/jaoafa/vcspeaker/tools/Emoji.kt +++ b/src/main/kotlin/com/jaoafa/vcspeaker/tools/Emoji.kt @@ -26,11 +26,11 @@ object Emoji { val emojiDataString = lines.map { it.split("#").last().trim() } val emojiData = emojiDataString.map { val stringPart = it.split(" ") - val emoji = stringPart.first() + val unicode = stringPart.first() val name = stringPart.drop(2).joinToString(" ") - EmojiData(emoji, name) - } + EmojiData(unicode, name) + }.filter { it.unicode.isNotEmpty() && it.name.isNotEmpty() } logger.info { "Loading emojis complete." } @@ -53,12 +53,14 @@ object Emoji { replaced.replace(emoji.unicode, "") } - fun String.replaceEmojiToName(): String { - return emojis.fold(this) { replaced, emoji -> - replaced.replace(emoji.unicode, emoji.name) - } + fun String.replaceEmojisToName() = emojis.fold(this) { replaced, emoji -> + replaced.replace(emoji.unicode, emoji.name) } + fun String.containsEmojis() = emojis.any { contains(it.unicode) } + + fun String.getEmojis() = emojis.filter { contains(it.unicode) } + private fun List.startsWith(list: List): Boolean { if (this.size < list.size) return false diff --git a/src/main/kotlin/com/jaoafa/vcspeaker/tts/TextProcessor.kt b/src/main/kotlin/com/jaoafa/vcspeaker/tts/TextProcessor.kt index bc7183fc..23d8c21f 100644 --- a/src/main/kotlin/com/jaoafa/vcspeaker/tts/TextProcessor.kt +++ b/src/main/kotlin/com/jaoafa/vcspeaker/tts/TextProcessor.kt @@ -2,7 +2,7 @@ package com.jaoafa.vcspeaker.tts import com.jaoafa.vcspeaker.stores.IgnoreStore import com.jaoafa.vcspeaker.stores.IgnoreType -import com.jaoafa.vcspeaker.tools.Emoji.replaceEmojiToName +import com.jaoafa.vcspeaker.tools.Emoji.replaceEmojisToName import com.jaoafa.vcspeaker.tools.getClassesIn import com.jaoafa.vcspeaker.tts.api.Emotion import com.jaoafa.vcspeaker.tts.api.Speaker @@ -17,20 +17,20 @@ object TextProcessor { it.kotlin.objectInstance }.sortedByDescending { it.priority.level } - private fun String.shouldIgnoreOn(guildId: Snowflake) = + fun String.shouldIgnoreOn(guildId: Snowflake) = IgnoreStore.filter(guildId).any { when (it.type) { - IgnoreType.Equals -> this == it.text - IgnoreType.Contains -> contains(it.text) + IgnoreType.Equals -> this == it.search + IgnoreType.Contains -> contains(it.search) } } 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 }.replaceEmojisToName() 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..bfe32b5c --- /dev/null +++ b/src/main/kotlin/com/jaoafa/vcspeaker/tts/Token.kt @@ -0,0 +1,5 @@ +package com.jaoafa.vcspeaker.tts + +data class Token(val text: String, val replacer: String? = null) { + fun replaced() = replacer != null +} 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..be4ad92c 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,35 @@ 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 + + // should be skipped + if (replacedToken.replaced() || !text.contains(alias.search)) { + add(replacedToken) + continue + } + + val splitTexts = text.split(alias.search) + + val additions = splitTexts.mixin { + Token(alias.replace, "Text Alias「${alias.search}」→「${alias.replace}」") + } + + 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..29b521dd 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, "Mentionable `$id` →「$name」") + } + + newTokens.addAll(additions) } - return replacedText + return newTokens } + + + fun List.mixin(provider: suspend (Int) -> Token) = buildList { + // ["text1", "text2", "text3"] -> [Token1, provider(0), Token2, provider(1), 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..f0d06bf4 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, "Emoji Alias「${alias.search}」→「${alias.replace}」") + } + + 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..b732d4fa 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, "Guild Emoji `${match.value}` →「$emojiName」") + } + + 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..7ff7f812 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, "Regex Alias `${alias.search}` →「${alias.replace}」") + } + + 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..5bad0947 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 @@ -69,7 +70,7 @@ class TextProcessorTest : FunSpec({ guildId = Snowflake(0), userId = Snowflake(0), type = IgnoreType.Contains, - text = "Kotlin" + search = "Kotlin" ) ) @@ -83,7 +84,7 @@ class TextProcessorTest : FunSpec({ guildId = Snowflake(0), userId = Snowflake(0), type = IgnoreType.Contains, - text = "world" + search = "world" ) ) @@ -107,7 +108,7 @@ class TextProcessorTest : FunSpec({ guildId = Snowflake(0), userId = Snowflake(0), type = IgnoreType.Contains, - text = "Domain" + search = "Domain" ) ) @@ -121,7 +122,7 @@ class TextProcessorTest : FunSpec({ guildId = Snowflake(0), userId = Snowflake(0), type = IgnoreType.Contains, - text = "https://" + search = "https://" ) ) @@ -138,7 +139,7 @@ class TextProcessorTest : FunSpec({ guildId = Snowflake(0), userId = Snowflake(0), type = IgnoreType.Equals, - text = text + search = text ) ) @@ -152,7 +153,7 @@ class TextProcessorTest : FunSpec({ guildId = Snowflake(0), userId = Snowflake(0), type = IgnoreType.Equals, - text = "Hello, world" + search = "Hello, world" ) ) @@ -168,7 +169,7 @@ class TextProcessorTest : FunSpec({ guildId = Snowflake(0), userId = Snowflake(0), type = IgnoreType.Contains, - text = "world" + search = "world" ) ) @@ -182,7 +183,7 @@ class TextProcessorTest : FunSpec({ guildId = Snowflake(0), userId = Snowflake(0), type = IgnoreType.Contains, - text = "worlds" + search = "worlds" ) ) @@ -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