1
0
mirror of https://github.com/shazow/ssh-chat.git synced 2025-04-17 09:22:21 +03:00

/timestamp now prefixes each line with a timestamp

Freakin' Timestamps On Every Freakin' Line
This commit is contained in:
Andrey Petrov 2019-03-17 14:05:22 -04:00 committed by GitHub
commit c02b6390d2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 141 additions and 56 deletions

@ -6,6 +6,7 @@ import (
"errors"
"fmt"
"strings"
"time"
"github.com/shazow/ssh-chat/chat/message"
"github.com/shazow/ssh-chat/internal/sanitize"
@ -283,16 +284,34 @@ func InitCommands(c *Commands) {
})
c.Add(Command{
Prefix: "/timestamp",
Help: "Timestamps after 30min of inactivity.",
Prefix: "/timestamp",
PrefixHelp: "[TZINFO]",
Help: "Prefix messages with a timestamp. (Example: America/Toronto)",
Handler: func(room *Room, msg message.CommandMsg) error {
u := msg.From()
cfg := u.Config()
cfg.Timestamp = !cfg.Timestamp
args := msg.Args()
if len(args) >= 1 {
// FIXME: This is an annoying format to demand from users, but
// hopefully we can make it a non-primary flow if we add GeoIP
// someday.
timeLoc, err := time.LoadLocation(args[0])
if err != nil {
err = fmt.Errorf("%s: Use a location name such as \"America/Toronto\" or refer to the IANA Time Zone database for the full list of names: https://wikipedia.org/wiki/List_of_tz_database_time_zones", err)
return err
}
cfg.Timezone = timeLoc
cfg.Timestamp = true
} else {
cfg.Timestamp = !cfg.Timestamp
}
u.SetConfig(cfg)
var body string
if cfg.Timestamp {
if cfg.Timestamp && cfg.Timezone != nil {
body = fmt.Sprintf("Timestamp is toggled ON with timezone %q", cfg.Timezone)
} else if cfg.Timestamp {
body = "Timestamp is toggled ON"
} else {
body = "Timestamp is toggled OFF"

@ -112,6 +112,7 @@ func (m PublicMsg) Render(t *Theme) string {
return fmt.Sprintf("%s: %s", t.ColorName(m.from), m.body)
}
// RenderFor renders the message for other users to see.
func (m PublicMsg) RenderFor(cfg UserConfig) string {
if cfg.Highlight == nil || cfg.Theme == nil {
return m.Render(cfg.Theme)
@ -128,6 +129,11 @@ func (m PublicMsg) RenderFor(cfg UserConfig) string {
return fmt.Sprintf("%s: %s", cfg.Theme.ColorName(m.from), body)
}
// RenderSelf renders the message for when it's echoing your own message.
func (m PublicMsg) RenderSelf(cfg UserConfig) string {
return fmt.Sprintf("[%s] %s", cfg.Theme.ColorName(m.from), m.body)
}
func (m PublicMsg) String() string {
return fmt.Sprintf("%s: %s", m.from.Name(), m.body)
}

@ -1,6 +1,11 @@
package message
import "fmt"
import (
"fmt"
"time"
)
const timestampLayout = "2006-01-02 15:04:05 MST"
const (
// Reset resets the color
@ -122,43 +127,49 @@ type Theme struct {
names *Palette
}
func (t Theme) ID() string {
return t.id
func (theme Theme) ID() string {
return theme.id
}
// Colorize name string given some index
func (t Theme) ColorName(u *User) string {
if t.names == nil {
func (theme Theme) ColorName(u *User) string {
if theme.names == nil {
return u.Name()
}
return t.names.Get(u.colorIdx).Format(u.Name())
return theme.names.Get(u.colorIdx).Format(u.Name())
}
// Colorize the PM string
func (t Theme) ColorPM(s string) string {
if t.pm == nil {
func (theme Theme) ColorPM(s string) string {
if theme.pm == nil {
return s
}
return t.pm.Format(s)
return theme.pm.Format(s)
}
// Colorize the Sys message
func (t Theme) ColorSys(s string) string {
if t.sys == nil {
func (theme Theme) ColorSys(s string) string {
if theme.sys == nil {
return s
}
return t.sys.Format(s)
return theme.sys.Format(s)
}
// Highlight a matched string, usually name
func (t Theme) Highlight(s string) string {
if t.highlight == nil {
func (theme Theme) Highlight(s string) string {
if theme.highlight == nil {
return s
}
return t.highlight.Format(s)
return theme.highlight.Format(s)
}
// Timestamp formats and colorizes the timestamp.
func (theme Theme) Timestamp(t time.Time) string {
// TODO: Change this per-theme? Or config?
return theme.sys.Format(t.Format(timestampLayout))
}
// List of initialzied themes

@ -16,7 +16,6 @@ const messageBuffer = 5
const messageTimeout = 5 * time.Second
const reHighlight = `\b(%s)\b`
const timestampTimeout = 30 * time.Minute
const timestampLayout = "2006-01-02 15:04:05 UTC"
var ErrUserClosed = errors.New("user closed")
@ -158,17 +157,34 @@ func (u *User) SetHighlight(s string) error {
func (u *User) render(m Message) string {
cfg := u.Config()
var out string
switch m := m.(type) {
case PublicMsg:
return m.RenderFor(cfg) + Newline
case *PrivateMsg:
if cfg.Bell {
return m.Render(cfg.Theme) + Bel + Newline
if u == m.From() {
out += m.RenderSelf(cfg)
} else {
out += m.RenderFor(cfg)
}
return m.Render(cfg.Theme) + Newline
case *PrivateMsg:
out += m.Render(cfg.Theme)
if cfg.Bell {
out += Bel
}
case *CommandMsg:
out += m.RenderSelf(cfg)
default:
return m.Render(cfg.Theme) + Newline
out += m.Render(cfg.Theme)
}
if cfg.Timestamp {
ts := m.Timestamp()
if cfg.Timezone != nil {
ts = ts.In(cfg.Timezone)
} else {
ts = ts.UTC()
}
return cfg.Theme.Timestamp(ts) + " " + out + Newline
}
return out + Newline
}
// writeMsg renders the message and attempts to write it, will Close the user
@ -186,20 +202,8 @@ func (u *User) writeMsg(m Message) error {
// HandleMsg will render the message to the screen, blocking.
func (u *User) HandleMsg(m Message) error {
u.mu.Lock()
cfg := u.config
lastMsg := u.lastMsg
u.lastMsg = m.Timestamp()
injectTimestamp := !lastMsg.IsZero() && cfg.Timestamp && u.lastMsg.Sub(lastMsg) > timestampTimeout
u.mu.Unlock()
if injectTimestamp {
// Inject a timestamp at most once every timestampTimeout between message intervals
ts := NewSystemMsg(fmt.Sprintf("Timestamp: %s", m.Timestamp().UTC().Format(timestampLayout)), u)
if err := u.writeMsg(ts); err != nil {
return err
}
}
return u.writeMsg(m)
}
@ -223,6 +227,7 @@ type UserConfig struct {
Bell bool
Quiet bool
Timestamp bool
Timezone *time.Location
Theme *Theme
}

@ -90,12 +90,7 @@ func (r *Room) HandleMsg(m message.Message) {
user := m.To()
user.Send(m)
default:
fromMsg, skip := m.(message.MessageFrom)
var skipUser *message.User
if skip {
skipUser = fromMsg.From()
}
fromMsg, _ := m.(message.MessageFrom)
r.history.Add(m)
r.Members.Each(func(_ string, item set.Item) (err error) {
user := item.Value().(*Member).User
@ -105,10 +100,6 @@ func (r *Room) HandleMsg(m message.Message) {
return
}
if skip && skipUser == user {
// Skip self
return
}
if _, ok := m.(*message.AnnounceMsg); ok {
if user.Config().Quiet {
// Skip announcements

14
host.go

@ -162,6 +162,20 @@ func (h *Host) Connect(term *sshd.Terminal) {
m := message.ParseInput(line, user)
// Gross hack to override line echo in golang.org/x/crypto/ssh/terminal
// It needs to live before we render the resulting message.
term.Write([]byte{
27, '[', 'A', // Up
27, '[', '2', 'K', // Clear line
})
// May the gods have mercy on our souls.
if m, ok := m.(*message.CommandMsg); ok {
// Other messages render themselves by the room, commands we'll
// have to re-echo ourselves manually.
user.HandleMsg(m)
}
// FIXME: Any reason to use h.room.Send(m) instead?
h.HandleMsg(m)

@ -6,6 +6,7 @@ import (
"crypto/rsa"
"errors"
"io"
mathRand "math/rand"
"strings"
"testing"
@ -15,22 +16,53 @@ import (
)
func stripPrompt(s string) string {
pos := strings.LastIndex(s, "\033[K")
if pos < 0 {
return s
// FIXME: Is there a better way to do this?
if endPos := strings.Index(s, "\x1b[K "); endPos > 0 {
return s[endPos+3:]
}
if endPos := strings.Index(s, "\x1b[2K "); endPos > 0 {
return s[endPos+4:]
}
if endPos := strings.Index(s, "] "); endPos > 0 {
return s[endPos+2:]
}
return s
}
func TestStripPrompt(t *testing.T) {
tests := []struct {
Input string
Want string
}{
{
Input: "\x1b[A\x1b[2K[quux] hello",
Want: "hello",
},
{
Input: "[foo] \x1b[D\x1b[D\x1b[D\x1b[D\x1b[D\x1b[D\x1b[K * Guest1 joined. (Connected: 2)\r",
Want: " * Guest1 joined. (Connected: 2)\r",
},
}
for i, tc := range tests {
if got, want := stripPrompt(tc.Input), tc.Want; got != want {
t.Errorf("case #%d:\n got: %q\nwant: %q", i, got, want)
}
}
return s[pos+3:]
}
func TestHostGetPrompt(t *testing.T) {
var expected, actual string
// Make the random colors consistent across tests
mathRand.Seed(1)
u := message.NewUser(&Identity{id: "foo"})
actual = GetPrompt(u)
expected = "[foo] "
if actual != expected {
t.Errorf("Got: %q; Expected: %q", actual, expected)
t.Errorf("Invalid host prompt:\n Got: %q;\nWant: %q", actual, expected)
}
u.SetConfig(message.UserConfig{
@ -39,7 +71,7 @@ func TestHostGetPrompt(t *testing.T) {
actual = GetPrompt(u)
expected = "[\033[38;05;88mfoo\033[0m] "
if actual != expected {
t.Errorf("Got: %q; Expected: %q", actual, expected)
t.Errorf("Invalid host prompt:\n Got: %q;\nWant: %q", actual, expected)
}
}
@ -205,18 +237,23 @@ func TestHostKick(t *testing.T) {
// Change nicks, make sure op sticks
w.Write([]byte("/nick quux\r\n"))
scanner.Scan() // Prompt
scanner.Scan() // Prompt echo
scanner.Scan() // Nick change response
// Signal for the second client to connect
connected <- struct{}{}
// Block until second client is here
connected <- struct{}{}
scanner.Scan() // Connected message
w.Write([]byte("/kick bar\r\n"))
scanner.Scan() // Prompt
scanner.Scan() // Prompt echo
scanner.Scan()
scanner.Scan() // Kick result
if actual, expected := stripPrompt(scanner.Text()), " * bar was kicked by quux.\r"; actual != expected {
t.Errorf("Got %q; expected %q", actual, expected)
t.Errorf("Failed to detect kick:\n Got: %q;\nWant: %q", actual, expected)
}
kicked <- struct{}{}
@ -231,6 +268,8 @@ func TestHostKick(t *testing.T) {
}()
go func() {
<-connected
// Second client
err := sshd.ConnectShell(addr, "bar", func(r io.Reader, w io.WriteCloser) error {
scanner := bufio.NewScanner(r)