commit 8d2b4a9

delthas  ·  2021-12-01 12:46:42 +0000 UTC
parent 8091752
MONITOR user with whom we have an open buffer
7 files changed,  +145, -16
M app.go
M app.go
+13, -0
 1@@ -95,6 +95,8 @@ type App struct {
 2 	lastNetID     string
 3 	lastBuffer    string
 4 
 5+	monitor map[string]map[string]struct{} // set of targets we want to monitor per netID, best-effort. netID->target->{}
 6+
 7 	lastMessageTime time.Time
 8 	lastCloseTime   time.Time
 9 }
10@@ -105,6 +107,7 @@ func NewApp(cfg Config) (app *App, err error) {
11 		events:        make(chan event, eventChanSize),
12 		cfg:           cfg,
13 		messageBounds: map[boundKey]bound{},
14+		monitor:       make(map[string]map[string]struct{}),
15 	}
16 
17 	if cfg.Highlights != nil {
18@@ -582,6 +585,9 @@ func (app *App) handleIRCEvent(netID string, ev interface{}) {
19 			s.Close()
20 		}
21 		app.sessions[netID] = s
22+		if _, ok := app.monitor[netID]; !ok {
23+			app.monitor[netID] = make(map[string]struct{})
24+		}
25 		return
26 	}
27 	if _, ok := ev.(irc.Typing); ok {
28@@ -633,6 +639,10 @@ func (app *App) handleIRCEvent(netID string, ev interface{}) {
29 			Head: "--",
30 			Body: ui.PlainString(body),
31 		})
32+		for target := range app.monitor[s.NetID()] {
33+			// TODO: batch MONITOR +
34+			s.MonitorAdd(target)
35+		}
36 	case irc.SelfNickEvent:
37 		var body ui.StyledStringBuilder
38 		body.WriteString(fmt.Sprintf("%s\u2192%s", ev.FormerNick, s.Nick()))
39@@ -729,6 +739,8 @@ func (app *App) handleIRCEvent(netID string, ev interface{}) {
40 		buffer, line, notification := app.formatMessage(s, ev)
41 		if buffer != "" && !s.IsChannel(buffer) {
42 			if _, added := app.win.AddBuffer(netID, "", buffer); added {
43+				app.monitor[netID][buffer] = struct{}{}
44+				s.MonitorAdd(buffer)
45 				s.NewHistoryRequest(buffer).
46 					WithLimit(500).
47 					Before(msg.TimeOrNow())
48@@ -750,6 +762,7 @@ func (app *App) handleIRCEvent(netID string, ev interface{}) {
49 			if s.IsChannel(target) {
50 				continue
51 			}
52+			s.MonitorAdd(target)
53 			app.win.AddBuffer(netID, "", target)
54 			// CHATHISTORY BEFORE excludes its bound, so add 1ms
55 			// (precision of the time tag) to include that last message.
+3, -0
 1@@ -341,6 +341,8 @@ func commandDoMsg(app *App, args []string) (err error) {
 2 			Time:            time.Now(),
 3 		})
 4 		if buffer != "" && !s.IsChannel(target) {
 5+			app.monitor[netID][buffer] = struct{}{}
 6+			s.MonitorAdd(buffer)
 7 			app.win.AddBuffer(netID, "", buffer)
 8 		}
 9 
10@@ -449,6 +451,7 @@ func commandDoQuery(app *App, args []string) (err error) {
11 	if s.IsChannel(target) {
12 		return fmt.Errorf("cannot query a channel, use JOIN instead")
13 	}
14+	s.MonitorAdd(target)
15 	i, _ := app.win.AddBuffer(netID, "", target)
16 	s.NewHistoryRequest(target).WithLimit(200).Before(time.Now())
17 	app.win.JumpBufferIndex(i)
+8, -0
 1@@ -50,6 +50,14 @@ type UserQuitEvent struct {
 2 	Time     time.Time
 3 }
 4 
 5+type UserOnlineEvent struct {
 6+	User string
 7+}
 8+
 9+type UserOfflineEvent struct {
10+	User string
11+}
12+
13 type TopicChangeEvent struct {
14 	Channel string
15 	Topic   string
+6, -0
 1@@ -87,6 +87,12 @@ const (
 2 	errUmodeunknownflag = "501" // :Unknown mode flag
 3 	errUsersdontmatch   = "502" // :Can't change mode for other users
 4 
 5+	rplMononline     = "730" // <nick> :target[!user@host][,target[!user@host]]*
 6+	rplMonoffline    = "731" // <nick> :target[,target2]*
 7+	rplMonlist       = "732" // <nick> :target[,target2]*
 8+	rplEndofmonlist  = "733" // <nick> :End of MONITOR list
 9+	errMonlistisfull = "734" // <nick> <limit> <targets> :Monitor list is full.
10+
11 	rplLoggedin    = "900" // <nick> <nick>!<ident>@<host> <account> :You are now logged in as <user>
12 	rplLoggedout   = "901" // <nick> <nick>!<ident>@<host> :You are now logged out
13 	errNicklocked  = "902" // :You must use a nick assigned to you
+97, -11
  1@@ -71,10 +71,11 @@ const (
  2 	TypingDone
  3 )
  4 
  5-// User is a known IRC user (we share a channel with it).
  6+// User is a known IRC user.
  7 type User struct {
  8-	Name *Prefix // the nick, user and hostname of the user if known.
  9-	Away bool    // whether the user is away or not
 10+	Name         *Prefix // the nick, user and hostname of the user if known.
 11+	Away         bool    // whether the user is away or not
 12+	Disconnected bool    // can only be true for monitored users.
 13 }
 14 
 15 // Channel is a joined channel.
 16@@ -124,6 +125,7 @@ type Session struct {
 17 	historyLimit  int
 18 	prefixSymbols string
 19 	prefixModes   string
 20+	monitor       bool
 21 
 22 	users          map[string]*User        // known users.
 23 	channels       map[string]Channel      // joined channels.
 24@@ -131,6 +133,7 @@ type Session struct {
 25 	chReqs         map[string]struct{}     // set of targets for which history is currently requested.
 26 	targetsBatchID string                  // ID of the channel history targets batch being processed.
 27 	targetsBatch   HistoryTargetsEvent     // channel history targets batch being processed.
 28+	monitors       map[string]struct{}     // set of users we want to monitor (and keep even if they are disconnected).
 29 
 30 	pendingChannels map[string]time.Time // set of join requests stamps for channels.
 31 }
 32@@ -158,6 +161,7 @@ func NewSession(out chan<- Message, params SessionParams) *Session {
 33 		channels:        map[string]Channel{},
 34 		chBatches:       map[string]HistoryEvent{},
 35 		chReqs:          map[string]struct{}{},
 36+		monitors:        map[string]struct{}{},
 37 		pendingChannels: map[string]time.Time{},
 38 	}
 39 
 40@@ -235,16 +239,18 @@ func (s *Session) Names(target string) []Member {
 41 			names = make([]Member, 0, len(c.Members))
 42 			for u, pl := range c.Members {
 43 				names = append(names, Member{
 44-					PowerLevel: pl,
 45-					Name:       u.Name.Copy(),
 46-					Away:       u.Away,
 47+					PowerLevel:   pl,
 48+					Name:         u.Name.Copy(),
 49+					Away:         u.Away,
 50+					Disconnected: u.Disconnected,
 51 				})
 52 			}
 53 		}
 54 	} else if u, ok := s.users[s.Casemap(target)]; ok {
 55 		names = append(names, Member{
 56-			Name: u.Name.Copy(),
 57-			Away: u.Away,
 58+			Name:         u.Name.Copy(),
 59+			Away:         u.Away,
 60+			Disconnected: u.Disconnected,
 61 		})
 62 		names = append(names, Member{
 63 			Name: &Prefix{
 64@@ -278,7 +284,7 @@ func (s *Session) TypingStops() <-chan Typing {
 65 
 66 func (s *Session) ChannelsSharedWith(name string) []string {
 67 	var user *User
 68-	if u, ok := s.users[s.Casemap(name)]; ok {
 69+	if u, ok := s.users[s.Casemap(name)]; ok && !u.Disconnected {
 70 		user = u
 71 	} else {
 72 		return nil
 73@@ -420,6 +426,26 @@ func (s *Session) TypingStop(target string) {
 74 	s.out <- NewMessage("TAGMSG", target).WithTag("+typing", "done")
 75 }
 76 
 77+func (s *Session) MonitorAdd(target string) {
 78+	targetCf := s.casemap(target)
 79+	if _, ok := s.monitors[targetCf]; !ok {
 80+		s.monitors[targetCf] = struct{}{}
 81+		if s.monitor {
 82+			s.out <- NewMessage("MONITOR", "+", target)
 83+		}
 84+	}
 85+}
 86+
 87+func (s *Session) MonitorRemove(target string) {
 88+	targetCf := s.casemap(target)
 89+	if _, ok := s.monitors[targetCf]; ok {
 90+		delete(s.monitors, targetCf)
 91+		if s.monitor {
 92+			s.out <- NewMessage("MONITOR", "-", target)
 93+		}
 94+	}
 95+}
 96+
 97 type HistoryRequest struct {
 98 	s       *Session
 99 	target  string
100@@ -603,12 +629,12 @@ func (s *Session) handleMessageRegistered(msg Message, playback bool) (Event, er
101 		if s.host == "" {
102 			s.out <- NewMessage("WHO", s.nick)
103 		}
104-		return RegisteredEvent{}, nil
105 	case rplIsupport:
106 		if len(msg.Params) < 3 {
107 			return nil, msg.errNotEnoughParams(3)
108 		}
109 		s.updateFeatures(msg.Params[1 : len(msg.Params)-1])
110+		return RegisteredEvent{}, nil
111 	case rplWhoreply:
112 		var nick, host, flags, username string
113 		if err := msg.ParseParams(nil, nil, &username, &host, nil, &nick, &flags, nil); err != nil {
114@@ -812,6 +838,7 @@ func (s *Session) handleMessageRegistered(msg Message, playback bool) (Event, er
115 		nickCf := s.Casemap(msg.Prefix.Name)
116 
117 		if u, ok := s.users[nickCf]; ok {
118+			u.Disconnected = true
119 			var channels []string
120 			for channelCf, c := range s.channels {
121 				if _, ok := c.Members[u]; ok {
122@@ -827,6 +854,54 @@ func (s *Session) handleMessageRegistered(msg Message, playback bool) (Event, er
123 				Time:     msg.TimeOrNow(),
124 			}, nil
125 		}
126+	case rplMononline:
127+		for _, target := range strings.Split(msg.Params[1], ",") {
128+			prefix := ParsePrefix(target)
129+			if prefix == nil {
130+				continue
131+			}
132+			nickCf := s.casemap(prefix.Name)
133+
134+			if _, ok := s.monitors[nickCf]; ok {
135+				u, ok := s.users[nickCf]
136+				if !ok {
137+					u = &User{
138+						Name: prefix,
139+					}
140+					s.users[nickCf] = u
141+				}
142+				if u.Disconnected {
143+					u.Disconnected = false
144+					return UserOnlineEvent{
145+						User: u.Name.Name,
146+					}, nil
147+				}
148+			}
149+		}
150+	case rplMonoffline:
151+		for _, target := range strings.Split(msg.Params[1], ",") {
152+			prefix := ParsePrefix(target)
153+			if prefix == nil {
154+				continue
155+			}
156+			nickCf := s.casemap(prefix.Name)
157+
158+			if _, ok := s.monitors[nickCf]; ok {
159+				u, ok := s.users[nickCf]
160+				if !ok {
161+					u = &User{
162+						Name: prefix,
163+					}
164+					s.users[nickCf] = u
165+				}
166+				if !u.Disconnected {
167+					u.Disconnected = true
168+					return UserOfflineEvent{
169+						User: u.Name.Name,
170+					}, nil
171+				}
172+			}
173+		}
174 	case rplNamreply:
175 		var channel, names string
176 		if err := msg.ParseParams(nil, nil, &channel, &names); err != nil {
177@@ -1205,6 +1280,8 @@ func (s *Session) handleMessageRegistered(msg Message, playback bool) (Event, er
178 			Code:     code,
179 			Message:  strings.Join(msg.Params[2:], " "),
180 		}, nil
181+	case errMonlistisfull:
182+		// silence monlist full error, we don't care because we do it best-effort
183 	default:
184 		if msg.IsReply() {
185 			if len(msg.Params) < 2 {
186@@ -1248,12 +1325,16 @@ func (s *Session) newMessageEvent(msg Message) (ev MessageEvent, err error) {
187 }
188 
189 func (s *Session) cleanUser(parted *User) {
190+	nameCf := s.Casemap(parted.Name.Name)
191+	if _, ok := s.monitors[nameCf]; ok {
192+		return
193+	}
194 	for _, c := range s.channels {
195 		if _, ok := c.Members[parted]; ok {
196 			return
197 		}
198 	}
199-	delete(s.users, s.Casemap(parted.Name.Name))
200+	delete(s.users, nameCf)
201 }
202 
203 func (s *Session) updateFeatures(features []string) {
204@@ -1315,6 +1396,11 @@ func (s *Session) updateFeatures(features []string) {
205 			if err == nil && linelen != 0 {
206 				s.linelen = linelen
207 			}
208+		case "MONITOR":
209+			monitor, err := strconv.Atoi(value)
210+			if err == nil && monitor > 0 {
211+				s.monitor = true
212+			}
213 		case "PREFIX":
214 			if value == "" {
215 				s.prefixModes = ""
+4, -3
 1@@ -460,9 +460,10 @@ func ParseCaps(caps string) (diff []Cap) {
 2 
 3 // Member is a token in RPL_NAMREPLY's last parameter.
 4 type Member struct {
 5-	PowerLevel string
 6-	Name       *Prefix
 7-	Away       bool
 8+	PowerLevel   string
 9+	Name         *Prefix
10+	Away         bool
11+	Disconnected bool
12 }
13 
14 type members []Member
+14, -2
 1@@ -403,15 +403,27 @@ func drawVerticalMemberList(screen tcell.Screen, x0, y0, width, height int, memb
 2 	width--
 3 	clearArea(screen, x0, y0, width, height)
 4 
 5+	padding := 1
 6+	for _, m := range members {
 7+		if m.Disconnected {
 8+			padding = runeWidth(0x274C)
 9+			break
10+		}
11+	}
12+
13 	for i, m := range members[*offset:] {
14 		x := x0
15 		y := y0 + i
16-		if m.PowerLevel != "" {
17+		if m.Disconnected {
18+			disconnectedSt := tcell.StyleDefault.Foreground(tcell.ColorRed)
19+			printString(screen, &x, y, Styled("\u274C", disconnectedSt))
20+		} else if m.PowerLevel != "" {
21+			x += padding - 1
22 			powerLevelText := m.PowerLevel[:1]
23 			powerLevelSt := tcell.StyleDefault.Foreground(tcell.ColorGreen)
24 			printString(screen, &x, y, Styled(powerLevelText, powerLevelSt))
25 		} else {
26-			x++
27+			x += padding
28 		}
29 
30 		var name StyledString