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
+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, ×tamp); 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