diff options
author | delthas <delthas@dille.cc> | 2021-12-01 13:46:42 +0100 |
---|---|---|
committer | delthas <delthas@dille.cc> | 2022-02-10 11:21:26 +0100 |
commit | 8d2b4a928c2783657cccbfe7b9aa5a9f77be3c9e (patch) | |
tree | 6212dabe20439b901bf267dd8d1a1666376aa17b | |
parent | docs: update Ctrl-C behaviour (diff) |
MONITOR user with whom we have an open buffer
-rw-r--r-- | app.go | 13 | ||||
-rw-r--r-- | commands.go | 3 | ||||
-rw-r--r-- | irc/events.go | 8 | ||||
-rw-r--r-- | irc/rpl.go | 6 | ||||
-rw-r--r-- | irc/session.go | 108 | ||||
-rw-r--r-- | irc/tokens.go | 7 | ||||
-rw-r--r-- | ui/ui.go | 16 |
7 files changed, 145 insertions, 16 deletions
@@ -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 { @@ -582,6 +585,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 { @@ -633,6 +639,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())) @@ -729,6 +739,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(500). Before(msg.TimeOrNow()) @@ -750,6 +762,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 403aec8..09582b6 100644 --- a/commands.go +++ b/commands.go @@ -341,6 +341,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) } @@ -449,6 +451,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 @@ -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 3dd979c..96632bf 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 @@ -603,12 +629,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 { @@ -812,6 +838,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 { @@ -827,6 +854,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 { @@ -1205,6 +1280,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 { @@ -1248,12 +1325,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) { @@ -1315,6 +1396,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 @@ -403,15 +403,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 |