Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ dist/
logs/
*.log

# State data
data/

# Config (contains secrets)
config.json

Expand Down
29 changes: 17 additions & 12 deletions cmd/stream-notifier/main.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Package main はStream Notifierのエントリーポイントを提供する
// Package main はStream Notifierのエントリーポイントを提供する
package main

import (
Expand Down Expand Up @@ -40,7 +40,7 @@ var levelColors = map[slog.Level]string{
slog.LevelError: colorRed,
}

// consoleHandler はANSI色付きのコンソール出力ハンドラ
// consoleHandler はANSI色付きのコンソール出力ハンドラ
type consoleHandler struct {
level slog.Level
mu sync.Mutex
Expand Down Expand Up @@ -99,28 +99,30 @@ func (h *consoleHandler) WithGroup(name string) slog.Handler {
return h
}

// fileHandler はJSON形式のファイル出力ハンドラ
// fileHandler はJSON形式のファイル出力ハンドラ
type fileHandler struct {
level slog.Level
logDir string
mu sync.Mutex
ensured bool
}

// ensureDir はログディレクトリを確保する。
var jst = time.FixedZone("JST", 9*60*60)

// ensureDir はログディレクトリを確保する
func (h *fileHandler) ensureDir() {
if h.ensured {
return
}
if err := os.MkdirAll(h.logDir, 0755); err != nil {
fmt.Fprintf(os.Stderr, "Failed to create log directory: %v\n", err)
return
}
h.ensured = true
}

// getDateString はJST日付をYYYY-MM-DD形式で返す
// getDateString はJST日付をYYYY-MM-DD形式で返す
func getDateString() string {
jst := time.FixedZone("JST", 9*60*60)
return time.Now().In(jst).Format("2006-01-02")
}

Expand Down Expand Up @@ -184,12 +186,12 @@ func (h *fileHandler) WithGroup(name string) slog.Handler {
return h
}

// jsonMarshal はJSON marshalerのラッパー
// jsonMarshal はJSON marshalerのラッパー
func jsonMarshal(v any) ([]byte, error) {
return json.Marshal(v)
}

// multiHandler は複数のslog.Handlerに同時出力する
// multiHandler は複数のslog.Handlerに同時出力する
type multiHandler struct {
handlers []slog.Handler
}
Expand Down Expand Up @@ -230,7 +232,7 @@ func (m *multiHandler) WithGroup(name string) slog.Handler {
return &multiHandler{handlers: handlers}
}

// parseSlogLevel はログレベル文字列をslog.Levelに変換する
// parseSlogLevel はログレベル文字列をslog.Levelに変換する
func parseSlogLevel(level string) slog.Level {
switch level {
case config.LogDebug:
Expand All @@ -246,7 +248,7 @@ func parseSlogLevel(level string) slog.Level {
}
}

// setupLogger はslogのグローバルロガーをセットアップする
// setupLogger はslogのグローバルロガーをセットアップする
func setupLogger(level string) {
slogLevel := parseSlogLevel(level)

Expand All @@ -266,7 +268,10 @@ func startMonitor() error {

slog.Info("Stream Notifier 起動中...")

cfg, err := config.Load("./config.json")
const configPath = "./config.json"
const statePath = "./data/state.json"

cfg, err := config.Load(configPath)
if err != nil {
return err
}
Expand All @@ -279,7 +284,7 @@ func startMonitor() error {
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()

poller := monitor.NewPoller(api, cfg, func(changes []monitor.DetectedChange, sc config.StreamerConfig) {
poller := monitor.NewPoller(api, cfg, configPath, statePath, func(changes []monitor.DetectedChange, sc config.StreamerConfig) {
for _, change := range changes {
embed := discord.BuildEmbed(change)
streamerInfo := discord.StreamerInfo{
Expand Down
63 changes: 30 additions & 33 deletions internal/cli/cli.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Package cli は設定管理CLIを提供する
// Package cli は設定管理CLIを提供する
package cli

import (
Expand All @@ -16,15 +16,15 @@ const configPath = "./config.json"

var scanner *bufio.Scanner

// getScanner はstdin用のscannerを遅延初期化して返す
// getScanner はstdin用のscannerを遅延初期化して返す
func getScanner() *bufio.Scanner {
if scanner == nil {
scanner = bufio.NewScanner(os.Stdin)
}
return scanner
}

// promptInput はプロンプトを表示しユーザー入力を取得する
// promptInput はプロンプトを表示しユーザー入力を取得する
func promptInput(message string) string {
fmt.Print(message)
s := getScanner()
Expand All @@ -34,12 +34,12 @@ func promptInput(message string) string {
return ""
}

// validateWebhookURL はWebhook URLの形式を検証する
// validateWebhookURL はWebhook URLの形式を検証する
func validateWebhookURL(url string) bool {
return strings.HasPrefix(url, config.WebhookURLPrefix)
}

// getEnabledNotificationTypes は有効な通知タイプを文字列で返す
// getEnabledNotificationTypes は有効な通知タイプを文字列で返す
func getEnabledNotificationTypes(n config.NotificationSettings) string {
var types []string
if n.Online {
Expand All @@ -60,18 +60,7 @@ func getEnabledNotificationTypes(n config.NotificationSettings) string {
return strings.Join(types, ", ")
}

// findStreamer はユーザー名で配信者を検索する。
func findStreamer(streamers []config.StreamerConfig, username string) *config.StreamerConfig {
lower := strings.ToLower(username)
for i := range streamers {
if strings.ToLower(streamers[i].Username) == lower {
return &streamers[i]
}
}
return nil
}

// findStreamerIndex はユーザー名で配信者のインデックスを検索する。
// findStreamerIndex はユーザー名で配信者のインデックスを検索する(-1: 未検出)
func findStreamerIndex(streamers []config.StreamerConfig, username string) int {
lower := strings.ToLower(username)
for i := range streamers {
Expand All @@ -82,7 +71,15 @@ func findStreamerIndex(streamers []config.StreamerConfig, username string) int {
return -1
}

// defaultNotifications は全通知有効のデフォルト設定を返す。
// findStreamer はユーザー名で配信者を検索する
func findStreamer(streamers []config.StreamerConfig, username string) *config.StreamerConfig {
if i := findStreamerIndex(streamers, username); i >= 0 {
return &streamers[i]
}
return nil
}

// defaultNotifications は全通知有効のデフォルト設定を返す
func defaultNotifications() config.NotificationSettings {
return config.NotificationSettings{
Online: true,
Expand All @@ -92,7 +89,7 @@ func defaultNotifications() config.NotificationSettings {
}
}

// addStreamer は配信者を追加する
// addStreamer は配信者を追加する
func addStreamer(username string) {
cfg, err := config.Load(configPath)
if err != nil {
Expand Down Expand Up @@ -131,7 +128,7 @@ func addStreamer(username string) {
fmt.Printf("%s を追加しました\n", username)
}

// removeStreamer は配信者を削除する
// removeStreamer は配信者を削除する
func removeStreamer(username string) {
cfg, err := config.Load(configPath)
if err != nil {
Expand All @@ -153,7 +150,7 @@ func removeStreamer(username string) {
fmt.Printf("%s を削除しました\n", username)
}

// listStreamers は登録済み配信者一覧を表示する
// listStreamers は登録済み配信者一覧を表示する
func listStreamers() {
cfg, err := config.Load(configPath)
if err != nil {
Expand All @@ -172,7 +169,7 @@ func listStreamers() {
}
}

// addWebhook は配信者にWebhookを追加する
// addWebhook は配信者にWebhookを追加する
func addWebhook(username string) {
cfg, err := config.Load(configPath)
if err != nil {
Expand Down Expand Up @@ -213,7 +210,7 @@ func addWebhook(username string) {
fmt.Printf("%s にWebhookを追加しました (合計: %d件)\n", username, len(streamer.Webhooks))
}

// removeWebhook は配信者からWebhookを削除する
// removeWebhook は配信者からWebhookを削除する
func removeWebhook(username string) {
cfg, err := config.Load(configPath)
if err != nil {
Expand Down Expand Up @@ -258,7 +255,7 @@ func removeWebhook(username string) {
fmt.Printf("Webhookを削除しました (残り: %d件)\n", len(streamer.Webhooks))
}

// configureWebhook は配信者のWebhook通知設定を変更する
// configureWebhook は配信者のWebhook通知設定を変更する
func configureWebhook(username string) {
cfg, err := config.Load(configPath)
if err != nil {
Expand Down Expand Up @@ -317,36 +314,36 @@ func configureWebhook(username string) {
fmt.Printf("\nWebhook %d の設定を更新しました\n", index+1)
}

// parseYesNo は入力をboolに変換する。空文字列の場合は現在値を返す
// parseYesNo は入力をboolに変換し、空文字列の場合は現在値を返す
func parseYesNo(input string, current bool) bool {
if input == "" {
return current
}
return strings.ToLower(input) == "y"
}

// boolToYN はboolをy/nに変換する
// boolToYN はboolをy/nに変換する
func boolToYN(b bool) string {
if b {
return "y"
}
return "n"
}

// truncateURL はURLを指定長で切り詰める
// truncateURL はURLを指定長で切り詰める
func truncateURL(s string, maxLen int) string {
if len(s) <= maxLen {
return s
}
return s[:maxLen] + "..."
}

// getExeName は実行ファイル名を返す
// getExeName は実行ファイル名を返す
func getExeName() string {
return filepath.Base(os.Args[0])
}

// printUsage は使い方を表示する
// printUsage は使い方を表示する
func printUsage() {
exe := getExeName()
fmt.Printf(`
Expand All @@ -362,7 +359,7 @@ func printUsage() {
`, exe, exe, exe, exe, exe, exe, exe, exe)
}

// promptUsername はユーザー名を対話的に取得する
// promptUsername はユーザー名を対話的に取得する
func promptUsername() string {
username := promptInput("ユーザー名: ")
if username == "" {
Expand All @@ -378,7 +375,7 @@ type menuItem struct {
action func()
}

// interactiveMode は対話モードを実行する
// interactiveMode は対話モードを実行する
func interactiveMode() {
items := []menuItem{
{key: "1", label: "配信者を追加", action: func() { addStreamer(promptUsername()) }},
Expand Down Expand Up @@ -415,7 +412,7 @@ func interactiveMode() {
os.Exit(1)
}

// requireUsername はユーザー名引数が必須であることを検証する
// requireUsername はユーザー名引数が必須であることを検証する
func requireUsername(args []string, index int) string {
if index >= len(args) || args[index] == "" {
fmt.Fprintln(os.Stderr, "エラー: ユーザー名を指定してください")
Expand All @@ -424,7 +421,7 @@ func requireUsername(args []string, index int) string {
return args[index]
}

// Run はCLIを実行する。argsが空の場合は対話モードを起動する
// Run はCLIを実行し、argsが空の場合は対話モードを起動する
func Run(args []string) {
if len(args) == 0 {
interactiveMode()
Expand Down
Loading
Loading