commit 6014ba1

delthas  ·  2021-11-19 11:33:01 +0000 UTC
parent 7f70f10
Add support for CHATHISTORY TARGETS
5 files changed,  +137, -36
M app.go
M app.go
+34, -0
 1@@ -91,6 +91,9 @@ type App struct {
 2 	messageBounds map[boundKey]bound
 3 	lastNetID     string
 4 	lastBuffer    string
 5+
 6+	lastMessageTime time.Time
 7+	lastCloseTime   time.Time
 8 }
 9 
10 func NewApp(cfg Config) (app *App, err error) {
11@@ -153,6 +156,9 @@ func (app *App) SwitchToBuffer(netID, buffer string) {
12 }
13 
14 func (app *App) Run() {
15+	if app.lastCloseTime.IsZero() {
16+		app.lastCloseTime = time.Now()
17+	}
18 	go app.uiLoop()
19 	go app.ircLoop("")
20 	app.eventLoop()
21@@ -167,6 +173,14 @@ func (app *App) CurrentBuffer() (netID, buffer string) {
22 	return app.win.CurrentBuffer()
23 }
24 
25+func (app *App) LastMessageTime() time.Time {
26+	return app.lastMessageTime
27+}
28+
29+func (app *App) SetLastClose(t time.Time) {
30+	app.lastCloseTime = t
31+}
32+
33 // eventLoop retrieves events (in batches) from the event channel and handle
34 // them, then draws the interface after each batch is handled.
35 func (app *App) eventLoop() {
36@@ -589,6 +603,10 @@ func (app *App) handleIRCEvent(netID string, ev interface{}) {
37 		})
38 		return
39 	}
40+	t := msg.TimeOrNow()
41+	if t.After(app.lastMessageTime) {
42+		app.lastMessageTime = t
43+	}
44 
45 	// Mutate UI state
46 	switch ev := ev.(type) {
47@@ -598,6 +616,9 @@ func (app *App) handleIRCEvent(netID string, ev interface{}) {
48 			// TODO: support autojoining channels with keys
49 			s.Join(channel, "")
50 		}
51+		s.NewHistoryRequest("").
52+			WithLimit(1000).
53+			Targets(app.lastCloseTime, msg.TimeOrNow())
54 		body := "Connected to the server"
55 		if s.Nick() != app.cfg.Nick {
56 			body = fmt.Sprintf("Connected to the server as %s", s.Nick())
57@@ -768,6 +789,19 @@ func (app *App) handleIRCEvent(netID string, ev interface{}) {
58 		bounds := app.messageBounds[boundKey{netID, ev.Target}]
59 		bounds.Update(&line)
60 		app.messageBounds[boundKey{netID, ev.Target}] = bounds
61+	case irc.HistoryTargetsEvent:
62+		for target, last := range ev.Targets {
63+			if s.IsChannel(target) {
64+				continue
65+			}
66+			app.win.AddBuffer(netID, "", target)
67+			// CHATHISTORY BEFORE excludes its bound, so add 1ms
68+			// (precision of the time tag) to include that last message.
69+			last = last.Add(1 * time.Millisecond)
70+			s.NewHistoryRequest(target).
71+				WithLimit(200).
72+				Before(last)
73+		}
74 	case irc.HistoryEvent:
75 		var linesBefore []ui.Line
76 		var linesAfter []ui.Line
+40, -7
 1@@ -52,6 +52,7 @@ func main() {
 2 
 3 	lastNetID, lastBuffer := getLastBuffer()
 4 	app.SwitchToBuffer(lastNetID, lastBuffer)
 5+	app.SetLastClose(getLastStamp())
 6 
 7 	sigCh := make(chan os.Signal, 1)
 8 	signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP)
 9@@ -64,25 +65,28 @@ func main() {
10 	app.Run()
11 	app.Close()
12 	writeLastBuffer(app)
13+	writeLastStamp(app)
14 }
15 
16-func getLastBufferPath() string {
17+func cachePath() string {
18 	cacheDir, err := os.UserCacheDir()
19 	if err != nil {
20 		panic(err)
21 	}
22-	cachePath := path.Join(cacheDir, "senpai")
23-	err = os.MkdirAll(cachePath, 0755)
24+	cache := path.Join(cacheDir, "senpai")
25+	err = os.MkdirAll(cache, 0755)
26 	if err != nil {
27 		panic(err)
28 	}
29+	return cache
30+}
31 
32-	lastBufferPath := path.Join(cachePath, "lastbuffer.txt")
33-	return lastBufferPath
34+func lastBufferPath() string {
35+	return path.Join(cachePath(), "lastbuffer.txt")
36 }
37 
38 func getLastBuffer() (netID, buffer string) {
39-	buf, err := ioutil.ReadFile(getLastBufferPath())
40+	buf, err := ioutil.ReadFile(lastBufferPath())
41 	if err != nil {
42 		return "", ""
43 	}
44@@ -96,10 +100,39 @@ func getLastBuffer() (netID, buffer string) {
45 }
46 
47 func writeLastBuffer(app *senpai.App) {
48-	lastBufferPath := getLastBufferPath()
49+	lastBufferPath := lastBufferPath()
50 	lastNetID, lastBuffer := app.CurrentBuffer()
51 	err := os.WriteFile(lastBufferPath, []byte(fmt.Sprintf("%s %s", lastNetID, lastBuffer)), 0666)
52 	if err != nil {
53 		fmt.Fprintf(os.Stderr, "failed to write last buffer at %q: %s\n", lastBufferPath, err)
54 	}
55 }
56+
57+func lastStampPath() string {
58+	return path.Join(cachePath(), "laststamp.txt")
59+}
60+
61+func getLastStamp() time.Time {
62+	buf, err := ioutil.ReadFile(lastStampPath())
63+	if err != nil {
64+		return time.Time{}
65+	}
66+
67+	t, err := time.Parse(time.RFC3339Nano, string(buf))
68+	if err != nil {
69+		return time.Time{}
70+	}
71+	return t
72+}
73+
74+func writeLastStamp(app *senpai.App) {
75+	lastStampPath := lastStampPath()
76+	last := app.LastMessageTime()
77+	if last.IsZero() {
78+		return
79+	}
80+	err := os.WriteFile(lastStampPath, []byte(last.Format(time.RFC3339Nano)), 0666)
81+	if err != nil {
82+		fmt.Fprintf(os.Stderr, "failed to write last stamp at %q: %s\n", lastStampPath, err)
83+	}
84+}
+4, -0
 1@@ -76,6 +76,10 @@ type HistoryEvent struct {
 2 	Messages []Event
 3 }
 4 
 5+type HistoryTargetsEvent struct {
 6+	Targets map[string]time.Time
 7+}
 8+
 9 type BouncerNetworkEvent struct {
10 	ID   string
11 	Name string
+45, -15
  1@@ -124,10 +124,12 @@ type Session struct {
  2 	prefixSymbols string
  3 	prefixModes   string
  4 
  5-	users     map[string]*User        // known users.
  6-	channels  map[string]Channel      // joined channels.
  7-	chBatches map[string]HistoryEvent // channel history batches being processed.
  8-	chReqs    map[string]struct{}     // set of targets for which history is currently requested.
  9+	users          map[string]*User        // known users.
 10+	channels       map[string]Channel      // joined channels.
 11+	chBatches      map[string]HistoryEvent // channel history batches being processed.
 12+	chReqs         map[string]struct{}     // set of targets for which history is currently requested.
 13+	targetsBatchID string                  // ID of the channel history targets batch being processed.
 14+	targetsBatch   HistoryTargetsEvent     // channel history targets batch being processed.
 15 
 16 	pendingChannels map[string]time.Time // set of join requests stamps for channels.
 17 }
 18@@ -228,16 +230,16 @@ func (s *Session) Names(target string) []Member {
 19 			names = make([]Member, 0, len(c.Members))
 20 			for u, pl := range c.Members {
 21 				names = append(names, Member{
 22-					PowerLevel:   pl,
 23-					Name:         u.Name.Copy(),
 24-					Away:         u.Away,
 25+					PowerLevel: pl,
 26+					Name:       u.Name.Copy(),
 27+					Away:       u.Away,
 28 				})
 29 			}
 30 		}
 31 	} else if u, ok := s.users[s.Casemap(target)]; ok {
 32 		names = append(names, Member{
 33-			Name:         u.Name.Copy(),
 34-			Away:         u.Away,
 35+			Name: u.Name.Copy(),
 36+			Away: u.Away,
 37 		})
 38 		names = append(names, Member{
 39 			Name: &Prefix{
 40@@ -448,7 +450,9 @@ func (r *HistoryRequest) doRequest() {
 41 
 42 	args := make([]string, 0, len(r.bounds)+3)
 43 	args = append(args, r.command)
 44-	args = append(args, r.target)
 45+	if r.target != "" {
 46+		args = append(args, r.target)
 47+	}
 48 	args = append(args, r.bounds...)
 49 	args = append(args, strconv.Itoa(r.limit))
 50 	r.s.out <- NewMessage("CHATHISTORY", args...)
 51@@ -466,6 +470,13 @@ func (r *HistoryRequest) Before(t time.Time) {
 52 	r.doRequest()
 53 }
 54 
 55+func (r *HistoryRequest) Targets(start time.Time, end time.Time) {
 56+	r.command = "TARGETS"
 57+	r.bounds = []string{formatTimestamp(start), formatTimestamp(end)}
 58+	r.target = ""
 59+	r.doRequest()
 60+}
 61+
 62 func (s *Session) NewHistoryRequest(target string) *HistoryRequest {
 63 	return &HistoryRequest{
 64 		s:      s,
 65@@ -505,7 +516,17 @@ func (s *Session) handleUnregistered(msg Message) (Event, error) {
 66 
 67 func (s *Session) handleRegistered(msg Message) (Event, error) {
 68 	if id, ok := msg.Tags["batch"]; ok {
 69-		if b, ok := s.chBatches[id]; ok {
 70+		if id == s.targetsBatchID {
 71+			var target, timestamp string
 72+			if err := msg.ParseParams(nil, &target, &timestamp); err != nil {
 73+				return nil, err
 74+			}
 75+			t, ok := parseTimestamp(timestamp)
 76+			if !ok {
 77+				return nil, nil
 78+			}
 79+			s.targetsBatch.Targets[target] = t
 80+		} else if b, ok := s.chBatches[id]; ok {
 81 			ev, err := s.newMessageEvent(msg)
 82 			if err != nil {
 83 				return nil, err
 84@@ -1002,11 +1023,20 @@ func (s *Session) handleRegistered(msg Message) (Event, error) {
 85 				}
 86 
 87 				s.chBatches[id] = HistoryEvent{Target: target}
 88+			case "draft/chathistory-targets":
 89+				s.targetsBatchID = id
 90+				s.targetsBatch = HistoryTargetsEvent{Targets: make(map[string]time.Time)}
 91+			}
 92+		} else {
 93+			if b, ok := s.chBatches[id]; ok {
 94+				delete(s.chBatches, id)
 95+				delete(s.chReqs, s.Casemap(b.Target))
 96+				return b, nil
 97+			} else if s.targetsBatchID == id {
 98+				s.targetsBatchID = ""
 99+				delete(s.chReqs, "")
100+				return s.targetsBatch, nil
101 			}
102-		} else if b, ok := s.chBatches[id]; ok {
103-			delete(s.chBatches, id)
104-			delete(s.chReqs, s.Casemap(b.Target))
105-			return b, nil
106 		}
107 	case "NICK":
108 		if msg.Prefix == nil {
+14, -14
 1@@ -360,26 +360,26 @@ func (msg *Message) ParseParams(out ...*string) error {
 2 	return nil
 3 }
 4 
 5-// Time returns the time when the message has been sent, if present.
 6-func (msg *Message) Time() (t time.Time, ok bool) {
 7-	var tag string
 8+func parseTimestamp(timestamp string) (time.Time, bool) {
 9 	var year, month, day, hour, minute, second, millis int
10 
11-	tag, ok = msg.Tags["time"]
12-	if !ok {
13-		return
14-	}
15-
16-	tag = strings.TrimSuffix(tag, "Z")
17+	timestamp = strings.TrimSuffix(timestamp, "Z")
18 
19-	_, err := fmt.Sscanf(tag, "%4d-%2d-%2dT%2d:%2d:%2d.%3d", &year, &month, &day, &hour, &minute, &second, &millis)
20+	_, err := fmt.Sscanf(timestamp, "%4d-%2d-%2dT%2d:%2d:%2d.%3d", &year, &month, &day, &hour, &minute, &second, &millis)
21 	if err != nil || month < 1 || 12 < month {
22-		ok = false
23-		return
24+		return time.Time{}, false
25 	}
26 
27-	t = time.Date(year, time.Month(month), day, hour, minute, second, millis*1e6, time.UTC)
28-	return
29+	return time.Date(year, time.Month(month), day, hour, minute, second, millis*1e6, time.UTC), true
30+}
31+
32+// Time returns the time when the message has been sent, if present.
33+func (msg *Message) Time() (t time.Time, ok bool) {
34+	tag, ok := msg.Tags["time"]
35+	if !ok {
36+		return time.Time{}, false
37+	}
38+	return parseTimestamp(tag)
39 }
40 
41 // TimeOrNow returns the time when the message has been sent, or time.Now() if