diff --git a/.env.example b/.env.example index 1f9a764..a883638 100644 --- a/.env.example +++ b/.env.example @@ -16,12 +16,4 @@ SKIP_START=0 # 通知停止終了時刻 (0-23) -SKIP_END=6 - -# Bot判定を無効化 - -DISABLE_BOT_FILTER=true - -# 特定ユーザーを除外 - -EXCLUDE_USER_IDS=123456789,987654321 \ No newline at end of file +SKIP_END=6 \ No newline at end of file diff --git a/README.md b/README.md index be90a49..e87d7f5 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,6 @@ ## 概要 - Voice Channel参加を検知し、指定時間帯のみ通知を行う -- Botユーザーの参加通知を自動的に除外 -- 特定ユーザーの参加通知を除外可能 ## 動作要件 - Go 1.21以上 @@ -13,18 +11,7 @@ ## セットアップ 1. `.env.example` をコピーして `.env` にリネーム -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`) - +2. 環境変数を設定 (`DISCORD_TOKEN`, `GUILD_ID`, `CHANNEL_ID`, `SKIP_START`, `SKIP_END`) 3. Botをローカルで起動: ```bash go run bot/main.go diff --git a/bot/main.go b/bot/main.go index 8db7aca..6ddab97 100644 --- a/bot/main.go +++ b/bot/main.go @@ -1,111 +1,84 @@ package main import ( - "fmt" - "log" - "os" - "strconv" - "strings" - "time" + "fmt" + "log" + "os" + "strconv" + "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")) - - // 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 {} + // 前回の 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 {} }