diff --git a/README.md b/README.md index 624f5e0..5d83e6c 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,9 @@ Forgejo上にbot専用アカウント(例: `yaju-keisatsu-bot`)を作り、そ cp config.example.yaml config.yaml ``` -`config.yaml` を編集し、`forgejo.base_url` / `forgejo.token` / `webhook_secret` / `bot.username` を埋める。`phrases` は同梱の一部だけなので、必要に応じて自分の `config.yaml` に自由に追加してよい(このリストは本体の意図的にライトな内容にとどめてあります)。 +`config.yaml` を編集し、`forgejo.base_url` / `forgejo.token` / `webhook_secret` / `bot.username` を埋める。 + +語録リストは [phrases.yaml](./phrases.yaml) にコミュニティ管理のものが同梱されており、`config.yaml` の `phrases_file: ./phrases.yaml` で読み込む(デフォルトで設定済み)。追加・削除の提案はPRで歓迎。自分の運用だけで使いたい追加語録があれば、それとは別に `config.yaml` の `phrases:` に書けば `phrases_file` の内容とマージされる。 `config.yaml` は秘密情報を含むため絶対にコミットしないこと(`.gitignore` 済み)。 diff --git a/config.example.yaml b/config.example.yaml index 38a7a61..21bf209 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -1,4 +1,4 @@ -# goroku-keisatsu の設定ファイル例。 +# yaju-is-always-watching-you の設定ファイル例。 # コピーして config.yaml として使う。秘密情報が入るので config.yaml は絶対にコミットしないこと。 listen_addr: ":8080" @@ -11,13 +11,13 @@ forgejo: # 自ホストのForgejoのベースURL base_url: "https://git.example.dev" # bot専用アカウントで発行したAPIトークン(Settings > Applications)。 - # 権限は対象リポジトリへの read/write + issue + pull-request で十分。 + # スコープは repository(write) + issue(write) で十分。 token: "change-me" bot: # 上のtokenを発行したbotアカウントのユーザー名。 # このアカウント自身の発言/pushには反応しない(無限ループ防止)。 - username: "goroku-keisatsu-bot" + username: "yaju-keisatsu-bot" # 是正PR用に作成するブランチの接頭辞。 branch_prefix: "bot/goroku-fix/" # push時にスキャンする対象拡張子。空にすると全ファイルを対象にする。 @@ -29,18 +29,10 @@ bot: - ".js" - ".ts" -# 検知する語録リスト。有名どころの一部だけを同梱しているので、 -# 必要に応じて自分のconfig.yamlで自由に追加・削除すること。 -phrases: - - "しょうもな" - - "たまげたなあ" - - "変態糞土方" - - "絶対に許さない" - - "24時間戦えますか" - - "本当に感謝しております" - - "巨大感謝" - - "覚悟はいいか?俺はできてる" - - "もうだめだ" - - "ンアッー" - - "ファッ!?" - - "24時間タタカエマスカ" +# phrases_file: リポジトリ同梱のコミュニティ管理語録リスト(phrases.yaml)を読む。 +# 相対パスはこのconfig.yamlからの相対で解決される。 +phrases_file: "./phrases.yaml" + +# phrases: 自分の運用だけで使いたい追加語録があればここに書く(phrases_fileの内容とマージされる)。 +# 空のままでもOK。 +phrases: [] diff --git a/internal/config/config.go b/internal/config/config.go index 569d1e1..5ea8e3f 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -4,16 +4,25 @@ package config import ( "fmt" "os" + "path/filepath" "gopkg.in/yaml.v3" ) type Config struct { - ListenAddr string `yaml:"listen_addr"` - WebhookSecret string `yaml:"webhook_secret"` - Forgejo Forgejo `yaml:"forgejo"` - Bot Bot `yaml:"bot"` - Phrases []string `yaml:"phrases"` + ListenAddr string `yaml:"listen_addr"` + WebhookSecret string `yaml:"webhook_secret"` + Forgejo Forgejo `yaml:"forgejo"` + Bot Bot `yaml:"bot"` + // PhrasesFile, if set, is loaded and merged with Phrases. It lets a + // deployment point at the repo's community-maintained phrases.yaml while + // still keeping any private additions inline below. + PhrasesFile string `yaml:"phrases_file"` + Phrases []string `yaml:"phrases"` +} + +type phrasesFile struct { + Phrases []string `yaml:"phrases"` } type Forgejo struct { @@ -48,6 +57,22 @@ func Load(path string) (*Config, error) { return nil, fmt.Errorf("parse config: %w", err) } + if cfg.PhrasesFile != "" { + fp := cfg.PhrasesFile + if !filepath.IsAbs(fp) { + fp = filepath.Join(filepath.Dir(path), fp) + } + data, err := os.ReadFile(fp) + if err != nil { + return nil, fmt.Errorf("read phrases_file: %w", err) + } + var pf phrasesFile + if err := yaml.Unmarshal(data, &pf); err != nil { + return nil, fmt.Errorf("parse phrases_file: %w", err) + } + cfg.Phrases = append(pf.Phrases, cfg.Phrases...) + } + if cfg.Forgejo.BaseURL == "" { return nil, fmt.Errorf("forgejo.base_url is required") } @@ -58,7 +83,7 @@ func Load(path string) (*Config, error) { return nil, fmt.Errorf("webhook_secret is required") } if len(cfg.Phrases) == 0 { - return nil, fmt.Errorf("phrases list is empty") + return nil, fmt.Errorf("phrases list is empty (set phrases and/or phrases_file)") } return cfg, nil diff --git a/internal/config/config_test.go b/internal/config/config_test.go new file mode 100644 index 0000000..3d8a8a7 --- /dev/null +++ b/internal/config/config_test.go @@ -0,0 +1,55 @@ +package config + +import ( + "os" + "path/filepath" + "reflect" + "testing" +) + +func writeFile(t *testing.T, path, content string) { + t.Helper() + if err := os.WriteFile(path, []byte(content), 0o644); err != nil { + t.Fatalf("write %s: %v", path, err) + } +} + +func TestLoad_PhrasesFileMergesWithInline(t *testing.T) { + dir := t.TempDir() + + writeFile(t, filepath.Join(dir, "phrases.yaml"), "phrases:\n - しょうもな\n - たまげたなあ\n") + writeFile(t, filepath.Join(dir, "config.yaml"), ` +listen_addr: ":8080" +webhook_secret: "s" +forgejo: + base_url: "http://forgejo:3000" + token: "t" +phrases_file: "./phrases.yaml" +phrases: + - 独自の追加語録 +`) + + cfg, err := Load(filepath.Join(dir, "config.yaml")) + if err != nil { + t.Fatalf("Load() error = %v", err) + } + + want := []string{"しょうもな", "たまげたなあ", "独自の追加語録"} + if !reflect.DeepEqual(cfg.Phrases, want) { + t.Fatalf("Phrases = %v, want %v", cfg.Phrases, want) + } +} + +func TestLoad_MissingPhrasesIsError(t *testing.T) { + dir := t.TempDir() + writeFile(t, filepath.Join(dir, "config.yaml"), ` +webhook_secret: "s" +forgejo: + base_url: "http://forgejo:3000" + token: "t" +`) + + if _, err := Load(filepath.Join(dir, "config.yaml")); err == nil { + t.Fatal("Load() error = nil, want error for empty phrases") + } +} diff --git a/phrases.yaml b/phrases.yaml new file mode 100644 index 0000000..7e5bd23 --- /dev/null +++ b/phrases.yaml @@ -0,0 +1,16 @@ +# コミュニティ管理の語録リスト(誰でもPRで追加・削除を提案してOK)。 +# 個人の運用固有の追加語録は、この場所ではなく各自の config.yaml の +# `phrases:` に書くこと(config.yaml はコミットされないので分離される)。 +phrases: + - "しょうもな" + - "たまげたなあ" + - "変態糞土方" + - "絶対に許さない" + - "24時間戦えますか" + - "本当に感謝しております" + - "巨大感謝" + - "覚悟はいいか?俺はできてる" + - "もうだめだ" + - "ンアッー" + - "ファッ!?" + - "24時間タタカエマスカ"