Skip to content
Merged
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
10 changes: 9 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,12 @@ SKIP_START=0

# 通知停止終了時刻 (0-23)

SKIP_END=6
SKIP_END=6

# Bot判定を無効化

DISABLE_BOT_FILTER=true

# 特定ユーザーを除外

EXCLUDE_USER_IDS=123456789,987654321
15 changes: 14 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,27 @@

## 概要
- Voice Channel参加を検知し、指定時間帯のみ通知を行う
- Botユーザーの参加通知を自動的に除外
- 特定ユーザーの参加通知を除外可能

## 動作要件
- Go 1.21以上
- Discord Botトークン

## セットアップ
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
Expand Down
179 changes: 103 additions & 76 deletions bot/main.go
Original file line number Diff line number Diff line change
@@ -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 {}
}
Loading