diff --git a/bridge/bridge.go b/bridge/bridge.go index ab660fb..26f420e 100644 --- a/bridge/bridge.go +++ b/bridge/bridge.go @@ -40,6 +40,10 @@ type Config struct { IRCFilteredMessages []glob.Glob DiscordFilteredMessages []glob.Glob + // formatters + DiscordFormat map[string]string // formatting for non-PRIVMSG relays from IRC to Discord + IRCFormat string // format for messages relayed in simple mode + // NoTLS constrols whether to use TLS at all when connecting to the IRC server NoTLS bool diff --git a/bridge/irc_connection.go b/bridge/irc_connection.go index 7deb643..da289bf 100644 --- a/bridge/irc_connection.go +++ b/bridge/irc_connection.go @@ -147,12 +147,15 @@ func (i *ircConnection) OnPrivateMessage(e *irc.Event) { } d := i.manager.bridge.discord + msg := i.manager.formatDiscordMessage("PM", e, e.Message(), "") + + // if we have an empty message + if msg == "" { + return // do nothing, Discord doesn't like those + } i.introducePM(e.Nick) - msg := fmt.Sprintf( - "%s,%s - %s@%s: %s", e.Connection.Server, e.Source, - e.Nick, i.manager.bridge.Config.Discriminator, e.Message()) _, err := d.Session.ChannelMessageSend(i.pmDiscordChannel, msg) if err != nil { log.Warnln("Could not send PM", i.discord, err) diff --git a/bridge/irc_listener.go b/bridge/irc_listener.go index 329cfa9..0f573e0 100644 --- a/bridge/irc_listener.go +++ b/bridge/irc_listener.go @@ -1,7 +1,6 @@ package bridge import ( - "fmt" "strings" ircf "github.com/qaisjp/go-discord-irc/irc/format" @@ -67,12 +66,17 @@ func (i *ircListener) OnNickRelayToDiscord(event *irc.Event) { return } - oldNick := event.Nick newNick := event.Message() + message := i.bridge.ircManager.formatDiscordMessage("NICK", event, newNick, "") + + // if the message is empty... + if message == "" { + return // do nothing, Discord doesn't like empty messages anyway + } msg := IRCMessage{ Username: "", - Message: fmt.Sprintf("_%s changed their nick to %s_", oldNick, newNick), + Message: message, } for _, m := range i.bridge.mappings { @@ -136,29 +140,33 @@ func (i *ircListener) OnJoinQuitCallback(event *irc.Event) { return } - who := event.Nick - message := event.Nick - id := " (" + event.User + "@" + event.Host + ") " + message := "" + content := "" + target := "" + manager := i.bridge.ircManager switch event.Code { case "STJOIN": - message += " joined" + id + message = manager.formatDiscordMessage("JOIN", event, "", "") case "STPART": - message += " left" + id if len(event.Arguments) > 1 { - message += ": " + event.Arguments[1] + content = event.Arguments[1] } + message = manager.formatDiscordMessage("PART", event, content, "") case "STQUIT": - message += " quit" + id - - reason := event.Nick + content := event.Nick if len(event.Arguments) == 1 { - reason = event.Arguments[0] + content = event.Arguments[0] } - message += "Quit: " + reason + message = manager.formatDiscordMessage("QUIT", event, content, "") case "KICK": - who = event.Arguments[1] - message = event.Arguments[1] + " was kicked by " + event.Nick + ": " + event.Arguments[2] + target, content = event.Arguments[1], event.Arguments[2] + message = manager.formatDiscordMessage("KICK", event, content, target) + } + + // if the message is empty... + if message == "" { + return // do nothing, Discord doesn't like empty messages anyway } msg := IRCMessage{ @@ -173,10 +181,10 @@ func (i *ircListener) OnJoinQuitCallback(event *irc.Event) { channel := m.IRCChannel channelObj, ok := i.Connection.GetChannel(channel) if !ok { - log.WithField("channel", channel).WithField("who", who).Warnln("Trying to process QUIT. Channel not found in irc listener cache.") + log.WithField("channel", channel).WithField("who", event.Nick).Warnln("Trying to process QUIT. Channel not found in irc listener cache.") continue } - if _, ok := channelObj.GetUser(who); !ok { + if _, ok := channelObj.GetUser(event.Nick); !ok { continue } msg.IRCChannel = channel diff --git a/bridge/irc_manager.go b/bridge/irc_manager.go index 6d5b5a1..358117e 100644 --- a/bridge/irc_manager.go +++ b/bridge/irc_manager.go @@ -369,6 +369,18 @@ func (m *IRCManager) generateNickname(discord DiscordUser) string { return newNick } +func (m *IRCManager) formatIRCMessage(message *DiscordMessage, content string) string { + msg := m.bridge.Config.IRCFormat + length := len(message.Author.Username) + msg = strings.ReplaceAll(msg, "${USER}", message.Author.Username[:1]+"\u200B"+message.Author.Username[1:length]) + msg = strings.ReplaceAll(msg, "${DISCRIMINATOR}", message.Author.Discriminator) + msg = strings.ReplaceAll(msg, "${CONTENT}", content) + + // we don't do trimming and later checks here, IRC doesn't mind blank messages or at least doesn't complain + // as loudly as Discord + return msg +} + // SendMessage sends a broken down Discord Message to a particular IRC channel. func (m *IRCManager) SendMessage(channel string, msg *DiscordMessage) { if m.ircIgnoredDiscord(msg.Author.ID) { @@ -383,14 +395,8 @@ func (m *IRCManager) SendMessage(channel string, msg *DiscordMessage) { // Person is appearing offline (or the bridge is running in Simple Mode) if !ok { - length := len(msg.Author.Username) for _, line := range strings.Split(content, "\n") { - m.bridge.ircListener.Privmsg(channel, fmt.Sprintf( - "<%s#%s> %s", - msg.Author.Username[:1]+"\u200B"+msg.Author.Username[1:length], - msg.Author.Discriminator, - line, - )) + m.bridge.ircListener.Privmsg(channel, m.formatIRCMessage(msg, line)) } return } @@ -428,6 +434,22 @@ func (m *IRCManager) SendMessage(channel string, msg *DiscordMessage) { } } +func (m *IRCManager) formatDiscordMessage(msgFormat string, e *irc.Event, content string, target string) string { + msg := "" + if format, ok := m.bridge.Config.DiscordFormat[strings.ToLower(msgFormat)]; ok && format != "" { + msg = format + msg = strings.ReplaceAll(msg, "${NICK}", e.Nick) + msg = strings.ReplaceAll(msg, "${IDENT}", e.User) + msg = strings.ReplaceAll(msg, "${HOST}", e.Host) + msg = strings.ReplaceAll(msg, "${CONTENT}", content) + msg = strings.ReplaceAll(msg, "${TARGET}", target) + msg = strings.ReplaceAll(msg, "${SERVER}", e.Connection.Server) + msg = strings.ReplaceAll(msg, "${DISCRIMINATOR}", m.bridge.Config.Discriminator) + } + + return strings.Trim(msg, " ") +} + // RequestChannels finds all the Discord channels this user belongs to, // and then find pairings in the global pairings list // Currently just returns all participating IRC channels diff --git a/main.go b/main.go index a89eda8..48fa014 100644 --- a/main.go +++ b/main.go @@ -2,6 +2,7 @@ package main import ( "flag" + "fmt" "os" "os/signal" "path/filepath" @@ -98,10 +99,21 @@ func main() { // viper.SetDefault("irc_puppet_prejoin_commands", []string{"MODE ${NICK} +D"}) ircPuppetPrejoinCommands := viper.GetStringSlice("irc_puppet_prejoin_commands") // Commands for each connection to send before joining channels + rawDiscordFormat := viper.GetStringMapString("discord_format") + var discordFormat map[string]string + if df, err := setupDiscordFormat(rawDiscordFormat); err == nil { + discordFormat = df + } else { + log.WithError(err).Fatal("discord_format setting is invalid") + return + } // viper.SetDefault("avatar_url", "https://robohash.org/${USERNAME}.png?set=set4") avatarURL := viper.GetString("avatar_url") // + viper.SetDefault("irc_format", "<${USER}#${DISCRIMINATOR}> ${CONTENT}") + ircFormat := viper.GetString("irc_format") + // 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 @@ -146,7 +158,9 @@ func main() { AvatarURL: avatarURL, Discriminator: discriminator, DiscordBotToken: discordBotToken, + DiscordFormat: discordFormat, GuildID: guildID, + IRCFormat: ircFormat, IRCListenerName: ircUsername, IRCServer: ircServer, IRCServerPass: ircPassword, @@ -230,6 +244,14 @@ func main() { } dib.Config.DiscordIgnores = discordIgnores + rawDiscordFormat := viper.GetStringMapString("discord_format") + if discordFormat, err := setupDiscordFormat(rawDiscordFormat); err == nil { + dib.Config.DiscordFormat = discordFormat + } else { + log.WithError(err).Error("discord_format setting is invalid, this setting has not been updated") + } + dib.Config.IRCFormat = viper.GetString("irc_format") + chans := viper.GetStringMapString("channel_mappings") equalChans := reflect.DeepEqual(chans, channelMappings) if !equalChans { @@ -285,6 +307,34 @@ func setupFilter(filters []string) []glob.Glob { return matchers } +func setupDiscordFormat(discordFormat map[string]string) (map[string]string, error) { + var err error + // lowercase to match that YAML lowercases it + discordFormatDefaults := map[string]string{ + "pm": "${SERVER},${NICK}!${IDENT}@${HOST} - ${NICK}@${DISCRIMINATOR}: ${CONTENT}", + "join": "_${NICK} joined (${IDENT}@${HOST})_", + "part": "_${NICK} left (${IDENT}@${HOST}) - ${CONTENT}_", + "quit": "_${NICK} quit (${IDENT}@${HOST}) - Quit: ${CONTENT}_", + "kick": "_${TARGET} was kicked by ${NICK} - ${CONTENT}_", + "nick": "_${NICK} changed nick to ${CONTENT}_", + } + + for ev, format := range discordFormatDefaults { + if df, ok := discordFormat[ev]; !ok || df == "" { + discordFormat[ev] = format + } + } + + for ev := range discordFormat { + if _, ok := discordFormatDefaults[ev]; !ok { + err = fmt.Errorf("Unknown format key %s", ev) + break + } + } + + return discordFormat, err +} + func SetLogDebug(debug bool) { logger := log.StandardLogger() if debug {