Пример #1
0
// Connect connects the IRC client to the server configured in Config.Server.
// To enable explicit SSL on the connection to the IRC server, set Config.SSL
// to true before calling Connect(). The port will default to 6697 if SSL is
// enabled, and 6667 otherwise.
// To enable connecting via a proxy server, set Config.Proxy to the proxy URL
// (example socks5://localhost:9000) before calling Connect().
//
// Upon successful connection, Connected will return true and a REGISTER event
// will be fired. This is mostly for internal use; it is suggested that a
// handler for the CONNECTED event is used to perform any initial client work
// like joining channels and sending messages.
func (conn *Conn) Connect() error {
	conn.mu.Lock()
	defer conn.mu.Unlock()
	conn.initialise()

	if conn.cfg.Server == "" {
		return fmt.Errorf("irc.Connect(): cfg.Server must be non-empty")
	}
	if conn.connected {
		return fmt.Errorf("irc.Connect(): Cannot connect to %s, already connected.", conn.cfg.Server)
	}

	if !hasPort(conn.cfg.Server) {
		if conn.cfg.SSL {
			conn.cfg.Server = net.JoinHostPort(conn.cfg.Server, "6697")
		} else {
			conn.cfg.Server = net.JoinHostPort(conn.cfg.Server, "6667")
		}
	}

	if conn.cfg.Proxy != "" {
		proxyURL, err := url.Parse(conn.cfg.Proxy)
		if err != nil {
			return err
		}
		conn.proxyDialer, err = proxy.FromURL(proxyURL, conn.dialer)
		if err != nil {
			return err
		}

		logging.Info("irc.Connect(): Connecting to %s.", conn.cfg.Server)
		if s, err := conn.proxyDialer.Dial("tcp", conn.cfg.Server); err == nil {
			conn.sock = s
		} else {
			return err
		}
	} else {
		logging.Info("irc.Connect(): Connecting to %s.", conn.cfg.Server)
		if s, err := conn.dialer.Dial("tcp", conn.cfg.Server); err == nil {
			conn.sock = s
		} else {
			return err
		}
	}

	if conn.cfg.SSL {
		logging.Info("irc.Connect(): Performing SSL handshake.")
		s := tls.Client(conn.sock, conn.cfg.SSLConfig)
		if err := s.Handshake(); err != nil {
			return err
		}
		conn.sock = s
	}

	conn.postConnect(true)
	conn.connected = true
	conn.dispatch(&Line{Cmd: REGISTER, Time: time.Now()})
	return nil
}
Пример #2
0
// Parse mode strings for a Nick.
func (nk *nick) parseModes(modes string) {
	var modeop bool // true => add mode, false => remove mode
	for i := 0; i < len(modes); i++ {
		switch m := modes[i]; m {
		case '+':
			modeop = true
		case '-':
			modeop = false
		case 'B':
			nk.modes.Bot = modeop
		case 'i':
			nk.modes.Invisible = modeop
		case 'o':
			nk.modes.Oper = modeop
		case 'w':
			nk.modes.WallOps = modeop
		case 'x':
			nk.modes.HiddenHost = modeop
		case 'z':
			nk.modes.SSL = modeop
		default:
			logging.Info("Nick.ParseModes(): unknown mode char %c", m)
		}
	}
}
Пример #3
0
func (tbmgr *TemporaryBanManager) Import(r io.Reader) error {
	tbmgr.dataSync.Lock()
	defer tbmgr.dataSync.Unlock()

	decoder := gob.NewDecoder(r)

	var exportVersion uint64
	if err := decoder.Decode(&exportVersion); err != nil {
		return err
	}

	switch exportVersion {
	case 0x0:
		// Structure:
		// - Bans = []*TemporaryBan
		if err := decoder.Decode(tbmgr.data); err != nil {
			return err
		}
	default:
		return errors.New("Invalid version found in input data")
	}

	// Run background handler for each temporary ban
	if !tbmgr.DisableExpiry {
		for _, ban := range tbmgr.data.Bans {
			tbmgr.handleBan(&ban)
		}
	}

	logging.Info("Imported %v bans.", len(tbmgr.data.Bans))

	return nil
}
Пример #4
0
func (p *Plugin) dumpBans(target string) {
	num := 0

	// Fetch ban list
	banlist, err := p.mode.Bans(target)
	if err != nil {
		logging.Warn("Could not fetch ban list, old bans won't get handled")
		return
	}

	tbmgr := p.ensureTemporaryBanManager(target)

	// Save only bans from us
	for _, ban := range banlist {
		if ban.Nick != p.bot.Me().Nick {
			// Not a ban from us (going by the nickname at least)
			isOldHostmask := false
			for _, hostmask := range p.OldHostmasks {
				// TODO - Test this implementation
				hostmask = regexp.QuoteMeta(hostmask)
				hostmask = strings.Replace(hostmask, "\\*", ".*", -1)
				hostmask = strings.Replace(hostmask, "\\?", ".?", -1)
				if matched, err := regexp.MatchString(hostmask, ban.Src); matched {
					isOldHostmask = true
					break
				} else if err != nil {
					logging.Error("vpnbot.Plugin: dumpBans regular expression failed: %v",
						err)
					break
				}
			}
			if !isOldHostmask {
				// Not a ban from an old hostmask either
				continue
			}
		}

		if _, ok := tbmgr.Get(ban.Hostmask); ok {
			// We already have this ban saved
			continue
		}

		if err := tbmgr.Add(NewTemporaryBan(
			ban.Nick,
			ban.Hostmask,
			ban.Src,
			"Migrated old ban",
			48*time.Hour+ban.Timestamp.Sub(time.Now()))); err != nil {
			logging.Warn("Could not migrate ban on %v: %v", ban.Hostmask, err)
		}

		num++
	}

	if num > 0 {
		p.syncBans(target)
		logging.Info("Migrated %v bans", num)
	}
}
Пример #5
0
func (conn *Conn) Connect() error {
	conn.mu.Lock()
	defer conn.mu.Unlock()
	conn.initialise()

	if conn.cfg.Server == "" {
		return fmt.Errorf("irc.Connect(): cfg.Server must be non-empty")
	}
	if conn.connected {
		return fmt.Errorf("irc.Connect(): Cannot connect to %s, already connected.", conn.cfg.Server)
	}
	if conn.cfg.SSL {
		if !hasPort(conn.cfg.Server) {
			conn.cfg.Server = net.JoinHostPort(conn.cfg.Server, "6697")
		}
		logging.Info("irc.Connect(): Connecting to %s with SSL.", conn.cfg.Server)
		if s, err := tls.DialWithDialer(conn.dialer, "tcp", conn.cfg.Server, conn.cfg.SSLConfig); err == nil {
			conn.sock = s
		} else {
			return err
		}
	} else {
		if !hasPort(conn.cfg.Server) {
			conn.cfg.Server = net.JoinHostPort(conn.cfg.Server, "6667")
		}
		logging.Info("irc.Connect(): Connecting to %s without SSL.", conn.cfg.Server)
		if s, err := conn.dialer.Dial("tcp", conn.cfg.Server); err == nil {
			conn.sock = s
		} else {
			return err
		}
	}
	conn.connected = true
	conn.postConnect(true)
	conn.dispatch(&Line{Cmd: REGISTER, Time: time.Now()})
	return nil
}
Пример #6
0
func (conn *Conn) shutdown() {
	// Guard against double-call of shutdown() if we get an error in send()
	// as calling sock.Close() will cause recv() to receive EOF in readstring()
	conn.mu.Lock()
	defer conn.mu.Unlock()
	if !conn.connected {
		return
	}
	logging.Info("irc.shutdown(): Disconnected from server.")
	conn.connected = false
	conn.sock.Close()
	close(conn.die)
	conn.wg.Wait()
	// reinit datastructures ready for next connection
	conn.initialise()
	conn.dispatch(&Line{Cmd: DISCONNECTED, Time: time.Now()})
}
Пример #7
0
// shutdown tears down all connection-related state. It is called when either
// the sending or receiving goroutines encounter an error.
func (conn *Conn) shutdown() {
	// Guard against double-call of shutdown() if we get an error in send()
	// as calling sock.Close() will cause recv() to receive EOF in readstring()
	conn.mu.Lock()
	if !conn.connected {
		conn.mu.Unlock()
		return
	}
	logging.Info("irc.shutdown(): Disconnected from server.")
	conn.connected = false
	conn.sock.Close()
	close(conn.die)
	conn.wg.Wait()
	conn.mu.Unlock()
	// Dispatch after closing connection but before reinit
	// so event handlers can still access state information.
	conn.dispatch(&Line{Cmd: DISCONNECTED, Time: time.Now()})
}
Пример #8
0
// write writes a \r\n terminated line of output to the connected server,
// using Hybrid's algorithm to rate limit if conn.cfg.Flood is false.
func (conn *Conn) write(line string) error {
	if !conn.cfg.Flood {
		if t := conn.rateLimit(len(line)); t != 0 {
			// sleep for the current line's time value before sending it
			logging.Info("irc.rateLimit(): Flood! Sleeping for %.2f secs.",
				t.Seconds())
			<-time.After(t)
		}
	}

	if _, err := conn.io.WriteString(line + "\r\n"); err != nil {
		return err
	}
	if err := conn.io.Flush(); err != nil {
		return err
	}
	logging.Debug("-> %s", line)
	return nil
}
Пример #9
0
// shutdown tears down all connection-related state. It is called when either
// the sending or receiving goroutines encounter an error.
func (conn *Conn) shutdown() {
	// Guard against double-call of shutdown() if we get an error in send()
	// as calling sock.Close() will cause recv() to receive EOF in readstring()
	conn.mu.Lock()
	if !conn.connected {
		conn.mu.Unlock()
		return
	}
	logging.Info("irc.shutdown(): Disconnected from server.")
	conn.connected = false
	conn.sock.Close()
	close(conn.die)
	// Drain both in and out channels to avoid a deadlock if the buffers
	// have filled. See TestSendDeadlockOnFullBuffer in connection_test.go.
	conn.drainIn()
	conn.drainOut()
	conn.wg.Wait()
	conn.mu.Unlock()
	// Dispatch after closing connection but before reinit
	// so event handlers can still access state information.
	conn.dispatch(&Line{Cmd: DISCONNECTED, Time: time.Now()})
}
Пример #10
0
func (plugin *Plugin) OnJoin(conn *client.Conn, line *client.Line) {
	logging.Info("vpnbot.Plugin: %v joined %v", line.Src, line.Target())

	if lastCheck, ok := plugin.lastCheckNicks[line.Nick]; ok && time.Now().Sub(lastCheck) < 15*time.Minute {
		// There is a check in progress or another one has been done earlier
		logging.Debug("vpnbot.Plugin: Not checking %v, last check was %v",
			line.Nick, humanize.Time(plugin.lastCheckNicks[line.Nick]))
		return
	}
	logging.Debug("vpnbot.Plugin: Checking %v...", line.Nick)
	plugin.lastCheckNicks[line.Nick] = time.Now()

	// Is this me?
	if line.Nick == conn.Me().Nick {
		logging.Debug("vpnbot.Plugin: %v is actually me, skipping.", line.Nick)
		return
	}

	// Nickname == Ident? (9 chars cut)
	if !strings.HasPrefix(nonWordCharsRegex.ReplaceAllString(line.Nick, ""),
		strings.TrimLeft(line.Ident, "~")) {
		logging.Debug("vpnbot.Plugin: %v's nick doesn't match the ident, skipping.", line.Nick)
		return
	}

	// Hostname is masked RDNS vhost/IP?
	// TODO - Use regex to avoid banning due to similar vhosts
	if !maskedAddrRegex.MatchString(line.Host) {
		// Detected custom vHost
		logging.Debug("vpnbot.Plugin: %v has a custom vhost, skipping.", line.Nick)
		return
	}

	go func() {
		botNick := line.Nick

		nobotActivityChan := make(chan string)
		defer plugin.bot.HandleFunc("privmsg",
			func(conn *client.Conn, line *client.Line) {
				if line.Nick == botNick {
					nobotActivityChan <- "User sent a message"
				}
			}).Remove()
		defer plugin.bot.HandleFunc("noticed",
			func(conn *client.Conn, line *client.Line) {
				if line.Nick == botNick {
					nobotActivityChan <- "User sent a notice"
				}
			}).Remove()
		defer plugin.bot.HandleFunc("part",
			func(conn *client.Conn, line *client.Line) {
				if line.Nick == botNick {
					nobotActivityChan <- "User left"
				}
			}).Remove()
		defer plugin.bot.HandleFunc("part",
			func(conn *client.Conn, line *client.Line) {
				if line.Nick == botNick {
				}

				if len(line.Args) > 0 {
					switch line.Args[0] {
					case "Excess flood":
						// If this was an excess flood, consider it spam that should
						// be good to ban anyways
						nobotActivityChan <- "Excess flood, banning early"
						banmask := fmt.Sprintf("%v!%v@%v", "*", "*", line.Host)
						// TODO - Ramp up/down the duration with increasing/decreasing activity of the bots
						plugin.banGlobal(plugin.generateBan(line.Nick, banmask,
							"Instant excess flood", 2*24*time.Hour))
					default:
						nobotActivityChan <- "User quit normally"
					}
				}
			}).Remove()

		// Give nobotActivityChan some time to prove this is not a bot
		select {
		case reason := <-nobotActivityChan:
			logging.Info("vpnbot.Plugin: %v, skipping.", reason)
			return
		case <-time.After(1 * time.Second):
		}

		// Get WHOIS info
		info, err := plugin.whois.WhoIs(line.Nick)
		if err != nil && err != whois.ErrNoSuchNick {
			logging.Warn("vpnbot.Plugin: Can't get WHOIS info for %v, skipping: %v",
				line.Nick, err.Error())
			return
		}

		// Not an oper?
		if info.IsOperator {
			logging.Debug("vpnbot.Plugin: %v is operator, skipping.",
				line.Nick)
			return
		}

		// Not away
		if info.IsAway {
			logging.Debug("vpnbot.Plugin: %v is away, skipping.", line.Nick)
			return
		}

		// Realname == Nickname?
		if info.Realname != line.Nick {
			logging.Debug(
				"vpnbot.Plugin: %v's nick doesn't match the realname, skipping.",
				line.Nick)
			return
		}

		// Count of channels at least 48
		if len(info.Channels) < 48 {
			logging.Debug(
				"vpnbot.Plugin: %v is not in a high amount of channels, skipping.",
				line.Nick)
			return
		}

		// Not halfop, op, aop or owner in any channel
		for _, prefix := range info.Channels {
			if prefix == '%' || prefix == '@' ||
				prefix == '&' || prefix == '~' {
				logging.Debug(
					"vpnbot.Plugin: %v is opped in a channel, skipping.",
					line.Nick)
				return
			}
		}

		// Give nobotActivityChan some time to prove this is not a bot
		select {
		case reason := <-nobotActivityChan:
			logging.Info("vpnbot.Plugin: %v, skipping.", reason)
			return
		case <-time.After(250 * time.Millisecond):
		}

		// More expensive tests below...

		// Make sure we deal with an unregistered client

		status := plugin.nickserv.Status(line.Nick)[line.Nick]
		if status.Error != nil {
			logging.Warn("vpnbot.Plugin: Can't get auth status for %v, skipping: %v",
				line.Nick, status.Error)
			return
		}
		if status.Level >= nickserv.Status_IdentifiedViaPassword {
			logging.Debug("vpnbot.Plugin: %v is identified, skipping.",
				line.Nick)
			return
		}

		// Give nobotActivityChan some time to prove this is not a bot
		select {
		case reason := <-nobotActivityChan:
			logging.Info("vpnbot.Plugin: %v, skipping.", reason)
			return
		case <-time.After(250 * time.Millisecond):
		}

		// This is a bot we need to ban!
		banmask := fmt.Sprintf("%v!%v@%v", "*", "*", line.Host)
		// TODO - Ramp up/down the duration with increasing/decreasing activity of the bots
		plugin.banGlobal(plugin.generateBan(line.Nick, banmask,
			"Known pattern of ban-evading/logging bot", 2*24*time.Hour))
	}()
}
Пример #11
0
// Parses mode strings for a channel.
func (ch *Channel) ParseModes(modes string, modeargs ...string) {
	var modeop bool // true => add mode, false => remove mode
	var modestr string
	for i := 0; i < len(modes); i++ {
		switch m := modes[i]; m {
		case '+':
			modeop = true
			modestr = string(m)
		case '-':
			modeop = false
			modestr = string(m)
		case 'i':
			ch.Modes.InviteOnly = modeop
		case 'm':
			ch.Modes.Moderated = modeop
		case 'n':
			ch.Modes.NoExternalMsg = modeop
		case 'p':
			ch.Modes.Private = modeop
		case 'r':
			ch.Modes.Registered = modeop
		case 's':
			ch.Modes.Secret = modeop
		case 't':
			ch.Modes.ProtectedTopic = modeop
		case 'z':
			ch.Modes.SSLOnly = modeop
		case 'Z':
			ch.Modes.AllSSL = modeop
		case 'O':
			ch.Modes.OperOnly = modeop
		case 'k':
			if modeop && len(modeargs) != 0 {
				ch.Modes.Key, modeargs = modeargs[0], modeargs[1:]
			} else if !modeop {
				ch.Modes.Key = ""
			} else {
				logging.Warn("Channel.ParseModes(): not enough arguments to "+
					"process MODE %s %s%c", ch.Name, modestr, m)
			}
		case 'l':
			if modeop && len(modeargs) != 0 {
				ch.Modes.Limit, _ = strconv.Atoi(modeargs[0])
				modeargs = modeargs[1:]
			} else if !modeop {
				ch.Modes.Limit = 0
			} else {
				logging.Warn("Channel.ParseModes(): not enough arguments to "+
					"process MODE %s %s%c", ch.Name, modestr, m)
			}
		case 'q', 'a', 'o', 'h', 'v':
			if len(modeargs) != 0 {
				if nk, ok := ch.lookup[modeargs[0]]; ok {
					cp := ch.nicks[nk]
					switch m {
					case 'q':
						cp.Owner = modeop
					case 'a':
						cp.Admin = modeop
					case 'o':
						cp.Op = modeop
					case 'h':
						cp.HalfOp = modeop
					case 'v':
						cp.Voice = modeop
					}
					modeargs = modeargs[1:]
				} else {
					logging.Warn("Channel.ParseModes(): untracked nick %s "+
						"received MODE on channel %s", modeargs[0], ch.Name)
				}
			} else {
				logging.Warn("Channel.ParseModes(): not enough arguments to "+
					"process MODE %s %s%c", ch.Name, modestr, m)
			}
		default:
			logging.Info("Channel.ParseModes(): unknown mode char %c", m)
		}
	}
}
Пример #12
0
// The main program logic.
func main() {
	// Load configuration path from flags
	flag.Parse()

	// Initialize the logger
	logger = glogging.GLogger{}
	logging.SetLogger(logger)

	// Check if we're supposed to generate a default config
	if *generateDefault {
		logger.Debug("Saving default configuration...")
		if err := defaultConfiguration.Save(*configPath); err != nil {
			logger.Error("Failed at saving default configuration: %v", err)
			os.Exit(1)
		}
		logger.Info("Saved default configuration.")
		os.Exit(0)
	}

	// Check if we're supposed to migrate an old config
	if *migratePath != "" {
		logger.Debug("Migrating old configuration...")
		if c, err := LoadV1Config(*migratePath); err != nil {
			logger.Error("Failed to load old configuration: %v", err)
			os.Exit(1)
		} else {
			newC := c.Migrate()
			if err := newC.Save(*configPath); err != nil {
				logger.Error("Migration failed: %v", err)
				os.Exit(1)
			}
			if err := newC.Validate(); err != nil {
				logger.Warn("Migration successful but found errors while "+
					"validating the new configuration, you should fix this before "+
					"running the bot: %v", err)
				os.Exit(2)
			}
		}
		logger.Info("Migration successful.")
		os.Exit(0)
	}

	// Load configuration from configuration path
	if c, err := Load(*configPath); err != nil {
		logger.Error("Can't load configuration from %v: %v\n", *configPath, err)
		os.Exit(1)
	} else {
		loadedConfiguration = c
	}

	logger.Debug("Loaded configuration will be printed below.")
	logger.Debug("%#v", loadedConfiguration)

	// Validate configuration
	if err := loadedConfiguration.Validate(); err != nil {
		logger.Error("The configuration is invalid: %v\n", err)
		os.Exit(2)
	}

	// Now initialize the bot
	logger.Info("Initializing vpnbot %v...", version)
	b := bot.NewBot(
		loadedConfiguration.Server.Address,
		loadedConfiguration.Server.SSL,
		loadedConfiguration.Nick,
		loadedConfiguration.Ident,
		[]string{})
	b.Conn().Config().Version = fmt.Sprintf("vpnbot/%v", version)
	b.Conn().Config().Recover = func(conn *client.Conn, line *client.Line) {
		if err := recover(); err != nil {
			logging.Error("An internal error occurred: %v\n%v",
				err, string(debug.Stack()))
		}
	}
	b.Conn().Config().Pass = loadedConfiguration.Server.Password
	if loadedConfiguration.Name != "" {
		b.Conn().Config().Me.Name = loadedConfiguration.Name
	}

	// Load plugins
	// TODO - Move this into its own little intelligent loader struct, maybe.
	isupportPlugin := isupport.Register(b)
	modePlugin := mode.Register(b, isupportPlugin)
	nickservPlugin := nickserv.Register(b, modePlugin)
	nickservPlugin.Username = loadedConfiguration.NickServ.Username
	nickservPlugin.Password = loadedConfiguration.NickServ.Password
	nickservPlugin.Channels = loadedConfiguration.Channels

	switch {
	case *makeTempBans: // Run in tempban dumping mode
		// Prepare channels to let us know about dumped bans
		doneChan := make(map[string]chan interface{})
		for _, channel := range loadedConfiguration.Channels {
			doneChan[strings.ToLower(channel)] = make(chan interface{}, 1)
		}

		// Load the tempban dumping plugin
		dumptempbanPlugin := dumptempban.Register(b, isupportPlugin, modePlugin)
		dumptempbanPlugin.DumpedBansFunc = func(target string, num int, err error) {
			if err != nil {
				logging.Error("Failed to dump bans for %v: %v", target, err)
			} else {
				logging.Info("Dumped %v bans for %v successfully.", num, target)
			}
			if done, ok := doneChan[strings.ToLower(target)]; ok {
				done <- nil
			}
		}

		// Start up the bot asynchronously
		go b.Run()

		// Wait for all channels to be done
		for _, done := range doneChan {
			<-done
		}
		b.Quit("Ban dumping done.")

	default: // Run normally
		// Load plugins
		autojoin.Register(b)
		adminplugin.Register(b, loadedConfiguration.Admins)
		bots.Register(b, isupportPlugin)
		tempbanPlugin := tempban.Register(b, isupportPlugin, modePlugin)
		tempbanPlugin.OldHostmasks = loadedConfiguration.OldHostmasks
		whoisPlugin := whois.Register(b, isupportPlugin)
		vpnbotPlugin := vpnbot.Register(b, whoisPlugin, isupportPlugin,
			tempbanPlugin, nickservPlugin)
		vpnbotPlugin.Admins = loadedConfiguration.Admins

		// This is to update the configuration when the bot joins channels
		b.HandleFunc("join",
			func(c *client.Conn, line *client.Line) {
				// Arguments: [ <channel> ]

				// Make sure this is about us
				if line.Nick != c.Me().Nick {
					return
				}

				// I don't think method calls are a good idea in a loop
				channel := line.Target()

				// See if we already had this channel saved
				for _, savedChannel := range loadedConfiguration.Channels {
					if strings.EqualFold(savedChannel, channel) {
						return // Channel already saved
					}
				}

				// Store this channel
				logger.Info("Adding %v to configured channels", channel)
				loadedConfiguration.Channels = append(
					loadedConfiguration.Channels, channel)

				// And save to configuration file!
				loadedConfiguration.Save(*configPath)
			})
		b.HandleFunc("kick",
			func(c *client.Conn, line *client.Line) {
				// Arguments: [ <channel>, <nick>, <reason> ]

				// Make sure this is about us
				if line.Args[1] != c.Me().Nick {
					return
				}

				// I don't think method calls are a good idea in a loop
				channel := line.Target()

				for index, savedChannel := range loadedConfiguration.Channels {
					if strings.EqualFold(savedChannel, channel) {
						// Delete the channel
						logger.Info("Removing %v from configured channels", savedChannel)
						loadedConfiguration.Channels = append(
							loadedConfiguration.Channels[0:index],
							loadedConfiguration.Channels[index+1:]...)

						// And save to configuration file!
						loadedConfiguration.Save(*configPath)
						return
					}
				}
			})
		b.HandleFunc("part",
			func(c *client.Conn, line *client.Line) {
				// Arguments: [ <channel> (, <reason>) ]

				// Make sure this is about us
				if line.Nick != c.Me().Nick {
					return
				}

				// I don't think method calls are a good idea in a loop
				channel := line.Target()

				for index, savedChannel := range loadedConfiguration.Channels {
					if strings.EqualFold(savedChannel, channel) {
						// Delete the channel
						logger.Info("Removing %v from configured channels", savedChannel)
						loadedConfiguration.Channels = append(
							loadedConfiguration.Channels[0:index],
							loadedConfiguration.Channels[index+1:]...)

						// And save to configuration file!
						loadedConfiguration.Save(*configPath)
						return
					}
				}
			})

		// Run the bot
		b.Run()
	}
}