bridg/main.go

309 lines
10 KiB
Go

package main
import (
"flag"
"os"
"os/signal"
"path/filepath"
"reflect"
"strings"
"syscall"
"time"
"github.com/fsnotify/fsnotify"
"github.com/gobwas/glob"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
"github.com/spf13/viper"
"bridg/bridge"
ircnick "bridg/irc/nick"
)
func main() {
config := flag.String("config", "", "Config file to read configuration stuff from")
simple := flag.Bool("simple", false, "When in simple mode, the bridge will only spawn one IRC connection for listening and speaking")
debugMode := flag.Bool("debug", false, "Debug mode? (false = use value from settings)")
notls := flag.Bool("no-tls", false, "Avoids using TLS att all when connecting to IRC server ")
insecure := flag.Bool("insecure", false, "Skip TLS certificate verification? (INSECURE MODE) (false = use value from settings)")
// Secret devmode
devMode := flag.Bool("dev", false, "")
debugPresence := flag.Bool("debug-presence", false, "Include presence in debug output")
flag.Parse()
bridge.DevMode = *devMode
if *config == "" {
log.Fatalln("--config argument is required!")
return
}
if *simple {
log.Println("Running in simple mode.")
}
viper := viper.New()
ext := filepath.Ext(*config)
configName := strings.TrimSuffix(filepath.Base(*config), ext)
configType := ext[1:]
configPath := filepath.Dir(*config)
viper.SetConfigName(configName)
viper.SetConfigType(configType)
viper.AddConfigPath(configPath)
log.WithFields(log.Fields{
"ConfigName": configName,
"ConfigType": configType,
"ConfigPath": configPath,
}).Infoln("Loading configuration...")
err := viper.ReadInConfig()
if err != nil {
log.Fatalln(errors.Wrap(err, "could not read config"))
}
if viper.GetString("nickserv_identify") != "" {
log.Fatalln("Please see https://github.com/qaisjp/go-discord-irc/blob/master/config.yml for an example config. `nickserv_identify` is deprecated and superseded by `irc_puppet_prejoin_commands`.")
return
}
discriminator := viper.GetString("irc_server_name") // unique per IRC network connected to, keeps PMs working
if discriminator == "" {
log.Fatalln("'irc_server_name' config option is required and cannot be empty")
return
}
discordBotToken := viper.GetString("discord_token") // Discord Bot User Token
channelMappings := viper.GetStringMapString("channel_mappings") // Discord:IRC mappings in format '#discord1:#irc1,#discord2:#irc2,...'
ircServer := viper.GetString("irc_server") // Server address to use, example `irc.freenode.net:7000`.
ircPassword := viper.GetString("irc_pass") // Optional password for connecting to the IRC server
ircListenerPrejoinCommands := viper.GetStringSlice("irc_listener_prejoin_commands") // Commands for each connection to send before joining channels
guildID := viper.GetString("guild_id") // Guild to use
webIRCPass := viper.GetString("webirc_pass") // Password for WEBIRC
ircIgnores := viper.GetStringSlice("ignored_irc_hostmasks") // IRC hosts to not relay to Discord
rawDiscordIgnores := viper.GetStringSlice("ignored_discord_ids") // Ignore these Discord users on IRC
rawDiscordAllowed := viper.GetStringSlice("allowed_discord_ids")
rawIRCFilter := viper.GetStringSlice("irc_message_filter") // Ignore lines containing matched text from IRC
rawDiscordFilter := viper.GetStringSlice("discord_message_filter") // Ignore lines containing matched text from Discord
connectionLimit := viper.GetInt("connection_limit") // Limiter on how many IRC Connections we can spawn
//
if !*debugMode {
*debugMode = viper.GetBool("debug")
}
//
if !*notls {
*notls = viper.GetBool("no_tls")
}
if !*insecure {
*insecure = viper.GetBool("insecure")
}
//
viper.SetDefault("avatar_url", "https://robohash.org/${USERNAME}.png?set=set4")
avatarURL := viper.GetString("avatar_url")
//
viper.SetDefault("irc_listener_name", "~d")
ircUsername := viper.GetString("irc_listener_name") // Name for IRC-side bot, for listening to messages.
// Name to Connect to IRC puppet account with
viper.SetDefault("puppet_username", "")
puppetUsername := viper.GetString("puppet_username")
//
viper.SetDefault("suffix", "~d")
suffix := viper.GetString("suffix") // The suffix to append to IRC connections (not in use when simple mode is on)
//
viper.SetDefault("separator", "~")
separator := viper.GetString("separator")
//
viper.SetDefault("cooldown_duration", int64((time.Hour * 24).Seconds()))
cooldownDuration := viper.GetInt64("cooldown_duration")
//
viper.SetDefault("show_joinquit", false)
showJoinQuit := viper.GetBool("show_joinquit")
// Maximum length of user nicks aloud
viper.SetDefault("max_nick_length", ircnick.MAXLENGTH)
maxNickLength := viper.GetInt("max_nick_length")
if webIRCPass == "" {
log.Warnln("webirc_pass is empty")
}
// Validate mappings
if len(channelMappings) == 0 {
log.Warnln("Channel mappings are missing!")
}
matchers := setupHostmaskMatchers(ircIgnores)
discordFilter := setupFilter(rawDiscordFilter)
ircFilter := setupFilter(rawIRCFilter)
SetLogDebug(*debugMode)
// Check for nil, as nil means we don't use this list
var discordAllowed map[string]struct{}
if rawDiscordAllowed != nil {
log.Println("allowed_discord_ids is set, so only specific Discord users will be bridged")
discordAllowed = stringSliceToMap(rawDiscordAllowed)
}
dib, err := bridge.New(&bridge.Config{
AvatarURL: avatarURL,
Discriminator: discriminator,
DiscordBotToken: discordBotToken,
GuildID: guildID,
IRCListenerName: ircUsername,
IRCServer: ircServer,
IRCServerPass: ircPassword,
IRCListenerPrejoinCommands: ircListenerPrejoinCommands,
ConnectionLimit: connectionLimit,
IRCIgnores: matchers,
IRCFilteredMessages: ircFilter,
DiscordIgnores: stringSliceToMap(rawDiscordIgnores),
DiscordAllowed: discordAllowed,
DiscordFilteredMessages: discordFilter,
PuppetUsername: puppetUsername,
WebIRCPass: webIRCPass,
NoTLS: *notls,
InsecureSkipVerify: *insecure,
Suffix: suffix,
Separator: separator,
SimpleMode: *simple,
ChannelMappings: channelMappings,
CooldownDuration: time.Second * time.Duration(cooldownDuration),
ShowJoinQuit: showJoinQuit,
MaxNickLength: maxNickLength,
Debug: *debugMode,
DebugPresence: *debugPresence,
})
if err != nil {
log.WithField("error", err).Fatalln("Go-Discord-IRC failed to initialise.")
return
}
log.Infoln("Cooldown duration for IRC puppets is", dib.Config.CooldownDuration)
// Create new signal receiver
sc := make(chan os.Signal, 1)
signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM, os.Interrupt)
// Open the bot
go func() {
err = dib.Open()
if err != nil {
// log.WithField("error", err).Fatalln("Go-Discord-IRC failed to start.")
panic(err)
}
}()
// Inform the user that things are happening!
log.Infoln("Go-Discord-IRC is now running. Press Ctrl-C to exit.")
// Start watching for live changes...
viper.WatchConfig()
viper.OnConfigChange(func(e fsnotify.Event) {
log.Println("Configuration file has changed!")
if newUsername := viper.GetString("irc_listener_name"); ircUsername != newUsername {
log.Printf("Changed irc_listener_name from '%s' to '%s'", ircUsername, newUsername)
// Listener name has changed
ircUsername = newUsername
dib.SetIRCListenerName(ircUsername)
}
ircIgnores := viper.GetStringSlice("ignored_irc_hostmasks")
dib.Config.IRCIgnores = setupHostmaskMatchers(ircIgnores)
rawIRCFilter := viper.GetStringSlice("irc_message_filter")
rawDiscordFilter := viper.GetStringSlice("discord_message_filter")
dib.Config.DiscordFilteredMessages = setupFilter(rawDiscordFilter)
dib.Config.IRCFilteredMessages = setupFilter(rawIRCFilter)
avatarURL := viper.GetString("avatar_url")
dib.Config.AvatarURL = avatarURL
if debug := viper.GetBool("debug"); *debugMode != debug {
log.Printf("Debug changed from %+v to %+v", *debugMode, debug)
*debugMode = debug
dib.SetDebugMode(debug)
SetLogDebug(debug)
}
rawDiscordIgnores := viper.GetStringSlice("ignored_discord_ids")
dib.Config.DiscordIgnores = stringSliceToMap(rawDiscordIgnores)
rawDiscordAllowed := viper.GetStringSlice("allowed_discord_ids")
if rawDiscordAllowed == nil {
dib.Config.DiscordAllowed = nil
} else {
dib.Config.DiscordAllowed = stringSliceToMap(rawDiscordAllowed)
}
chans := viper.GetStringMapString("channel_mappings")
equalChans := reflect.DeepEqual(chans, channelMappings)
if !equalChans {
log.Println("Channel mappings updated!")
if len(chans) == 0 {
log.Println("Channel mappings are missing! Not applying changes in case this was an accident.")
} else {
if err := dib.SetChannelMappings(chans); err != nil {
log.WithField("error", err).Errorln("could not set channel mappings")
} else {
channelMappings = chans
}
}
}
})
// Watch for a shutdown signal
<-sc
log.Infoln("Shutting down Go-Discord-IRC...")
// Cleanly close down the bridge.
dib.Close()
}
func stringSliceToMap(list []string) map[string]struct{} {
m := make(map[string]struct{}, len(list))
for _, v := range list {
m[v] = struct{}{}
}
return m
}
func setupHostmaskMatchers(hostmasks []string) []glob.Glob {
var matchers []glob.Glob
for _, mask := range hostmasks {
g, err := glob.Compile(mask)
if err != nil {
log.WithField("error", err).WithField("hostmask", mask).Errorln("Failed to compile hostmask ban!")
continue
}
matchers = append(matchers, g)
}
return matchers
}
func setupFilter(filters []string) []glob.Glob {
var matchers []glob.Glob
for _, filter := range filters {
g, err := glob.Compile(filter)
if err != nil {
log.WithField("error", err).WithField("filter", filter).Errorln("Failed to compile message filter!")
continue
}
matchers = append(matchers, g)
}
return matchers
}
func SetLogDebug(debug bool) {
logger := log.StandardLogger()
if debug {
logger.SetLevel(log.DebugLevel)
} else {
logger.SetLevel(log.InfoLevel)
}
}