diff options
Diffstat (limited to 'teleirc/matterbridge/bridge/irc')
| -rw-r--r-- | teleirc/matterbridge/bridge/irc/charset.go | 32 | ||||
| -rw-r--r-- | teleirc/matterbridge/bridge/irc/handlers.go | 265 | ||||
| -rw-r--r-- | teleirc/matterbridge/bridge/irc/irc.go | 415 |
3 files changed, 712 insertions, 0 deletions
diff --git a/teleirc/matterbridge/bridge/irc/charset.go b/teleirc/matterbridge/bridge/irc/charset.go new file mode 100644 index 0000000..57872ec --- /dev/null +++ b/teleirc/matterbridge/bridge/irc/charset.go @@ -0,0 +1,32 @@ +package birc + +import ( + "golang.org/x/text/encoding" + "golang.org/x/text/encoding/japanese" + "golang.org/x/text/encoding/korean" + "golang.org/x/text/encoding/simplifiedchinese" + "golang.org/x/text/encoding/traditionalchinese" + "golang.org/x/text/encoding/unicode" +) + +var encoders = map[string]encoding.Encoding{ + "utf-8": unicode.UTF8, + "iso-2022-jp": japanese.ISO2022JP, + "big5": traditionalchinese.Big5, + "gbk": simplifiedchinese.GBK, + "euc-kr": korean.EUCKR, + "gb2312": simplifiedchinese.HZGB2312, + "shift-jis": japanese.ShiftJIS, + "euc-jp": japanese.EUCJP, + "gb18030": simplifiedchinese.GB18030, +} + +func toUTF8(from string, input string) string { + enc, ok := encoders[from] + if !ok { + return input + } + + res, _ := enc.NewDecoder().String(input) + return res +} diff --git a/teleirc/matterbridge/bridge/irc/handlers.go b/teleirc/matterbridge/bridge/irc/handlers.go new file mode 100644 index 0000000..74db768 --- /dev/null +++ b/teleirc/matterbridge/bridge/irc/handlers.go @@ -0,0 +1,265 @@ +package birc + +import ( + "bytes" + "fmt" + "io/ioutil" + "strconv" + "strings" + "time" + + "github.com/42wim/matterbridge/bridge/config" + "github.com/42wim/matterbridge/bridge/helper" + "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 = toUTF8(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 to 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) handleInvite(client *girc.Client, event girc.Event) { + if len(event.Params) != 2 { + return + } + + channel := event.Params[1] + + b.Log.Debugf("got invite for %s", channel) + + if _, ok := b.channels[channel]; ok { + b.i.Cmd.Join(channel) + } +} + +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.Last(), "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 + } + msg := config.Message{Username: "system", Text: event.Source.Name + " " + strings.ToLower(event.Command) + "s", Channel: channel, Account: b.Account, Event: config.EventJoinLeave} + if b.GetBool("verbosejoinpart") { + b.Log.Debugf("<= Sending verbose JOIN_LEAVE event from %s to gateway", b.Account) + msg = config.Message{Username: "system", Text: event.Source.Name + " (" + event.Source.Ident + "@" + event.Source.Host + ") " + strings.ToLower(event.Command) + "s", Channel: channel, Account: b.Account, Event: config.EventJoinLeave} + } else { + b.Log.Debugf("<= Sending JOIN_LEAVE event from %s to gateway", b.Account) + } + 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.AddBg("PRIVMSG", b.handlePrivMsg) + i.Handlers.AddBg("CTCP_ACTION", b.handlePrivMsg) + i.Handlers.Add(girc.RPL_TOPICWHOTIME, b.handleTopicWhoTime) + i.Handlers.AddBg(girc.NOTICE, b.handleNotice) + i.Handlers.AddBg("JOIN", b.handleJoinPart) + i.Handlers.AddBg("PART", b.handleJoinPart) + i.Handlers.AddBg("QUIT", b.handleJoinPart) + i.Handlers.AddBg("KICK", b.handleJoinPart) + i.Handlers.Add("INVITE", b.handleInvite) +} + +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 + // only send on first connection + if b.FirstConnection { + 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.Last(), event) + + // set action event + if event.IsAction() { + rmsg.Event = config.EventUserAction + } + + // set NOTICE event + if event.Command == "NOTICE" { + rmsg.Event = config.EventNoticeIRC + } + + // strip action, we made an event if it was an action + rmsg.Text += event.StripAction() + + // start detecting the charset + 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 = toUTF8(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") { + cmd = strings.ReplaceAll(cmd, "{BOTNICK}", b.Nick) + 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/teleirc/matterbridge/bridge/irc/irc.go b/teleirc/matterbridge/bridge/irc/irc.go new file mode 100644 index 0000000..7202df5 --- /dev/null +++ b/teleirc/matterbridge/bridge/irc/irc.go @@ -0,0 +1,415 @@ +package birc + +import ( + "crypto/tls" + "errors" + "fmt" + "hash/crc32" + "io/ioutil" + "net" + "sort" + "strconv" + "strings" + "time" + + "github.com/42wim/matterbridge/bridge" + "github.com/42wim/matterbridge/bridge/config" + "github.com/42wim/matterbridge/bridge/helper" + "github.com/lrstanley/girc" + stripmd "github.com/writeas/go-strip-markdown" + + // 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" +) + +type Birc struct { + i *girc.Client + Nick string + names map[string][]string + connected chan error + Local chan config.Message // local queue for flood control + FirstConnection, authDone bool + MessageDelay, MessageQueue, MessageLength int + channels map[string]bool + + *bridge.Config +} + +func New(cfg *bridge.Config) bridge.Bridger { + b := &Birc{} + b.Config = cfg + b.Nick = b.GetString("Nick") + b.names = make(map[string][]string) + b.connected = make(chan error) + b.channels = make(map[string]bool) + + if b.GetInt("MessageDelay") == 0 { + b.MessageDelay = 1300 + } else { + b.MessageDelay = b.GetInt("MessageDelay") + } + if b.GetInt("MessageQueue") == 0 { + b.MessageQueue = 30 + } else { + b.MessageQueue = b.GetInt("MessageQueue") + } + if b.GetInt("MessageLength") == 0 { + b.MessageLength = 400 + } else { + b.MessageLength = b.GetInt("MessageLength") + } + b.FirstConnection = true + return b +} + +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) //nolint:errcheck + } + return "" +} + +func (b *Birc) Connect() error { + if b.GetBool("UseSASL") && b.GetString("TLSClientCertificate") != "" { + return errors.New("you can't enable SASL and TLSClientCertificate at the same time") + } + + b.Local = make(chan config.Message, b.MessageQueue+10) + b.Log.Infof("Connecting %s", b.GetString("Server")) + + i, err := b.getClient() + if err != nil { + return err + } + + if b.GetBool("UseSASL") { + i.Config.SASL = &girc.SASLPlain{ + User: b.GetString("NickServNick"), + Pass: b.GetString("NickServPassword"), + } + } + + i.Handlers.Add(girc.RPL_WELCOME, b.handleNewConnection) + i.Handlers.Add(girc.RPL_ENDOFMOTD, b.handleOtherAuth) + i.Handlers.Add(girc.ERR_NOMOTD, b.handleOtherAuth) + i.Handlers.Add(girc.ALL_EVENTS, b.handleOther) + b.i = i + + go b.doConnect() + + err = <-b.connected + if err != nil { + return fmt.Errorf("connection failed %s", err) + } + b.Log.Info("Connection succeeded") + b.FirstConnection = false + if b.GetInt("DebugLevel") == 0 { + i.Handlers.Clear(girc.ALL_EVENTS) + } + go b.doSend() + return nil +} + +func (b *Birc) Disconnect() error { + b.i.Close() + close(b.Local) + return nil +} + +func (b *Birc) JoinChannel(channel config.ChannelInfo) error { + b.channels[channel.Name] = true + // need to check if we have nickserv auth done before joining channels + for { + if b.authDone { + break + } + time.Sleep(time.Second) + } + if channel.Options.Key != "" { + b.Log.Debugf("using key %s for channel %s", channel.Options.Key, channel.Name) + b.i.Cmd.JoinKey(channel.Name, channel.Options.Key) + } else { + b.i.Cmd.Join(channel.Name) + } + return nil +} + +func (b *Birc) Send(msg config.Message) (string, error) { + // ignore delete messages + if msg.Event == config.EventMsgDelete { + return "", nil + } + + b.Log.Debugf("=> Receiving %#v", msg) + + // we can be in between reconnects #385 + if !b.i.IsConnected() { + b.Log.Error("Not connected to server, dropping message") + return "", nil + } + + // Execute a command + if strings.HasPrefix(msg.Text, "!") { + b.Command(&msg) + } + + // convert to specified charset + if err := b.handleCharset(&msg); err != nil { + return "", err + } + + // handle files, return if we're done here + if ok := b.handleFiles(&msg); ok { + return "", nil + } + + var msgLines []string + if b.GetBool("StripMarkdown") { + msg.Text = stripmd.Strip(msg.Text) + } + + if b.GetBool("MessageSplit") { + msgLines = helper.GetSubLines(msg.Text, b.MessageLength, b.GetString("MessageClipped")) + } else { + msgLines = helper.GetSubLines(msg.Text, 0, b.GetString("MessageClipped")) + } + for i := range msgLines { + if len(b.Local) >= b.MessageQueue { + b.Log.Debugf("flooding, dropping message (queue at %d)", len(b.Local)) + return "", nil + } + + msg.Text = msgLines[i] + b.Local <- msg + } + 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 + }) + } +} + +// Sanitize nicks for RELAYMSG: replace IRC characters with special meanings with "-" +func sanitizeNick(nick string) string { + sanitize := func(r rune) rune { + if strings.ContainsRune("!+%@&#$:'\"?*,. ", r) { + return '-' + } + return r + } + return strings.Map(sanitize, nick) +} + +func (b *Birc) doSend() { + rate := time.Millisecond * time.Duration(b.MessageDelay) + throttle := time.NewTicker(rate) + for msg := range b.Local { + <-throttle.C + username := msg.Username + // Optional support for the proposed RELAYMSG extension, described at + // https://github.com/jlu5/ircv3-specifications/blob/master/extensions/relaymsg.md + // nolint:nestif + if (b.i.HasCapability("overdrivenetworks.com/relaymsg") || b.i.HasCapability("draft/relaymsg")) && + b.GetBool("UseRelayMsg") { + username = sanitizeNick(username) + text := msg.Text + + // Work around girc chomping leading commas on single word messages? + if strings.HasPrefix(text, ":") && !strings.ContainsRune(text, ' ') { + text = ":" + text + } + + if msg.Event == config.EventUserAction { + b.i.Cmd.SendRawf("RELAYMSG %s %s :\x01ACTION %s\x01", msg.Channel, username, text) //nolint:errcheck + } else { + b.Log.Debugf("Sending RELAYMSG to channel %s: nick=%s", msg.Channel, username) + b.i.Cmd.SendRawf("RELAYMSG %s %s :%s", msg.Channel, username, text) //nolint:errcheck + } + } else { + if b.GetBool("Colornicks") { + checksum := crc32.ChecksumIEEE([]byte(msg.Username)) + colorCode := checksum%14 + 2 // quick fix - prevent white or black color codes + username = fmt.Sprintf("\x03%02d%s\x0F", colorCode, msg.Username) + } + switch msg.Event { + case config.EventUserAction: + b.i.Cmd.Action(msg.Channel, username+msg.Text) + case config.EventNoticeIRC: + b.Log.Debugf("Sending notice to channel %s", msg.Channel) + b.i.Cmd.Notice(msg.Channel, username+msg.Text) + default: + b.Log.Debugf("Sending to channel %s", msg.Channel) + b.i.Cmd.Message(msg.Channel, username+msg.Text) + } + } + } +} + +// 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 + } + user := b.GetString("UserName") + if user == "" { + user = b.GetString("Nick") + } + // fix strict user handling of girc + for !girc.IsValidUser(user) { + if len(user) == 1 || len(user) == 0 { + user = "matterbridge" + break + } + user = user[1:] + } + realName := b.GetString("RealName") + if realName == "" { + realName = b.GetString("Nick") + } + + debug := ioutil.Discard + if b.GetInt("DebugLevel") == 2 { + debug = b.Log.Writer() + } + + pingDelay, err := time.ParseDuration(b.GetString("pingdelay")) + if err != nil || pingDelay == 0 { + pingDelay = time.Minute + } + + b.Log.Debugf("setting pingdelay to %s", pingDelay) + + tlsConfig, err := b.getTLSConfig() + if err != nil { + return nil, err + } + + i := girc.New(girc.Config{ + Server: server, + ServerPass: b.GetString("Password"), + Port: port, + Nick: b.GetString("Nick"), + User: user, + Name: realName, + SSL: b.GetBool("UseTLS"), + Bind: b.GetString("Bind"), + TLSConfig: tlsConfig, + PingDelay: pingDelay, + // skip gIRC internal rate limiting, since we have our own throttling + AllowFlood: true, + Debug: debug, + SupportedCaps: map[string][]string{"overdrivenetworks.com/relaymsg": nil, "draft/relaymsg": nil}, + }) + return i, nil +} + +func (b *Birc) endNames(client *girc.Client, event girc.Event) { + channel := event.Params[1] + sort.Strings(b.names[channel]) + maxNamesPerPost := (300 / b.nicksPerRow()) * b.nicksPerRow() + for len(b.names[channel]) > maxNamesPerPost { + b.Remote <- config.Message{ + Username: b.Nick, Text: b.formatnicks(b.names[channel][0:maxNamesPerPost]), + Channel: channel, Account: b.Account, + } + b.names[channel] = b.names[channel][maxNamesPerPost:] + } + b.Remote <- config.Message{ + Username: b.Nick, Text: b.formatnicks(b.names[channel]), + Channel: channel, Account: b.Account, + } + b.names[channel] = nil + b.i.Handlers.Clear(girc.RPL_NAMREPLY) + b.i.Handlers.Clear(girc.RPL_ENDOFNAMES) +} + +func (b *Birc) skipPrivMsg(event girc.Event) bool { + // Our nick can be changed + b.Nick = b.i.GetNick() + + // freenode doesn't send 001 as first reply + if event.Command == "NOTICE" && len(event.Params) != 2 { + return true + } + // don't forward queries to the bot + if event.Params[0] == b.Nick { + return true + } + // don't forward message from ourself + if event.Source != nil { + if event.Source.Name == b.Nick { + return true + } + } + // don't forward messages we sent via RELAYMSG + if relayedNick, ok := event.Tags.Get("draft/relaymsg"); ok && relayedNick == b.Nick { + return true + } + // This is the old name of the cap sent in spoofed messages; I've kept this in + // for compatibility reasons + if relayedNick, ok := event.Tags.Get("relaymsg"); ok && relayedNick == b.Nick { + return true + } + return false +} + +func (b *Birc) nicksPerRow() int { + return 4 +} + +func (b *Birc) storeNames(client *girc.Client, event girc.Event) { + channel := event.Params[2] + b.names[channel] = append( + b.names[channel], + strings.Split(strings.TrimSpace(event.Last()), " ")...) +} + +func (b *Birc) formatnicks(nicks []string) string { + return strings.Join(nicks, ", ") + " currently on IRC" +} + +func (b *Birc) getTLSConfig() (*tls.Config, error) { + server, _, _ := net.SplitHostPort(b.GetString("server")) + + tlsConfig := &tls.Config{ + InsecureSkipVerify: b.GetBool("skiptlsverify"), //nolint:gosec + ServerName: server, + } + + if filename := b.GetString("TLSClientCertificate"); filename != "" { + cert, err := tls.LoadX509KeyPair(filename, filename) + if err != nil { + return nil, err + } + + tlsConfig.Certificates = []tls.Certificate{cert} + } + + return tlsConfig, nil +} |
