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 {
M
ui/ui.go
+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