Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
20b0dcf
無効なTTSエンジンの初期化をスキップ
yuu1111 Jan 29, 2026
a9f8d66
正規表現バリデーションユーティリティを追加
yuu1111 Jan 30, 2026
9802eaf
Voice エンジンの API レスポンスバリデーションを追加
yuu1111 Jan 30, 2026
bc37909
辞書の正規表現パターン処理を堅牢化
yuu1111 Jan 30, 2026
34396ea
TTSManager の ignore regex バリデーションを追加
yuu1111 Jan 30, 2026
1986144
ニックネームコマンドの入力バリデーションを追加
yuu1111 Jan 30, 2026
df0ac14
DictCommand の入力バリデーションを追加
yuu1111 Jan 30, 2026
8e5b80f
DictionaryManager のアップロード処理バリデーションを追加
yuu1111 Jan 30, 2026
11098f8
ConfigCommand の regex バリデーションを追加
yuu1111 Jan 30, 2026
884e015
DenyCommand の表示を改善
yuu1111 Jan 30, 2026
9f0b736
バリデーション機能のテストを追加
yuu1111 Jan 31, 2026
261c394
AGENTS.mdを追加し、CLAUDE.mdをシンボリックリンク化
yuu1111 Jan 31, 2026
92d2e96
BotへのDM送信時のNullPointerExceptionを修正
yuu1111 Feb 12, 2026
b3d8cef
ドメイン省略をIANAのTLDリストにある物だけに限定する
shiro8613 Feb 16, 2026
4131746
Merge pull request #27 from yuu1111/fix/skip-disabled-engine-init
MORIMORI0317 Mar 7, 2026
c9812cb
Merge pull request #30 from yuu1111/docs/ai-documentation
MORIMORI0317 Mar 7, 2026
268840a
Merge pull request #31 from yuu1111/fix/dm-message-error
MORIMORI0317 Mar 7, 2026
b8362cf
AGENTS.mdからCLAUDE.md特有の記述を削除
yuu1111 Mar 8, 2026
0d0b443
Merge pull request #34 from yuu1111/docs/rename-claude-to-agents
MORIMORI0317 Mar 20, 2026
28ffd18
Merge pull request #33 from shiro8613/main
MORIMORI0317 Mar 23, 2026
4c5d771
Merge pull request #29 from yuu1111/fix/add-input-validation
MORIMORI0317 Mar 23, 2026
43f63de
変更ログとライブラリを更新
MORIMORI0317 Mar 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -157,4 +157,7 @@ $RECYCLE.BIN/

# コンパイル関係
core/bin/*
selfhost/bin/*
selfhost/bin/*

# Claude Code
nul
78 changes: 78 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# プロジェクト概要

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文字以内
- タブ文字禁止
5 changes: 3 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,14 @@
### Added

### Changed

- IANAのTLDリストを参照してドメイン省略するように変更
### Deprecated

### Removed

### Fixed

- 多数の不備を修正
- 辞書登録のバリデーションを強化
### Security

## [2.1.0] - 2026-02-11
Expand Down
1 change: 1 addition & 0 deletions CLAUDE.md
10 changes: 5 additions & 5 deletions core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
11 changes: 10 additions & 1 deletion core/src/main/java/dev/felnull/itts/core/ITTSRuntime.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -99,6 +100,11 @@ public class ITTSRuntime {
*/
private final VoiceAudioManager voiceAudioManager = new VoiceAudioManager();

/**
* ドメイン名リストマネージャー
*/
private final DomainListManager domainListManager = new DomainListManager();

/**
* 辞書マネージャー
*/
Expand Down Expand Up @@ -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);
}

/**
Expand Down Expand Up @@ -265,6 +271,9 @@ public DictionaryManager getDictionaryManager() {
return dictionaryManager;
}

public DomainListManager getDomainListManager() {
return domainListManager;
}

public ITTSNetworkManager getNetworkManager() {
return networkManager;
Expand Down
5 changes: 5 additions & 0 deletions core/src/main/java/dev/felnull/itts/core/ITTSRuntimeUse.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -63,6 +64,10 @@ default DictionaryManager getDictionaryManager() {
return getITTSRuntime().getDictionaryManager();
}

default DomainListManager getDomainListManager() {
return getITTSRuntime().getDomainListManager();
}

default VoiceManager getVoiceManager() {
return getITTSRuntime().getVoiceManager();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

/**
* グローバル辞書
*/
Expand Down Expand Up @@ -254,35 +265,55 @@ public List<LegacyDictData> 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<String, JsonElement> en : entry.entrySet()) {
String target = en.getKey();

if (!en.getValue().isJsonPrimitive() || !en.getValue().getAsJsonPrimitive().isString()) {
continue;
}

String read = en.getValue().getAsString();

for (Map.Entry<String, JsonElement> 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);
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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<String> 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;
}
}
Loading
Loading