diff options
author | delthas <delthas@dille.cc> | 2021-11-19 12:33:01 +0100 |
---|---|---|
committer | Hubert Hirtz <hubert@hirtz.pm> | 2021-11-19 13:39:04 +0100 |
commit | 6014ba12270933c74018f2914713ec6fb2ed4a77 (patch) | |
tree | fc7dee4491600afea3ea9ba23177591b3b507738 | |
parent | Also write the last buffer on SIGTERM, SIGINT and SIGHUP (diff) |
Add support for CHATHISTORY TARGETS
-rw-r--r-- | app.go | 34 | ||||
-rw-r--r-- | cmd/senpai/main.go | 47 | ||||
-rw-r--r-- | irc/events.go | 4 | ||||
-rw-r--r-- | irc/session.go | 60 | ||||
-rw-r--r-- | irc/tokens.go | 28 |
5 files changed, 137 insertions, 36 deletions
@@ -91,6 +91,9 @@ type App struct { messageBounds map[boundKey]bound lastNetID string lastBuffer string + + lastMessageTime time.Time + lastCloseTime time.Time } func NewApp(cfg Config) (app *App, err error) { @@ -153,6 +156,9 @@ func (app *App) SwitchToBuffer(netID, buffer string) { } func (app *App) Run() { + if app.lastCloseTime.IsZero() { + app.lastCloseTime = time.Now() + } go app.uiLoop() go app.ircLoop("") app.eventLoop() @@ -167,6 +173,14 @@ func (app *App) CurrentBuffer() (netID, buffer string) { return app.win.CurrentBuffer() } +func (app *App) LastMessageTime() time.Time { + return app.lastMessageTime +} + +func (app *App) SetLastClose(t time.Time) { + app.lastCloseTime = t +} + // eventLoop retrieves events (in batches) from the event channel and handle // them, then draws the interface after each batch is handled. func (app *App) eventLoop() { @@ -589,6 +603,10 @@ func (app *App) handleIRCEvent(netID string, ev interface{}) { }) return } + t := msg.TimeOrNow() + if t.After(app.lastMessageTime) { + app.lastMessageTime = t + } // Mutate UI state switch ev := ev.(type) { @@ -598,6 +616,9 @@ func (app *App) handleIRCEvent(netID string, ev interface{}) { // TODO: support autojoining channels with keys s.Join(channel, "") } + s.NewHistoryRequest(""). + WithLimit(1000). + Targets(app.lastCloseTime, msg.TimeOrNow()) body := "Connected to the server" if s.Nick() != app.cfg.Nick { body = fmt.Sprintf("Connected to the server as %s", s.Nick()) @@ -768,6 +789,19 @@ func (app *App) handleIRCEvent(netID string, ev interface{}) { bounds := app.messageBounds[boundKey{netID, ev.Target}] bounds.Update(&line) app.messageBounds[boundKey{netID, ev.Target}] = bounds + case irc.HistoryTargetsEvent: + for target, last := range ev.Targets { + if s.IsChannel(target) { + continue + } + app.win.AddBuffer(netID, "", target) + // CHATHISTORY BEFORE excludes its bound, so add 1ms + // (precision of the time tag) to include that last message. + last = last.Add(1 * time.Millisecond) + s.NewHistoryRequest(target). + WithLimit(200). + Before(last) + } case irc.HistoryEvent: var linesBefore []ui.Line var linesAfter []ui.Line diff --git a/cmd/senpai/main.go b/cmd/senpai/main.go index 46ad1bb..675228f 100644 --- a/cmd/senpai/main.go +++ b/cmd/senpai/main.go @@ -52,6 +52,7 @@ func main() { lastNetID, lastBuffer := getLastBuffer() app.SwitchToBuffer(lastNetID, lastBuffer) + app.SetLastClose(getLastStamp()) sigCh := make(chan os.Signal, 1) signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP) @@ -64,25 +65,28 @@ func main() { app.Run() app.Close() writeLastBuffer(app) + writeLastStamp(app) } -func getLastBufferPath() string { +func cachePath() string { cacheDir, err := os.UserCacheDir() if err != nil { panic(err) } - cachePath := path.Join(cacheDir, "senpai") - err = os.MkdirAll(cachePath, 0755) + cache := path.Join(cacheDir, "senpai") + err = os.MkdirAll(cache, 0755) if err != nil { panic(err) } + return cache +} - lastBufferPath := path.Join(cachePath, "lastbuffer.txt") - return lastBufferPath +func lastBufferPath() string { + return path.Join(cachePath(), "lastbuffer.txt") } func getLastBuffer() (netID, buffer string) { - buf, err := ioutil.ReadFile(getLastBufferPath()) + buf, err := ioutil.ReadFile(lastBufferPath()) if err != nil { return "", "" } @@ -96,10 +100,39 @@ func getLastBuffer() (netID, buffer string) { } func writeLastBuffer(app *senpai.App) { - lastBufferPath := getLastBufferPath() + lastBufferPath := lastBufferPath() lastNetID, lastBuffer := app.CurrentBuffer() err := os.WriteFile(lastBufferPath, []byte(fmt.Sprintf("%s %s", lastNetID, lastBuffer)), 0666) if err != nil { fmt.Fprintf(os.Stderr, "failed to write last buffer at %q: %s\n", lastBufferPath, err) } } + +func lastStampPath() string { + return path.Join(cachePath(), "laststamp.txt") +} + +func getLastStamp() time.Time { + buf, err := ioutil.ReadFile(lastStampPath()) + if err != nil { + return time.Time{} + } + + t, err := time.Parse(time.RFC3339Nano, string(buf)) + if err != nil { + return time.Time{} + } + return t +} + +func writeLastStamp(app *senpai.App) { + lastStampPath := lastStampPath() + last := app.LastMessageTime() + if last.IsZero() { + return + } + err := os.WriteFile(lastStampPath, []byte(last.Format(time.RFC3339Nano)), 0666) + if err != nil { + fmt.Fprintf(os.Stderr, "failed to write last stamp at %q: %s\n", lastStampPath, err) + } +} diff --git a/irc/events.go b/irc/events.go index c90f518..70a6c1d 100644 --- a/irc/events.go +++ b/irc/events.go @@ -76,6 +76,10 @@ type HistoryEvent struct { Messages []Event } +type HistoryTargetsEvent struct { + Targets map[string]time.Time +} + type BouncerNetworkEvent struct { ID string Name string diff --git a/irc/session.go b/irc/session.go index 7e1c664..8467f2c 100644 --- a/irc/session.go +++ b/irc/session.go @@ -124,10 +124,12 @@ type Session struct { prefixSymbols string prefixModes string - users map[string]*User // known users. - channels map[string]Channel // joined channels. - chBatches map[string]HistoryEvent // channel history batches being processed. - chReqs map[string]struct{} // set of targets for which history is currently requested. + users map[string]*User // known users. + channels map[string]Channel // joined channels. + chBatches map[string]HistoryEvent // channel history batches being processed. + 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. pendingChannels map[string]time.Time // set of join requests stamps for channels. } @@ -228,16 +230,16 @@ 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, }) } } } 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, }) names = append(names, Member{ Name: &Prefix{ @@ -448,7 +450,9 @@ func (r *HistoryRequest) doRequest() { args := make([]string, 0, len(r.bounds)+3) args = append(args, r.command) - args = append(args, r.target) + if r.target != "" { + args = append(args, r.target) + } args = append(args, r.bounds...) args = append(args, strconv.Itoa(r.limit)) r.s.out <- NewMessage("CHATHISTORY", args...) @@ -466,6 +470,13 @@ func (r *HistoryRequest) Before(t time.Time) { r.doRequest() } +func (r *HistoryRequest) Targets(start time.Time, end time.Time) { + r.command = "TARGETS" + r.bounds = []string{formatTimestamp(start), formatTimestamp(end)} + r.target = "" + r.doRequest() +} + func (s *Session) NewHistoryRequest(target string) *HistoryRequest { return &HistoryRequest{ s: s, @@ -505,7 +516,17 @@ func (s *Session) handleUnregistered(msg Message) (Event, error) { func (s *Session) handleRegistered(msg Message) (Event, error) { if id, ok := msg.Tags["batch"]; ok { - if b, ok := s.chBatches[id]; ok { + if id == s.targetsBatchID { + var target, timestamp string + if err := msg.ParseParams(nil, &target, ×tamp); err != nil { + return nil, err + } + t, ok := parseTimestamp(timestamp) + if !ok { + return nil, nil + } + s.targetsBatch.Targets[target] = t + } else if b, ok := s.chBatches[id]; ok { ev, err := s.newMessageEvent(msg) if err != nil { return nil, err @@ -1002,11 +1023,20 @@ func (s *Session) handleRegistered(msg Message) (Event, error) { } s.chBatches[id] = HistoryEvent{Target: target} + case "draft/chathistory-targets": + s.targetsBatchID = id + s.targetsBatch = HistoryTargetsEvent{Targets: make(map[string]time.Time)} + } + } else { + if b, ok := s.chBatches[id]; ok { + delete(s.chBatches, id) + delete(s.chReqs, s.Casemap(b.Target)) + return b, nil + } else if s.targetsBatchID == id { + s.targetsBatchID = "" + delete(s.chReqs, "") + return s.targetsBatch, nil } - } else if b, ok := s.chBatches[id]; ok { - delete(s.chBatches, id) - delete(s.chReqs, s.Casemap(b.Target)) - return b, nil } case "NICK": if msg.Prefix == nil { diff --git a/irc/tokens.go b/irc/tokens.go index b28ef12..ab7b418 100644 --- a/irc/tokens.go +++ b/irc/tokens.go @@ -360,26 +360,26 @@ func (msg *Message) ParseParams(out ...*string) error { return nil } -// Time returns the time when the message has been sent, if present. -func (msg *Message) Time() (t time.Time, ok bool) { - var tag string +func parseTimestamp(timestamp string) (time.Time, bool) { var year, month, day, hour, minute, second, millis int - tag, ok = msg.Tags["time"] - if !ok { - return - } - - tag = strings.TrimSuffix(tag, "Z") + timestamp = strings.TrimSuffix(timestamp, "Z") - _, err := fmt.Sscanf(tag, "%4d-%2d-%2dT%2d:%2d:%2d.%3d", &year, &month, &day, &hour, &minute, &second, &millis) + _, err := fmt.Sscanf(timestamp, "%4d-%2d-%2dT%2d:%2d:%2d.%3d", &year, &month, &day, &hour, &minute, &second, &millis) if err != nil || month < 1 || 12 < month { - ok = false - return + return time.Time{}, false } - t = time.Date(year, time.Month(month), day, hour, minute, second, millis*1e6, time.UTC) - return + return time.Date(year, time.Month(month), day, hour, minute, second, millis*1e6, time.UTC), true +} + +// Time returns the time when the message has been sent, if present. +func (msg *Message) Time() (t time.Time, ok bool) { + tag, ok := msg.Tags["time"] + if !ok { + return time.Time{}, false + } + return parseTimestamp(tag) } // TimeOrNow returns the time when the message has been sent, or time.Now() if |