delthas: 1 MONITOR user with whom we have an open buffer 7 files changed, 145 insertions(+), 16 deletions(-)
Copy & paste the following snippet into your terminal to import this patchset into git:
curl -s https://lists.sr.ht/~taiite/public-inbox/patches/27019/mbox | git am -3Learn more about email & git
--- Rebased against master. Adressed simple comments for v4. Moved RegisteredEvent from 001 to 005. This makes sure ISUPPORT are updated before processing the RegisteredEvent handler. (Mainly for MonitorAdd when reconnecting.) app.go | 13 ++++++ commands.go | 3 ++ irc/events.go | 8 ++++ irc/rpl.go | 6 +++ irc/session.go | 108 ++++++++++++++++++++++++++++++++++++++++++++----- irc/tokens.go | 7 ++-- ui/ui.go | 16 +++++++- 7 files changed, 145 insertions(+), 16 deletions(-) diff --git a/app.go b/app.go index 2b0ba82..c23cfdf 100644 --- a/app.go +++ b/app.go @@ -95,6 +95,8 @@ type App struct { lastNetID string lastBuffer string + monitor map[string]map[string]struct{} // set of targets we want to monitor per netID, best-effort. netID->target->{} + lastMessageTime time.Time lastCloseTime time.Time } @@ -105,6 +107,7 @@ func NewApp(cfg Config) (app *App, err error) { events: make(chan event, eventChanSize), cfg: cfg, messageBounds: map[boundKey]bound{}, + monitor: make(map[string]map[string]struct{}), } if cfg.Highlights != nil { @@ -577,6 +580,9 @@ func (app *App) handleIRCEvent(netID string, ev interface{}) { s.Close() } app.sessions[netID] = s + if _, ok := app.monitor[netID]; !ok { + app.monitor[netID] = make(map[string]struct{}) + } return } if _, ok := ev.(irc.Typing); ok { @@ -628,6 +634,10 @@ func (app *App) handleIRCEvent(netID string, ev interface{}) { Head: "--", Body: ui.PlainString(body), }) + for target := range app.monitor[s.NetID()] { + // TODO: batch MONITOR + + s.MonitorAdd(target) + } case irc.SelfNickEvent: var body ui.StyledStringBuilder body.WriteString(fmt.Sprintf("%s\u2192%s", ev.FormerNick, s.Nick())) @@ -724,6 +734,8 @@ func (app *App) handleIRCEvent(netID string, ev interface{}) { buffer, line, notification := app.formatMessage(s, ev) if buffer != "" && !s.IsChannel(buffer) { if _, added := app.win.AddBuffer(netID, "", buffer); added { + app.monitor[netID][buffer] = struct{}{} + s.MonitorAdd(buffer) s.NewHistoryRequest(buffer). WithLimit(200). Before(msg.TimeOrNow()) @@ -745,6 +757,7 @@ func (app *App) handleIRCEvent(netID string, ev interface{}) { if s.IsChannel(target) { continue } + s.MonitorAdd(target) app.win.AddBuffer(netID, "", target) // CHATHISTORY BEFORE excludes its bound, so add 1ms // (precision of the time tag) to include that last message. diff --git a/commands.go b/commands.go index 5dff158..9d5d0b8 100644 --- a/commands.go +++ b/commands.go @@ -317,6 +317,8 @@ func commandDoMsg(app *App, args []string) (err error) { Time: time.Now(), }) if buffer != "" && !s.IsChannel(target) { + app.monitor[netID][buffer] = struct{}{} + s.MonitorAdd(buffer) app.win.AddBuffer(netID, "", buffer) } @@ -425,6 +427,7 @@ func commandDoQuery(app *App, args []string) (err error) { if s.IsChannel(target) { return fmt.Errorf("cannot query a channel, use JOIN instead") } + s.MonitorAdd(target) i, _ := app.win.AddBuffer(netID, "", target) s.NewHistoryRequest(target).WithLimit(200).Before(time.Now()) app.win.JumpBufferIndex(i) diff --git a/irc/events.go b/irc/events.go index 0c84f2b..f8f002f 100644 --- a/irc/events.go +++ b/irc/events.go @@ -50,6 +50,14 @@ type UserQuitEvent struct { Time time.Time } +type UserOnlineEvent struct { + User string +} + +type UserOfflineEvent struct { + User string +} + type TopicChangeEvent struct { Channel string Topic string diff --git a/irc/rpl.go b/irc/rpl.go index 6373972..398b5b3 100644 --- a/irc/rpl.go +++ b/irc/rpl.go @@ -87,6 +87,12 @@ const ( errUmodeunknownflag = "501" // :Unknown mode flag errUsersdontmatch = "502" // :Can't change mode for other users + rplMononline = "730" // <nick> :target[!user@host][,target[!user@host]]* + rplMonoffline = "731" // <nick> :target[,target2]* + rplMonlist = "732" // <nick> :target[,target2]* + rplEndofmonlist = "733" // <nick> :End of MONITOR list + errMonlistisfull = "734" // <nick> <limit> <targets> :Monitor list is full. + rplLoggedin = "900" // <nick> <nick>!<ident>@<host> <account> :You are now logged in as <user> rplLoggedout = "901" // <nick> <nick>!<ident>@<host> :You are now logged out errNicklocked = "902" // :You must use a nick assigned to you diff --git a/irc/session.go b/irc/session.go index b6d5873..7d3f166 100644 --- a/irc/session.go +++ b/irc/session.go @@ -71,10 +71,11 @@ const ( TypingDone ) -// User is a known IRC user (we share a channel with it). +// User is a known IRC user. type User struct { - Name *Prefix // the nick, user and hostname of the user if known. - Away bool // whether the user is away or not + Name *Prefix // the nick, user and hostname of the user if known. + Away bool // whether the user is away or not + Disconnected bool // can only be true for monitored users.
Instead of a bool + Session.monitors, this field could be a three-state enum (not monitored, online, offline), unless I'm missing something? Otherwise, lgtm. I will test this tonight.
} // Channel is a joined channel. @@ -124,6 +125,7 @@ type Session struct { historyLimit int prefixSymbols string prefixModes string + monitor bool users map[string]*User // known users. channels map[string]Channel // joined channels. @@ -131,6 +133,7 @@ type Session struct { chReqs map[string]struct{} // set of targets for which history is currently requested. targetsBatchID string // ID of the channel history targets batch being processed. targetsBatch HistoryTargetsEvent // channel history targets batch being processed. + monitors map[string]struct{} // set of users we want to monitor (and keep even if they are disconnected). pendingChannels map[string]time.Time // set of join requests stamps for channels. } @@ -158,6 +161,7 @@ func NewSession(out chan<- Message, params SessionParams) *Session { channels: map[string]Channel{}, chBatches: map[string]HistoryEvent{}, chReqs: map[string]struct{}{}, + monitors: map[string]struct{}{}, pendingChannels: map[string]time.Time{}, } @@ -235,16 +239,18 @@ func (s *Session) Names(target string) []Member { names = make([]Member, 0, len(c.Members)) for u, pl := range c.Members { names = append(names, Member{ - PowerLevel: pl, - Name: u.Name.Copy(), - Away: u.Away, + PowerLevel: pl, + Name: u.Name.Copy(), + Away: u.Away, + Disconnected: u.Disconnected, }) } } } else if u, ok := s.users[s.Casemap(target)]; ok { names = append(names, Member{ - Name: u.Name.Copy(), - Away: u.Away, + Name: u.Name.Copy(), + Away: u.Away, + Disconnected: u.Disconnected, }) names = append(names, Member{ Name: &Prefix{ @@ -278,7 +284,7 @@ func (s *Session) TypingStops() <-chan Typing { func (s *Session) ChannelsSharedWith(name string) []string { var user *User - if u, ok := s.users[s.Casemap(name)]; ok { + if u, ok := s.users[s.Casemap(name)]; ok && !u.Disconnected { user = u } else { return nil @@ -420,6 +426,26 @@ func (s *Session) TypingStop(target string) { s.out <- NewMessage("TAGMSG", target).WithTag("+typing", "done") } +func (s *Session) MonitorAdd(target string) { + targetCf := s.casemap(target) + if _, ok := s.monitors[targetCf]; !ok { + s.monitors[targetCf] = struct{}{} + if s.monitor { + s.out <- NewMessage("MONITOR", "+", target) + } + } +} + +func (s *Session) MonitorRemove(target string) { + targetCf := s.casemap(target) + if _, ok := s.monitors[targetCf]; ok { + delete(s.monitors, targetCf) + if s.monitor { + s.out <- NewMessage("MONITOR", "-", target) + } + } +} + type HistoryRequest struct { s *Session target string @@ -595,12 +621,12 @@ func (s *Session) handleMessageRegistered(msg Message, playback bool) (Event, er if s.host == "" { s.out <- NewMessage("WHO", s.nick) } - return RegisteredEvent{}, nil case rplIsupport: if len(msg.Params) < 3 { return nil, msg.errNotEnoughParams(3) } s.updateFeatures(msg.Params[1 : len(msg.Params)-1]) + return RegisteredEvent{}, nil case rplWhoreply: var nick, host, flags, username string if err := msg.ParseParams(nil, nil, &username, &host, nil, &nick, &flags, nil); err != nil { @@ -802,6 +828,7 @@ func (s *Session) handleMessageRegistered(msg Message, playback bool) (Event, er nickCf := s.Casemap(msg.Prefix.Name) if u, ok := s.users[nickCf]; ok { + u.Disconnected = true var channels []string for channelCf, c := range s.channels { if _, ok := c.Members[u]; ok { @@ -817,6 +844,54 @@ func (s *Session) handleMessageRegistered(msg Message, playback bool) (Event, er Time: msg.TimeOrNow(), }, nil } + case rplMononline: + for _, target := range strings.Split(msg.Params[1], ",") { + prefix := ParsePrefix(target) + if prefix == nil { + continue + } + nickCf := s.casemap(prefix.Name) + + if _, ok := s.monitors[nickCf]; ok { + u, ok := s.users[nickCf] + if !ok { + u = &User{ + Name: prefix, + } + s.users[nickCf] = u + } + if u.Disconnected { + u.Disconnected = false + return UserOnlineEvent{ + User: u.Name.Name, + }, nil + } + } + } + case rplMonoffline: + for _, target := range strings.Split(msg.Params[1], ",") { + prefix := ParsePrefix(target) + if prefix == nil { + continue + } + nickCf := s.casemap(prefix.Name) + + if _, ok := s.monitors[nickCf]; ok { + u, ok := s.users[nickCf] + if !ok { + u = &User{ + Name: prefix, + } + s.users[nickCf] = u + } + if !u.Disconnected { + u.Disconnected = true + return UserOfflineEvent{ + User: u.Name.Name, + }, nil + } + } + } case rplNamreply: var channel, names string if err := msg.ParseParams(nil, nil, &channel, &names); err != nil { @@ -1195,6 +1270,8 @@ func (s *Session) handleMessageRegistered(msg Message, playback bool) (Event, er Code: code, Message: strings.Join(msg.Params[2:], " "), }, nil + case errMonlistisfull: + // silence monlist full error, we don't care because we do it best-effort default: if msg.IsReply() { if len(msg.Params) < 2 { @@ -1238,12 +1315,16 @@ func (s *Session) newMessageEvent(msg Message) (ev MessageEvent, err error) { } func (s *Session) cleanUser(parted *User) { + nameCf := s.Casemap(parted.Name.Name) + if _, ok := s.monitors[nameCf]; ok { + return + } for _, c := range s.channels { if _, ok := c.Members[parted]; ok { return } } - delete(s.users, s.Casemap(parted.Name.Name)) + delete(s.users, nameCf) } func (s *Session) updateFeatures(features []string) { @@ -1305,6 +1386,11 @@ func (s *Session) updateFeatures(features []string) { if err == nil && linelen != 0 { s.linelen = linelen } + case "MONITOR": + monitor, err := strconv.Atoi(value) + if err == nil && monitor > 0 { + s.monitor = true + } case "PREFIX": if value == "" { s.prefixModes = "" diff --git a/irc/tokens.go b/irc/tokens.go index ab7b418..aaffc08 100644 --- a/irc/tokens.go +++ b/irc/tokens.go @@ -460,9 +460,10 @@ func ParseCaps(caps string) (diff []Cap) { // Member is a token in RPL_NAMREPLY's last parameter. type Member struct { - PowerLevel string - Name *Prefix - Away bool + PowerLevel string + Name *Prefix + Away bool + Disconnected bool } type members []Member diff --git a/ui/ui.go b/ui/ui.go index fe5ceef..de9f5b7 100644 --- a/ui/ui.go +++ b/ui/ui.go @@ -402,15 +402,27 @@ func drawVerticalMemberList(screen tcell.Screen, x0, y0, width, height int, memb width-- clearArea(screen, x0, y0, width, height) + padding := 1 + for _, m := range members { + if m.Disconnected { + padding = runeWidth(0x274C) + break + } + } + for i, m := range members[*offset:] { x := x0 y := y0 + i - if m.PowerLevel != "" { + if m.Disconnected { + disconnectedSt := tcell.StyleDefault.Foreground(tcell.ColorRed) + printString(screen, &x, y, Styled("\u274C", disconnectedSt)) + } else if m.PowerLevel != "" { + x += padding - 1 powerLevelText := m.PowerLevel[:1] powerLevelSt := tcell.StyleDefault.Foreground(tcell.ColorGreen) printString(screen, &x, y, Styled(powerLevelText, powerLevelSt)) } else { - x++ + x += padding } var name StyledString base-commit: 3904c9190d94f273c0ae9937d3161b9fe4adf856 -- 2.17.1