diff --git a/bridge/irc/handlers.go b/bridge/irc/handlers.go index 987df2c5b7..2506fec531 100644 --- a/bridge/irc/handlers.go +++ b/bridge/irc/handlers.go @@ -80,33 +80,52 @@ func (b *Birc) handleInvite(client *girc.Client, event girc.Event) { } } +func isKill(quitmsg string) bool { + return (strings.HasPrefix(quitmsg, "Killed") || + strings.HasPrefix(quitmsg, "Local kill") || + (len(quitmsg) > 7 && strings.HasPrefix(quitmsg[1:], "-lined"))) +} + 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} + if event.Command == "KICK" { + if 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 b.GetBool("nosendjoinpart") { return } + msg := config.Message{ + Username: "system", + Text: event.Source.Name + " kicked " + event.Params[1] + " with message: " + event.Last(), + Channel: channel, + Account: b.Account, + Event: config.EventJoinLeave, + } + b.Log.Debugf("<= Message is %#v", msg) + b.Remote <- msg + return } if event.Source.Name != b.Nick { - if b.GetBool("nosendjoinpart") { + if isActive, _ := b.isUserActive(event.Source.Name, channel); !isActive || + 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} + partmsg := "" + if event.Command == "PART" && len(event.Params) >= 2 { + partmsg = " with message: " + event.Last() + } + msg := config.Message{Username: "system", Text: event.Source.Name + " " + strings.ToLower(event.Command) + "s" + partmsg, 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} + msg = config.Message{Username: "system", Text: event.Source.Name + " (" + event.Source.Ident + "@" + event.Source.Host + ") " + strings.ToLower(event.Command) + "s" + partmsg, Channel: channel, Account: b.Account, Event: config.EventJoinLeave} } else { b.Log.Debugf("<= Sending JOIN_LEAVE event from %s to gateway", b.Account) } @@ -122,15 +141,43 @@ func (b *Birc) handleNewConnection(client *girc.Client, event girc.Event) { 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.Add("INVITE", b.handleInvite) 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) + i.Handlers.AddBg("NICK", b.handleNick) + i.Handlers.AddBg(girc.NOTICE, b.handleNotice) + i.Handlers.AddBg("PRIVMSG", b.handlePrivMsg) + i.Handlers.AddBg("CTCP_ACTION", b.handlePrivMsg) + i.Handlers.AddBg("QUIT", b.handleQuit) + i.Handlers.Add(girc.RPL_TOPICWHOTIME, b.handleTopicWhoTime) +} + +func (b *Birc) handleNick(client *girc.Client, event girc.Event) { + if len(event.Params) != 1 { + b.Log.Debugf("handleJoinPart: malformed nick change? %#v", event) + return + } + if b.GetBool("nosendjoinpart") { + return + } + if activeChannels := b.getActiveChannels(event.Source.Name); len(activeChannels) > 0 { + for _, activityInfo := range activeChannels { + msg := config.Message{ + Username: "system", + Text: event.Source.Name + " changed nick to " + event.Params[0], + Channel: activityInfo.channel, + Account: b.Account, + Event: config.EventJoinLeave, + } + b.Log.Debugf("<= Message is %#v", msg) + b.Remote <- msg + if b.ActivityTimeout != 0 { + // This doesn't count as new activity, but it does preserve the value + b.markUserActive(event.Params[0], activityInfo.channel, activityInfo.activeTime) + } + } + } } func (b *Birc) handleNickServ() { @@ -185,9 +232,10 @@ func (b *Birc) handlePrivMsg(client *girc.Client, event girc.Event) { return } + channel := strings.ToLower(event.Params[0]) rmsg := config.Message{ Username: event.Source.Name, - Channel: strings.ToLower(event.Params[0]), + Channel: channel, Account: b.Account, UserID: event.Source.Ident + "@" + event.Source.Host, } @@ -238,7 +286,41 @@ func (b *Birc) handlePrivMsg(client *girc.Client, event girc.Event) { } b.Log.Debugf("<= Sending message from %s on %s to gateway", event.Params[0], b.Account) + if b.ActivityTimeout > 0 { + b.markUserActive(event.Source.Name, channel, time.Now().Unix()) + } b.Remote <- rmsg + b.cleanActiveMap() +} + +func (b *Birc) handleQuit(client *girc.Client, event girc.Event) { + if event.Source.Name == b.Nick && strings.Contains(event.Last(), "Ping timeout") { + b.Log.Infof("%s reconnecting ..", b.Account) + for mychan := range b.channels { + b.Remote <- config.Message{Username: "system", Text: "reconnect", Channel: mychan, Account: b.Account, Event: config.EventFailure} + } + return + } else if b.GetBool("nosendjoinpart") { + return + } else if activeChannels, found := b.activeUsers[event.Source.Name]; found { + userWasKilled := isKill(event.Last()) + verbosequit := "" + quitmsg := "" + nowTime := time.Now().Unix() + if len(event.Params) >= 1 { + quitmsg = " with message: " + event.Last() + } + if b.GetBool("verbosejoinpart") { + verbosequit = " (" + event.Source.Ident + "@" + event.Source.Host + ")" + } + for channel, activeTime := range activeChannels { + if userWasKilled || b.isActive(activeTime, nowTime) { + msg := config.Message{Username: "system", Text: event.Source.Name + verbosequit + " quit" + quitmsg, Channel: channel, Account: b.Account, Event: config.EventJoinLeave} + b.Log.Debugf("<= Message is %#v", msg) + b.Remote <- msg + } + } + } } func (b *Birc) handleRunCommands() { diff --git a/bridge/irc/irc.go b/bridge/irc/irc.go index 7202df5e5e..64273f83b4 100644 --- a/bridge/irc/irc.go +++ b/bridge/irc/irc.go @@ -10,6 +10,7 @@ import ( "sort" "strconv" "strings" + "sync" "time" "github.com/42wim/matterbridge/bridge" @@ -27,20 +28,30 @@ type Birc struct { i *girc.Client Nick string names map[string][]string + activeUsers map[string]map[string]int64 + activeUsersLastCleaned int64 + activeUsersMutex sync.RWMutex connected chan error Local chan config.Message // local queue for flood control FirstConnection, authDone bool MessageDelay, MessageQueue, MessageLength int + ActivityTimeout int64 channels map[string]bool *bridge.Config } +type ActivityInfo struct { + channel string + activeTime int64 +} + func New(cfg *bridge.Config) bridge.Bridger { b := &Birc{} b.Config = cfg b.Nick = b.GetString("Nick") b.names = make(map[string][]string) + b.activeUsers = make(map[string]map[string]int64) b.connected = make(chan error) b.channels = make(map[string]bool) @@ -59,6 +70,16 @@ func New(cfg *bridge.Config) bridge.Bridger { } else { b.MessageLength = b.GetInt("MessageLength") } + if b.GetBool("ShowActiveUserEvents") { + if b.GetInt("ActivityTimeout") == 0 { + b.ActivityTimeout = 1800 // 30 minutes + } else { + b.ActivityTimeout = int64(b.GetInt("ActivityTimeout")) + } + b.activeUsersLastCleaned = time.Now().Unix() + } else { + b.ActivityTimeout = 0 // Disable + } b.FirstConnection = true return b } @@ -77,6 +98,10 @@ func (b *Birc) Connect() error { return errors.New("you can't enable SASL and TLSClientCertificate at the same time") } + if b.GetBool("NoSendJoinPart") && b.GetBool("ShowActiveUserEvents") { + return errors.New("you must disable NoSendJoinPart to use ShowActiveUserEvents") + } + b.Local = make(chan config.Message, b.MessageQueue+10) b.Log.Infof("Connecting %s", b.GetString("Server")) @@ -413,3 +438,76 @@ func (b *Birc) getTLSConfig() (*tls.Config, error) { return tlsConfig, nil } + +func (b *Birc) isActive(activityTime int64, nowTime int64) bool { + return (nowTime - activityTime) < b.ActivityTimeout +} + +func (b *Birc) isUserActive(nick string, channel string) (bool, int64) { + b.Log.Debugf("checking activity for %s", nick) + if b.ActivityTimeout == 0 { + return true, 0 + } + b.activeUsersMutex.RLock() + defer b.activeUsersMutex.RUnlock() + if activeTime, ok := b.activeUsers[nick][channel]; ok { + now := time.Now().Unix() + b.Log.Debugf("last activity for %s was %d, currently %d", nick, activeTime, now) + if now < activeTime { + b.Log.Errorf("User %s has active time in the future: %d", nick, activeTime) + return true, now // err on the side of caution + } + return b.isActive(activeTime, now), activeTime + } + return false, 0 +} + +func (b *Birc) getActiveChannels(nick string) []ActivityInfo { + retval := make([]ActivityInfo, 0) + if channels, found := b.activeUsers[nick]; found { + now := time.Now().Unix() + for channel, activeTime := range channels { + if now < activeTime { + b.Log.Errorf("User %s has active time for channel %s in the future: %d", + nick, + channel, + activeTime) + } else if (now - activeTime) < b.ActivityTimeout { + retval = append(retval, ActivityInfo{channel, activeTime}) + } + } + } + return retval +} + +func (b *Birc) cleanActiveMap() { + now := time.Now().Unix() + if b.ActivityTimeout == 0 || (b.activeUsersLastCleaned-now < b.ActivityTimeout) { + return + } + b.activeUsersMutex.Lock() + defer b.activeUsersMutex.Unlock() + for nick, activeChannels := range b.activeUsers { + for channel, activeTime := range activeChannels { + if now-activeTime > b.ActivityTimeout { + b.Log.Debugf("last activity for %s was %d, currently %d. Deleting.", nick, activeTime, now) + delete(activeChannels, channel) + } + } + if 0 == len(activeChannels) { + delete(b.activeUsers, nick) + } + } +} + +func (b *Birc) markUserActive(nick string, channel string, activeTime int64) { + b.Log.Debugf("<= Updating last-active time for user %s in channel %s to %d", nick, channel, activeTime) + b.activeUsersMutex.Lock() + defer b.activeUsersMutex.Unlock() + nickActivity, found := b.activeUsers[nick] + if !found { + b.activeUsers[nick] = make(map[string]int64) + nickActivity = b.activeUsers[nick] + } + nickActivity[channel] = activeTime +} diff --git a/matterbridge.toml.sample b/matterbridge.toml.sample index 97b27d7387..4984802394 100644 --- a/matterbridge.toml.sample +++ b/matterbridge.toml.sample @@ -191,6 +191,14 @@ RemoteNickFormat="[{PROTOCOL}] <{NICK}> " #OPTIONAL (default false) ShowJoinPart=false +#Only show join/part/quit information for users who have been active recently. +#OPTIONAL (default false) +ShowActiveUserEvents=false + +#A user is considered active for ShowActiveUserEvents if they've spoken within this number of seconds. +#OPTIONAL (default 1800, which is 30 minutes) +ActivityTimeout=1800 + #Enable to show verbose users joins/parts (ident@host) from other bridges #Currently works for messages from the following bridges: irc #OPTIONAL (default false)