diff --git a/.gitignore b/.gitignore
index 66c0676782f1cbca3e7f289f6be5790c3963c718..584bf4c90f0f2c3752514131d70224951cd3ca16 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
 go-voice-bot
 go-voice-bot.*
-.idea/
\ No newline at end of file
+.idea/
+server.conf
\ No newline at end of file
diff --git a/bs.patch b/bs.patch
new file mode 100644
index 0000000000000000000000000000000000000000..03758e675f4195572cf7ad67501dd7a40e16be93
--- /dev/null
+++ b/bs.patch
@@ -0,0 +1,190 @@
+diff --git a/.gitignore b/.gitignore
+index 66c0676..584bf4c 100644
+--- a/.gitignore
++++ b/.gitignore
+@@ -1,3 +1,4 @@
+ go-voice-bot
+ go-voice-bot.*
+-.idea/
+\ No newline at end of file
++.idea/
++server.conf
+\ No newline at end of file
+diff --git a/config.go b/config.go
+new file mode 100644
+index 0000000..40ca7c1
+--- /dev/null
++++ b/config.go
+@@ -0,0 +1,53 @@
++package main
++
++import (
++	"flag"
++	"git.randomchars.net/freenitori/log"
++	"github.com/BurntSushi/toml"
++	"os"
++)
++
++var config configPayload
++var configPath string
++var defaultConfig = configPayload{
++	Token:     "TOKEN",
++	ChannelID: []string{},
++	Prefix:    "!",
++}
++
++type configPayload struct {
++	Token     string
++	ChannelID []string
++	Prefix    string
++}
++
++func init() {
++	flag.StringVar(&configPath, "-t", "server.conf", "Specify configuration file location.")
++}
++
++func parse() {
++	if _, err := toml.DecodeFile(configPath, &config); err != nil {
++		if os.IsNotExist(err) {
++			var file *os.File
++			if file, err = os.Create(configPath); err != nil {
++				log.Fatalf("Error while creating configuration file, %s", err)
++				os.Exit(1)
++			}
++			if err = toml.NewEncoder(file).Encode(defaultConfig); err != nil {
++				log.Fatalf("Error while encoding default configuration, %s", err)
++				os.Exit(1)
++			}
++			log.Warnf("Default configuration generated at %s, edit before next startup.", configPath)
++			os.Exit(1)
++		}
++		log.Fatalf("Error while decoding configuration file, %s", err)
++		os.Exit(1)
++	} else {
++		log.Infof("Loaded config at %s.", configPath)
++	}
++
++	allowedChannels = make(map[string]bool)
++	for _, id := range config.ChannelID {
++		allowedChannels[id] = true
++	}
++}
+diff --git a/go.mod b/go.mod
+index 4b448f1..3c02d0d 100644
+--- a/go.mod
++++ b/go.mod
+@@ -6,6 +6,7 @@ require (
+ 	git.randomchars.net/freenitori/embedutil v1.0.2
+ 	git.randomchars.net/freenitori/log v1.0.0
+ 	git.randomchars.net/freenitori/multiplexer v1.0.12
++	github.com/BurntSushi/toml v0.3.1
+ 	github.com/bwmarrin/discordgo v0.23.2
+ 	github.com/sirupsen/logrus v1.8.1
+ )
+diff --git a/go.sum b/go.sum
+index 367e548..ede86a0 100644
+--- a/go.sum
++++ b/go.sum
+@@ -10,6 +10,8 @@ git.randomchars.net/freenitori/log v1.0.0 h1:hU99jGk940I1O5OcaTfnXOpN8ozXiarxhu6
+ git.randomchars.net/freenitori/log v1.0.0/go.mod h1:YZFRZgVWDIrbyDGHyDeRlIRWeq0DXamXONxIt12eq2Q=
+ git.randomchars.net/freenitori/multiplexer v1.0.12 h1:XsMMSeeaeBtavlsMl7M6ZrW00wa2+zsx/w5gYWR5Qh0=
+ git.randomchars.net/freenitori/multiplexer v1.0.12/go.mod h1:Bx9vu2RXDtBrsKBslrhrc8v3IJl5Dna7I/rsHF586w0=
++github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
++github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+ github.com/bwmarrin/discordgo v0.23.2 h1:BzrtTktixGHIu9Tt7dEE6diysEF9HWnXeHuoJEt2fH4=
+ github.com/bwmarrin/discordgo v0.23.2/go.mod h1:c1WtWUGN6nREDmzIpyTp/iD3VYt4Fpx+bVyfBG7JE+M=
+ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+diff --git a/handler.go b/handler.go
+index 7bdb866..0efca9b 100644
+--- a/handler.go
++++ b/handler.go
+@@ -3,23 +3,54 @@ package main
+ import (
+ 	"git.randomchars.net/freenitori/log"
+ 	"git.randomchars.net/freenitori/multiplexer"
++	"github.com/bwmarrin/discordgo"
+ )
+ 
+ func init() {
+ 	m.VoiceStateUpdate = append(m.VoiceStateUpdate, handleVoiceUpdate)
+ }
+ 
++var allowedChannels map[string]bool
++
+ func handleVoiceUpdate(context *multiplexer.Context) {
+ 
+ 	if context.Channel == nil {
+ 		return
+ 	}
+ 
+-	log.Info(context.Channel.ID)
++	if !allowedChannels[context.Channel.ID] {
++		return
++	}
++
++	var event *discordgo.VoiceStateUpdate
++	if e, ok := context.Event.(*discordgo.VoiceStateUpdate); !ok {
++		return
++	} else {
++		event = e
++	}
++
++	var member *discordgo.Member
++	if u, err := context.Session.State.Member(event.GuildID, event.UserID); err != nil {
++		if u, err = context.Session.GuildMember(event.GuildID, event.UserID); err != nil {
++			log.Errorf("Error getting member %s (%s), %s", event.UserID, event.GuildID, err)
++			return
++		} else {
++			member = u
++			_ = context.Session.State.MemberAdd(u)
++		}
++	} else {
++		member = u
++	}
++
++	if member == nil {
++		return
++	}
+ 
+-	// TODO: unhardcode
+-	//if context.Channel.ID == "783703981620723762" {
+-	//	log.Infof("%s joined click me channel", context.Member.User.Username)
+-	//}
+-	
+-}
+\ No newline at end of file
++	log.Infof("%s#%s (%s) has joined the designated channel #%s (%s).",
++		member.User.Username,
++		member.User.Discriminator,
++		member.User.ID,
++		context.Channel.Name,
++		context.Channel.ID,
++		)
++}
+diff --git a/main.go b/main.go
+index 714ace3..abf5657 100644
+--- a/main.go
++++ b/main.go
+@@ -18,10 +18,7 @@ var system = multiplexer.NewCategory("System", "System-related utilities.")
+ 
+ func main() {
+ 	flag.Parse()
+-	if len(flag.Args()) != 1 {
+-		fmt.Println("expecting 1 argument: token")
+-		os.Exit(1)
+-	}
++	parse()
+ 
+ 	// Set discordgo log handler
+ 	discordgo.Logger = func(msgL, _ int, format string, a ...interface{}) {
+@@ -51,8 +48,8 @@ func main() {
+ 	} else {
+ 		session = s
+ 	}
+-	session.UserAgent = "DiscordBot (ticket-bot)"
+-	session.Token = "Bot " + flag.Arg(0)
++	session.UserAgent = "DiscordBot (voice-bot)"
++	session.Token = "Bot " + config.Token
+ 	session.ShouldReconnectOnError = true
+ 	session.Identify.Intents = discordgo.IntentsAllWithoutPrivileged
+ 
diff --git a/config.go b/config.go
new file mode 100644
index 0000000000000000000000000000000000000000..40ca7c18048a9c785ae1461bf2d1c98adbb59d37
--- /dev/null
+++ b/config.go
@@ -0,0 +1,53 @@
+package main
+
+import (
+	"flag"
+	"git.randomchars.net/freenitori/log"
+	"github.com/BurntSushi/toml"
+	"os"
+)
+
+var config configPayload
+var configPath string
+var defaultConfig = configPayload{
+	Token:     "TOKEN",
+	ChannelID: []string{},
+	Prefix:    "!",
+}
+
+type configPayload struct {
+	Token     string
+	ChannelID []string
+	Prefix    string
+}
+
+func init() {
+	flag.StringVar(&configPath, "-t", "server.conf", "Specify configuration file location.")
+}
+
+func parse() {
+	if _, err := toml.DecodeFile(configPath, &config); err != nil {
+		if os.IsNotExist(err) {
+			var file *os.File
+			if file, err = os.Create(configPath); err != nil {
+				log.Fatalf("Error while creating configuration file, %s", err)
+				os.Exit(1)
+			}
+			if err = toml.NewEncoder(file).Encode(defaultConfig); err != nil {
+				log.Fatalf("Error while encoding default configuration, %s", err)
+				os.Exit(1)
+			}
+			log.Warnf("Default configuration generated at %s, edit before next startup.", configPath)
+			os.Exit(1)
+		}
+		log.Fatalf("Error while decoding configuration file, %s", err)
+		os.Exit(1)
+	} else {
+		log.Infof("Loaded config at %s.", configPath)
+	}
+
+	allowedChannels = make(map[string]bool)
+	for _, id := range config.ChannelID {
+		allowedChannels[id] = true
+	}
+}
diff --git a/go.mod b/go.mod
index 4b448f1122b8db6f805e464cca48e9c0a4ae5027..3c02d0d5c7465bb7aa0475c264fbbbef8e43eb08 100644
--- a/go.mod
+++ b/go.mod
@@ -6,6 +6,7 @@ require (
 	git.randomchars.net/freenitori/embedutil v1.0.2
 	git.randomchars.net/freenitori/log v1.0.0
 	git.randomchars.net/freenitori/multiplexer v1.0.12
+	github.com/BurntSushi/toml v0.3.1
 	github.com/bwmarrin/discordgo v0.23.2
 	github.com/sirupsen/logrus v1.8.1
 )
diff --git a/go.sum b/go.sum
index 367e5487c1414f4f9766b8fb1b6e26e0165a55ab..ede86a0e4a85a8526464e2d0f1f46174e7eeb9e2 100644
--- a/go.sum
+++ b/go.sum
@@ -10,6 +10,8 @@ git.randomchars.net/freenitori/log v1.0.0 h1:hU99jGk940I1O5OcaTfnXOpN8ozXiarxhu6
 git.randomchars.net/freenitori/log v1.0.0/go.mod h1:YZFRZgVWDIrbyDGHyDeRlIRWeq0DXamXONxIt12eq2Q=
 git.randomchars.net/freenitori/multiplexer v1.0.12 h1:XsMMSeeaeBtavlsMl7M6ZrW00wa2+zsx/w5gYWR5Qh0=
 git.randomchars.net/freenitori/multiplexer v1.0.12/go.mod h1:Bx9vu2RXDtBrsKBslrhrc8v3IJl5Dna7I/rsHF586w0=
+github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
+github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/bwmarrin/discordgo v0.23.2 h1:BzrtTktixGHIu9Tt7dEE6diysEF9HWnXeHuoJEt2fH4=
 github.com/bwmarrin/discordgo v0.23.2/go.mod h1:c1WtWUGN6nREDmzIpyTp/iD3VYt4Fpx+bVyfBG7JE+M=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
diff --git a/handler.go b/handler.go
index 7bdb866494870095358f8fc25c1f26482f682300..0efca9b38e8254df83e79a66965f0fec9020a7e1 100644
--- a/handler.go
+++ b/handler.go
@@ -3,23 +3,54 @@ package main
 import (
 	"git.randomchars.net/freenitori/log"
 	"git.randomchars.net/freenitori/multiplexer"
+	"github.com/bwmarrin/discordgo"
 )
 
 func init() {
 	m.VoiceStateUpdate = append(m.VoiceStateUpdate, handleVoiceUpdate)
 }
 
+var allowedChannels map[string]bool
+
 func handleVoiceUpdate(context *multiplexer.Context) {
 
 	if context.Channel == nil {
 		return
 	}
 
-	log.Info(context.Channel.ID)
+	if !allowedChannels[context.Channel.ID] {
+		return
+	}
+
+	var event *discordgo.VoiceStateUpdate
+	if e, ok := context.Event.(*discordgo.VoiceStateUpdate); !ok {
+		return
+	} else {
+		event = e
+	}
+
+	var member *discordgo.Member
+	if u, err := context.Session.State.Member(event.GuildID, event.UserID); err != nil {
+		if u, err = context.Session.GuildMember(event.GuildID, event.UserID); err != nil {
+			log.Errorf("Error getting member %s (%s), %s", event.UserID, event.GuildID, err)
+			return
+		} else {
+			member = u
+			_ = context.Session.State.MemberAdd(u)
+		}
+	} else {
+		member = u
+	}
+
+	if member == nil {
+		return
+	}
 
-	// TODO: unhardcode
-	//if context.Channel.ID == "783703981620723762" {
-	//	log.Infof("%s joined click me channel", context.Member.User.Username)
-	//}
-	
-}
\ No newline at end of file
+	log.Infof("%s#%s (%s) has joined the designated channel #%s (%s).",
+		member.User.Username,
+		member.User.Discriminator,
+		member.User.ID,
+		context.Channel.Name,
+		context.Channel.ID,
+		)
+}
diff --git a/main.go b/main.go
index 714ace3fe13abbfa88bd9f17d8ec1265376270e5..abf5657226ced354a92de82e643c25b34e559692 100644
--- a/main.go
+++ b/main.go
@@ -18,10 +18,7 @@ var system = multiplexer.NewCategory("System", "System-related utilities.")
 
 func main() {
 	flag.Parse()
-	if len(flag.Args()) != 1 {
-		fmt.Println("expecting 1 argument: token")
-		os.Exit(1)
-	}
+	parse()
 
 	// Set discordgo log handler
 	discordgo.Logger = func(msgL, _ int, format string, a ...interface{}) {
@@ -51,8 +48,8 @@ func main() {
 	} else {
 		session = s
 	}
-	session.UserAgent = "DiscordBot (ticket-bot)"
-	session.Token = "Bot " + flag.Arg(0)
+	session.UserAgent = "DiscordBot (voice-bot)"
+	session.Token = "Bot " + config.Token
 	session.ShouldReconnectOnError = true
 	session.Identify.Intents = discordgo.IntentsAllWithoutPrivileged