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:
commit
c02b6390d2
@ -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
|
||||
}
|
||||
|
||||
|
11
chat/room.go
11
chat/room.go
@ -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
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)
|
||||
|
||||
|
55
host_test.go
55
host_test.go
@ -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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user