~taiite/public-inbox

senpai: MONITOR user with whom we have an open buffer v5 NEEDS REVISION

delthas: 1
 MONITOR user with whom we have an open buffer

 7 files changed, 145 insertions(+), 16 deletions(-)
Export patchset (mbox)
How do I use this?

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 -3
Learn more about email & git

[PATCH senpai v5] MONITOR user with whom we have an open buffer Export this patch

---
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.
}

// 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