190 lines
6.3 KiB
Go
190 lines
6.3 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"compress/zlib"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/clockworksoul/smudge"
|
|
)
|
|
|
|
// messageType is an alias for int8. Whenever you see "messageType" in the code,
|
|
// think int8. Using this alias however allows us to increase code clarity and
|
|
// safety beyond simply passing meaningless numbers around.
|
|
type messageType int8
|
|
|
|
const (
|
|
// Go does not have any native support for enum types, however there is an
|
|
// idiomatic way to accomplish the same goal.
|
|
// https://golang.org/ref/spec#Iota
|
|
|
|
messageTypeChat messageType = iota + 1
|
|
messageTypeUsernames
|
|
messageTypeUsernameReq
|
|
)
|
|
|
|
// message represents the structure of the contents in a smudge.Broadcast. We
|
|
// can use the Type to determine what the Body will contain.
|
|
type message struct {
|
|
// The text in backticks after each field here is called a Struct Tag.
|
|
// The standard JSON marshaller uses the "json" struct tag to name fields.
|
|
//
|
|
// https://golang.org/pkg/reflect/#StructTag
|
|
// https://golang.org/pkg/encoding/json/#Marshal
|
|
//
|
|
// The JSON marshaller can only interact with "exported fields", therefore
|
|
// these field names must start with an uppercase letter.
|
|
|
|
// Type informs us what to expect in the body of this message, and what
|
|
// action to take on it.
|
|
Type messageType `json:"type"`
|
|
|
|
// Body contains the bulk of the message
|
|
Body string `json:"body"`
|
|
|
|
// Usernames is filled only in a messageTypeUsernames. It contains a map
|
|
// of the address->username pairings know by the sending client.
|
|
Usernames map[NodeAddress]string `json:"usernames"`
|
|
}
|
|
|
|
// Encode converts the message into a form which can be sent to other clients
|
|
// through Smudge (a []byte, pronounced byte slice).
|
|
//
|
|
// Notice how this func has an extra set of parens before the name? This is how
|
|
// methods are defined in Go. Unlike a function, methods are not in the global
|
|
// namespace and must be called on an instance of an object. That object being
|
|
// called on is in the first set of parens, and is called the receiver. This of
|
|
// this as "self" in python, or "this" in many other languages.
|
|
// More info: https://tour.golang.org/methods/1
|
|
func (m *message) Encode() []byte {
|
|
// There is a lot happening here in a pretty small space. We first create an
|
|
// empty buffer in which we can temporarily store some bytes. This buffer
|
|
// implements the io.Writer interface, but we want to write compressed
|
|
// bytes, so we wrap that writer in the zlib writer which also implements
|
|
// the io.Writer interface. Finally we create a json encoder which will
|
|
// output the json format of m into the zlib writer.
|
|
var b bytes.Buffer
|
|
w := zlib.NewWriter(&b)
|
|
err := json.NewEncoder(w).Encode(m)
|
|
if err != nil {
|
|
printError("Failed to marshal a chat message to send: %s", err)
|
|
}
|
|
err = w.Close() // The bytes might not actually be written until closed (or flushed)
|
|
if err != nil {
|
|
printError("Failed to close the encoding writer: %s", err)
|
|
}
|
|
|
|
// read out the contents from our temporary buffer, and return them
|
|
return b.Bytes()
|
|
}
|
|
|
|
// Messenger contains all the messages which we know have been sent in the past.
|
|
// Additionally, it provides the interface for sending and receiving new
|
|
// messages.
|
|
//
|
|
// To send and receive messages we must implement the Broadcast functionality in
|
|
// the smudge library.
|
|
//
|
|
// https://godoc.org/github.com/clockworksoul/smudge#BroadcastListener
|
|
// https://godoc.org/github.com/clockworksoul/smudge#BroadcastString
|
|
type Messenger struct {
|
|
// clients is the list of all known and alive clients. Maintaining a
|
|
// reference here will allow us to update status based on broadcasts.
|
|
clients ClientList
|
|
}
|
|
|
|
// Decode converts the byte slice received from a broadcast into a usable
|
|
// message. This is the reverse of the Encode() operation.
|
|
//
|
|
// Just like how the encode method above uses the json and zlib packages to
|
|
// json marshal and then compress a message, here we are doing the reverse.
|
|
func (m *message) Decode(data []byte) error {
|
|
bb := bytes.NewReader(data)
|
|
r, err := zlib.NewReader(bb)
|
|
if err != nil {
|
|
return fmt.Errorf("Failed to decompress message: %s", err)
|
|
}
|
|
|
|
// msg is what the decompressed bytes will be un-json-marshalled into
|
|
err = json.NewDecoder(r).Decode(m)
|
|
if err != nil {
|
|
return fmt.Errorf("Failed to decode message: %s", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// OnBroadcast is the only method defined on the smudge.BroadcastListener
|
|
// interface. By implementing this method on the Messenger struct, that struct
|
|
// will satisfy the interface and we can register it with smudge.
|
|
//
|
|
// When another node in the gossip cluster sends a broadcast message, this
|
|
// function will be called.
|
|
func (m *Messenger) OnBroadcast(b *smudge.Broadcast) {
|
|
senderAddr := NodeAddress(b.Origin().Address())
|
|
|
|
printDebug("Received %d bytes", len(b.Bytes()))
|
|
var msg message
|
|
err := msg.Decode(b.Bytes())
|
|
if err != nil {
|
|
printError("Failed to receive message from %s: %s", senderAddr, err)
|
|
return
|
|
}
|
|
|
|
switch msg.Type {
|
|
case messageTypeUsernames:
|
|
printDebug("Received a broadcast containing usernames")
|
|
|
|
if msg.Usernames == nil || len(msg.Usernames) == 0 {
|
|
printError("Received an empty username list")
|
|
return
|
|
}
|
|
|
|
err := m.clients.AddUsernames(msg.Usernames)
|
|
if err != nil {
|
|
printError("Failed to process received usernames: %s", err)
|
|
}
|
|
case messageTypeUsernameReq:
|
|
printDebug("Received a broadcast requesting %s send usernames, my localAddress is %s", msg.Body, localAddress)
|
|
|
|
if msg.Body == string(localAddress) {
|
|
// The request targeted us...
|
|
// Let's send all the usernames we know about to minimize requests
|
|
// for a new client.
|
|
err := m.clients.BroadcastUsernames()
|
|
if err == nil {
|
|
printInfo("Successfully broadcast usernames to the group")
|
|
} else {
|
|
printError("Tried to broadcast usernames but failed: %s", err)
|
|
}
|
|
}
|
|
case messageTypeChat:
|
|
// Received a chat message
|
|
|
|
sender := m.clients[senderAddr]
|
|
printChatMessage(msg.Body, sender.GetName())
|
|
}
|
|
}
|
|
|
|
// SendMessage takes a chat message to be sent and broadcasts it to the cluster
|
|
// and posts to the local chat view.
|
|
func SendMessage(text string) error {
|
|
text = strings.TrimSpace(text)
|
|
if text == "" {
|
|
return nil
|
|
}
|
|
|
|
// First let's make the message show up in our own chat history
|
|
printChatMessage(text, localUsername)
|
|
|
|
// Now we can send it on to others
|
|
msg := message{
|
|
Type: messageTypeChat,
|
|
Body: text,
|
|
}
|
|
|
|
return smudge.BroadcastBytes(msg.Encode())
|
|
}
|