commit 3d338ee

Hubert Hirtz  ·  2020-06-13 16:05:55 +0000 UTC
parent 33f6a5b
draft/chathistory support???
6 files changed,  +238, -56
+55, -7
 1@@ -42,9 +42,9 @@ func main() {
 2 	}
 3 
 4 	s, err := irc.NewSession(conn, irc.SessionParams{
 5-		Nickname: "taiite",
 6-		Username: "taiitent",
 7-		RealName: "Taiite Ier",
 8+		Nickname: "ME",
 9+		Username: "MEMEMEMEMEM",
10+		RealName: "Le me",
11 		Auth:     &irc.SASLPlain{Username: cfg.User, Password: cfg.Password},
12 	})
13 	if err != nil {
14@@ -71,6 +71,29 @@ func main() {
15 			case irc.ChannelMessageEvent:
16 				line := formatIRCMessage(ev.Nick, ev.Content)
17 				app.AddLine(ev.Channel, line, ev.Time, false)
18+			case irc.HistoryEvent:
19+				var lines []ui.Line
20+				var lastT time.Time
21+				isChannel := ev.Target[0] == '#'
22+				for _, m := range ev.Messages {
23+					switch m := m.(type) {
24+					case irc.ChannelMessageEvent:
25+						if isChannel {
26+							line := formatIRCMessage(m.Nick, m.Content)
27+							line = strings.TrimRight(line, "\t ")
28+							if lastT.Truncate(time.Minute) != m.Time.Truncate(time.Minute) {
29+								lastT = m.Time
30+								hour := lastT.Hour()
31+								minute := lastT.Minute()
32+								line = fmt.Sprintf("\x02%02d:%02d\x00 %s", hour, minute, line)
33+							}
34+							lines = append(lines, ui.NewLine(m.Time, false, line))
35+						} else {
36+							panic("TODO")
37+						}
38+					}
39+				}
40+				app.AddHistoryLines(ev.Target, lines)
41 			case error:
42 				log.Panicln(ev)
43 			}
44@@ -85,22 +108,47 @@ func main() {
45 				case tcell.KeyCtrlL:
46 					app.Resize()
47 				case tcell.KeyCtrlU:
48+					fallthrough
49+				case tcell.KeyPgUp:
50 					app.ScrollUp()
51+					if app.IsAtTop() {
52+						buffer := app.CurrentBuffer()
53+						t := app.CurrentBufferOldestTime()
54+						s.RequestHistory(buffer, t)
55+					}
56 				case tcell.KeyCtrlD:
57+					fallthrough
58+				case tcell.KeyPgDn:
59 					app.ScrollDown()
60 				case tcell.KeyCtrlN:
61-					app.NextBuffer()
62+					if app.NextBuffer() && app.IsAtTop() {
63+						buffer := app.CurrentBuffer()
64+						t := app.CurrentBufferOldestTime()
65+						s.RequestHistory(buffer, t)
66+					}
67 				case tcell.KeyCtrlP:
68-					app.PreviousBuffer()
69+					if app.PreviousBuffer() && app.IsAtTop() {
70+						buffer := app.CurrentBuffer()
71+						t := app.CurrentBufferOldestTime()
72+						s.RequestHistory(buffer, t)
73+					}
74 				case tcell.KeyRight:
75 					if ev.Modifiers() == tcell.ModAlt {
76-						app.NextBuffer()
77+						if app.NextBuffer() && app.IsAtTop() {
78+							buffer := app.CurrentBuffer()
79+							t := app.CurrentBufferOldestTime()
80+							s.RequestHistory(buffer, t)
81+						}
82 					} else {
83 						app.InputRight()
84 					}
85 				case tcell.KeyLeft:
86 					if ev.Modifiers() == tcell.ModAlt {
87-						app.PreviousBuffer()
88+						if app.PreviousBuffer() && app.IsAtTop() {
89+							buffer := app.CurrentBuffer()
90+							t := app.CurrentBufferOldestTime()
91+							s.RequestHistory(buffer, t)
92+						}
93 					} else {
94 						app.InputLeft()
95 					}
+5, -0
1@@ -61,3 +61,8 @@ type ChannelMessageEvent struct {
2 	Content string
3 	Time    time.Time
4 }
5+
6+type HistoryEvent struct {
7+	Target   string
8+	Messages []Event
9+}
+90, -37
  1@@ -47,6 +47,7 @@ var SupportedCapabilities = map[string]struct{}{
  2 	"away-notify":       {},
  3 	"batch":             {},
  4 	"cap-notify":        {},
  5+	"draft/chathistory": {},
  6 	"echo-message":      {},
  7 	"extended-join":     {},
  8 	"invite-notify":     {},
  9@@ -92,7 +93,7 @@ type (
 10 	}
 11 
 12 	actionPrivMsg struct {
 13-		Channel string
 14+		Target  string
 15 		Content string
 16 	}
 17 
 18@@ -102,6 +103,11 @@ type (
 19 	actionTypingStop struct {
 20 		Channel string
 21 	}
 22+
 23+	actionRequestHistory struct {
 24+		Target string
 25+		Before time.Time
 26+	}
 27 )
 28 
 29 type SessionParams struct {
 30@@ -137,16 +143,17 @@ type Session struct {
 31 	enabledCaps   map[string]struct{}
 32 	features      map[string]string
 33 
 34-	users    map[string]User
 35-	channels map[string]Channel
 36+	users     map[string]User
 37+	channels  map[string]Channel
 38+	chBatches map[string]HistoryEvent
 39 }
 40 
 41 func NewSession(conn io.ReadWriteCloser, params SessionParams) (s Session, err error) {
 42 	s = Session{
 43 		conn:          conn,
 44-		msgs:          make(chan Message, 128),
 45-		acts:          make(chan action, 128),
 46-		evts:          make(chan Event, 128),
 47+		msgs:          make(chan Message, 16),
 48+		acts:          make(chan action, 16),
 49+		evts:          make(chan Event, 16),
 50 		typingStamps:  map[string]time.Time{},
 51 		nick:          params.Nickname,
 52 		lNick:         strings.ToLower(params.Nickname),
 53@@ -158,6 +165,7 @@ func NewSession(conn io.ReadWriteCloser, params SessionParams) (s Session, err e
 54 		features:      map[string]string{},
 55 		users:         map[string]User{},
 56 		channels:      map[string]Channel{},
 57+		chBatches:     map[string]HistoryEvent{},
 58 	}
 59 
 60 	s.running.Store(true)
 61@@ -230,12 +238,12 @@ func (s *Session) part(act actionPart) (err error) {
 62 	return
 63 }
 64 
 65-func (s *Session) PrivMsg(channel, content string) {
 66-	s.acts <- actionPrivMsg{channel, content}
 67+func (s *Session) PrivMsg(target, content string) {
 68+	s.acts <- actionPrivMsg{target, content}
 69 }
 70 
 71 func (s *Session) privMsg(act actionPrivMsg) (err error) {
 72-	err = s.send("PRIVMSG %s :%s\r\n", act.Channel, act.Content)
 73+	err = s.send("PRIVMSG %s :%s\r\n", act.Target, act.Content)
 74 	return
 75 }
 76 
 77@@ -261,8 +269,8 @@ func (s *Session) typing(act actionTyping) (err error) {
 78 	return
 79 }
 80 
 81-func (s *Session) TypingStop(to string) {
 82-	s.acts <- actionTypingStop{to}
 83+func (s *Session) TypingStop(channel string) {
 84+	s.acts <- actionTypingStop{channel}
 85 }
 86 
 87 func (s *Session) typingStop(act actionTypingStop) (err error) {
 88@@ -274,6 +282,21 @@ func (s *Session) typingStop(act actionTypingStop) (err error) {
 89 	return
 90 }
 91 
 92+func (s *Session) RequestHistory(target string, before time.Time) {
 93+	s.acts <- actionRequestHistory{target, before}
 94+}
 95+
 96+func (s *Session) requestHistory(act actionRequestHistory) (err error) {
 97+	if _, ok := s.enabledCaps["draft/chathistory"]; !ok {
 98+		return
 99+	}
100+
101+	t := act.Before
102+	err = s.send("CHATHISTORY BEFORE %s timestamp=%04d-%02d-%02dT%02d:%02d:%02d.%03dZ 100\r\n", act.Target, t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), t.Nanosecond()/1e6)
103+
104+	return
105+}
106+
107 func (s *Session) run() {
108 	for s.Running() {
109 		var (
110@@ -294,6 +317,8 @@ func (s *Session) run() {
111 				err = s.typing(act)
112 			case actionTypingStop:
113 				err = s.typingStop(act)
114+			case actionRequestHistory:
115+				err = s.requestHistory(act)
116 			}
117 		case msg := <-s.msgs:
118 			if s.state == ConnStart {
119@@ -415,6 +440,16 @@ func (s *Session) handleStart(msg Message) (ev Event, err error) {
120 }
121 
122 func (s *Session) handle(msg Message) (ev Event, err error) {
123+	if id, ok := msg.Tags["batch"]; ok {
124+		if b, ok := s.chBatches[id]; ok {
125+			s.chBatches[id] = HistoryEvent{
126+				Target:   b.Target,
127+				Messages: append(b.Messages, s.privmsgToEvent(msg)),
128+			}
129+			return
130+		}
131+	}
132+
133 	switch msg.Command {
134 	case "001": // RPL_WELCOME
135 		s.nick = msg.Params[0]
136@@ -608,33 +643,19 @@ func (s *Session) handle(msg Message) (ev Event, err error) {
137 			c.Topic = msg.Params[2]
138 		}
139 	case "PRIVMSG":
140-		nick, _, _ := FullMask(msg.Prefix)
141-		target := strings.ToLower(msg.Params[0])
142-
143-		if target == s.lNick {
144-			// PRIVMSG to self
145-			t, ok := msg.Time()
146-			if !ok {
147-				t = time.Now()
148-			}
149-			ev = QueryMessageEvent{
150-				UserEvent: UserEvent{Nick: nick},
151-				Content:   msg.Params[1],
152-				Time:      t,
153-			}
154-		} else if _, ok := s.channels[target]; ok {
155-			// PRIVMSG to channel
156-			t, ok := msg.Time()
157-			if !ok {
158-				t = time.Now()
159-			}
160-			ev = ChannelMessageEvent{
161-				UserEvent:    UserEvent{Nick: nick},
162-				ChannelEvent: ChannelEvent{Channel: msg.Params[0]},
163-				Content:      msg.Params[1],
164-				Time:         t,
165-			}
166+		ev = s.privmsgToEvent(msg)
167+	case "BATCH":
168+		batchStart := msg.Params[0][0] == '+'
169+		id := msg.Params[0][1:]
170+
171+		if batchStart && msg.Params[1] == "chathistory" {
172+			s.chBatches[id] = HistoryEvent{Target: msg.Params[2]}
173+		} else if b, ok := s.chBatches[id]; ok {
174+			ev = b
175+			delete(s.chBatches, id)
176 		}
177+	case "FAIL":
178+		fmt.Println("FAIL", msg.Params)
179 	case "PING":
180 		err = s.send("PONG :%s\r\n", msg.Params[0])
181 		if err != nil {
182@@ -651,6 +672,38 @@ func (s *Session) handle(msg Message) (ev Event, err error) {
183 	return
184 }
185 
186+func (s *Session) privmsgToEvent(msg Message) (ev Event) {
187+	nick, _, _ := FullMask(msg.Prefix)
188+	target := strings.ToLower(msg.Params[0])
189+
190+	if target == s.lNick {
191+		// PRIVMSG to self
192+		t, ok := msg.Time()
193+		if !ok {
194+			t = time.Now()
195+		}
196+		ev = QueryMessageEvent{
197+			UserEvent: UserEvent{Nick: nick},
198+			Content:   msg.Params[1],
199+			Time:      t,
200+		}
201+	} else if _, ok := s.channels[target]; ok {
202+		// PRIVMSG to channel
203+		t, ok := msg.Time()
204+		if !ok {
205+			t = time.Now()
206+		}
207+		ev = ChannelMessageEvent{
208+			UserEvent:    UserEvent{Nick: nick},
209+			ChannelEvent: ChannelEvent{Channel: msg.Params[0]},
210+			Content:      msg.Params[1],
211+			Time:         t,
212+		}
213+	}
214+
215+	return
216+}
217+
218 func (s *Session) updateFeatures(features []string) {
219 	for _, f := range features {
220 		if f == "" || f == "-" || f == "=" || f == "-=" {
+22, -1
 1@@ -90,6 +90,7 @@ var (
 2 )
 3 
 4 var (
 5+	errEmptyBatchID    = errors.New("empty BATCH ID")
 6 	errNoPrefix        = errors.New("missing prefix")
 7 	errNotEnoughParams = errors.New("not enough params")
 8 	errUnknownCommand  = errors.New("unknown command")
 9@@ -223,6 +224,26 @@ func (msg *Message) Validate() (err error) {
10 		if len(msg.Params) < 2 {
11 			err = errNotEnoughParams
12 		}
13+	case "BATCH":
14+		if len(msg.Params) < 1 {
15+			err = errNotEnoughParams
16+			break
17+		}
18+		if len(msg.Params[0]) < 2 {
19+			err = errEmptyBatchID
20+			break
21+		}
22+		if msg.Params[0][0] == '+' {
23+			if len(msg.Params) < 2 {
24+				err = errNotEnoughParams
25+				break
26+			}
27+			if msg.Params[1] == "chathistory" && len(msg.Params) < 3 {
28+				err = errNotEnoughParams
29+			}
30+		} else if msg.Params[0][0] != '-' {
31+			err = errEmptyBatchID
32+		}
33 	case "PING":
34 		if len(msg.Params) < 1 {
35 			err = errNotEnoughParams
36@@ -253,7 +274,7 @@ func (msg *Message) Time() (t time.Time, ok bool) {
37 		return
38 	}
39 
40-	t = time.Date(year, time.Month(month), day, hour, minute, second, millis*1000000, time.UTC)
41+	t = time.Date(year, time.Month(month), day, hour, minute, second, millis*1e6, time.UTC)
42 	t = t.Local()
43 
44 	return
+27, -2
 1@@ -52,11 +52,11 @@ func NewLineNow(content string) (line Line) {
 2 }
 3 
 4 func (line *Line) Invalidate() {
 5-	line.renderedHeight = -1
 6+	line.renderedHeight = 0
 7 }
 8 
 9 func (line *Line) RenderedHeight(screenWidth int) (height int) {
10-	if line.renderedHeight < 0 {
11+	if line.renderedHeight <= 0 {
12 		line.computeRenderedHeight(screenWidth)
13 	}
14 	height = line.renderedHeight
15@@ -250,6 +250,31 @@ func (bs *BufferList) AddLine(idx int, line string, t time.Time, isStatus bool)
16 	}
17 }
18 
19+func (bs *BufferList) AddHistoryLines(idx int, lines []Line) {
20+	if len(lines) == 0 {
21+		return
22+	}
23+
24+	b := &bs.List[idx]
25+	limit := -1
26+
27+	if len(b.Content) != 0 {
28+		firstTime := b.Content[0].Time.Round(time.Millisecond)
29+		for i := len(lines) - 1; i >= 0; i-- {
30+			if firstTime == lines[i].Time.Round(time.Millisecond) {
31+				limit = i
32+				break
33+			}
34+		}
35+	}
36+
37+	if limit == -1 {
38+		limit = len(lines)
39+	}
40+
41+	bs.List[idx].Content = append(lines[:limit], b.Content...)
42+}
43+
44 func (bs *BufferList) Invalidate() {
45 	for i := range bs.List {
46 		for j := range bs.List[i].Content {
+39, -9
  1@@ -80,24 +80,36 @@ func (ui *UI) CurrentBuffer() (title string) {
  2 	return
  3 }
  4 
  5-func (ui *UI) NextBuffer() {
  6-	ok := ui.bufferList.Next()
  7+func (ui *UI) CurrentBufferOldestTime() (t time.Time) {
  8+	b := ui.bufferList.List[ui.bufferList.Current].Content
  9+	if len(b) == 0 {
 10+		t = time.Now()
 11+	} else {
 12+		t = b[0].Time
 13+	}
 14+	return
 15+}
 16+
 17+func (ui *UI) NextBuffer() (ok bool) {
 18+	ok = ui.bufferList.Next()
 19 	if ok {
 20 		ui.scrollAmt = 0
 21 		ui.scrollAtTop = false
 22 		ui.drawBuffer()
 23 		ui.drawStatus()
 24 	}
 25+	return
 26 }
 27 
 28-func (ui *UI) PreviousBuffer() {
 29-	ok := ui.bufferList.Previous()
 30+func (ui *UI) PreviousBuffer() (ok bool) {
 31+	ok = ui.bufferList.Previous()
 32 	if ok {
 33 		ui.scrollAmt = 0
 34 		ui.scrollAtTop = false
 35 		ui.drawBuffer()
 36 		ui.drawStatus()
 37 	}
 38+	return
 39 }
 40 
 41 func (ui *UI) ScrollUp() {
 42@@ -105,8 +117,8 @@ func (ui *UI) ScrollUp() {
 43 		return
 44 	}
 45 
 46-	w, _ := ui.screen.Size()
 47-	ui.scrollAmt += w / 2
 48+	_, h := ui.screen.Size()
 49+	ui.scrollAmt += h / 2
 50 	ui.drawBuffer()
 51 }
 52 
 53@@ -115,8 +127,8 @@ func (ui *UI) ScrollDown() {
 54 		return
 55 	}
 56 
 57-	w, _ := ui.screen.Size()
 58-	ui.scrollAmt -= w / 2
 59+	_, h := ui.screen.Size()
 60+	ui.scrollAmt -= h / 2
 61 	if ui.scrollAmt < 0 {
 62 		ui.scrollAmt = 0
 63 	}
 64@@ -125,6 +137,10 @@ func (ui *UI) ScrollDown() {
 65 	ui.drawBuffer()
 66 }
 67 
 68+func (ui *UI) IsAtTop() bool {
 69+	return ui.scrollAtTop
 70+}
 71+
 72 func (ui *UI) AddBuffer(title string) {
 73 	_, ok := ui.bufferList.Add(title)
 74 	if ok {
 75@@ -158,6 +174,20 @@ func (ui *UI) AddLine(buffer string, line string, t time.Time, isStatus bool) {
 76 	}
 77 }
 78 
 79+func (ui *UI) AddHistoryLines(buffer string, lines []Line) {
 80+	idx := ui.bufferList.Idx(buffer)
 81+	if idx < 0 {
 82+		return
 83+	}
 84+
 85+	ui.bufferList.AddHistoryLines(idx, lines)
 86+
 87+	if idx == ui.bufferList.Current {
 88+		ui.scrollAtTop = false
 89+		ui.drawBuffer()
 90+	}
 91+}
 92+
 93 func (ui *UI) Input() string {
 94 	return string(ui.textInput)
 95 }
 96@@ -430,7 +460,7 @@ func (ui *UI) drawBuffer() {
 97 		colorState = 0
 98 	}
 99 
100-	ui.scrollAtTop = true
101+	ui.scrollAtTop = 0 <= y0
102 	ui.screen.Show()
103 }
104