diff options
Diffstat (limited to '')
| -rw-r--r-- | cmd/irc/main.go | 62 | ||||
| -rw-r--r-- | irc/events.go | 5 | ||||
| -rw-r--r-- | irc/states.go | 127 | ||||
| -rw-r--r-- | irc/tokens.go | 23 | ||||
| -rw-r--r-- | ui/buffers.go | 29 | ||||
| -rw-r--r-- | ui/ui.go | 48 |
6 files changed, 238 insertions, 56 deletions
diff --git a/cmd/irc/main.go b/cmd/irc/main.go index 7b63d3c..2e597bd 100644 --- a/cmd/irc/main.go +++ b/cmd/irc/main.go @@ -42,9 +42,9 @@ func main() { } s, err := irc.NewSession(conn, irc.SessionParams{ - Nickname: "taiite", - Username: "taiitent", - RealName: "Taiite Ier", + Nickname: "ME", + Username: "MEMEMEMEMEM", + RealName: "Le me", Auth: &irc.SASLPlain{Username: cfg.User, Password: cfg.Password}, }) if err != nil { @@ -71,6 +71,29 @@ func main() { case irc.ChannelMessageEvent: line := formatIRCMessage(ev.Nick, ev.Content) app.AddLine(ev.Channel, line, ev.Time, false) + case irc.HistoryEvent: + var lines []ui.Line + var lastT time.Time + isChannel := ev.Target[0] == '#' + for _, m := range ev.Messages { + switch m := m.(type) { + case irc.ChannelMessageEvent: + if isChannel { + line := formatIRCMessage(m.Nick, m.Content) + line = strings.TrimRight(line, "\t ") + if lastT.Truncate(time.Minute) != m.Time.Truncate(time.Minute) { + lastT = m.Time + hour := lastT.Hour() + minute := lastT.Minute() + line = fmt.Sprintf("\x02%02d:%02d\x00 %s", hour, minute, line) + } + lines = append(lines, ui.NewLine(m.Time, false, line)) + } else { + panic("TODO") + } + } + } + app.AddHistoryLines(ev.Target, lines) case error: log.Panicln(ev) } @@ -85,22 +108,47 @@ func main() { case tcell.KeyCtrlL: app.Resize() case tcell.KeyCtrlU: + fallthrough + case tcell.KeyPgUp: app.ScrollUp() + if app.IsAtTop() { + buffer := app.CurrentBuffer() + t := app.CurrentBufferOldestTime() + s.RequestHistory(buffer, t) + } case tcell.KeyCtrlD: + fallthrough + case tcell.KeyPgDn: app.ScrollDown() case tcell.KeyCtrlN: - app.NextBuffer() + if app.NextBuffer() && app.IsAtTop() { + buffer := app.CurrentBuffer() + t := app.CurrentBufferOldestTime() + s.RequestHistory(buffer, t) + } case tcell.KeyCtrlP: - app.PreviousBuffer() + if app.PreviousBuffer() && app.IsAtTop() { + buffer := app.CurrentBuffer() + t := app.CurrentBufferOldestTime() + s.RequestHistory(buffer, t) + } case tcell.KeyRight: if ev.Modifiers() == tcell.ModAlt { - app.NextBuffer() + if app.NextBuffer() && app.IsAtTop() { + buffer := app.CurrentBuffer() + t := app.CurrentBufferOldestTime() + s.RequestHistory(buffer, t) + } } else { app.InputRight() } case tcell.KeyLeft: if ev.Modifiers() == tcell.ModAlt { - app.PreviousBuffer() + if app.PreviousBuffer() && app.IsAtTop() { + buffer := app.CurrentBuffer() + t := app.CurrentBufferOldestTime() + s.RequestHistory(buffer, t) + } } else { app.InputLeft() } diff --git a/irc/events.go b/irc/events.go index 96bf786..66237e9 100644 --- a/irc/events.go +++ b/irc/events.go @@ -61,3 +61,8 @@ type ChannelMessageEvent struct { Content string Time time.Time } + +type HistoryEvent struct { + Target string + Messages []Event +} diff --git a/irc/states.go b/irc/states.go index c25bda6..6363ae8 100644 --- a/irc/states.go +++ b/irc/states.go @@ -47,6 +47,7 @@ var SupportedCapabilities = map[string]struct{}{ "away-notify": {}, "batch": {}, "cap-notify": {}, + "draft/chathistory": {}, "echo-message": {}, "extended-join": {}, "invite-notify": {}, @@ -92,7 +93,7 @@ type ( } actionPrivMsg struct { - Channel string + Target string Content string } @@ -102,6 +103,11 @@ type ( actionTypingStop struct { Channel string } + + actionRequestHistory struct { + Target string + Before time.Time + } ) type SessionParams struct { @@ -137,16 +143,17 @@ type Session struct { enabledCaps map[string]struct{} features map[string]string - users map[string]User - channels map[string]Channel + users map[string]User + channels map[string]Channel + chBatches map[string]HistoryEvent } func NewSession(conn io.ReadWriteCloser, params SessionParams) (s Session, err error) { s = Session{ conn: conn, - msgs: make(chan Message, 128), - acts: make(chan action, 128), - evts: make(chan Event, 128), + msgs: make(chan Message, 16), + acts: make(chan action, 16), + evts: make(chan Event, 16), typingStamps: map[string]time.Time{}, nick: params.Nickname, lNick: strings.ToLower(params.Nickname), @@ -158,6 +165,7 @@ func NewSession(conn io.ReadWriteCloser, params SessionParams) (s Session, err e features: map[string]string{}, users: map[string]User{}, channels: map[string]Channel{}, + chBatches: map[string]HistoryEvent{}, } s.running.Store(true) @@ -230,12 +238,12 @@ func (s *Session) part(act actionPart) (err error) { return } -func (s *Session) PrivMsg(channel, content string) { - s.acts <- actionPrivMsg{channel, content} +func (s *Session) PrivMsg(target, content string) { + s.acts <- actionPrivMsg{target, content} } func (s *Session) privMsg(act actionPrivMsg) (err error) { - err = s.send("PRIVMSG %s :%s\r\n", act.Channel, act.Content) + err = s.send("PRIVMSG %s :%s\r\n", act.Target, act.Content) return } @@ -261,8 +269,8 @@ func (s *Session) typing(act actionTyping) (err error) { return } -func (s *Session) TypingStop(to string) { - s.acts <- actionTypingStop{to} +func (s *Session) TypingStop(channel string) { + s.acts <- actionTypingStop{channel} } func (s *Session) typingStop(act actionTypingStop) (err error) { @@ -274,6 +282,21 @@ func (s *Session) typingStop(act actionTypingStop) (err error) { return } +func (s *Session) RequestHistory(target string, before time.Time) { + s.acts <- actionRequestHistory{target, before} +} + +func (s *Session) requestHistory(act actionRequestHistory) (err error) { + if _, ok := s.enabledCaps["draft/chathistory"]; !ok { + return + } + + t := act.Before + 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) + + return +} + func (s *Session) run() { for s.Running() { var ( @@ -294,6 +317,8 @@ func (s *Session) run() { err = s.typing(act) case actionTypingStop: err = s.typingStop(act) + case actionRequestHistory: + err = s.requestHistory(act) } case msg := <-s.msgs: if s.state == ConnStart { @@ -415,6 +440,16 @@ func (s *Session) handleStart(msg Message) (ev Event, err error) { } func (s *Session) handle(msg Message) (ev Event, err error) { + if id, ok := msg.Tags["batch"]; ok { + if b, ok := s.chBatches[id]; ok { + s.chBatches[id] = HistoryEvent{ + Target: b.Target, + Messages: append(b.Messages, s.privmsgToEvent(msg)), + } + return + } + } + switch msg.Command { case "001": // RPL_WELCOME s.nick = msg.Params[0] @@ -608,33 +643,19 @@ func (s *Session) handle(msg Message) (ev Event, err error) { c.Topic = msg.Params[2] } case "PRIVMSG": - nick, _, _ := FullMask(msg.Prefix) - target := strings.ToLower(msg.Params[0]) - - if target == s.lNick { - // PRIVMSG to self - t, ok := msg.Time() - if !ok { - t = time.Now() - } - ev = QueryMessageEvent{ - UserEvent: UserEvent{Nick: nick}, - Content: msg.Params[1], - Time: t, - } - } else if _, ok := s.channels[target]; ok { - // PRIVMSG to channel - t, ok := msg.Time() - if !ok { - t = time.Now() - } - ev = ChannelMessageEvent{ - UserEvent: UserEvent{Nick: nick}, - ChannelEvent: ChannelEvent{Channel: msg.Params[0]}, - Content: msg.Params[1], - Time: t, - } + ev = s.privmsgToEvent(msg) + case "BATCH": + batchStart := msg.Params[0][0] == '+' + id := msg.Params[0][1:] + + if batchStart && msg.Params[1] == "chathistory" { + s.chBatches[id] = HistoryEvent{Target: msg.Params[2]} + } else if b, ok := s.chBatches[id]; ok { + ev = b + delete(s.chBatches, id) } + case "FAIL": + fmt.Println("FAIL", msg.Params) case "PING": err = s.send("PONG :%s\r\n", msg.Params[0]) if err != nil { @@ -651,6 +672,38 @@ func (s *Session) handle(msg Message) (ev Event, err error) { return } +func (s *Session) privmsgToEvent(msg Message) (ev Event) { + nick, _, _ := FullMask(msg.Prefix) + target := strings.ToLower(msg.Params[0]) + + if target == s.lNick { + // PRIVMSG to self + t, ok := msg.Time() + if !ok { + t = time.Now() + } + ev = QueryMessageEvent{ + UserEvent: UserEvent{Nick: nick}, + Content: msg.Params[1], + Time: t, + } + } else if _, ok := s.channels[target]; ok { + // PRIVMSG to channel + t, ok := msg.Time() + if !ok { + t = time.Now() + } + ev = ChannelMessageEvent{ + UserEvent: UserEvent{Nick: nick}, + ChannelEvent: ChannelEvent{Channel: msg.Params[0]}, + Content: msg.Params[1], + Time: t, + } + } + + return +} + func (s *Session) updateFeatures(features []string) { for _, f := range features { if f == "" || f == "-" || f == "=" || f == "-=" { diff --git a/irc/tokens.go b/irc/tokens.go index 3d9a034..b282488 100644 --- a/irc/tokens.go +++ b/irc/tokens.go @@ -90,6 +90,7 @@ var ( ) var ( + errEmptyBatchID = errors.New("empty BATCH ID") errNoPrefix = errors.New("missing prefix") errNotEnoughParams = errors.New("not enough params") errUnknownCommand = errors.New("unknown command") @@ -223,6 +224,26 @@ func (msg *Message) Validate() (err error) { if len(msg.Params) < 2 { err = errNotEnoughParams } + case "BATCH": + if len(msg.Params) < 1 { + err = errNotEnoughParams + break + } + if len(msg.Params[0]) < 2 { + err = errEmptyBatchID + break + } + if msg.Params[0][0] == '+' { + if len(msg.Params) < 2 { + err = errNotEnoughParams + break + } + if msg.Params[1] == "chathistory" && len(msg.Params) < 3 { + err = errNotEnoughParams + } + } else if msg.Params[0][0] != '-' { + err = errEmptyBatchID + } case "PING": if len(msg.Params) < 1 { err = errNotEnoughParams @@ -253,7 +274,7 @@ func (msg *Message) Time() (t time.Time, ok bool) { return } - t = time.Date(year, time.Month(month), day, hour, minute, second, millis*1000000, time.UTC) + t = time.Date(year, time.Month(month), day, hour, minute, second, millis*1e6, time.UTC) t = t.Local() return diff --git a/ui/buffers.go b/ui/buffers.go index 0141a04..7b24831 100644 --- a/ui/buffers.go +++ b/ui/buffers.go @@ -52,11 +52,11 @@ func NewLineNow(content string) (line Line) { } func (line *Line) Invalidate() { - line.renderedHeight = -1 + line.renderedHeight = 0 } func (line *Line) RenderedHeight(screenWidth int) (height int) { - if line.renderedHeight < 0 { + if line.renderedHeight <= 0 { line.computeRenderedHeight(screenWidth) } height = line.renderedHeight @@ -250,6 +250,31 @@ func (bs *BufferList) AddLine(idx int, line string, t time.Time, isStatus bool) } } +func (bs *BufferList) AddHistoryLines(idx int, lines []Line) { + if len(lines) == 0 { + return + } + + b := &bs.List[idx] + limit := -1 + + if len(b.Content) != 0 { + firstTime := b.Content[0].Time.Round(time.Millisecond) + for i := len(lines) - 1; i >= 0; i-- { + if firstTime == lines[i].Time.Round(time.Millisecond) { + limit = i + break + } + } + } + + if limit == -1 { + limit = len(lines) + } + + bs.List[idx].Content = append(lines[:limit], b.Content...) +} + func (bs *BufferList) Invalidate() { for i := range bs.List { for j := range bs.List[i].Content { @@ -80,24 +80,36 @@ func (ui *UI) CurrentBuffer() (title string) { return } -func (ui *UI) NextBuffer() { - ok := ui.bufferList.Next() +func (ui *UI) CurrentBufferOldestTime() (t time.Time) { + b := ui.bufferList.List[ui.bufferList.Current].Content + if len(b) == 0 { + t = time.Now() + } else { + t = b[0].Time + } + return +} + +func (ui *UI) NextBuffer() (ok bool) { + ok = ui.bufferList.Next() if ok { ui.scrollAmt = 0 ui.scrollAtTop = false ui.drawBuffer() ui.drawStatus() } + return } -func (ui *UI) PreviousBuffer() { - ok := ui.bufferList.Previous() +func (ui *UI) PreviousBuffer() (ok bool) { + ok = ui.bufferList.Previous() if ok { ui.scrollAmt = 0 ui.scrollAtTop = false ui.drawBuffer() ui.drawStatus() } + return } func (ui *UI) ScrollUp() { @@ -105,8 +117,8 @@ func (ui *UI) ScrollUp() { return } - w, _ := ui.screen.Size() - ui.scrollAmt += w / 2 + _, h := ui.screen.Size() + ui.scrollAmt += h / 2 ui.drawBuffer() } @@ -115,8 +127,8 @@ func (ui *UI) ScrollDown() { return } - w, _ := ui.screen.Size() - ui.scrollAmt -= w / 2 + _, h := ui.screen.Size() + ui.scrollAmt -= h / 2 if ui.scrollAmt < 0 { ui.scrollAmt = 0 } @@ -125,6 +137,10 @@ func (ui *UI) ScrollDown() { ui.drawBuffer() } +func (ui *UI) IsAtTop() bool { + return ui.scrollAtTop +} + func (ui *UI) AddBuffer(title string) { _, ok := ui.bufferList.Add(title) if ok { @@ -158,6 +174,20 @@ func (ui *UI) AddLine(buffer string, line string, t time.Time, isStatus bool) { } } +func (ui *UI) AddHistoryLines(buffer string, lines []Line) { + idx := ui.bufferList.Idx(buffer) + if idx < 0 { + return + } + + ui.bufferList.AddHistoryLines(idx, lines) + + if idx == ui.bufferList.Current { + ui.scrollAtTop = false + ui.drawBuffer() + } +} + func (ui *UI) Input() string { return string(ui.textInput) } @@ -430,7 +460,7 @@ func (ui *UI) drawBuffer() { colorState = 0 } - ui.scrollAtTop = true + ui.scrollAtTop = 0 <= y0 ui.screen.Show() } |
