From 57fbd3c723d293b4181c166fe03d0dc9ea4d4e35 Mon Sep 17 00:00:00 2001 From: Wim Date: Wed, 28 Nov 2018 10:58:56 +0100 Subject: [PATCH] Refactor irc handlers. Fix linting (#611) --- bridge/irc/handlers.go | 238 +++++++++++++++++++++++++++++ bridge/irc/irc.go | 329 +++++++++-------------------------------- 2 files changed, 306 insertions(+), 261 deletions(-) create mode 100644 bridge/irc/handlers.go diff --git a/bridge/irc/handlers.go b/bridge/irc/handlers.go new file mode 100644 index 00000000..41d69b0a --- /dev/null +++ b/bridge/irc/handlers.go @@ -0,0 +1,238 @@ +package birc + +import ( + "bytes" + "fmt" + "io" + "io/ioutil" + "regexp" + "strconv" + "strings" + "time" + + "github.com/42wim/matterbridge/bridge/config" + "github.com/42wim/matterbridge/bridge/helper" + "github.com/dfordsoft/golib/ic" + "github.com/lrstanley/girc" + "github.com/paulrosania/go-charset/charset" + "github.com/saintfish/chardet" + + // We need to import the 'data' package as an implicit dependency. + // See: https://godoc.org/github.com/paulrosania/go-charset/charset + _ "github.com/paulrosania/go-charset/data" +) + +func (b *Birc) handleCharset(msg *config.Message) error { + if b.GetString("Charset") != "" { + switch b.GetString("Charset") { + case "gbk", "gb18030", "gb2312", "big5", "euc-kr", "euc-jp", "shift-jis", "iso-2022-jp": + msg.Text = ic.ConvertString("utf-8", b.GetString("Charset"), msg.Text) + default: + buf := new(bytes.Buffer) + w, err := charset.NewWriter(b.GetString("Charset"), buf) + if err != nil { + b.Log.Errorf("charset from utf-8 conversion failed: %s", err) + return err + } + fmt.Fprint(w, msg.Text) + w.Close() + msg.Text = buf.String() + } + } + return nil +} + +// handleFiles returns true if we have handled the files, otherwise return false +func (b *Birc) handleFiles(msg *config.Message) bool { + if msg.Extra == nil { + return false + } + for _, rmsg := range helper.HandleExtra(msg, b.General) { + b.Local <- rmsg + } + if len(msg.Extra["file"]) == 0 { + return false + } + for _, f := range msg.Extra["file"] { + fi := f.(config.FileInfo) + if fi.Comment != "" { + msg.Text += fi.Comment + ": " + } + if fi.URL != "" { + msg.Text = fi.URL + if fi.Comment != "" { + msg.Text = fi.Comment + ": " + fi.URL + } + } + b.Local <- config.Message{Text: msg.Text, Username: msg.Username, Channel: msg.Channel, Event: msg.Event} + } + return true +} + +func (b *Birc) handleJoinPart(client *girc.Client, event girc.Event) { + if len(event.Params) == 0 { + b.Log.Debugf("handleJoinPart: empty Params? %#v", event) + return + } + channel := strings.ToLower(event.Params[0]) + if event.Command == "KICK" && event.Params[1] == b.Nick { + b.Log.Infof("Got kicked from %s by %s", channel, event.Source.Name) + time.Sleep(time.Duration(b.GetInt("RejoinDelay")) * time.Second) + b.Remote <- config.Message{Username: "system", Text: "rejoin", Channel: channel, Account: b.Account, Event: config.EventRejoinChannels} + return + } + if event.Command == "QUIT" { + if event.Source.Name == b.Nick && strings.Contains(event.Trailing, "Ping timeout") { + b.Log.Infof("%s reconnecting ..", b.Account) + b.Remote <- config.Message{Username: "system", Text: "reconnect", Channel: channel, Account: b.Account, Event: config.EventFailure} + return + } + } + if event.Source.Name != b.Nick { + if b.GetBool("nosendjoinpart") { + return + } + b.Log.Debugf("<= Sending JOIN_LEAVE event from %s to gateway", b.Account) + msg := config.Message{Username: "system", Text: event.Source.Name + " " + strings.ToLower(event.Command) + "s", Channel: channel, Account: b.Account, Event: config.EventJoinLeave} + b.Log.Debugf("<= Message is %#v", msg) + b.Remote <- msg + return + } + b.Log.Debugf("handle %#v", event) +} + +func (b *Birc) handleNewConnection(client *girc.Client, event girc.Event) { + b.Log.Debug("Registering callbacks") + i := b.i + b.Nick = event.Params[0] + + i.Handlers.Add("PRIVMSG", b.handlePrivMsg) + i.Handlers.Add("CTCP_ACTION", b.handlePrivMsg) + i.Handlers.Add(girc.RPL_TOPICWHOTIME, b.handleTopicWhoTime) + i.Handlers.Add(girc.NOTICE, b.handleNotice) + i.Handlers.Add("JOIN", b.handleJoinPart) + i.Handlers.Add("PART", b.handleJoinPart) + i.Handlers.Add("QUIT", b.handleJoinPart) + i.Handlers.Add("KICK", b.handleJoinPart) +} + +func (b *Birc) handleNickServ() { + if !b.GetBool("UseSASL") && b.GetString("NickServNick") != "" && b.GetString("NickServPassword") != "" { + b.Log.Debugf("Sending identify to nickserv %s", b.GetString("NickServNick")) + b.i.Cmd.Message(b.GetString("NickServNick"), "IDENTIFY "+b.GetString("NickServPassword")) + } + if strings.EqualFold(b.GetString("NickServNick"), "Q@CServe.quakenet.org") { + b.Log.Debugf("Authenticating %s against %s", b.GetString("NickServUsername"), b.GetString("NickServNick")) + b.i.Cmd.Message(b.GetString("NickServNick"), "AUTH "+b.GetString("NickServUsername")+" "+b.GetString("NickServPassword")) + } + // give nickserv some slack + time.Sleep(time.Second * 5) + b.authDone = true +} + +func (b *Birc) handleNotice(client *girc.Client, event girc.Event) { + if strings.Contains(event.String(), "This nickname is registered") && event.Source.Name == b.GetString("NickServNick") { + b.handleNickServ() + } else { + b.handlePrivMsg(client, event) + } +} + +func (b *Birc) handleOther(client *girc.Client, event girc.Event) { + if b.GetInt("DebugLevel") == 1 { + if event.Command != "CLIENT_STATE_UPDATED" && + event.Command != "CLIENT_GENERAL_UPDATED" { + b.Log.Debugf("%#v", event.String()) + } + return + } + switch event.Command { + case "372", "375", "376", "250", "251", "252", "253", "254", "255", "265", "266", "002", "003", "004", "005": + return + } + b.Log.Debugf("%#v", event.String()) +} + +func (b *Birc) handleOtherAuth(client *girc.Client, event girc.Event) { + b.handleNickServ() + b.handleRunCommands() + // we are now fully connected + b.connected <- nil +} + +func (b *Birc) handlePrivMsg(client *girc.Client, event girc.Event) { + if b.skipPrivMsg(event) { + return + } + rmsg := config.Message{Username: event.Source.Name, Channel: strings.ToLower(event.Params[0]), Account: b.Account, UserID: event.Source.Ident + "@" + event.Source.Host} + b.Log.Debugf("== Receiving PRIVMSG: %s %s %#v", event.Source.Name, event.Trailing, event) + + // set action event + if event.IsAction() { + rmsg.Event = config.EventUserAction + } + + // strip action, we made an event if it was an action + rmsg.Text += event.StripAction() + + // strip IRC colors + re := regexp.MustCompile(`\x03(?:\d{1,2}(?:,\d{1,2})?)?|[[:cntrl:]]`) + rmsg.Text = re.ReplaceAllString(rmsg.Text, "") + + // start detecting the charset + var r io.Reader + var err error + mycharset := b.GetString("Charset") + if mycharset == "" { + // detect what were sending so that we convert it to utf-8 + detector := chardet.NewTextDetector() + result, err := detector.DetectBest([]byte(rmsg.Text)) + if err != nil { + b.Log.Infof("detection failed for rmsg.Text: %#v", rmsg.Text) + return + } + b.Log.Debugf("detected %s confidence %#v", result.Charset, result.Confidence) + mycharset = result.Charset + // if we're not sure, just pick ISO-8859-1 + if result.Confidence < 80 { + mycharset = "ISO-8859-1" + } + } + switch mycharset { + case "gbk", "gb18030", "gb2312", "big5", "euc-kr", "euc-jp", "shift-jis", "iso-2022-jp": + rmsg.Text = ic.ConvertString("utf-8", b.GetString("Charset"), rmsg.Text) + default: + r, err = charset.NewReader(mycharset, strings.NewReader(rmsg.Text)) + if err != nil { + b.Log.Errorf("charset to utf-8 conversion failed: %s", err) + return + } + output, _ := ioutil.ReadAll(r) + rmsg.Text = string(output) + } + + b.Log.Debugf("<= Sending message from %s on %s to gateway", event.Params[0], b.Account) + b.Remote <- rmsg +} + +func (b *Birc) handleRunCommands() { + for _, cmd := range b.GetStringSlice("RunCommands") { + if err := b.i.Cmd.SendRaw(cmd); err != nil { + b.Log.Errorf("RunCommands %s failed: %s", cmd, err) + } + time.Sleep(time.Second) + } +} + +func (b *Birc) handleTopicWhoTime(client *girc.Client, event girc.Event) { + parts := strings.Split(event.Params[2], "!") + t, err := strconv.ParseInt(event.Params[3], 10, 64) + if err != nil { + b.Log.Errorf("Invalid time stamp: %s", event.Params[3]) + } + user := parts[0] + if len(parts) > 1 { + user += " [" + parts[1] + "]" + } + b.Log.Debugf("%s: Topic set by %s [%s]", event.Command, user, time.Unix(t, 0)) +} diff --git a/bridge/irc/irc.go b/bridge/irc/irc.go index 1e813246..721aba64 100644 --- a/bridge/irc/irc.go +++ b/bridge/irc/irc.go @@ -1,14 +1,10 @@ package birc import ( - "bytes" "crypto/tls" "fmt" "hash/crc32" - "io" - "io/ioutil" "net" - "regexp" "sort" "strconv" "strings" @@ -17,10 +13,7 @@ import ( "github.com/42wim/matterbridge/bridge" "github.com/42wim/matterbridge/bridge/config" "github.com/42wim/matterbridge/bridge/helper" - "github.com/dfordsoft/golib/ic" "github.com/lrstanley/girc" - "github.com/paulrosania/go-charset/charset" - "github.com/saintfish/chardet" // We need to import the 'data' package as an implicit dependency. // See: https://godoc.org/github.com/paulrosania/go-charset/charset @@ -68,7 +61,7 @@ func (b *Birc) Command(msg *config.Message) string { if msg.Text == "!users" { b.i.Handlers.Add(girc.RPL_NAMREPLY, b.storeNames) b.i.Handlers.Add(girc.RPL_ENDOFNAMES, b.endNames) - b.i.Cmd.SendRaw("NAMES " + msg.Channel) + b.i.Cmd.SendRaw("NAMES " + msg.Channel) //nolint:errcheck } return "" } @@ -76,35 +69,11 @@ func (b *Birc) Command(msg *config.Message) string { func (b *Birc) Connect() error { b.Local = make(chan config.Message, b.MessageQueue+10) b.Log.Infof("Connecting %s", b.GetString("Server")) - server, portstr, err := net.SplitHostPort(b.GetString("Server")) - if err != nil { - return err - } - port, err := strconv.Atoi(portstr) - if err != nil { - return err - } - // fix strict user handling of girc - user := b.GetString("Nick") - for !girc.IsValidUser(user) { - if len(user) == 1 { - user = "matterbridge" - break - } - user = user[1:] - } - i := girc.New(girc.Config{ - Server: server, - ServerPass: b.GetString("Password"), - Port: port, - Nick: b.GetString("Nick"), - User: user, - Name: b.GetString("Nick"), - SSL: b.GetBool("UseTLS"), - TLSConfig: &tls.Config{InsecureSkipVerify: b.GetBool("SkipTLSVerify"), ServerName: server}, - PingDelay: time.Minute, - }) + i, err := b.getClient() + if err != nil { + return err + } if b.GetBool("UseSASL") { i.Config.SASL = &girc.SASLPlain{ @@ -117,27 +86,8 @@ func (b *Birc) Connect() error { i.Handlers.Add(girc.RPL_ENDOFMOTD, b.handleOtherAuth) i.Handlers.Add(girc.ALL_EVENTS, b.handleOther) - go func() { - for { - if err := i.Connect(); err != nil { - b.Log.Errorf("disconnect: error: %s", err) - if b.FirstConnection { - b.connected <- err - return - } - } else { - b.Log.Info("disconnect: client requested quit") - } - b.Log.Info("reconnecting in 30 seconds...") - time.Sleep(30 * time.Second) - i.Handlers.Clear(girc.RPL_WELCOME) - i.Handlers.Add(girc.RPL_WELCOME, func(client *girc.Client, event girc.Event) { - b.Remote <- config.Message{Username: "system", Text: "rejoin", Channel: "", Account: b.Account, Event: config.EventRejoinChannels} - // set our correct nick on reconnect if necessary - b.Nick = event.Source.Name - }) - } - }() + go b.doConnect() + b.i = i err = <-b.connected if err != nil { @@ -195,44 +145,13 @@ func (b *Birc) Send(msg config.Message) (string, error) { } // convert to specified charset - if b.GetString("Charset") != "" { - switch b.GetString("Charset") { - case "gbk", "gb18030", "gb2312", "big5", "euc-kr", "euc-jp", "shift-jis", "iso-2022-jp": - msg.Text = ic.ConvertString("utf-8", b.GetString("Charset"), msg.Text) - default: - buf := new(bytes.Buffer) - w, err := charset.NewWriter(b.GetString("Charset"), buf) - if err != nil { - b.Log.Errorf("charset from utf-8 conversion failed: %s", err) - return "", err - } - fmt.Fprint(w, msg.Text) - w.Close() - msg.Text = buf.String() - } + if err := b.handleCharset(&msg); err != nil { + return "", err } - // Handle files - if msg.Extra != nil { - for _, rmsg := range helper.HandleExtra(&msg, b.General) { - b.Local <- rmsg - } - if len(msg.Extra["file"]) > 0 { - for _, f := range msg.Extra["file"] { - fi := f.(config.FileInfo) - if fi.Comment != "" { - msg.Text += fi.Comment + ": " - } - if fi.URL != "" { - msg.Text = fi.URL - if fi.Comment != "" { - msg.Text = fi.Comment + ": " + fi.URL - } - } - b.Local <- config.Message{Text: msg.Text, Username: msg.Username, Channel: msg.Channel, Event: msg.Event} - } - return "", nil - } + // handle files, return if we're done here + if ok := b.handleFiles(&msg); ok { + return "", nil } var msgLines []string @@ -257,6 +176,28 @@ func (b *Birc) Send(msg config.Message) (string, error) { return "", nil } +func (b *Birc) doConnect() { + for { + if err := b.i.Connect(); err != nil { + b.Log.Errorf("disconnect: error: %s", err) + if b.FirstConnection { + b.connected <- err + return + } + } else { + b.Log.Info("disconnect: client requested quit") + } + b.Log.Info("reconnecting in 30 seconds...") + time.Sleep(30 * time.Second) + b.i.Handlers.Clear(girc.RPL_WELCOME) + b.i.Handlers.Add(girc.RPL_WELCOME, func(client *girc.Client, event girc.Event) { + b.Remote <- config.Message{Username: "system", Text: "rejoin", Channel: "", Account: b.Account, Event: config.EventRejoinChannels} + // set our correct nick on reconnect if necessary + b.Nick = event.Source.Name + }) + } +} + func (b *Birc) doSend() { rate := time.Millisecond * time.Duration(b.MessageDelay) throttle := time.NewTicker(rate) @@ -277,6 +218,40 @@ func (b *Birc) doSend() { } } +// validateInput validates the server/port/nick configuration. Returns a *girc.Client if successful +func (b *Birc) getClient() (*girc.Client, error) { + server, portstr, err := net.SplitHostPort(b.GetString("Server")) + if err != nil { + return nil, err + } + port, err := strconv.Atoi(portstr) + if err != nil { + return nil, err + } + // fix strict user handling of girc + user := b.GetString("Nick") + for !girc.IsValidUser(user) { + if len(user) == 1 { + user = "matterbridge" + break + } + user = user[1:] + } + + i := girc.New(girc.Config{ + Server: server, + ServerPass: b.GetString("Password"), + Port: port, + Nick: b.GetString("Nick"), + User: user, + Name: b.GetString("Nick"), + SSL: b.GetBool("UseTLS"), + TLSConfig: &tls.Config{InsecureSkipVerify: b.GetBool("SkipTLSVerify"), ServerName: server}, //nolint:gosec + PingDelay: time.Minute, + }) + return i, nil +} + func (b *Birc) endNames(client *girc.Client, event girc.Event) { channel := event.Params[1] sort.Strings(b.names[channel]) @@ -293,83 +268,6 @@ func (b *Birc) endNames(client *girc.Client, event girc.Event) { b.i.Handlers.Clear(girc.RPL_ENDOFNAMES) } -func (b *Birc) handleNewConnection(client *girc.Client, event girc.Event) { - b.Log.Debug("Registering callbacks") - i := b.i - b.Nick = event.Params[0] - - i.Handlers.Add("PRIVMSG", b.handlePrivMsg) - i.Handlers.Add("CTCP_ACTION", b.handlePrivMsg) - i.Handlers.Add(girc.RPL_TOPICWHOTIME, b.handleTopicWhoTime) - i.Handlers.Add(girc.NOTICE, b.handleNotice) - i.Handlers.Add("JOIN", b.handleJoinPart) - i.Handlers.Add("PART", b.handleJoinPart) - i.Handlers.Add("QUIT", b.handleJoinPart) - i.Handlers.Add("KICK", b.handleJoinPart) -} - -func (b *Birc) handleJoinPart(client *girc.Client, event girc.Event) { - if len(event.Params) == 0 { - b.Log.Debugf("handleJoinPart: empty Params? %#v", event) - return - } - channel := strings.ToLower(event.Params[0]) - if event.Command == "KICK" && event.Params[1] == b.Nick { - b.Log.Infof("Got kicked from %s by %s", channel, event.Source.Name) - time.Sleep(time.Duration(b.GetInt("RejoinDelay")) * time.Second) - b.Remote <- config.Message{Username: "system", Text: "rejoin", Channel: channel, Account: b.Account, Event: config.EventRejoinChannels} - return - } - if event.Command == "QUIT" { - if event.Source.Name == b.Nick && strings.Contains(event.Trailing, "Ping timeout") { - b.Log.Infof("%s reconnecting ..", b.Account) - b.Remote <- config.Message{Username: "system", Text: "reconnect", Channel: channel, Account: b.Account, Event: config.EventFailure} - return - } - } - if event.Source.Name != b.Nick { - if b.GetBool("nosendjoinpart") { - return - } - b.Log.Debugf("<= Sending JOIN_LEAVE event from %s to gateway", b.Account) - msg := config.Message{Username: "system", Text: event.Source.Name + " " + strings.ToLower(event.Command) + "s", Channel: channel, Account: b.Account, Event: config.EventJoinLeave} - b.Log.Debugf("<= Message is %#v", msg) - b.Remote <- msg - return - } - b.Log.Debugf("handle %#v", event) -} - -func (b *Birc) handleNotice(client *girc.Client, event girc.Event) { - if strings.Contains(event.String(), "This nickname is registered") && event.Source.Name == b.GetString("NickServNick") { - b.handleNickServ() - } else { - b.handlePrivMsg(client, event) - } -} - -func (b *Birc) handleOther(client *girc.Client, event girc.Event) { - if b.GetInt("DebugLevel") == 1 { - if event.Command != "CLIENT_STATE_UPDATED" && - event.Command != "CLIENT_GENERAL_UPDATED" { - b.Log.Debugf("%#v", event.String()) - } - return - } - switch event.Command { - case "372", "375", "376", "250", "251", "252", "253", "254", "255", "265", "266", "002", "003", "004", "005": - return - } - b.Log.Debugf("%#v", event.String()) -} - -func (b *Birc) handleOtherAuth(client *girc.Client, event girc.Event) { - b.handleNickServ() - b.handleRunCommands() - // we are now fully connected - b.connected <- nil -} - func (b *Birc) skipPrivMsg(event girc.Event) bool { // Our nick can be changed b.Nick = b.i.GetNick() @@ -389,74 +287,6 @@ func (b *Birc) skipPrivMsg(event girc.Event) bool { return false } -func (b *Birc) handlePrivMsg(client *girc.Client, event girc.Event) { - if b.skipPrivMsg(event) { - return - } - rmsg := config.Message{Username: event.Source.Name, Channel: strings.ToLower(event.Params[0]), Account: b.Account, UserID: event.Source.Ident + "@" + event.Source.Host} - b.Log.Debugf("== Receiving PRIVMSG: %s %s %#v", event.Source.Name, event.Trailing, event) - - // set action event - if event.IsAction() { - rmsg.Event = config.EventUserAction - } - - // strip action, we made an event if it was an action - rmsg.Text += event.StripAction() - - // strip IRC colors - re := regexp.MustCompile(`\x03(?:\d{1,2}(?:,\d{1,2})?)?|[[:cntrl:]]`) - rmsg.Text = re.ReplaceAllString(rmsg.Text, "") - - // start detecting the charset - var r io.Reader - var err error - mycharset := b.GetString("Charset") - if mycharset == "" { - // detect what were sending so that we convert it to utf-8 - detector := chardet.NewTextDetector() - result, err := detector.DetectBest([]byte(rmsg.Text)) - if err != nil { - b.Log.Infof("detection failed for rmsg.Text: %#v", rmsg.Text) - return - } - b.Log.Debugf("detected %s confidence %#v", result.Charset, result.Confidence) - mycharset = result.Charset - // if we're not sure, just pick ISO-8859-1 - if result.Confidence < 80 { - mycharset = "ISO-8859-1" - } - } - switch mycharset { - case "gbk", "gb18030", "gb2312", "big5", "euc-kr", "euc-jp", "shift-jis", "iso-2022-jp": - rmsg.Text = ic.ConvertString("utf-8", b.GetString("Charset"), rmsg.Text) - default: - r, err = charset.NewReader(mycharset, strings.NewReader(rmsg.Text)) - if err != nil { - b.Log.Errorf("charset to utf-8 conversion failed: %s", err) - return - } - output, _ := ioutil.ReadAll(r) - rmsg.Text = string(output) - } - - b.Log.Debugf("<= Sending message from %s on %s to gateway", event.Params[0], b.Account) - b.Remote <- rmsg -} - -func (b *Birc) handleTopicWhoTime(client *girc.Client, event girc.Event) { - parts := strings.Split(event.Params[2], "!") - t, err := strconv.ParseInt(event.Params[3], 10, 64) - if err != nil { - b.Log.Errorf("Invalid time stamp: %s", event.Params[3]) - } - user := parts[0] - if len(parts) > 1 { - user += " [" + parts[1] + "]" - } - b.Log.Debugf("%s: Topic set by %s [%s]", event.Command, user, time.Unix(t, 0)) -} - func (b *Birc) nicksPerRow() int { return 4 } @@ -471,26 +301,3 @@ func (b *Birc) storeNames(client *girc.Client, event girc.Event) { func (b *Birc) formatnicks(nicks []string) string { return strings.Join(nicks, ", ") + " currently on IRC" } - -func (b *Birc) handleRunCommands() { - for _, cmd := range b.GetStringSlice("RunCommands") { - if err := b.i.Cmd.SendRaw(cmd); err != nil { - b.Log.Errorf("RunCommands %s failed: %s", cmd, err) - } - time.Sleep(time.Second) - } -} - -func (b *Birc) handleNickServ() { - if !b.GetBool("UseSASL") && b.GetString("NickServNick") != "" && b.GetString("NickServPassword") != "" { - b.Log.Debugf("Sending identify to nickserv %s", b.GetString("NickServNick")) - b.i.Cmd.Message(b.GetString("NickServNick"), "IDENTIFY "+b.GetString("NickServPassword")) - } - if strings.EqualFold(b.GetString("NickServNick"), "Q@CServe.quakenet.org") { - b.Log.Debugf("Authenticating %s against %s", b.GetString("NickServUsername"), b.GetString("NickServNick")) - b.i.Cmd.Message(b.GetString("NickServNick"), "AUTH "+b.GetString("NickServUsername")+" "+b.GetString("NickServPassword")) - } - // give nickserv some slack - time.Sleep(time.Second * 5) - b.authDone = true -}