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
+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
M
ui/ui.go
+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