mirror of
https://github.com/shazow/ssh-chat.git
synced 2025-07-23 14:04:40 +02:00
Progress, most of this probably doesnt work.
This commit is contained in:
parent
4c8d73b932
commit
1652511bf2
@ -1,5 +1,7 @@
|
|||||||
package chat
|
package chat
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
const historyLen = 20
|
const historyLen = 20
|
||||||
|
|
||||||
// Channel definition, also a Set of User Items
|
// Channel definition, also a Set of User Items
|
||||||
@ -8,13 +10,13 @@ type Channel struct {
|
|||||||
topic string
|
topic string
|
||||||
history *History
|
history *History
|
||||||
users *Set
|
users *Set
|
||||||
out chan<- Message
|
broadcast chan<- Message
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewChannel(id string, out chan<- Message) *Channel {
|
func NewChannel(id string, broadcast chan<- Message) *Channel {
|
||||||
ch := Channel{
|
ch := Channel{
|
||||||
id: id,
|
id: id,
|
||||||
out: out,
|
broadcast: broadcast,
|
||||||
history: NewHistory(historyLen),
|
history: NewHistory(historyLen),
|
||||||
users: NewSet(),
|
users: NewSet(),
|
||||||
}
|
}
|
||||||
@ -22,16 +24,24 @@ func NewChannel(id string, out chan<- Message) *Channel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (ch *Channel) Send(m Message) {
|
func (ch *Channel) Send(m Message) {
|
||||||
ch.out <- m
|
ch.broadcast <- m
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ch *Channel) Join(u *User) error {
|
func (ch *Channel) Join(u *User) error {
|
||||||
err := ch.users.Add(u)
|
err := ch.users.Add(u)
|
||||||
|
if err != nil {
|
||||||
|
s := fmt.Sprintf("%s joined. (Connected: %d)", u.Name(), ch.users.Len())
|
||||||
|
ch.Send(*NewMessage(s))
|
||||||
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ch *Channel) Leave(u *User) error {
|
func (ch *Channel) Leave(u *User) error {
|
||||||
err := ch.users.Remove(u)
|
err := ch.users.Remove(u)
|
||||||
|
if err != nil {
|
||||||
|
s := fmt.Sprintf("%s left.", u.Name())
|
||||||
|
ch.Send(*NewMessage(s))
|
||||||
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
27
chat/channel_test.go
Normal file
27
chat/channel_test.go
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
package chat
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestChannel(t *testing.T) {
|
||||||
|
s := &MockScreen{}
|
||||||
|
out := make(chan Message)
|
||||||
|
defer close(out)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for msg := range out {
|
||||||
|
s.Write([]byte(msg.Render(nil)))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
u := NewUser("foo", s)
|
||||||
|
ch := NewChannel("", out)
|
||||||
|
ch.Join(u)
|
||||||
|
|
||||||
|
expected := []byte(" * foo joined. (Connected: 1)")
|
||||||
|
if !reflect.DeepEqual(s.received, expected) {
|
||||||
|
t.Errorf("Got: %s, Expected: %s", s.received, expected)
|
||||||
|
}
|
||||||
|
}
|
52
chat/command.go
Normal file
52
chat/command.go
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
package chat
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrInvalidCommand error = errors.New("invalid command")
|
||||||
|
var ErrNoOwner error = errors.New("command without owner")
|
||||||
|
|
||||||
|
type CmdHandler func(host Host, msg Message, args []string) error
|
||||||
|
|
||||||
|
type Commands map[string]CmdHandler
|
||||||
|
|
||||||
|
// Register command
|
||||||
|
func (c Commands) Add(cmd string, handler CmdHandler) {
|
||||||
|
c[cmd] = handler
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute command message, assumes IsCommand was checked
|
||||||
|
func (c Commands) Run(host Host, msg Message) error {
|
||||||
|
if msg.from == nil {
|
||||||
|
return ErrNoOwner
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd, args := msg.ParseCommand()
|
||||||
|
handler, ok := c[cmd]
|
||||||
|
if !ok {
|
||||||
|
return ErrInvalidCommand
|
||||||
|
}
|
||||||
|
|
||||||
|
return handler(host, msg, args)
|
||||||
|
}
|
||||||
|
|
||||||
|
var defaultCmdHandlers Commands
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
c := Commands{}
|
||||||
|
|
||||||
|
c.Add("me", func(host Host, msg Message, args []string) error {
|
||||||
|
me := strings.TrimLeft(msg.Body, "/me")
|
||||||
|
if me == "" {
|
||||||
|
me = " is at a loss for words."
|
||||||
|
}
|
||||||
|
|
||||||
|
// XXX: Finish this.
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
defaultCmdHandlers = c
|
||||||
|
}
|
43
chat/host.go
Normal file
43
chat/host.go
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
package chat
|
||||||
|
|
||||||
|
const messageBuffer = 20
|
||||||
|
|
||||||
|
// Host knows about all the commands and channels.
|
||||||
|
type Host struct {
|
||||||
|
defaultChannel *Channel
|
||||||
|
commands Commands
|
||||||
|
done chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHost() *Host {
|
||||||
|
h := Host{
|
||||||
|
commands: defaultCmdHandlers,
|
||||||
|
}
|
||||||
|
h.defaultChannel = h.CreateChannel("")
|
||||||
|
return &h
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Host) handleCommand(m Message) {
|
||||||
|
// TODO: ...
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Host) broadcast(ch *Channel, m Message) {
|
||||||
|
// TODO: ...
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Host) CreateChannel(id string) *Channel {
|
||||||
|
out := make(chan Message, 20)
|
||||||
|
ch := NewChannel(id, out)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for msg := range out {
|
||||||
|
if msg.IsCommand() {
|
||||||
|
go h.handleCommand(msg)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
h.broadcast(ch, msg)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return ch
|
||||||
|
}
|
@ -2,6 +2,7 @@ package chat
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -14,10 +15,9 @@ type Message struct {
|
|||||||
themeCache *map[*Theme]string
|
themeCache *map[*Theme]string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewMessage(from *User, body string) *Message {
|
func NewMessage(body string) *Message {
|
||||||
m := Message{
|
m := Message{
|
||||||
Body: body,
|
Body: body,
|
||||||
from: from,
|
|
||||||
timestamp: time.Now(),
|
timestamp: time.Now(),
|
||||||
}
|
}
|
||||||
return &m
|
return &m
|
||||||
@ -37,19 +37,35 @@ func (m *Message) From(u *User) *Message {
|
|||||||
|
|
||||||
// Render message based on the given theme
|
// Render message based on the given theme
|
||||||
func (m *Message) Render(*Theme) string {
|
func (m *Message) Render(*Theme) string {
|
||||||
// TODO: Render based on theme.
|
// TODO: Render based on theme
|
||||||
// TODO: Cache based on theme
|
// TODO: Cache based on theme
|
||||||
var msg string
|
var msg string
|
||||||
if m.to != nil {
|
if m.to != nil && m.from != nil {
|
||||||
msg = fmt.Sprintf("[PM from %s] %s", m.to, m.Body)
|
msg = fmt.Sprintf("[PM from %s] %s", m.from, m.Body)
|
||||||
} else if m.from != nil {
|
} else if m.from != nil {
|
||||||
msg = fmt.Sprintf("%s: %s", m.from, m.Body)
|
msg = fmt.Sprintf("%s: %s", m.from, m.Body)
|
||||||
|
} else if m.to != nil {
|
||||||
|
msg = fmt.Sprintf("-> %s", m.Body)
|
||||||
} else {
|
} else {
|
||||||
msg = fmt.Sprintf(" * %s", m.Body)
|
msg = fmt.Sprintf(" * %s", m.Body)
|
||||||
}
|
}
|
||||||
return msg
|
return msg
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Render message without a theme
|
||||||
func (m *Message) String() string {
|
func (m *Message) String() string {
|
||||||
return m.Render(nil)
|
return m.Render(nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Wether message is a command (starts with /)
|
||||||
|
func (m *Message) IsCommand() bool {
|
||||||
|
return strings.HasPrefix(m.Body, "/")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse command (assumes IsCommand was already called)
|
||||||
|
func (m *Message) ParseCommand() (string, []string) {
|
||||||
|
// TODO: Tokenize this properly, to support quoted args?
|
||||||
|
cmd := strings.Split(m.Body, " ")
|
||||||
|
args := cmd[1:]
|
||||||
|
return cmd[0][1:], args
|
||||||
|
}
|
||||||
|
@ -5,7 +5,7 @@ import "testing"
|
|||||||
func TestSet(t *testing.T) {
|
func TestSet(t *testing.T) {
|
||||||
var err error
|
var err error
|
||||||
s := NewSet()
|
s := NewSet()
|
||||||
u := NewUser("foo")
|
u := NewUser("foo", nil)
|
||||||
|
|
||||||
if s.In(u) {
|
if s.In(u) {
|
||||||
t.Errorf("Set should be empty.")
|
t.Errorf("Set should be empty.")
|
||||||
@ -20,7 +20,7 @@ func TestSet(t *testing.T) {
|
|||||||
t.Errorf("Set should contain user.")
|
t.Errorf("Set should contain user.")
|
||||||
}
|
}
|
||||||
|
|
||||||
u2 := NewUser("bar")
|
u2 := NewUser("bar", nil)
|
||||||
err = s.Add(u2)
|
err = s.Add(u2)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Error(err)
|
t.Error(err)
|
||||||
|
17
chat/user.go
17
chat/user.go
@ -1,21 +1,27 @@
|
|||||||
package chat
|
package chat
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"io"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// User definition, implemented set Item interface
|
// User definition, implemented set Item interface and io.Writer
|
||||||
type User struct {
|
type User struct {
|
||||||
name string
|
name string
|
||||||
op bool
|
op bool
|
||||||
colorIdx int
|
colorIdx int
|
||||||
joined time.Time
|
joined time.Time
|
||||||
|
screen io.Writer
|
||||||
Config UserConfig
|
Config UserConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewUser(name string) *User {
|
func NewUser(name string, screen io.Writer) *User {
|
||||||
u := User{Config: *DefaultUserConfig}
|
u := User{
|
||||||
|
screen: screen,
|
||||||
|
joined: time.Now(),
|
||||||
|
Config: *DefaultUserConfig,
|
||||||
|
}
|
||||||
u.SetName(name)
|
u.SetName(name)
|
||||||
return &u
|
return &u
|
||||||
}
|
}
|
||||||
@ -46,6 +52,11 @@ func (u *User) SetOp(op bool) {
|
|||||||
u.op = op
|
u.op = op
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Write to user's screen
|
||||||
|
func (u *User) Write(p []byte) (n int, err error) {
|
||||||
|
return u.screen.Write(p)
|
||||||
|
}
|
||||||
|
|
||||||
// Container for per-user configurations.
|
// Container for per-user configurations.
|
||||||
type UserConfig struct {
|
type UserConfig struct {
|
||||||
Highlight bool
|
Highlight bool
|
||||||
|
26
chat/user_test.go
Normal file
26
chat/user_test.go
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
package chat
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MockScreen struct {
|
||||||
|
received []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MockScreen) Write(data []byte) (n int, err error) {
|
||||||
|
s.received = append(s.received, data...)
|
||||||
|
return len(data), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMakeUser(t *testing.T) {
|
||||||
|
s := &MockScreen{}
|
||||||
|
u := NewUser("foo", s)
|
||||||
|
|
||||||
|
line := []byte("hello")
|
||||||
|
u.Write(line)
|
||||||
|
if !reflect.DeepEqual(s.received, line) {
|
||||||
|
t.Errorf("Expected hello but got: %s", s.received)
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user