diff --git a/mm-go-irckit/service.go b/mm-go-irckit/service.go index 5ac9fe09..ed76404e 100644 --- a/mm-go-irckit/service.go +++ b/mm-go-irckit/service.go @@ -1,8 +1,10 @@ package irckit import ( + "encoding/binary" "errors" "fmt" + "math" "regexp" "sort" "strconv" @@ -10,6 +12,8 @@ import ( "time" "unicode" + bolt "go.etcd.io/bbolt" + "github.com/42wim/matterircd/bridge" "github.com/mattermost/mattermost-server/v6/model" ) @@ -208,6 +212,176 @@ func login(u *User, toUser *User, args []string, service string) { u.MsgUser(toUser, "login OK") } +//nolint:funlen,gocognit,gocyclo,cyclop +func replay(u *User, toUser *User, args []string, service string) { + if len(args) == 0 || len(args) > 2 { + u.MsgUser(toUser, "need REPLAY (#)") + u.MsgUser(toUser, "e.g. REPLAY #bugs") + return + } + + channelName := strings.TrimPrefix(args[0], "#") + channelTeamID := u.br.GetMe().TeamID + if len(args) == 2 { + channelTeamID = args[1] + } + channelID := u.br.GetChannelID(channelName, channelTeamID) + brchannel, err := u.br.GetChannel(channelID) + if err != nil { + u.MsgUser(toUser, channelName+"not found") + return + } + + since := u.br.GetLastViewedAt(brchannel.ID) + // ignore invalid/deleted/old channels + if since == 0 { + return + } + + // create a spoof function (DM channels are replayed as PMs via createSpoof's "__" special-case) + spoof := u.createSpoof(brchannel) + + logSince := "server" + channame := brchannel.Name + if !brchannel.DM { + channame = "#" + brchannel.Name + } + + // We used to store last viewed at if present. + var lastViewedAt int64 + key := brchannel.ID + err = u.lastViewedAtDB.View(func(tx *bolt.Tx) error { + b := tx.Bucket([]byte(u.User)) + if v := b.Get([]byte(key)); v != nil { + val := binary.LittleEndian.Uint64(v) + if val > math.MaxInt64 { + logger.Errorf("timestamp value %d exceeds int64 range", val) + } else { + lastViewedAt = int64(val) + } + } + return nil + }) + if err != nil { + logger.Errorf("something wrong with u.lastViewedAtDB.View for %s for channel %s (%s)", u.Nick, channame, brchannel.ID) + lastViewedAt = since + } + + // But only use the stored last viewed if it's later than what the server knows. + if lastViewedAt > since { + since = lastViewedAt + 1 + logSince = "stored" + } + + // post everything to the channel you haven't seen yet + postlist := u.br.GetPostsSince(brchannel.ID, since) + if postlist == nil { + // if the channel is not from the primary team id, we can't get posts + if brchannel.TeamID == u.br.GetMe().TeamID { + logger.Errorf("something wrong with getPostsSince for %s for channel %s (%s)", u.Nick, channame, brchannel.ID) + } + return + } + + showReplayHdr := true + + mmPostList, _ := postlist.(*model.PostList) + if mmPostList == nil { + return + } + // traverse the order in reverse + for i := len(mmPostList.Order) - 1; i >= 0; i-- { + p := mmPostList.Posts[mmPostList.Order[i]] + + if p.DeleteAt > p.CreateAt { + continue + } + + // GetPostsSince will return older messages with reaction + // changes since LastViewedAt. This will be confusing as + // the user will think it's a duplicate, or a post out of + // order. Plus, we don't show reaction changes when + // relaying messages/logs so let's skip these. + if p.CreateAt < since { + continue + } + + ts := time.Unix(0, p.CreateAt*int64(time.Millisecond)) + + props := p.GetProps() + botname, override := props["override_username"].(string) + user := u.br.GetUser(p.UserId) + nick := user.Nick + if override { + nick = botname + } + + switch { + case p.Type == model.PostTypeAddToTeam: + nick = systemUser + ghost := u.createUserFromInfo(user) + u.Srv.Channel(brchannel.ID).Join(ghost) //nolint:errcheck + case p.Type == model.PostTypeRemoveFromTeam: + nick = systemUser + ghost := u.createUserFromInfo(user) + u.Srv.Channel(brchannel.ID).Part(ghost, "") + case p.Type == model.PostTypeJoinChannel: + ghost := u.createUserFromInfo(user) + u.Srv.Channel(brchannel.ID).Join(ghost) //nolint:errcheck + case p.Type == model.PostTypeLeaveChannel: + ghost := u.createUserFromInfo(user) + u.Srv.Channel(brchannel.ID).Part(ghost, "") + case p.Type == model.PostTypeAddToChannel: + if addedUserID, ok := props["addedUserId"].(string); ok { + ghost := u.createUserFromInfo(u.br.GetUser(addedUserID)) + u.Srv.Channel(brchannel.ID).Join(ghost) //nolint:errcheck + } + case p.Type == model.PostTypeRemoveFromChannel: + if removedUserID, ok := props["removedUserId"].(string); ok { + ghost := u.createUserFromInfo(u.br.GetUser(removedUserID)) + u.Srv.Channel(brchannel.ID).Part(ghost, "") + } + } + + for _, post := range strings.Split(p.Message, "\n") { + if showReplayHdr { + date := ts.Format("2006-01-02 15:04:05") + if brchannel.DM { + spoof(nick, fmt.Sprintf("\x02Replaying msgs since %s\x02 \x1d(%s)\x1d", date, logSince)) + } else { + spoof("matterircd", fmt.Sprintf("\x02Replaying msgs since %s\x02 \x1d(%s)\x1d", date, logSince)) + } + logger.Infof("Replaying msgs for %s for %s (%s) since %s (%s)", u.Nick, channame, brchannel.ID, date, logSince) + showReplayHdr = false + } + + if nick == systemUser { + post = "\x1d" + post + "\x1d" + } + + replayMsg := fmt.Sprintf("[%s] %s", ts.Format("15:04"), post) + if (u.v.GetBool(u.br.Protocol()+".prefixcontext") || u.v.GetBool(u.br.Protocol()+".suffixcontext")) && nick != systemUser { + threadMsgID := u.prefixContext(brchannel.ID, p.Id, p.RootId, "replay") + replayMsg = u.formatContextMessage(ts.Format("15:04"), threadMsgID, post) + } + spoof(nick, replayMsg) + } + + if len(p.FileIds) == 0 { + continue + } + + for _, fname := range u.br.GetFileLinks(p.FileIds) { + fileMsg := "\x1ddownload file - " + fname + "\x1d" + if u.v.GetBool(u.br.Protocol()+".prefixcontext") || u.v.GetBool(u.br.Protocol()+".suffixcontext") { + threadMsgID := u.prefixContext(brchannel.ID, p.Id, p.RootId, "replay_file") + fileMsg = u.formatContextMessage(ts.Format("15:04"), threadMsgID, fileMsg) + } + spoof(nick, fileMsg) + } + } +} + //nolint:forcetypeassert,goconst func details(u *User, toUser *User, args []string, service string) { if service == "slack" { @@ -645,9 +819,10 @@ var cmds = map[string]Command{ "login": {handler: login, minParams: 2, maxParams: 5}, "logout": {handler: logout, login: true, minParams: 0, maxParams: 0}, "part": {handler: part, login: true, minParams: 1, maxParams: 1}, + "replay": {handler: replay, login: true, minParams: 1, maxParams: 2}, + "scrollback": {handler: scrollback, login: true, minParams: 2, maxParams: 2}, "search": {handler: search, login: true, minParams: 1, maxParams: -1}, "searchusers": {handler: searchUsers, login: true, minParams: 1, maxParams: -1}, - "scrollback": {handler: scrollback, login: true, minParams: 2, maxParams: 2}, "updatelastviewed": {handler: updatelastviewed, login: true, minParams: 1, maxParams: 1}, } diff --git a/mm-go-irckit/userbridge.go b/mm-go-irckit/userbridge.go index abdb3895..a685506c 100644 --- a/mm-go-irckit/userbridge.go +++ b/mm-go-irckit/userbridge.go @@ -743,26 +743,16 @@ func (u *User) createSpoof(mmchannel *bridge.ChannelInfo) func(string, string, . } } - channelName := mmchannel.Name - - if mmchannel.TeamID != u.br.GetMe().TeamID || u.v.GetBool(u.br.Protocol()+".prefixmainteam") { - channelName = u.br.GetTeamName(mmchannel.TeamID) + "/" + mmchannel.Name - } - - u.syncChannel(mmchannel.ID, "#"+channelName) ch := u.Srv.Channel(mmchannel.ID) return ch.SpoofMessage } -//nolint:funlen,gocognit,gocyclo,cyclop func (u *User) addUserToChannelWorker(channels <-chan *bridge.ChannelInfo, throttle *time.Ticker) { for brchannel := range channels { logger.Debug("addUserToChannelWorker", brchannel) <-throttle.C - // exclude direct messages - spoof := u.createSpoof(brchannel) since := u.br.GetLastViewedAt(brchannel.ID) // ignore invalid/deleted/old channels @@ -770,147 +760,24 @@ func (u *User) addUserToChannelWorker(channels <-chan *bridge.ChannelInfo, throt continue } - logSince := "server" - channame := brchannel.Name - if !brchannel.DM { - channame = "#" + brchannel.Name - } - - // We used to stored last viewed at if present. - var lastViewedAt int64 - key := brchannel.ID - err := u.lastViewedAtDB.View(func(tx *bolt.Tx) error { - b := tx.Bucket([]byte(u.User)) - if v := b.Get([]byte(key)); v != nil { - lastViewedAt = int64(binary.LittleEndian.Uint64(v)) - } - return nil - }) - if err != nil { - logger.Errorf("something wrong with u.lastViewedAtDB.View for %s for channel %s (%s)", u.Nick, channame, brchannel.ID) - lastViewedAt = since - } - - // But only use the stored last viewed if it's later than what the server knows. - if lastViewedAt > since { - since = lastViewedAt + 1 - logSince = "stored" - } + if !strings.Contains(brchannel.Name, "__") { + channelName := brchannel.Name - // post everything to the channel you haven't seen yet - postlist := u.br.GetPostsSince(brchannel.ID, since) - if postlist == nil { - // if the channel is not from the primary team id, we can't get posts - if brchannel.TeamID == u.br.GetMe().TeamID { - logger.Errorf("something wrong with getPostsSince for %s for channel %s (%s)", u.Nick, channame, brchannel.ID) + if brchannel.TeamID != u.br.GetMe().TeamID || u.v.GetBool(u.br.Protocol()+".prefixmainteam") { + channelName = u.br.GetTeamName(brchannel.TeamID) + "/" + brchannel.Name } - continue - } - - showReplayHdr := true - mmPostList, _ := postlist.(*model.PostList) - if mmPostList == nil { - continue + u.syncChannel(brchannel.ID, "#"+channelName) } - // traverse the order in reverse - for i := len(mmPostList.Order) - 1; i >= 0; i-- { - p := mmPostList.Posts[mmPostList.Order[i]] - - if p.DeleteAt > p.CreateAt { - continue - } - - // GetPostsSince will return older messages with reaction - // changes since LastViewedAt. This will be confusing as - // the user will think it's a duplicate, or a post out of - // order. Plus, we don't show reaction changes when - // relaying messages/logs so let's skip these. - if p.CreateAt < since { - continue - } - ts := time.Unix(0, p.CreateAt*int64(time.Millisecond)) + args := []string{brchannel.Name, brchannel.TeamID} - props := p.GetProps() - botname, override := props["override_username"].(string) - user := u.br.GetUser(p.UserId) - nick := user.Nick - if override { - nick = botname - } - - switch { - case p.Type == model.PostTypeAddToTeam: - nick = systemUser - ghost := u.createUserFromInfo(user) - u.Srv.Channel(brchannel.ID).Join(ghost) //nolint:errcheck - case p.Type == model.PostTypeRemoveFromTeam: - nick = systemUser - ghost := u.createUserFromInfo(user) - u.Srv.Channel(brchannel.ID).Part(ghost, "") - case p.Type == model.PostTypeJoinChannel: - ghost := u.createUserFromInfo(user) - u.Srv.Channel(brchannel.ID).Join(ghost) //nolint:errcheck - case p.Type == model.PostTypeLeaveChannel: - ghost := u.createUserFromInfo(user) - u.Srv.Channel(brchannel.ID).Part(ghost, "") - case p.Type == model.PostTypeAddToChannel: - if addedUserID, ok := props["addedUserId"].(string); ok { - ghost := u.createUserFromInfo(u.br.GetUser(addedUserID)) - u.Srv.Channel(brchannel.ID).Join(ghost) //nolint:errcheck - } - case p.Type == model.PostTypeRemoveFromChannel: - if removedUserID, ok := props["removedUserId"].(string); ok { - ghost := u.createUserFromInfo(u.br.GetUser(removedUserID)) - u.Srv.Channel(brchannel.ID).Part(ghost, "") - } - } - - for _, post := range strings.Split(p.Message, "\n") { - if showReplayHdr { - date := ts.Format("2006-01-02 15:04:05") - if brchannel.DM { - spoof(nick, fmt.Sprintf("\x02Replaying msgs since %s\x0f", date)) - } else { - spoof("matterircd", fmt.Sprintf("\x02Replaying msgs since %s\x0f", date)) - } - logger.Infof("Replaying msgs for %s for %s (%s) since %s (%s)", u.Nick, channame, brchannel.ID, date, logSince) - showReplayHdr = false - } - - if nick == systemUser { - post = "\x1d" + post + "\x1d" - } - - replayMsg := fmt.Sprintf("[%s] %s", ts.Format("15:04"), post) - if (u.v.GetBool(u.br.Protocol()+".prefixcontext") || u.v.GetBool(u.br.Protocol()+".suffixcontext")) && nick != systemUser { - threadMsgID := u.prefixContext(brchannel.ID, p.Id, p.RootId, "replay") - replayMsg = u.formatContextMessage(ts.Format("15:04"), threadMsgID, post) - } - spoof(nick, replayMsg) - } - - if len(p.FileIds) == 0 { - continue - } + replay(u, u, args, "") - for _, fname := range u.br.GetFileLinks(p.FileIds) { - fileMsg := "\x1ddownload file - " + fname + "\x1d" - if u.v.GetBool(u.br.Protocol()+".prefixcontext") || u.v.GetBool(u.br.Protocol()+".suffixcontext") { - threadMsgID := u.prefixContext(brchannel.ID, p.Id, p.RootId, "replay_file") - fileMsg = u.formatContextMessage(ts.Format("15:04"), threadMsgID, fileMsg) - } - spoof(nick, fileMsg) - } - } - - if len(mmPostList.Order) > 0 { - if !u.v.GetBool(u.br.Protocol() + ".disableautoview") { - u.updateLastViewed(brchannel.ID) - } - u.saveLastViewedAt(brchannel.ID) + if !u.v.GetBool(u.br.Protocol() + ".disableautoview") { + u.updateLastViewed(brchannel.ID) } + u.saveLastViewedAt(brchannel.ID) } }