summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authordelthas <delthas@dille.cc>2021-11-19 12:33:01 +0100
committerHubert Hirtz <hubert@hirtz.pm>2021-11-19 13:39:04 +0100
commit6014ba12270933c74018f2914713ec6fb2ed4a77 (patch)
treefc7dee4491600afea3ea9ba23177591b3b507738
parentAlso write the last buffer on SIGTERM, SIGINT and SIGHUP (diff)
Add support for CHATHISTORY TARGETS
-rw-r--r--app.go34
-rw-r--r--cmd/senpai/main.go47
-rw-r--r--irc/events.go4
-rw-r--r--irc/session.go60
-rw-r--r--irc/tokens.go28
5 files changed, 137 insertions, 36 deletions
diff --git a/app.go b/app.go
index db5c776..d652b97 100644
--- a/app.go
+++ b/app.go
@@ -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, &timestamp); 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