From 20b0dcf0627e33823e9992993e5b9b212d8215ab Mon Sep 17 00:00:00 2001 From: yuu111 Date: Thu, 29 Jan 2026 11:06:32 +0900 Subject: [PATCH 01/16] =?UTF-8?q?=E7=84=A1=E5=8A=B9=E3=81=AATTS=E3=82=A8?= =?UTF-8?q?=E3=83=B3=E3=82=B8=E3=83=B3=E3=81=AE=E5=88=9D=E6=9C=9F=E5=8C=96?= =?UTF-8?q?=E3=82=92=E3=82=B9=E3=82=AD=E3=83=83=E3=83=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 設定で無効にしているエンジン(voicevox, coeiroink, sharevox)が 起動時にURL確認を行わないように修正 --- .../main/java/dev/felnull/itts/core/voice/VoiceManager.java | 4 ++-- .../felnull/itts/core/voice/coeiroink/CoeiroinkManager.java | 3 +++ .../dev/felnull/itts/core/voice/voicevox/VoicevoxManager.java | 3 +++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/dev/felnull/itts/core/voice/VoiceManager.java b/core/src/main/java/dev/felnull/itts/core/voice/VoiceManager.java index aa53393..a35359e 100644 --- a/core/src/main/java/dev/felnull/itts/core/voice/VoiceManager.java +++ b/core/src/main/java/dev/felnull/itts/core/voice/VoiceManager.java @@ -63,8 +63,8 @@ private void registerVoiceTypes(Supplier> availableVoiceTypes) { return CompletableFuture.allOf( voicevoxManager.init(), coeiroinkManager.init(), - sharevoxManager.init()). - thenAcceptAsync(v -> { + sharevoxManager.init()) + .thenAcceptAsync(v -> { registerVoiceTypes(voiceTextManager::getVoiceTypes); registerVoiceTypes(voicevoxManager::getAvailableVoiceTypes); registerVoiceTypes(coeiroinkManager::getAvailableVoiceTypes); diff --git a/core/src/main/java/dev/felnull/itts/core/voice/coeiroink/CoeiroinkManager.java b/core/src/main/java/dev/felnull/itts/core/voice/coeiroink/CoeiroinkManager.java index a9c5aae..95521b6 100644 --- a/core/src/main/java/dev/felnull/itts/core/voice/coeiroink/CoeiroinkManager.java +++ b/core/src/main/java/dev/felnull/itts/core/voice/coeiroink/CoeiroinkManager.java @@ -76,6 +76,9 @@ protected VoicevoxConfig getConfig() { * @return 初期化の非同期CompletableFuture */ public CompletableFuture init() { + if (!getConfig().isEnable()) { + return CompletableFuture.completedFuture(null); + } return balancer.init(); } diff --git a/core/src/main/java/dev/felnull/itts/core/voice/voicevox/VoicevoxManager.java b/core/src/main/java/dev/felnull/itts/core/voice/voicevox/VoicevoxManager.java index 1fc846b..30f498f 100644 --- a/core/src/main/java/dev/felnull/itts/core/voice/voicevox/VoicevoxManager.java +++ b/core/src/main/java/dev/felnull/itts/core/voice/voicevox/VoicevoxManager.java @@ -77,6 +77,9 @@ protected VoicevoxConfig getConfig() { * @return 初期化の非同期CompletableFuture */ public CompletableFuture init() { + if (!getConfig().isEnable()) { + return CompletableFuture.completedFuture(null); + } return balancer.init(); } From a9f8d66976c4045fa4ffebf5942946afead641a5 Mon Sep 17 00:00:00 2001 From: yuu111 Date: Sat, 31 Jan 2026 03:10:52 +0900 Subject: [PATCH 02/16] =?UTF-8?q?=E6=AD=A3=E8=A6=8F=E8=A1=A8=E7=8F=BE?= =?UTF-8?q?=E3=83=90=E3=83=AA=E3=83=87=E3=83=BC=E3=82=B7=E3=83=A7=E3=83=B3?= =?UTF-8?q?=E3=83=A6=E3=83=BC=E3=83=86=E3=82=A3=E3=83=AA=E3=83=86=E3=82=A3?= =?UTF-8?q?=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ReDoS脆弱性検出を含む正規表現の構文検証を行うPatternValidatorクラスを追加 --- .../itts/core/util/PatternValidator.java | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 core/src/main/java/dev/felnull/itts/core/util/PatternValidator.java diff --git a/core/src/main/java/dev/felnull/itts/core/util/PatternValidator.java b/core/src/main/java/dev/felnull/itts/core/util/PatternValidator.java new file mode 100644 index 0000000..75bde2f --- /dev/null +++ b/core/src/main/java/dev/felnull/itts/core/util/PatternValidator.java @@ -0,0 +1,94 @@ +package dev.felnull.itts.core.util; + +import org.jetbrains.annotations.NotNull; + +import java.util.Optional; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + +/** + * 正規表現バリデーションユーティリティ + */ +public final class PatternValidator { + + /** + * ReDoS脆弱性を持つ可能性のあるパターン + * ネストされた量子 (例: (a+)+, (a*)*) を検出 + */ + private static final Pattern REDOS_PATTERN = Pattern.compile( + "\\([^)]*[+*][^)]*\\)[+*]|\\([^)]*\\|[^)]*\\)[+*]" + ); + + private PatternValidator() { + } + + /** + * 正規表現の構文を検証する + * + * @param regex 検証する正規表現 + * @return 検証結果 + */ + @NotNull + public static ValidationResult validate(@NotNull String regex) { + try { + Pattern pattern = Pattern.compile(regex); + + if (isPotentiallyDangerous(regex)) { + return ValidationResult.failure("この正規表現はパフォーマンス上の問題を引き起こす可能性があります"); + } + + return ValidationResult.success(pattern); + } catch (PatternSyntaxException e) { + return ValidationResult.failure("無効な正規表現です: " + e.getDescription()); + } + } + + /** + * 正規表現がReDoS脆弱性を持つ可能性があるかチェックする + * + * @param regex チェックする正規表現 + * @return 危険な可能性がある場合はtrue + */ + public static boolean isPotentiallyDangerous(@NotNull String regex) { + return REDOS_PATTERN.matcher(regex).find(); + } + + /** + * 正規表現バリデーションの結果 + * + * @param valid 有効かどうか + * @param pattern コンパイル済みPattern (有効な場合のみ) + * @param error エラーメッセージ (無効な場合のみ) + */ + public record ValidationResult(boolean valid, Pattern pattern, String error) { + + /** + * 成功結果を作成 + * + * @param pattern コンパイル済みPattern + * @return 成功結果 + */ + public static ValidationResult success(@NotNull Pattern pattern) { + return new ValidationResult(true, pattern, null); + } + + /** + * 失敗結果を作成 + * + * @param error エラーメッセージ + * @return 失敗結果 + */ + public static ValidationResult failure(@NotNull String error) { + return new ValidationResult(false, null, error); + } + + /** + * Patternを取得 (成功時のみ) + * + * @return コンパイル済みPattern + */ + public Optional getPattern() { + return Optional.ofNullable(pattern); + } + } +} From 9802eaf404e62c7bd7035c89fc528c8e390385cb Mon Sep 17 00:00:00 2001 From: yuu111 Date: Sat, 31 Jan 2026 03:11:16 +0900 Subject: [PATCH 03/16] =?UTF-8?q?Voice=20=E3=82=A8=E3=83=B3=E3=82=B8?= =?UTF-8?q?=E3=83=B3=E3=81=AE=20API=20=E3=83=AC=E3=82=B9=E3=83=9D=E3=83=B3?= =?UTF-8?q?=E3=82=B9=E3=83=90=E3=83=AA=E3=83=87=E3=83=BC=E3=82=B7=E3=83=A7?= =?UTF-8?q?=E3=83=B3=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - HTTPステータスコードの検証を追加 - JSONレスポンスがnullの場合のエラーハンドリングを追加 --- .../voice/coeiroink/CoeiroinkManager.java | 10 ++++++++ .../core/voice/voicevox/VoicevoxManager.java | 24 ++++++++++++++++++- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/dev/felnull/itts/core/voice/coeiroink/CoeiroinkManager.java b/core/src/main/java/dev/felnull/itts/core/voice/coeiroink/CoeiroinkManager.java index a9c5aae..ad948e1 100644 --- a/core/src/main/java/dev/felnull/itts/core/voice/coeiroink/CoeiroinkManager.java +++ b/core/src/main/java/dev/felnull/itts/core/voice/coeiroink/CoeiroinkManager.java @@ -115,12 +115,22 @@ protected List requestSpeakers(CIURL ciurl) throws IOException .timeout(Duration.of(3000, ChronoUnit.MILLIS)) .build(); HttpResponse rep = hc.send(req, HttpResponse.BodyHandlers.ofInputStream()); + + int statusCode = rep.statusCode(); + if (statusCode != 200) { + throw new IOException(name + " API error: HTTP " + statusCode); + } + JsonArray ja; try (InputStream stream = new BufferedInputStream(rep.body()); Reader reader = new InputStreamReader(stream, StandardCharsets.UTF_8)) { ja = GSON.fromJson(reader, JsonArray.class); } + if (ja == null) { + throw new IOException(name + " API returned invalid JSON response"); + } + ImmutableList.Builder speakerBuilder = new ImmutableList.Builder<>(); for (JsonElement je : ja) { diff --git a/core/src/main/java/dev/felnull/itts/core/voice/voicevox/VoicevoxManager.java b/core/src/main/java/dev/felnull/itts/core/voice/voicevox/VoicevoxManager.java index 1fc846b..27bc76c 100644 --- a/core/src/main/java/dev/felnull/itts/core/voice/voicevox/VoicevoxManager.java +++ b/core/src/main/java/dev/felnull/itts/core/voice/voicevox/VoicevoxManager.java @@ -116,12 +116,22 @@ protected List requestSpeakers(VVURL vvurl) throws IOException, .timeout(Duration.of(3000, ChronoUnit.MILLIS)) .build(); HttpResponse rep = hc.send(req, HttpResponse.BodyHandlers.ofInputStream()); + + int statusCode = rep.statusCode(); + if (statusCode != 200) { + throw new IOException(name + " API error: HTTP " + statusCode); + } + JsonArray ja; try (InputStream stream = new BufferedInputStream(rep.body()); Reader reader = new InputStreamReader(stream, StandardCharsets.UTF_8)) { ja = GSON.fromJson(reader, JsonArray.class); } + if (ja == null) { + throw new IOException(name + " API returned invalid JSON response"); + } + ImmutableList.Builder speakerBuilder = new ImmutableList.Builder<>(); for (JsonElement je : ja) { @@ -142,9 +152,21 @@ private JsonObject getQuery(String text, int speakerId) { .build(); HttpResponse rep = hc.send(req, HttpResponse.BodyHandlers.ofInputStream()); + int statusCode = rep.statusCode(); + if (statusCode != 200) { + throw new IOException(name + " audio_query API error: HTTP " + statusCode); + } + + JsonObject result; try (InputStream stream = new BufferedInputStream(rep.body()); Reader reader = new InputStreamReader(stream, StandardCharsets.UTF_8)) { - return GSON.fromJson(reader, JsonObject.class); + result = GSON.fromJson(reader, JsonObject.class); } + + if (result == null) { + throw new IOException(name + " audio_query API returned invalid JSON response"); + } + + return result; } catch (Exception e) { throw new RuntimeException(e); } From bc379092d40c03b7386587a56b4685e7982632c0 Mon Sep 17 00:00:00 2001 From: yuu111 Date: Sat, 31 Jan 2026 03:11:43 +0900 Subject: [PATCH 04/16] =?UTF-8?q?=E8=BE=9E=E6=9B=B8=E3=81=AE=E6=AD=A3?= =?UTF-8?q?=E8=A6=8F=E8=A1=A8=E7=8F=BE=E3=83=91=E3=82=BF=E3=83=BC=E3=83=B3?= =?UTF-8?q?=E5=87=A6=E7=90=86=E3=82=92=E5=A0=85=E7=89=A2=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 不正な正規表現パターンでPatternSyntaxExceptionが発生した場合に ログを出力してスキップするよう変更 --- .../dev/felnull/itts/core/dict/GlobalDictionary.java | 12 +++++++++++- .../dev/felnull/itts/core/dict/ServerDictionary.java | 12 +++++++++++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/dev/felnull/itts/core/dict/GlobalDictionary.java b/core/src/main/java/dev/felnull/itts/core/dict/GlobalDictionary.java index 833b012..e8fbcd3 100644 --- a/core/src/main/java/dev/felnull/itts/core/dict/GlobalDictionary.java +++ b/core/src/main/java/dev/felnull/itts/core/dict/GlobalDictionary.java @@ -11,7 +11,9 @@ import java.util.Map; import java.util.function.Function; import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; import java.util.stream.Collectors; +import java.util.stream.Stream; /** * グローバル辞書 @@ -50,7 +52,15 @@ public int getDefaultPriority() { protected @NotNull Map> getReplaces(long guildId) { LegacySaveDataLayer legacySaveDataLayer = SaveDataManager.getInstance().getLegacySaveDataLayer(); return legacySaveDataLayer.getAllGlobalDictData().stream() - .map(n -> Pair.of(Pattern.compile(n.getTarget()), n.getRead())) + .flatMap(n -> { + try { + Pattern pattern = Pattern.compile(n.getTarget()); + return Stream.of(Pair.of(pattern, n.getRead())); + } catch (PatternSyntaxException e) { + getITTSLogger().warn("Invalid regex pattern in global dict: {}", n.getTarget()); + return Stream.empty(); + } + }) .collect(Collectors.toMap(Pair::getLeft, patternStringPair -> n -> patternStringPair.getRight())); } } diff --git a/core/src/main/java/dev/felnull/itts/core/dict/ServerDictionary.java b/core/src/main/java/dev/felnull/itts/core/dict/ServerDictionary.java index cbd01ed..5f2b79c 100644 --- a/core/src/main/java/dev/felnull/itts/core/dict/ServerDictionary.java +++ b/core/src/main/java/dev/felnull/itts/core/dict/ServerDictionary.java @@ -11,7 +11,9 @@ import java.util.Map; import java.util.function.Function; import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; import java.util.stream.Collectors; +import java.util.stream.Stream; /** * サーバー辞書 @@ -50,7 +52,15 @@ public int getDefaultPriority() { protected @NotNull Map> getReplaces(long guildId) { LegacySaveDataLayer legacySaveDataLayer = SaveDataManager.getInstance().getLegacySaveDataLayer(); return legacySaveDataLayer.getAllServerDictData(guildId).stream() - .map(n -> Pair.of(Pattern.compile(n.getTarget()), n.getRead())) + .flatMap(n -> { + try { + Pattern pattern = Pattern.compile(n.getTarget()); + return Stream.of(Pair.of(pattern, n.getRead())); + } catch (PatternSyntaxException e) { + getITTSLogger().warn("Invalid regex pattern in server dict: {}", n.getTarget()); + return Stream.empty(); + } + }) .collect(Collectors.toMap(Pair::getLeft, patternStringPair -> n -> patternStringPair.getRight())); } } From 34396ea6ad23764d0b6dcb36bbac8c5f8e300ddc Mon Sep 17 00:00:00 2001 From: yuu111 Date: Sat, 31 Jan 2026 03:12:09 +0900 Subject: [PATCH 05/16] =?UTF-8?q?TTSManager=20=E3=81=AE=20ignore=20regex?= =?UTF-8?q?=20=E3=83=90=E3=83=AA=E3=83=87=E3=83=BC=E3=82=B7=E3=83=A7?= =?UTF-8?q?=E3=83=B3=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 不正な正規表現パターンでエラーが発生した場合に ログを出力して処理を継続するよう変更 --- .../java/dev/felnull/itts/core/tts/TTSManager.java | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/core/src/main/java/dev/felnull/itts/core/tts/TTSManager.java b/core/src/main/java/dev/felnull/itts/core/tts/TTSManager.java index 3f7bc75..0228610 100644 --- a/core/src/main/java/dev/felnull/itts/core/tts/TTSManager.java +++ b/core/src/main/java/dev/felnull/itts/core/tts/TTSManager.java @@ -24,6 +24,7 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.function.Function; import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; /** * TTS管理 @@ -186,9 +187,13 @@ public void sayChat(@NotNull Guild guild, @NotNull MessageChannel messageChannel String ignoreRegex = legacySaveDataLayer.getServerData(guild.getIdLong()).getIgnoreRegex(); if (ignoreRegex != null) { - Pattern ignorePattern = Pattern.compile(ignoreRegex); - if (ignorePattern.matcher(message.getContentDisplay()).matches()) { - return; + try { + Pattern ignorePattern = Pattern.compile(ignoreRegex); + if (ignorePattern.matcher(message.getContentDisplay()).matches()) { + return; + } + } catch (PatternSyntaxException e) { + getITTSLogger().warn("Invalid ignore regex for guild {}: {}", guild.getIdLong(), ignoreRegex); } } From 19861441a4a4d798b094e0f33b13da02b14cf47c Mon Sep 17 00:00:00 2001 From: yuu111 Date: Sat, 31 Jan 2026 03:12:32 +0900 Subject: [PATCH 06/16] =?UTF-8?q?=E3=83=8B=E3=83=83=E3=82=AF=E3=83=8D?= =?UTF-8?q?=E3=83=BC=E3=83=A0=E3=82=B3=E3=83=9E=E3=83=B3=E3=83=89=E3=81=AE?= =?UTF-8?q?=E5=85=A5=E5=8A=9B=E3=83=90=E3=83=AA=E3=83=87=E3=83=BC=E3=82=B7?= =?UTF-8?q?=E3=83=A7=E3=83=B3=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 空白のみの名前が設定されることを防止 --- .../dev/felnull/itts/core/discord/command/AdminCommand.java | 2 ++ .../dev/felnull/itts/core/discord/command/VnickCommand.java | 2 ++ 2 files changed, 4 insertions(+) diff --git a/core/src/main/java/dev/felnull/itts/core/discord/command/AdminCommand.java b/core/src/main/java/dev/felnull/itts/core/discord/command/AdminCommand.java index eef3812..5be6950 100644 --- a/core/src/main/java/dev/felnull/itts/core/discord/command/AdminCommand.java +++ b/core/src/main/java/dev/felnull/itts/core/discord/command/AdminCommand.java @@ -79,6 +79,8 @@ private void vnick(SlashCommandInteractionEvent event) { if (name == null) { sud.setNickName(null); event.reply(DiscordUtils.getEscapedName(event.getGuild(), user) + "の読み上げユーザ名をリセットしました。").queue(); + } else if (name.isBlank()) { + event.reply("名前を空にすることはできません。リセットするには名前を指定せずに実行してください。").setEphemeral(true).queue(); } else { sud.setNickName(name); event.reply(DiscordUtils.getEscapedName(event.getGuild(), user) + "の読み上げユーザ名を変更しました。").queue(); diff --git a/core/src/main/java/dev/felnull/itts/core/discord/command/VnickCommand.java b/core/src/main/java/dev/felnull/itts/core/discord/command/VnickCommand.java index c5aa5e0..9788909 100644 --- a/core/src/main/java/dev/felnull/itts/core/discord/command/VnickCommand.java +++ b/core/src/main/java/dev/felnull/itts/core/discord/command/VnickCommand.java @@ -51,6 +51,8 @@ public void commandInteraction(SlashCommandInteractionEvent event) { if (name == null) { sud.setNickName(null); event.reply("自分の読み上げユーザ名をリセットしました。").setEphemeral(true).queue(); + } else if (name.isBlank()) { + event.reply("名前を空にすることはできません。リセットするには名前を指定せずに実行してください。").setEphemeral(true).queue(); } else { sud.setNickName(name); event.reply("自分の読み上げユーザ名を変更しました。").setEphemeral(true).queue(); From df0ac14aaf478f24842f4e845941b0889755ef44 Mon Sep 17 00:00:00 2001 From: yuu111 Date: Sat, 31 Jan 2026 03:12:56 +0900 Subject: [PATCH 07/16] =?UTF-8?q?DictCommand=20=E3=81=AE=E5=85=A5=E5=8A=9B?= =?UTF-8?q?=E3=83=90=E3=83=AA=E3=83=87=E3=83=BC=E3=82=B7=E3=83=A7=E3=83=B3?= =?UTF-8?q?=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - アップロードファイルのサイズ制限 (1MB) を追加 - JSON形式の検証を追加 - 空白のみの単語/読みを拒否 - 正規表現パターンのバリデーションを追加 - エラーメッセージの改善 --- .../core/discord/command/DictCommand.java | 40 +++++++++++++++++-- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/dev/felnull/itts/core/discord/command/DictCommand.java b/core/src/main/java/dev/felnull/itts/core/discord/command/DictCommand.java index 142fc76..52230db 100644 --- a/core/src/main/java/dev/felnull/itts/core/discord/command/DictCommand.java +++ b/core/src/main/java/dev/felnull/itts/core/discord/command/DictCommand.java @@ -3,12 +3,14 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonObject; +import com.google.gson.JsonSyntaxException; import dev.felnull.itts.core.dict.Dictionary; import dev.felnull.itts.core.dict.DictionaryManager; import dev.felnull.itts.core.dict.ServerDictionary; import dev.felnull.itts.core.savedata.SaveDataManager; import dev.felnull.itts.core.savedata.legacy.LegacyDictData; import dev.felnull.itts.core.savedata.legacy.LegacySaveDataLayer; +import dev.felnull.itts.core.util.PatternValidator; import dev.felnull.itts.core.util.StringUtils; import net.dv8tion.jda.api.EmbedBuilder; import net.dv8tion.jda.api.entities.Guild; @@ -54,6 +56,11 @@ public class DictCommand extends BaseCommand { */ private static final int MAX_FIELD_TEXT_LENGTH = 125; + /** + * アップロードファイルの最大サイズ (1MB) + */ + private static final int MAX_UPLOAD_FILE_SIZE = 1024 * 1024; + /** * GSOUN */ @@ -175,11 +182,24 @@ private void upload(SlashCommandInteractionEvent event) { Message.Attachment file = Objects.requireNonNull(event.getOption("file", OptionMapping::getAsAttachment)); boolean overwrite = Objects.requireNonNull(event.getOption("overwrite", OptionMapping::getAsBoolean)); + if (file.getSize() > MAX_UPLOAD_FILE_SIZE) { + int maxSizeMB = MAX_UPLOAD_FILE_SIZE / (1024 * 1024); + event.reply(String.format("ファイルサイズが大きすぎます。最大%dMBまでです。", maxSizeMB)).setEphemeral(true).queue(); + return; + } + event.deferReply().queue(); file.getProxy().download().thenApplyAsync(stream -> { - try (InputStream st = new BufferedInputStream(stream); Reader reader = new InputStreamReader(st)) { - return GSON.fromJson(reader, JsonObject.class); + try (InputStream st = new BufferedInputStream(stream); + Reader reader = new InputStreamReader(st, StandardCharsets.UTF_8)) { + JsonObject result = GSON.fromJson(reader, JsonObject.class); + if (result == null) { + throw new RuntimeException("Invalid JSON file"); + } + return result; + } catch (JsonSyntaxException e) { + throw new RuntimeException("Invalid JSON format: " + e.getMessage()); } catch (IOException e) { throw new RuntimeException(e); } @@ -204,7 +224,8 @@ private void upload(SlashCommandInteractionEvent event) { event.getHook().sendMessageEmbeds(replayEmbedBuilder.build()).addContent(overwrite ? "以下の単語の読みを上書き登録しました" : "以下の単語の読みを登録しました").queue(); } else { getITTSLogger().error("Dictionary registration failure", error); - event.getHook().sendMessage("辞書ファイルの読み込み中にエラーが発生しました").queue(); + String errorMessage = error.getCause() != null ? error.getCause().getMessage() : error.getMessage(); + event.getHook().sendMessage("辞書ファイルの読み込み中にエラーが発生しました: " + errorMessage).queue(); } }, getAsyncExecutor()); @@ -266,8 +287,19 @@ private void add(SlashCommandInteractionEvent event) { String word = Objects.requireNonNull(event.getOption("word", OptionMapping::getAsString)); String reading = Objects.requireNonNull(event.getOption("reading", OptionMapping::getAsString)); + if (word.isBlank() || reading.isBlank()) { + event.reply("単語と読みを空にすることはできません。").setEphemeral(true).queue(); + return; + } + if (word.length() > MAX_TEXT_LENGTH || reading.length() > MAX_TEXT_LENGTH) { - event.reply(String.format("登録可能な最大文字数は%d文字です", MAX_TEXT_LENGTH)).queue(); + event.reply(String.format("登録可能な最大文字数は%d文字です", MAX_TEXT_LENGTH)).setEphemeral(true).queue(); + return; + } + + PatternValidator.ValidationResult validationResult = PatternValidator.validate(word); + if (!validationResult.valid()) { + event.reply(validationResult.error()).setEphemeral(true).queue(); return; } From 8e5b80f508bb43ca8e977af700c1562d67df4b50 Mon Sep 17 00:00:00 2001 From: yuu111 Date: Sat, 31 Jan 2026 03:13:21 +0900 Subject: [PATCH 08/16] =?UTF-8?q?DictionaryManager=20=E3=81=AE=E3=82=A2?= =?UTF-8?q?=E3=83=83=E3=83=97=E3=83=AD=E3=83=BC=E3=83=89=E5=87=A6=E7=90=86?= =?UTF-8?q?=E3=83=90=E3=83=AA=E3=83=87=E3=83=BC=E3=82=B7=E3=83=A7=E3=83=B3?= =?UTF-8?q?=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - エントリ数の上限 (1000件) を追加 - エントリの文字数制限 (1000文字) を追加 - 空白のみのエントリをスキップ - 正規表現パターンのバリデーションを追加 - JSONファイル形式の検証を強化 --- .../itts/core/dict/DictionaryManager.java | 69 ++++++++++++++----- 1 file changed, 50 insertions(+), 19 deletions(-) diff --git a/core/src/main/java/dev/felnull/itts/core/dict/DictionaryManager.java b/core/src/main/java/dev/felnull/itts/core/dict/DictionaryManager.java index 64ad2b6..3efd810 100644 --- a/core/src/main/java/dev/felnull/itts/core/dict/DictionaryManager.java +++ b/core/src/main/java/dev/felnull/itts/core/dict/DictionaryManager.java @@ -8,6 +8,7 @@ import dev.felnull.itts.core.savedata.legacy.LegacyDictData; import dev.felnull.itts.core.savedata.legacy.LegacySaveDataLayer; import dev.felnull.itts.core.util.JsonUtils; +import dev.felnull.itts.core.util.PatternValidator; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Unmodifiable; @@ -28,6 +29,16 @@ public class DictionaryManager implements ITTSRuntimeUse { */ private static final int FILE_VERSION = 0; + /** + * アップロード時の最大エントリ数 + */ + private static final int MAX_DICT_ENTRIES = 1000; + + /** + * エントリの最大文字数 + */ + private static final int MAX_TEXT_LENGTH = 1000; + /** * グローバル辞書 */ @@ -254,35 +265,55 @@ public List serverDictLoadFromJson(@NotNull JsonObject jo, long int version = JsonUtils.getInt(jo, "version", -1); if (version != FILE_VERSION) { - throw new RuntimeException("Unsupported dictionary file version."); + throw new RuntimeException("Unsupported dictionary file version"); + } + + JsonElement entryElement = jo.get("entry"); + if (entryElement == null || !entryElement.isJsonObject()) { + throw new RuntimeException("Invalid dictionary file format"); + } + + JsonObject entry = jo.getAsJsonObject("entry"); + + if (entry.size() > MAX_DICT_ENTRIES) { + throw new RuntimeException("Dictionary entry count exceeds limit. Maximum is " + MAX_DICT_ENTRIES); } - if (jo.get("entry").isJsonObject()) { - JsonObject entry = jo.getAsJsonObject("entry"); - LegacySaveDataLayer legacySaveDataLayer = SaveDataManager.getInstance().getLegacySaveDataLayer(); + LegacySaveDataLayer legacySaveDataLayer = SaveDataManager.getInstance().getLegacySaveDataLayer(); + + for (Map.Entry en : entry.entrySet()) { + String target = en.getKey(); + + if (!en.getValue().isJsonPrimitive() || !en.getValue().getAsJsonPrimitive().isString()) { + continue; + } + + String read = en.getValue().getAsString(); - for (Map.Entry en : entry.entrySet()) { - String target = en.getKey(); + if (target.length() > MAX_TEXT_LENGTH || read.length() > MAX_TEXT_LENGTH) { + continue; + } - if (!en.getValue().isJsonPrimitive() || !en.getValue().getAsJsonPrimitive().isString()) { - continue; - } + if (target.isBlank() || read.isBlank()) { + continue; + } - String read = en.getValue().getAsString(); + if (!PatternValidator.validate(target).valid()) { + continue; + } - LegacyDictData pre = legacySaveDataLayer.getServerDictData(guildId, target); + LegacyDictData pre = legacySaveDataLayer.getServerDictData(guildId, target); - if (!overwrite && pre != null) { - continue; - } + if (!overwrite && pre != null) { + continue; + } - legacySaveDataLayer.addServerDictData(guildId, target, read); + legacySaveDataLayer.addServerDictData(guildId, target, read); - LegacyDictData ndata = Objects.requireNonNull(legacySaveDataLayer.getServerDictData(guildId, target)); + LegacyDictData ndata = Objects.requireNonNull(legacySaveDataLayer.getServerDictData(guildId, target)); - if (!ndata.equals(pre)) { - ret.add(ndata); - } + if (!ndata.equals(pre)) { + ret.add(ndata); } } From 11098f854e86c2b3d20bfedd7906928374de55a4 Mon Sep 17 00:00:00 2001 From: yuu111 Date: Sat, 31 Jan 2026 03:13:47 +0900 Subject: [PATCH 09/16] =?UTF-8?q?ConfigCommand=20=E3=81=AE=20regex=20?= =?UTF-8?q?=E3=83=90=E3=83=AA=E3=83=87=E3=83=BC=E3=82=B7=E3=83=A7=E3=83=B3?= =?UTF-8?q?=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ignore regex 設定時に正規表現の構文を検証 --- .../felnull/itts/core/discord/command/ConfigCommand.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/core/src/main/java/dev/felnull/itts/core/discord/command/ConfigCommand.java b/core/src/main/java/dev/felnull/itts/core/discord/command/ConfigCommand.java index 3ae0e01..d1e0415 100644 --- a/core/src/main/java/dev/felnull/itts/core/discord/command/ConfigCommand.java +++ b/core/src/main/java/dev/felnull/itts/core/discord/command/ConfigCommand.java @@ -5,6 +5,7 @@ import dev.felnull.itts.core.savedata.legacy.LegacySaveDataLayer; import dev.felnull.itts.core.savedata.legacy.LegacyServerData; import dev.felnull.itts.core.savedata.repository.ServerData; +import dev.felnull.itts.core.util.PatternValidator; import dev.felnull.itts.core.voice.VoiceCategory; import dev.felnull.itts.core.voice.VoiceManager; import dev.felnull.itts.core.voice.VoiceType; @@ -171,6 +172,13 @@ private void readIgnore(SlashCommandInteractionEvent event) { Guild guild = Objects.requireNonNull(event.getGuild()); String op = Objects.requireNonNull(event.getOption("regex", OptionMapping::getAsString)); + + PatternValidator.ValidationResult validationResult = PatternValidator.validate(op); + if (!validationResult.valid()) { + event.reply(validationResult.error()).setEphemeral(true).queue(); + return; + } + LegacySaveDataLayer legacySaveDataLayer = SaveDataManager.getInstance().getLegacySaveDataLayer(); LegacyServerData sd = legacySaveDataLayer.getServerData(guild.getIdLong()); From 884e0159178c80704a8b0c27b97a44bcdad904a3 Mon Sep 17 00:00:00 2001 From: yuu111 Date: Sat, 31 Jan 2026 03:14:13 +0900 Subject: [PATCH 10/16] =?UTF-8?q?DenyCommand=20=E3=81=AE=E8=A1=A8=E7=A4=BA?= =?UTF-8?q?=E3=82=92=E6=94=B9=E5=96=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 空リスト時のエラーメッセージを追加 - コードブロック表示を改善 --- .../itts/core/discord/command/DenyCommand.java | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/dev/felnull/itts/core/discord/command/DenyCommand.java b/core/src/main/java/dev/felnull/itts/core/discord/command/DenyCommand.java index 5c2f9e8..68a9061 100644 --- a/core/src/main/java/dev/felnull/itts/core/discord/command/DenyCommand.java +++ b/core/src/main/java/dev/felnull/itts/core/discord/command/DenyCommand.java @@ -92,7 +92,6 @@ private void show(SlashCommandInteractionEvent event) { return; } - MessageCreateBuilder msg = new MessageCreateBuilder().addContent("読み上げ拒否されたユーザ一覧\n"); StringBuilder sb = new StringBuilder(); JDA jda = event.getJDA(); @@ -103,7 +102,15 @@ private void show(SlashCommandInteractionEvent event) { sb.append(DiscordUtils.getEscapedName(guild, user)).append("\n"); } } - msg.addContent("``" + sb + "``"); + + if (sb.isEmpty()) { + event.reply("読み上げ拒否されたユーザの情報を取得できませんでした。").setEphemeral(true).queue(); + return; + } + + MessageCreateBuilder msg = new MessageCreateBuilder() + .addContent("読み上げ拒否されたユーザ一覧\n") + .addContent("```\n" + sb + "```"); event.reply(msg.build()).setEphemeral(true).queue(); } From 9f0b7363a260f539a1fa4c9843260cb4fcb391ee Mon Sep 17 00:00:00 2001 From: yuu111 Date: Sat, 31 Jan 2026 10:26:08 +0900 Subject: [PATCH 11/16] =?UTF-8?q?=E3=83=90=E3=83=AA=E3=83=87=E3=83=BC?= =?UTF-8?q?=E3=82=B7=E3=83=A7=E3=83=B3=E6=A9=9F=E8=83=BD=E3=81=AE=E3=83=86?= =?UTF-8?q?=E3=82=B9=E3=83=88=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PatternValidatorとDictionaryManagerのバリデーションロジックをカバー --- .../itts/core/dict/DictionaryManagerTest.java | 104 ++++++++++++ .../itts/core/util/PatternValidatorTest.java | 157 ++++++++++++++++++ 2 files changed, 261 insertions(+) create mode 100644 core/src/test/java/dev/felnull/itts/core/dict/DictionaryManagerTest.java create mode 100644 core/src/test/java/dev/felnull/itts/core/util/PatternValidatorTest.java diff --git a/core/src/test/java/dev/felnull/itts/core/dict/DictionaryManagerTest.java b/core/src/test/java/dev/felnull/itts/core/dict/DictionaryManagerTest.java new file mode 100644 index 0000000..497bc2e --- /dev/null +++ b/core/src/test/java/dev/felnull/itts/core/dict/DictionaryManagerTest.java @@ -0,0 +1,104 @@ +package dev.felnull.itts.core.dict; + +import com.google.gson.JsonObject; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +/** + * DictionaryManagerのバリデーションテスト + */ +public class DictionaryManagerTest { + + private final DictionaryManager dictionaryManager = new DictionaryManager(); + + @Nested + @DisplayName("serverDictLoadFromJson()のバリデーションテスト") + class ServerDictLoadFromJsonTests { + + @Test + @DisplayName("バージョンが異なる場合は例外をスロー") + void invalidVersionThrowsException() { + JsonObject jo = new JsonObject(); + jo.addProperty("version", 999); + JsonObject entry = new JsonObject(); + entry.addProperty("test", "テスト"); + jo.add("entry", entry); + + RuntimeException exception = Assertions.assertThrows(RuntimeException.class, () -> + dictionaryManager.serverDictLoadFromJson(jo, 123456L, false)); + Assertions.assertEquals("Unsupported dictionary file version", exception.getMessage()); + } + + @Test + @DisplayName("versionフィールドがない場合は例外をスロー") + void missingVersionThrowsException() { + JsonObject jo = new JsonObject(); + JsonObject entry = new JsonObject(); + entry.addProperty("test", "テスト"); + jo.add("entry", entry); + + RuntimeException exception = Assertions.assertThrows(RuntimeException.class, () -> + dictionaryManager.serverDictLoadFromJson(jo, 123456L, false)); + Assertions.assertEquals("Unsupported dictionary file version", exception.getMessage()); + } + + @Test + @DisplayName("entryフィールドがない場合は例外をスロー") + void missingEntryFieldThrowsException() { + JsonObject jo = new JsonObject(); + jo.addProperty("version", 0); + + RuntimeException exception = Assertions.assertThrows(RuntimeException.class, () -> + dictionaryManager.serverDictLoadFromJson(jo, 123456L, false)); + Assertions.assertEquals("Invalid dictionary file format", exception.getMessage()); + } + + @Test + @DisplayName("entryがオブジェクトでない場合は例外をスロー") + void entryNotObjectThrowsException() { + JsonObject jo = new JsonObject(); + jo.addProperty("version", 0); + jo.addProperty("entry", "not an object"); + + RuntimeException exception = Assertions.assertThrows(RuntimeException.class, () -> + dictionaryManager.serverDictLoadFromJson(jo, 123456L, false)); + Assertions.assertEquals("Invalid dictionary file format", exception.getMessage()); + } + + @Test + @DisplayName("エントリ数が上限を超える場合は例外をスロー") + void tooManyEntriesThrowsException() { + JsonObject jo = new JsonObject(); + jo.addProperty("version", 0); + JsonObject entry = new JsonObject(); + + for (int i = 0; i <= 1000; i++) { + entry.addProperty("word" + i, "読み" + i); + } + jo.add("entry", entry); + + RuntimeException exception = Assertions.assertThrows(RuntimeException.class, () -> + dictionaryManager.serverDictLoadFromJson(jo, 123456L, false)); + Assertions.assertTrue(exception.getMessage().contains("Dictionary entry count exceeds limit")); + } + + @ParameterizedTest + @DisplayName("負のバージョン番号でも例外をスロー") + @ValueSource(ints = {-1, -100, Integer.MIN_VALUE}) + void negativeVersionThrowsException(int version) { + JsonObject jo = new JsonObject(); + jo.addProperty("version", version); + JsonObject entry = new JsonObject(); + entry.addProperty("test", "テスト"); + jo.add("entry", entry); + + RuntimeException exception = Assertions.assertThrows(RuntimeException.class, () -> + dictionaryManager.serverDictLoadFromJson(jo, 123456L, false)); + Assertions.assertEquals("Unsupported dictionary file version", exception.getMessage()); + } + } +} diff --git a/core/src/test/java/dev/felnull/itts/core/util/PatternValidatorTest.java b/core/src/test/java/dev/felnull/itts/core/util/PatternValidatorTest.java new file mode 100644 index 0000000..984e3f9 --- /dev/null +++ b/core/src/test/java/dev/felnull/itts/core/util/PatternValidatorTest.java @@ -0,0 +1,157 @@ +package dev.felnull.itts.core.util; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; + +import java.util.stream.Stream; + +/** + * PatternValidatorのテストクラス + */ +public class PatternValidatorTest { + + @Nested + @DisplayName("validate()メソッドのテスト") + class ValidateTests { + + @ParameterizedTest + @DisplayName("有効な正規表現が成功を返す") + @ValueSource(strings = { + "^[a-z]+$", + "[0-9]{3}-[0-9]{4}", + "hello", + "\\d+", + "a*b+c?", + "^(foo|bar)$", + "[A-Za-z0-9_]+", + "\\w+@\\w+\\.\\w+", + "(?:abc)+", + "\\p{L}+" + }) + void validPatternsReturnSuccess(String regex) { + PatternValidator.ValidationResult result = PatternValidator.validate(regex); + Assertions.assertTrue(result.valid()); + Assertions.assertNotNull(result.pattern()); + Assertions.assertNull(result.error()); + Assertions.assertTrue(result.getPattern().isPresent()); + } + + @ParameterizedTest + @DisplayName("無効な構文がエラーを返す") + @MethodSource("invalidPatterns") + void invalidSyntaxReturnsError(String regex, String expectedErrorContains) { + PatternValidator.ValidationResult result = PatternValidator.validate(regex); + Assertions.assertFalse(result.valid()); + Assertions.assertNull(result.pattern()); + Assertions.assertNotNull(result.error()); + Assertions.assertTrue(result.error().contains("無効な正規表現です")); + Assertions.assertTrue(result.getPattern().isEmpty()); + } + + private static Stream invalidPatterns() { + return Stream.of( + Arguments.of("[a-z", "Unclosed character class"), + Arguments.of("(abc", "Unclosed group"), + Arguments.of("(?P", "Unknown inline modifier"), + Arguments.of("*abc", "Dangling meta character"), + Arguments.of("+abc", "Dangling meta character"), + Arguments.of("?abc", "Dangling meta character"), + Arguments.of("\\", "Unexpected internal error"), + Arguments.of("[z-a]", "Illegal character range") + ); + } + + @ParameterizedTest + @DisplayName("ReDoS脆弱性パターンがエラーを返す") + @ValueSource(strings = { + "(a+)+", + "(a*)*", + "(a+)*", + "(a*)+", + "(x+)+b", + "(a|b)+", + "(a|b)*", + "(x|y)+z" + }) + void redosPatternsReturnError(String regex) { + PatternValidator.ValidationResult result = PatternValidator.validate(regex); + Assertions.assertFalse(result.valid()); + Assertions.assertNull(result.pattern()); + Assertions.assertNotNull(result.error()); + Assertions.assertTrue(result.error().contains("パフォーマンス上の問題")); + } + } + + @Nested + @DisplayName("isPotentiallyDangerous()メソッドのテスト") + class IsPotentiallyDangerousTests { + + @ParameterizedTest + @DisplayName("危険なパターンを検出する") + @ValueSource(strings = { + "(a+)+", + "(a*)*", + "(a+)*", + "(a*)+", + "(x+)+b", + "(a|b)+", + "(a|b)*", + "(foo|bar)*" + }) + void detectsDangerousPatterns(String regex) { + Assertions.assertTrue(PatternValidator.isPotentiallyDangerous(regex)); + } + + @ParameterizedTest + @DisplayName("安全なパターンは検出しない") + @ValueSource(strings = { + "^[a-z]+$", + "[0-9]{3}-[0-9]{4}", + "hello", + "\\d+", + "a+b+c+", + "^(foo|bar)$", + "[A-Za-z0-9_]+", + "\\w+@\\w+\\.\\w+", + "(?:abc)", + "a{1,10}" + }) + void safePatternNotDetected(String regex) { + Assertions.assertFalse(PatternValidator.isPotentiallyDangerous(regex)); + } + } + + @Nested + @DisplayName("ValidationResultのテスト") + class ValidationResultTests { + + @ParameterizedTest + @DisplayName("成功結果が正しく作成される") + @ValueSource(strings = {"abc", "\\d+", "[a-z]+"}) + void successResultCreatedCorrectly(String regex) { + PatternValidator.ValidationResult result = PatternValidator.ValidationResult.success( + java.util.regex.Pattern.compile(regex) + ); + Assertions.assertTrue(result.valid()); + Assertions.assertNotNull(result.pattern()); + Assertions.assertNull(result.error()); + Assertions.assertEquals(regex, result.pattern().pattern()); + } + + @ParameterizedTest + @DisplayName("失敗結果が正しく作成される") + @ValueSource(strings = {"エラー1", "エラー2", "無効な正規表現です"}) + void failureResultCreatedCorrectly(String errorMessage) { + PatternValidator.ValidationResult result = PatternValidator.ValidationResult.failure(errorMessage); + Assertions.assertFalse(result.valid()); + Assertions.assertNull(result.pattern()); + Assertions.assertEquals(errorMessage, result.error()); + Assertions.assertTrue(result.getPattern().isEmpty()); + } + } +} From 261c3949e5fac983c9129da25f046248c8355116 Mon Sep 17 00:00:00 2001 From: yuu111 Date: Sat, 31 Jan 2026 11:20:02 +0900 Subject: [PATCH 12/16] =?UTF-8?q?AGENTS.md=E3=82=92=E8=BF=BD=E5=8A=A0?= =?UTF-8?q?=E3=81=97=E3=80=81CLAUDE.md=E3=82=92=E3=82=B7=E3=83=B3=E3=83=9C?= =?UTF-8?q?=E3=83=AA=E3=83=83=E3=82=AF=E3=83=AA=E3=83=B3=E3=82=AF=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AI用ドキュメントを汎用的なAGENTS.mdに移行。 CLAUDE.mdはAGENTS.mdへのシンボリックリンクとして維持。 --- .gitignore | 5 +++- AGENTS.md | 82 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ CLAUDE.md | 1 + 3 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 AGENTS.md create mode 120000 CLAUDE.md diff --git a/.gitignore b/.gitignore index 7595ee1..30293c8 100644 --- a/.gitignore +++ b/.gitignore @@ -157,4 +157,7 @@ $RECYCLE.BIN/ # コンパイル関係 core/bin/* -selfhost/bin/* \ No newline at end of file +selfhost/bin/* + +# Claude Code +nul \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..79b2465 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,82 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## プロジェクト概要 + +I-TTS (Integration TTS) はDiscord用の読み上げBOT。VOICEVOX、COEIROINK、SHAREVOX、VoiceTextなどの音声合成APIに対応。 + +## ビルドコマンド + +```bash +# ビルド (テストとCheckstyle含む) +./gradlew build + +# テストのみ +./gradlew test + +# 特定モジュールのテスト +./gradlew :core:test +./gradlew :selfhost:test + +# 単一テストクラス実行 +./gradlew :core:test --tests "dev.felnull.itts.core.savedata.repository.BotStateDataTest" + +# Checkstyleのみ +./gradlew checkstyleMain + +# Shadow JAR作成 (実行可能JAR) +./gradlew :selfhost:shadowJar +``` + +成果物は `selfhost/build/libs/itts-selfhost-{version}.jar` に生成される。 + +## アーキテクチャ + +### モジュール構成 + +- **core**: BOTのコアロジック。JDAやLavaPlayerを使用したDiscord連携、音声合成、データ永続化 +- **selfhost**: セルフホスト用エントリポイント。設定ファイル読み込みとランタイムコンテキスト提供 + +### マネージャーパターン + +`ITTSRuntime`がシングルトンとして各マネージャーを保持: +- `ConfigManager`: 設定管理 +- `VoiceManager`: 音声タイプ管理 +- `TTSManager`: テキスト読み上げ処理 +- `DictionaryManager`: 辞書管理 +- `CacheManager`: 音声キャッシュ +- `SaveDataManager`: データ永続化 + +### 音声合成 (voice パッケージ) + +抽象化レイヤー: +- `VoiceType`: 音声合成エンジン種別 (VOICEVOX, COEIROINK等) +- `VoiceCategory`: エンジン内のキャラクター +- `Voice`: 具体的な声スタイル + +各エンジン実装は `voice.voicevox`, `voice.coeiroink`, `voice.voicetext` サブパッケージに配置。 + +### データ永続化 (savedata パッケージ) + +- **dao**: データアクセスオブジェクト。SQLite/MySQL対応 +- **repository**: ビジネスロジック向けリポジトリ層 +- **legacy**: 旧バージョンデータ移行 + +スキーマ定義は [docs/schema-sqlite.md](docs/schema-sqlite.md) を参照。 + +### Discordコマンド (discord.command パッケージ) + +`BaseCommand`を継承してスラッシュコマンドを実装。主要コマンド: +- `JoinCommand`, `LeaveCommand`: VC参加/退出 +- `VoiceCommand`: 音声タイプ変更 +- `DictCommand`: 辞書管理 +- `ConfigCommand`: サーバー設定 + +## コードスタイル + +Checkstyle (`config/checkstyle/checkstyle.xml`) で強制: +- `var` 禁止 (明示的な型宣言必須) +- 全publicメソッド/フィールドにJavadoc必須 +- 行長170文字以内 +- タブ文字禁止 diff --git a/CLAUDE.md b/CLAUDE.md new file mode 120000 index 0000000..47dc3e3 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file From 92d2e966a2e8cba984ed193999f6e13e37514d40 Mon Sep 17 00:00:00 2001 From: yuu111 Date: Fri, 13 Feb 2026 04:30:51 +0900 Subject: [PATCH 13/16] =?UTF-8?q?Bot=E3=81=B8=E3=81=AEDM=E9=80=81=E4=BF=A1?= =?UTF-8?q?=E6=99=82=E3=81=AENullPointerException=E3=82=92=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/dev/felnull/itts/core/discord/DCEventListener.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/src/main/java/dev/felnull/itts/core/discord/DCEventListener.java b/core/src/main/java/dev/felnull/itts/core/discord/DCEventListener.java index f613c12..1eca8ac 100644 --- a/core/src/main/java/dev/felnull/itts/core/discord/DCEventListener.java +++ b/core/src/main/java/dev/felnull/itts/core/discord/DCEventListener.java @@ -48,6 +48,10 @@ public void onCommandAutoCompleteInteraction(@NotNull CommandAutoCompleteInterac @Override public void onMessageReceived(@NotNull MessageReceivedEvent event) { + if (!event.isFromGuild()) { + return; + } + if (getTTSManager().canSpeak(event.getGuild())) { getTTSManager().sayChat(event.getGuild(), event.getChannel(), event.getMember(), event.getMessage()); getTTSManager().sayUploadFile(event.getGuild(), event.getChannel(), event.getMember(), event.getMessage().getAttachments()); From b3d8cef6234d09b682fb992214e1799ae0343cb5 Mon Sep 17 00:00:00 2001 From: toms0910 Date: Mon, 16 Feb 2026 13:54:46 +0900 Subject: [PATCH 14/16] =?UTF-8?q?=E3=83=89=E3=83=A1=E3=82=A4=E3=83=B3?= =?UTF-8?q?=E7=9C=81=E7=95=A5=E3=82=92IANA=E3=81=AETLD=E3=83=AA=E3=82=B9?= =?UTF-8?q?=E3=83=88=E3=81=AB=E3=81=82=E3=82=8B=E7=89=A9=E3=81=A0=E3=81=91?= =?UTF-8?q?=E3=81=AB=E9=99=90=E5=AE=9A=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dev/felnull/itts/core/ITTSRuntime.java | 11 +++- .../dev/felnull/itts/core/ITTSRuntimeUse.java | 5 ++ .../core/dict/AbbreviationDictionary.java | 10 ++- .../itts/core/dict/DomainListManager.java | 64 +++++++++++++++++++ .../itts/core/dict/DomainReplacer.java | 39 +++++++++++ 5 files changed, 126 insertions(+), 3 deletions(-) create mode 100644 core/src/main/java/dev/felnull/itts/core/dict/DomainListManager.java create mode 100644 core/src/main/java/dev/felnull/itts/core/dict/DomainReplacer.java diff --git a/core/src/main/java/dev/felnull/itts/core/ITTSRuntime.java b/core/src/main/java/dev/felnull/itts/core/ITTSRuntime.java index 16259b0..2142575 100644 --- a/core/src/main/java/dev/felnull/itts/core/ITTSRuntime.java +++ b/core/src/main/java/dev/felnull/itts/core/ITTSRuntime.java @@ -5,6 +5,7 @@ import dev.felnull.itts.core.cache.CacheManager; import dev.felnull.itts.core.config.ConfigManager; import dev.felnull.itts.core.dict.DictionaryManager; +import dev.felnull.itts.core.dict.DomainListManager; import dev.felnull.itts.core.discord.Bot; import dev.felnull.itts.core.savedata.SaveDataManager; import dev.felnull.itts.core.tts.TTSManager; @@ -99,6 +100,11 @@ public class ITTSRuntime { */ private final VoiceAudioManager voiceAudioManager = new VoiceAudioManager(); + /** + * ドメイン名リストマネージャー + */ + private final DomainListManager domainListManager = new DomainListManager(); + /** * 辞書マネージャー */ @@ -148,7 +154,7 @@ private ITTSRuntime(ITTSRuntimeContext runtimeContext) { this.configManager = new ConfigManager(runtimeContext.getConfigContext()); this.cacheManager = new CacheManager(runtimeContext.getGlobalCacheAccessFactory()); - this.managers = ImmutableList.of(configManager, voiceManager); + this.managers = ImmutableList.of(configManager, voiceManager, domainListManager); } /** @@ -265,6 +271,9 @@ public DictionaryManager getDictionaryManager() { return dictionaryManager; } + public DomainListManager getDomainListManager() { + return domainListManager; + } public ITTSNetworkManager getNetworkManager() { return networkManager; diff --git a/core/src/main/java/dev/felnull/itts/core/ITTSRuntimeUse.java b/core/src/main/java/dev/felnull/itts/core/ITTSRuntimeUse.java index 8c7c8f3..28208bb 100644 --- a/core/src/main/java/dev/felnull/itts/core/ITTSRuntimeUse.java +++ b/core/src/main/java/dev/felnull/itts/core/ITTSRuntimeUse.java @@ -4,6 +4,7 @@ import dev.felnull.itts.core.cache.CacheManager; import dev.felnull.itts.core.config.ConfigManager; import dev.felnull.itts.core.dict.DictionaryManager; +import dev.felnull.itts.core.dict.DomainListManager; import dev.felnull.itts.core.discord.Bot; import dev.felnull.itts.core.tts.TTSManager; import dev.felnull.itts.core.voice.VoiceManager; @@ -63,6 +64,10 @@ default DictionaryManager getDictionaryManager() { return getITTSRuntime().getDictionaryManager(); } + default DomainListManager getDomainListManager() { + return getITTSRuntime().getDomainListManager(); + } + default VoiceManager getVoiceManager() { return getITTSRuntime().getVoiceManager(); } diff --git a/core/src/main/java/dev/felnull/itts/core/dict/AbbreviationDictionary.java b/core/src/main/java/dev/felnull/itts/core/dict/AbbreviationDictionary.java index f6acf91..0cb467b 100644 --- a/core/src/main/java/dev/felnull/itts/core/dict/AbbreviationDictionary.java +++ b/core/src/main/java/dev/felnull/itts/core/dict/AbbreviationDictionary.java @@ -29,11 +29,11 @@ public class AbbreviationDictionary implements Dictionary { Matcher matcher = pattern.matcher(s); return matcher.find(); })*/ - .addOption(1, "ドメインショウリャク", s -> { + /*.addOption(1, "ドメインショウリャク", s -> { Pattern pattern = Pattern.compile("^([a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]*\\.)+[a-zA-Z]{2,}$"); Matcher matcher = pattern.matcher(s); return matcher.find(); - }) + })*/ .addOption(1, "アイピーブイフォーショウリャク", s -> { Pattern pattern = Pattern.compile("^((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\\.){3}(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])$"); Matcher matcher = pattern.matcher(s); @@ -61,9 +61,15 @@ public class AbbreviationDictionary implements Dictionary { */ private final URLReplacer urlReplacer = new URLReplacer("ユーアルエルショウリャク"); + /** + * ドメインリプレーサー + */ + private final DomainReplacer domainReplacer = new DomainReplacer("ドメインショウリャク"); + @Override public @NotNull String apply(@NotNull String text, long guildId) { text = urlReplacer.replace(text); + text = domainReplacer.replace(text); text = CODE_BLOCK_REGEX.matcher(text).replaceAll("コードブロックショウリャク"); return regexUtil.replaceText(text); } diff --git a/core/src/main/java/dev/felnull/itts/core/dict/DomainListManager.java b/core/src/main/java/dev/felnull/itts/core/dict/DomainListManager.java new file mode 100644 index 0000000..63feaa2 --- /dev/null +++ b/core/src/main/java/dev/felnull/itts/core/dict/DomainListManager.java @@ -0,0 +1,64 @@ +package dev.felnull.itts.core.dict; + +import dev.felnull.itts.core.ITTSBaseManager; +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.concurrent.CompletableFuture; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * TLDリストマネージャー + **/ + +public class DomainListManager implements ITTSBaseManager { + + /** + * 取得先URL + */ + private static final URI IANA_DATA_URI = URI.create("https://data.iana.org/TLD/tlds-alpha-by-domain.txt"); + + /** + * 読み込み済みTLDリスト + */ + private Pattern domainPattern; + + @Override + public @NotNull CompletableFuture init() { + return CompletableFuture.runAsync(() -> { + try { + HttpClient hc = getNetworkManager().getHttpClient(); + HttpRequest request = HttpRequest.newBuilder().uri(IANA_DATA_URI).GET().build(); + + HttpResponse res = hc.send(request, HttpResponse.BodyHandlers.ofString()); + + String processedDomains = res.body().lines() + .filter(line -> !line.startsWith("#")) + .filter(list -> !list.isBlank()) + .map(String::trim) + .map(String::toLowerCase) + .map(Pattern::quote) + .collect(Collectors.joining("|")); + + String regex = "\\b[a-zA-Z0-9.-]+\\.(?i:" + processedDomains + ")\\b"; + domainPattern = Pattern.compile(regex); + } catch (IOException | InterruptedException e) { + getITTSLogger().error("failed to get domain list."); + } + }, getAsyncExecutor()); + } + + /** + * 読み込み済みTLDゲッター + * + * @return ドメイン検知正規表現 + */ + public @Nullable Pattern getPattern() { + return domainPattern; + } +} diff --git a/core/src/main/java/dev/felnull/itts/core/dict/DomainReplacer.java b/core/src/main/java/dev/felnull/itts/core/dict/DomainReplacer.java new file mode 100644 index 0000000..b8934a3 --- /dev/null +++ b/core/src/main/java/dev/felnull/itts/core/dict/DomainReplacer.java @@ -0,0 +1,39 @@ +package dev.felnull.itts.core.dict; + +import dev.felnull.itts.core.ITTSRuntimeUse; +import java.util.regex.Pattern; + +/** + * ドメインを置き換える処理 + */ +public class DomainReplacer implements ITTSRuntimeUse { + + /** + * 置き換えるテキスト + */ + private final String replacedText; + + /** + * コンストラクタ + * + * @param replacedText 置き換えるテキスト + */ + public DomainReplacer(String replacedText) { + this.replacedText = replacedText; + } + + /** + * テキスト置き換え + * + * @param text テキスト + * @return 置き換えられたテキスト + */ + public String replace(String text) { + Pattern domainPattern = getDomainListManager().getPattern(); + if (domainPattern != null) { + return domainPattern.matcher(text).replaceAll(replacedText); + } + + return text; + } +} From b8362cfd69593228e8f92aa9c1ff49fd4bd89d5e Mon Sep 17 00:00:00 2001 From: yuu1111 Date: Sun, 8 Mar 2026 23:05:13 +0900 Subject: [PATCH 15/16] =?UTF-8?q?AGENTS.md=E3=81=8B=E3=82=89CLAUDE.md?= =?UTF-8?q?=E7=89=B9=E6=9C=89=E3=81=AE=E8=A8=98=E8=BF=B0=E3=82=92=E5=89=8A?= =?UTF-8?q?=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - タイトル「# CLAUDE.md」と説明文を削除 - 見出しレベルをAGENTS.mdとして適切な階層に修正 --- AGENTS.md | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 79b2465..9b05ab9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,12 +1,8 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## プロジェクト概要 +# プロジェクト概要 I-TTS (Integration TTS) はDiscord用の読み上げBOT。VOICEVOX、COEIROINK、SHAREVOX、VoiceTextなどの音声合成APIに対応。 -## ビルドコマンド +# ビルドコマンド ```bash # ビルド (テストとCheckstyle含む) @@ -31,14 +27,14 @@ I-TTS (Integration TTS) はDiscord用の読み上げBOT。VOICEVOX、COEIROINK 成果物は `selfhost/build/libs/itts-selfhost-{version}.jar` に生成される。 -## アーキテクチャ +# アーキテクチャ -### モジュール構成 +## モジュール構成 - **core**: BOTのコアロジック。JDAやLavaPlayerを使用したDiscord連携、音声合成、データ永続化 - **selfhost**: セルフホスト用エントリポイント。設定ファイル読み込みとランタイムコンテキスト提供 -### マネージャーパターン +## マネージャーパターン `ITTSRuntime`がシングルトンとして各マネージャーを保持: - `ConfigManager`: 設定管理 @@ -48,7 +44,7 @@ I-TTS (Integration TTS) はDiscord用の読み上げBOT。VOICEVOX、COEIROINK - `CacheManager`: 音声キャッシュ - `SaveDataManager`: データ永続化 -### 音声合成 (voice パッケージ) +## 音声合成 (voice パッケージ) 抽象化レイヤー: - `VoiceType`: 音声合成エンジン種別 (VOICEVOX, COEIROINK等) @@ -57,7 +53,7 @@ I-TTS (Integration TTS) はDiscord用の読み上げBOT。VOICEVOX、COEIROINK 各エンジン実装は `voice.voicevox`, `voice.coeiroink`, `voice.voicetext` サブパッケージに配置。 -### データ永続化 (savedata パッケージ) +## データ永続化 (savedata パッケージ) - **dao**: データアクセスオブジェクト。SQLite/MySQL対応 - **repository**: ビジネスロジック向けリポジトリ層 @@ -65,7 +61,7 @@ I-TTS (Integration TTS) はDiscord用の読み上げBOT。VOICEVOX、COEIROINK スキーマ定義は [docs/schema-sqlite.md](docs/schema-sqlite.md) を参照。 -### Discordコマンド (discord.command パッケージ) +## Discordコマンド (discord.command パッケージ) `BaseCommand`を継承してスラッシュコマンドを実装。主要コマンド: - `JoinCommand`, `LeaveCommand`: VC参加/退出 @@ -73,7 +69,7 @@ I-TTS (Integration TTS) はDiscord用の読み上げBOT。VOICEVOX、COEIROINK - `DictCommand`: 辞書管理 - `ConfigCommand`: サーバー設定 -## コードスタイル +# コードスタイル Checkstyle (`config/checkstyle/checkstyle.xml`) で強制: - `var` 禁止 (明示的な型宣言必須) From 43f63de2665d75236714106c8ba3ed97dadb2c47 Mon Sep 17 00:00:00 2001 From: MORIMORI0317 Date: Tue, 24 Mar 2026 00:02:42 +0900 Subject: [PATCH 16/16] =?UTF-8?q?=E5=A4=89=E6=9B=B4=E3=83=AD=E3=82=B0?= =?UTF-8?q?=E3=81=A8=E3=83=A9=E3=82=A4=E3=83=96=E3=83=A9=E3=83=AA=E3=82=92?= =?UTF-8?q?=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 5 +++-- core/build.gradle.kts | 10 +++++----- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dc460c9..3215ab1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,13 +8,14 @@ ### Added ### Changed - +- IANAのTLDリストを参照してドメイン省略するように変更 ### Deprecated ### Removed ### Fixed - +- 多数の不備を修正 +- 辞書登録のバリデーションを強化 ### Security ## [2.1.0] - 2026-02-11 diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 32f6aa4..41a51b2 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -18,17 +18,17 @@ dependencies { testRuntimeOnly("org.junit.platform:junit-platform-launcher") testImplementation("org.mockito:mockito-junit-jupiter:5.21.0") - api("net.dv8tion:JDA:6.3.0") + api("net.dv8tion:JDA:6.3.2") api("org.apache.commons:commons-lang3:3.20.0") api("com.google.code.gson:gson:2.13.2") api("com.google.guava:guava:33.5.0-jre") api("dev.felnull:felnull-java-library:1.75") api("dev.arbjerg:lavaplayer:2.2.6") api("club.minnced:jdave-api:0.1.5") - runtimeOnly("club.minnced:jdave-native-linux-x86-64:0.1.5") - runtimeOnly("club.minnced:jdave-native-linux-aarch64:0.1.5") - runtimeOnly("club.minnced:jdave-native-win-x86-64:0.1.5") - runtimeOnly("club.minnced:jdave-native-darwin:0.1.5") + runtimeOnly("club.minnced:jdave-native-linux-x86-64:0.1.7") + runtimeOnly("club.minnced:jdave-native-linux-aarch64:0.1.7") + runtimeOnly("club.minnced:jdave-native-win-x86-64:0.1.7") + runtimeOnly("club.minnced:jdave-native-darwin:0.1.7") api("commons-io:commons-io:2.20.0") api("com.ibm.icu:icu4j:77.1") api("com.atilika.kuromoji:kuromoji-ipadic:0.9.0")