Skip to content

Commit

Permalink
Initial support for message formatting
Browse files Browse the repository at this point in the history
Allows for the user to define how messages are formatted back and forth
between Discord and IRC.

Co-authored-by: Qais Patankar <[email protected]>
  • Loading branch information
llmII and qaisjp committed Feb 28, 2021
1 parent e6d8d5e commit a84c3fe
Show file tree
Hide file tree
Showing 5 changed files with 163 additions and 28 deletions.
3 changes: 3 additions & 0 deletions bridge/bridge.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ type Config struct {
IRCPuppetPrejoinCommands []string
IRCListenerPrejoinCommands []string

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

Expand Down
9 changes: 6 additions & 3 deletions bridge/irc_connection.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,12 +124,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.ChannelMessageSend(i.pmDiscordChannel, msg)
if err != nil {
log.Warnln("Could not send PM", i.discord, err)
Expand Down
93 changes: 75 additions & 18 deletions bridge/irc_listener.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,12 @@ type ircListener struct {
bridge *Bridge

joinQuitCallbacks map[string]int
relayNickTrackID int
}

func newIRCListener(dib *Bridge, webIRCPass string) *ircListener {
irccon := irc.IRC(dib.Config.IRCListenerName, "discord")
listener := &ircListener{irccon, dib, nil}
listener := &ircListener{irccon, dib, nil, 0}

dib.SetupIRCConnection(irccon, "discord.", "fd75:f5f5:226f::")
listener.SetDebugMode(dib.Config.Debug)
Expand All @@ -39,6 +40,8 @@ func newIRCListener(dib *Bridge, webIRCPass string) *ircListener {
listener.JoinChannels()
})

listener.AddCallback("NICK", listener.nickTrackNick)

// Note that this might override SetupNickTrack!
listener.OnJoinQuitSettingChange()

Expand Down Expand Up @@ -66,6 +69,51 @@ func (i *ircListener) nickTrackNick(event *irc.Event) {
}
}

func userOnChannelFix(user string, channel irc.Channel) bool {
if _, ok := channel.Users[user]; ok {
return true
}

// work around nicks being prefixed with mode characters for some reason
for _, c := range "!$~&@%+" {
if _, ok := channel.Users[string(c)+user]; ok {
return true
}
}

return false
}

func (i *ircListener) OnNickRelayToDiscord(event *irc.Event) {
newNick := event.Message()
message := i.bridge.ircManager.formatDiscordMessage(event.Code, event, newNick, "")

// if the message is empty...
if message == "" {
return // do nothing, Discord doesn't like empty messages anyway
}

msg := IRCMessage{
Username: "",
Message: message,
}

for _, m := range i.bridge.mappings {
channel := m.IRCChannel
channelObj, ok := i.Connection.Channels[channel]
if !ok {
continue
}

if !userOnChannelFix(newNick, channelObj) {
continue
}

msg.IRCChannel = channel
i.bridge.discordMessagesChan <- msg
}
}

// From irc_nicktrack.go.
func (i *ircListener) nickTrackQuit(e *irc.Event) {
for k := range i.Connection.Channels {
Expand All @@ -76,8 +124,11 @@ func (i *ircListener) nickTrackQuit(e *irc.Event) {
func (i *ircListener) OnJoinQuitSettingChange() {
// Clear Nicktrack QUIT callback as it races with this
i.ClearCallback("QUIT")
i.ClearCallback("NICK")
i.AddCallback("NICK", i.nickTrackNick)

if i.relayNickTrackID != 0 {
i.RemoveCallback("NICK", i.relayNickTrackID)
i.relayNickTrackID = 0
}

// If remove callbacks...
if !i.bridge.Config.ShowJoinQuit {
Expand All @@ -90,6 +141,8 @@ func (i *ircListener) OnJoinQuitSettingChange() {
return
}

i.relayNickTrackID = i.AddCallback("NICK", i.OnNickRelayToDiscord)

callbacks := []string{"JOIN", "PART", "QUIT", "KICK"}
cbs := make(map[string]int, len(callbacks))
for _, cb := range callbacks {
Expand All @@ -112,29 +165,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 "JOIN":
message += " joined" + id
message = manager.formatDiscordMessage(event.Code, event, "", "")
case "PART":
message += " left" + id
if len(event.Arguments) > 1 {
message += ": " + event.Arguments[1]
content = event.Arguments[1]
}
message = manager.formatDiscordMessage(event.Code, event, content, "")
case "QUIT":
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(event.Code, 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(event.Code, event, content, target)
}

// if the message is empty...
if message == "" {
return // do nothing, Discord doesn't like empty messages anyway
}

msg := IRCMessage{
Expand All @@ -149,10 +206,10 @@ func (i *ircListener) OnJoinQuitCallback(event *irc.Event) {
channel := m.IRCChannel
channelObj, ok := i.Connection.Channels[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.Users[who]; !ok {
if _, ok := channelObj.Users[event.Nick]; !ok {
continue
}
msg.IRCChannel = channel
Expand Down
36 changes: 29 additions & 7 deletions bridge/irc_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,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) {
Expand All @@ -351,14 +363,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
}
Expand Down Expand Up @@ -392,6 +398,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
Expand Down
50 changes: 50 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package main

import (
"flag"
"fmt"
"os"
"os/signal"
"path/filepath"
Expand Down Expand Up @@ -92,10 +93,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://ui-avatars.com/api/?name=${USERNAME}")
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
Expand Down Expand Up @@ -140,7 +152,9 @@ func main() {
AvatarURL: avatarURL,
Discriminator: discriminator,
DiscordBotToken: discordBotToken,
DiscordFormat: discordFormat,
GuildID: guildID,
IRCFormat: ircFormat,
IRCListenerName: ircUsername,
IRCServer: ircServer,
IRCServerPass: ircPassword,
Expand Down Expand Up @@ -219,6 +233,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 {
Expand Down Expand Up @@ -259,6 +281,34 @@ func setupHostmaskMatchers(hostmasks []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 {
Expand Down

0 comments on commit a84c3fe

Please sign in to comment.