mirror of
https://github.com/shazow/ssh-chat.git
synced 2025-04-14 16:17:17 +03:00
Framework for server-agnostic chat.
This commit is contained in:
parent
4e9bd419b0
commit
4c8d73b932
44
chat/channel.go
Normal file
44
chat/channel.go
Normal file
@ -0,0 +1,44 @@
|
||||
package chat
|
||||
|
||||
const historyLen = 20
|
||||
|
||||
// Channel definition, also a Set of User Items
|
||||
type Channel struct {
|
||||
id string
|
||||
topic string
|
||||
history *History
|
||||
users *Set
|
||||
out chan<- Message
|
||||
}
|
||||
|
||||
func NewChannel(id string, out chan<- Message) *Channel {
|
||||
ch := Channel{
|
||||
id: id,
|
||||
out: out,
|
||||
history: NewHistory(historyLen),
|
||||
users: NewSet(),
|
||||
}
|
||||
return &ch
|
||||
}
|
||||
|
||||
func (ch *Channel) Send(m Message) {
|
||||
ch.out <- m
|
||||
}
|
||||
|
||||
func (ch *Channel) Join(u *User) error {
|
||||
err := ch.users.Add(u)
|
||||
return err
|
||||
}
|
||||
|
||||
func (ch *Channel) Leave(u *User) error {
|
||||
err := ch.users.Remove(u)
|
||||
return err
|
||||
}
|
||||
|
||||
func (ch *Channel) Topic() string {
|
||||
return ch.topic
|
||||
}
|
||||
|
||||
func (ch *Channel) SetTopic(s string) {
|
||||
ch.topic = s
|
||||
}
|
58
chat/history.go
Normal file
58
chat/history.go
Normal file
@ -0,0 +1,58 @@
|
||||
package chat
|
||||
|
||||
import "sync"
|
||||
|
||||
// History contains the history entries
|
||||
type History struct {
|
||||
entries []interface{}
|
||||
head int
|
||||
size int
|
||||
sync.RWMutex
|
||||
}
|
||||
|
||||
// NewHistory constructs a new history of the given size
|
||||
func NewHistory(size int) *History {
|
||||
return &History{
|
||||
entries: make([]interface{}, size),
|
||||
}
|
||||
}
|
||||
|
||||
// Add adds the given entry to the entries in the history
|
||||
func (h *History) Add(entry interface{}) {
|
||||
h.Lock()
|
||||
defer h.Unlock()
|
||||
|
||||
max := cap(h.entries)
|
||||
h.head = (h.head + 1) % max
|
||||
h.entries[h.head] = entry
|
||||
if h.size < max {
|
||||
h.size++
|
||||
}
|
||||
}
|
||||
|
||||
// Len returns the number of entries in the history
|
||||
func (h *History) Len() int {
|
||||
return h.size
|
||||
}
|
||||
|
||||
// Get recent entries
|
||||
func (h *History) Get(num int) []interface{} {
|
||||
h.RLock()
|
||||
defer h.RUnlock()
|
||||
|
||||
max := cap(h.entries)
|
||||
if num > h.size {
|
||||
num = h.size
|
||||
}
|
||||
|
||||
r := make([]interface{}, num)
|
||||
for i := 0; i < num; i++ {
|
||||
idx := (h.head - i) % max
|
||||
if idx < 0 {
|
||||
idx += max
|
||||
}
|
||||
r[num-i-1] = h.entries[idx]
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
65
chat/history_test.go
Normal file
65
chat/history_test.go
Normal file
@ -0,0 +1,65 @@
|
||||
package chat
|
||||
|
||||
import "testing"
|
||||
|
||||
func equal(a []interface{}, b []string) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
|
||||
for i := 0; i < len(a); i++ {
|
||||
if a[0] != b[0] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func TestHistory(t *testing.T) {
|
||||
var r []interface{}
|
||||
var expected []string
|
||||
var size int
|
||||
|
||||
h := NewHistory(5)
|
||||
|
||||
r = h.Get(10)
|
||||
expected = []string{}
|
||||
if !equal(r, expected) {
|
||||
t.Errorf("Got: %v, Expected: %v", r, expected)
|
||||
}
|
||||
|
||||
h.Add("1")
|
||||
|
||||
if size = h.Len(); size != 1 {
|
||||
t.Errorf("Wrong size: %v", size)
|
||||
}
|
||||
|
||||
r = h.Get(1)
|
||||
expected = []string{"1"}
|
||||
if !equal(r, expected) {
|
||||
t.Errorf("Got: %v, Expected: %v", r, expected)
|
||||
}
|
||||
|
||||
h.Add("2")
|
||||
h.Add("3")
|
||||
h.Add("4")
|
||||
h.Add("5")
|
||||
h.Add("6")
|
||||
|
||||
if size = h.Len(); size != 5 {
|
||||
t.Errorf("Wrong size: %v", size)
|
||||
}
|
||||
|
||||
r = h.Get(2)
|
||||
expected = []string{"5", "6"}
|
||||
if !equal(r, expected) {
|
||||
t.Errorf("Got: %v, Expected: %v", r, expected)
|
||||
}
|
||||
|
||||
r = h.Get(10)
|
||||
expected = []string{"2", "3", "4", "5", "6"}
|
||||
if !equal(r, expected) {
|
||||
t.Errorf("Got: %v, Expected: %v", r, expected)
|
||||
}
|
||||
}
|
55
chat/message.go
Normal file
55
chat/message.go
Normal file
@ -0,0 +1,55 @@
|
||||
package chat
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Container for messages sent to chat
|
||||
type Message struct {
|
||||
Body string
|
||||
from *User // Not set for Sys messages
|
||||
to *User // Only set for PMs
|
||||
timestamp time.Time
|
||||
themeCache *map[*Theme]string
|
||||
}
|
||||
|
||||
func NewMessage(from *User, body string) *Message {
|
||||
m := Message{
|
||||
Body: body,
|
||||
from: from,
|
||||
timestamp: time.Now(),
|
||||
}
|
||||
return &m
|
||||
}
|
||||
|
||||
// Set message recipient
|
||||
func (m *Message) To(u *User) *Message {
|
||||
m.to = u
|
||||
return m
|
||||
}
|
||||
|
||||
// Set message sender
|
||||
func (m *Message) From(u *User) *Message {
|
||||
m.from = u
|
||||
return m
|
||||
}
|
||||
|
||||
// Render message based on the given theme
|
||||
func (m *Message) Render(*Theme) string {
|
||||
// TODO: Render based on theme.
|
||||
// TODO: Cache based on theme
|
||||
var msg string
|
||||
if m.to != nil {
|
||||
msg = fmt.Sprintf("[PM from %s] %s", m.to, m.Body)
|
||||
} else if m.from != nil {
|
||||
msg = fmt.Sprintf("%s: %s", m.from, m.Body)
|
||||
} else {
|
||||
msg = fmt.Sprintf(" * %s", m.Body)
|
||||
}
|
||||
return msg
|
||||
}
|
||||
|
||||
func (m *Message) String() string {
|
||||
return m.Render(nil)
|
||||
}
|
106
chat/set.go
Normal file
106
chat/set.go
Normal file
@ -0,0 +1,106 @@
|
||||
package chat
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var ErrIdTaken error = errors.New("id already taken")
|
||||
var ErrItemMissing error = errors.New("item does not exist")
|
||||
|
||||
// Unique identifier for an item
|
||||
type Id string
|
||||
|
||||
// A prefix for a unique identifier
|
||||
type IdPrefix Id
|
||||
|
||||
// An interface for items to store-able in the set
|
||||
type Item interface {
|
||||
Id() Id
|
||||
}
|
||||
|
||||
// Set with string lookup
|
||||
// TODO: Add trie for efficient prefix lookup?
|
||||
type Set struct {
|
||||
lookup map[Id]Item
|
||||
sync.RWMutex
|
||||
}
|
||||
|
||||
// Create a new set
|
||||
func NewSet() *Set {
|
||||
return &Set{
|
||||
lookup: map[Id]Item{},
|
||||
}
|
||||
}
|
||||
|
||||
// Size of the set right now
|
||||
func (s *Set) Len() int {
|
||||
return len(s.lookup)
|
||||
}
|
||||
|
||||
// Check if user belongs in this set
|
||||
func (s *Set) In(item Item) bool {
|
||||
s.RLock()
|
||||
_, ok := s.lookup[item.Id()]
|
||||
s.RUnlock()
|
||||
return ok
|
||||
}
|
||||
|
||||
// Get user by name
|
||||
func (s *Set) Get(id Id) (Item, error) {
|
||||
s.RLock()
|
||||
item, ok := s.lookup[id]
|
||||
s.RUnlock()
|
||||
|
||||
if !ok {
|
||||
return nil, ErrItemMissing
|
||||
}
|
||||
|
||||
return item, nil
|
||||
}
|
||||
|
||||
// Add user to set if user does not exist already
|
||||
func (s *Set) Add(item Item) error {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
|
||||
_, found := s.lookup[item.Id()]
|
||||
if found {
|
||||
return ErrIdTaken
|
||||
}
|
||||
|
||||
s.lookup[item.Id()] = item
|
||||
return nil
|
||||
}
|
||||
|
||||
// Remove user from set
|
||||
func (s *Set) Remove(item Item) error {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
id := item.Id()
|
||||
_, found := s.lookup[id]
|
||||
if found {
|
||||
return ErrItemMissing
|
||||
}
|
||||
delete(s.lookup, id)
|
||||
return nil
|
||||
}
|
||||
|
||||
// List users by prefix, case insensitive
|
||||
func (s *Set) ListPrefix(prefix string) []Item {
|
||||
r := []Item{}
|
||||
prefix = strings.ToLower(prefix)
|
||||
|
||||
s.RLock()
|
||||
defer s.RUnlock()
|
||||
|
||||
for id, item := range s.lookup {
|
||||
if !strings.HasPrefix(string(id), prefix) {
|
||||
continue
|
||||
}
|
||||
r = append(r, item)
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
38
chat/set_test.go
Normal file
38
chat/set_test.go
Normal file
@ -0,0 +1,38 @@
|
||||
package chat
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestSet(t *testing.T) {
|
||||
var err error
|
||||
s := NewSet()
|
||||
u := NewUser("foo")
|
||||
|
||||
if s.In(u) {
|
||||
t.Errorf("Set should be empty.")
|
||||
}
|
||||
|
||||
err = s.Add(u)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if !s.In(u) {
|
||||
t.Errorf("Set should contain user.")
|
||||
}
|
||||
|
||||
u2 := NewUser("bar")
|
||||
err = s.Add(u2)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
err = s.Add(u2)
|
||||
if err != ErrIdTaken {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
size := s.Len()
|
||||
if size != 2 {
|
||||
t.Errorf("Set wrong size: %d (expected %d)", size, 2)
|
||||
}
|
||||
}
|
141
chat/theme.go
Normal file
141
chat/theme.go
Normal file
@ -0,0 +1,141 @@
|
||||
package chat
|
||||
|
||||
import "fmt"
|
||||
|
||||
const (
|
||||
// Reset resets the color
|
||||
Reset = "\033[0m"
|
||||
|
||||
// Bold makes the following text bold
|
||||
Bold = "\033[1m"
|
||||
|
||||
// Dim dims the following text
|
||||
Dim = "\033[2m"
|
||||
|
||||
// Italic makes the following text italic
|
||||
Italic = "\033[3m"
|
||||
|
||||
// Underline underlines the following text
|
||||
Underline = "\033[4m"
|
||||
|
||||
// Blink blinks the following text
|
||||
Blink = "\033[5m"
|
||||
|
||||
// Invert inverts the following text
|
||||
Invert = "\033[7m"
|
||||
)
|
||||
|
||||
// Interface for Colors
|
||||
type Color interface {
|
||||
String() string
|
||||
Format(string) string
|
||||
}
|
||||
|
||||
// 256 color type, for terminals who support it
|
||||
type Color256 int8
|
||||
|
||||
// String version of this color
|
||||
func (c Color256) String() string {
|
||||
return fmt.Sprintf("38;05;%d", c)
|
||||
}
|
||||
|
||||
// Return formatted string with this color
|
||||
func (c Color256) Format(s string) string {
|
||||
return "\033[" + c.String() + "m" + s + Reset
|
||||
}
|
||||
|
||||
// No color, used for mono theme
|
||||
type Color0 struct{}
|
||||
|
||||
// No-op for Color0
|
||||
func (c Color0) String() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// No-op for Color0
|
||||
func (c Color0) Format(s string) string {
|
||||
return s
|
||||
}
|
||||
|
||||
// Container for a collection of colors
|
||||
type Palette struct {
|
||||
colors []Color
|
||||
size int
|
||||
}
|
||||
|
||||
// Get a color by index, overflows are looped around.
|
||||
func (p Palette) Get(i int) Color {
|
||||
return p.colors[i%p.size]
|
||||
}
|
||||
|
||||
// Collection of settings for chat
|
||||
type Theme struct {
|
||||
id string
|
||||
sys Color
|
||||
pm Color
|
||||
names *Palette
|
||||
}
|
||||
|
||||
// Colorize name string given some index
|
||||
func (t Theme) ColorName(s string, i int) string {
|
||||
if t.names == nil {
|
||||
return s
|
||||
}
|
||||
|
||||
return t.names.Get(i).Format(s)
|
||||
}
|
||||
|
||||
// Colorize the PM string
|
||||
func (t Theme) ColorPM(s string) string {
|
||||
if t.pm == nil {
|
||||
return s
|
||||
}
|
||||
|
||||
return t.pm.Format(s)
|
||||
}
|
||||
|
||||
// Colorize the Sys message
|
||||
func (t Theme) ColorSys(s string) string {
|
||||
if t.sys == nil {
|
||||
return s
|
||||
}
|
||||
|
||||
return t.sys.Format(s)
|
||||
}
|
||||
|
||||
// List of initialzied themes
|
||||
var Themes []Theme
|
||||
|
||||
// Default theme to use
|
||||
var DefaultTheme *Theme
|
||||
|
||||
func readableColors256() *Palette {
|
||||
p := Palette{
|
||||
colors: make([]Color, 246),
|
||||
size: 246,
|
||||
}
|
||||
for i := 0; i < 256; i++ {
|
||||
if (16 <= i && i <= 18) || (232 <= i && i <= 237) {
|
||||
// Remove the ones near black, this is kinda sadpanda.
|
||||
continue
|
||||
}
|
||||
p.colors = append(p.colors, Color256(i))
|
||||
}
|
||||
return &p
|
||||
}
|
||||
|
||||
func init() {
|
||||
palette := readableColors256()
|
||||
|
||||
Themes = []Theme{
|
||||
Theme{
|
||||
id: "colors",
|
||||
names: palette,
|
||||
},
|
||||
Theme{
|
||||
id: "mono",
|
||||
},
|
||||
}
|
||||
|
||||
DefaultTheme = &Themes[0]
|
||||
}
|
67
chat/user.go
Normal file
67
chat/user.go
Normal file
@ -0,0 +1,67 @@
|
||||
package chat
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"time"
|
||||
)
|
||||
|
||||
// User definition, implemented set Item interface
|
||||
type User struct {
|
||||
name string
|
||||
op bool
|
||||
colorIdx int
|
||||
joined time.Time
|
||||
Config UserConfig
|
||||
}
|
||||
|
||||
func NewUser(name string) *User {
|
||||
u := User{Config: *DefaultUserConfig}
|
||||
u.SetName(name)
|
||||
return &u
|
||||
}
|
||||
|
||||
// Return unique identifier for user
|
||||
func (u *User) Id() Id {
|
||||
return Id(u.name)
|
||||
}
|
||||
|
||||
// Return user's name
|
||||
func (u *User) Name() string {
|
||||
return u.name
|
||||
}
|
||||
|
||||
// Return set user's name
|
||||
func (u *User) SetName(name string) {
|
||||
u.name = name
|
||||
u.colorIdx = rand.Int()
|
||||
}
|
||||
|
||||
// Return whether user is an admin
|
||||
func (u *User) Op() bool {
|
||||
return u.op
|
||||
}
|
||||
|
||||
// Set whether user is an admin
|
||||
func (u *User) SetOp(op bool) {
|
||||
u.op = op
|
||||
}
|
||||
|
||||
// Container for per-user configurations.
|
||||
type UserConfig struct {
|
||||
Highlight bool
|
||||
Bell bool
|
||||
Theme *Theme
|
||||
}
|
||||
|
||||
// Default user configuration to use
|
||||
var DefaultUserConfig *UserConfig
|
||||
|
||||
func init() {
|
||||
DefaultUserConfig = &UserConfig{
|
||||
Highlight: true,
|
||||
Bell: false,
|
||||
Theme: DefaultTheme,
|
||||
}
|
||||
|
||||
// TODO: Seed random?
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user