diff --git a/.env.example b/.env.example index a883638..1f9a764 100644 --- a/.env.example +++ b/.env.example @@ -16,4 +16,12 @@ SKIP_START=0 # 通知停止終了時刻 (0-23) -SKIP_END=6 \ No newline at end of file +SKIP_END=6 + +# Bot判定を無効化 + +DISABLE_BOT_FILTER=true + +# 特定ユーザーを除外 + +EXCLUDE_USER_IDS=123456789,987654321 \ No newline at end of file diff --git a/README.md b/README.md index e87d7f5..be90a49 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,8 @@ ## 概要 - Voice Channel参加を検知し、指定時間帯のみ通知を行う +- Botユーザーの参加通知を自動的に除外 +- 特定ユーザーの参加通知を除外可能 ## 動作要件 - Go 1.21以上 @@ -11,7 +13,18 @@ ## セットアップ 1. `.env.example` をコピーして `.env` にリネーム -2. 環境変数を設定 (`DISCORD_TOKEN`, `GUILD_ID`, `CHANNEL_ID`, `SKIP_START`, `SKIP_END`) +2. 環境変数を設定 + +### 必須環境変数 +- `DISCORD_TOKEN`: Discord Botトークン +- `CHANNEL_ID`: 通知を送信するテキストチャンネルID +- `SKIP_START`: 通知をスキップする開始時刻(0-23の数字) +- `SKIP_END`: 通知をスキップする終了時刻(0-23の数字) + +### オプション環境変数 +- `DISABLE_BOT_FILTER`: Bot判定を無効化する場合に `true` を設定(デフォルト: 無効 = Botを自動除外) +- `EXCLUDE_USER_IDS`: 通知を除外するユーザーIDをカンマ区切りで指定(例: `123456789,987654321`) + 3. Botをローカルで起動: ```bash go run bot/main.go diff --git a/bot/main.go b/bot/main.go index 6ddab97..8db7aca 100644 --- a/bot/main.go +++ b/bot/main.go @@ -1,84 +1,111 @@ package main import ( - "fmt" - "log" - "os" - "strconv" - "time" + "fmt" + "log" + "os" + "strconv" + "strings" + "time" - "github.com/bwmarrin/discordgo" + "github.com/bwmarrin/discordgo" ) func main() { - // 前回の VoiceState を管理するマップ - lastVoice := make(map[string]string) - - // 環境変数取得 - token := os.Getenv("DISCORD_TOKEN") - if token == "" { - log.Fatal("DISCORD_TOKEN is not set") - } - skipStart, _ := strconv.Atoi(os.Getenv("SKIP_START")) - skipEnd, _ := strconv.Atoi(os.Getenv("SKIP_END")) - - // Discord セッション作成 - dg, err := discordgo.New(token) - if err != nil { - log.Fatal(err) - } - - // VoiceStateUpdate イベントを受け取る Intent を有効化 - dg.Identify.Intents = discordgo.IntentsGuilds | discordgo.IntentsGuildVoiceStates - - dg.AddHandler(func(s *discordgo.Session, vs *discordgo.VoiceStateUpdate) { - // 前回の状態と比較して、同じチャンネルでの更新(ミュート/画面共有など)はスキップ - prevChannel, _ := lastVoice[vs.UserID] - lastVoice[vs.UserID] = vs.ChannelID - if prevChannel == vs.ChannelID && vs.ChannelID != "" { - return - } - - // 退出イベントは通知しない - if vs.ChannelID == "" { - return - } - - // JST の時刻で判定 - jst := time.FixedZone("Asia/Tokyo", 9*60*60) - now := time.Now().In(jst) - if skipStart <= now.Hour() && now.Hour() < skipEnd { - return - } - - // ユーザーのサーバーニックネーム(未設定時はユーザー名) - member, err := s.GuildMember(vs.GuildID, vs.UserID) - var displayName string - if err == nil && member.Nick != "" { - displayName = member.Nick - } else { - user, err := s.User(vs.UserID) - if err == nil { - displayName = user.Username - } else { - displayName = vs.UserID - } - } - - // 通知送信 - channelID := os.Getenv("CHANNEL_ID") - message := fmt.Sprintf("🔔 %s がボイスチャンネルに参加しました", displayName) - if _, err := s.ChannelMessageSend(channelID, message); err != nil { - log.Printf("[ERROR] failed to send message: %v\n", err) - } - }) - - // WebSocket 接続をオープン - if err := dg.Open(); err != nil { - log.Fatal(err) - } - defer dg.Close() - - log.Println("VCNotify is running. Press CTRL-C to exit.") - select {} + // 前回の VoiceState を管理するマップ + lastVoice := make(map[string]string) + + // 環境変数取得 + token := os.Getenv("DISCORD_TOKEN") + if token == "" { + log.Fatal("DISCORD_TOKEN is not set") + } + skipStart, _ := strconv.Atoi(os.Getenv("SKIP_START")) + skipEnd, _ := strconv.Atoi(os.Getenv("SKIP_END")) + + // Bot判定を無効化するか(デフォルト: false = 有効) + disableBotFilter := os.Getenv("DISABLE_BOT_FILTER") == "true" + + // 除外するユーザーIDリスト + excludeUserIDs := make(map[string]bool) + if excludeIDs := os.Getenv("EXCLUDE_USER_IDS"); excludeIDs != "" { + for _, id := range strings.Split(excludeIDs, ",") { + id = strings.TrimSpace(id) + if id != "" { + excludeUserIDs[id] = true + } + } + } + + // Discord セッション作成 + dg, err := discordgo.New(token) + if err != nil { + log.Fatal(err) + } + + // VoiceStateUpdate イベントを受け取る Intent を有効化 + dg.Identify.Intents = discordgo.IntentsGuilds | discordgo.IntentsGuildVoiceStates + + dg.AddHandler(func(s *discordgo.Session, vs *discordgo.VoiceStateUpdate) { + // 前回の状態と比較して、同じチャンネルでの更新(ミュート/画面共有など)はスキップ + prevChannel, _ := lastVoice[vs.UserID] + lastVoice[vs.UserID] = vs.ChannelID + if prevChannel == vs.ChannelID && vs.ChannelID != "" { + return + } + + // 退出イベントは通知しない + if vs.ChannelID == "" { + return + } + + // ユーザー情報を取得(Bot判定と表示名取得のため) + user, err := s.User(vs.UserID) + if err != nil { + log.Printf("[WARN] failed to get user: %v\n", err) + return + } + + // Bot判定(環境変数で無効化可能) + if !disableBotFilter && user.Bot { + return + } + + // 除外ユーザーIDリストのチェック + if excludeUserIDs[vs.UserID] { + return + } + + // JST の時刻で判定 + jst := time.FixedZone("Asia/Tokyo", 9*60*60) + now := time.Now().In(jst) + if skipStart <= now.Hour() && now.Hour() < skipEnd { + return + } + + // ユーザーのサーバーニックネーム(未設定時はユーザー名) + member, err := s.GuildMember(vs.GuildID, vs.UserID) + var displayName string + if err == nil && member.Nick != "" { + displayName = member.Nick + } else { + displayName = user.Username + } + + // 通知送信 + channelID := os.Getenv("CHANNEL_ID") + message := fmt.Sprintf("🔔 %s がボイスチャンネルに参加しました", displayName) + if _, err := s.ChannelMessageSend(channelID, message); err != nil { + log.Printf("[ERROR] failed to send message: %v\n", err) + } + }) + + // WebSocket 接続をオープン + if err := dg.Open(); err != nil { + log.Fatal(err) + } + defer dg.Close() + + log.Println("VCNotify is running. Press CTRL-C to exit.") + select {} }