Chat updates (#92)

* Send PONG responses to PINGs

* Split out client IDs for viewer counts vs. websocket IDs

* WIP username change event

* Display username changes

* Revert commented out code

* Add support for building from the current branch

* Fix PONG

* Make username changes have a unique ID

* Add a version param to js to cachebust
This commit is contained in:
Gabe Kangas 2020-07-28 21:30:03 -07:00 committed by GitHub
parent 87636a4183
commit d9509f5606
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 161 additions and 47 deletions

View file

@ -1,6 +1,7 @@
package chat
import (
"encoding/json"
"fmt"
"io"
"time"
@ -21,14 +22,23 @@ type Client struct {
ConnectedAt time.Time
MessageCount int
id string
ws *websocket.Conn
ch chan models.ChatMessage
pingch chan models.PingMessage
clientID string // How we identify unique viewers when counting viewer counts.
socketID string // How we identify a single websocket client.
ws *websocket.Conn
ch chan models.ChatMessage
pingch chan models.PingMessage
usernameChangeChannel chan models.NameChangeEvent
doneCh chan bool
}
const (
CHAT = "CHAT"
NAMECHANGE = "NAME_CHANGE"
PING = "PING"
PONG = "PONG"
)
//NewClient creates a new chat client
func NewClient(ws *websocket.Conn) *Client {
if ws == nil {
@ -38,9 +48,12 @@ func NewClient(ws *websocket.Conn) *Client {
ch := make(chan models.ChatMessage, channelBufSize)
doneCh := make(chan bool)
pingch := make(chan models.PingMessage)
clientID := utils.GenerateClientIDFromRequest(ws.Request())
usernameChangeChannel := make(chan models.NameChangeEvent)
return &Client{time.Now(), 0, clientID, ws, ch, pingch, doneCh}
clientID := utils.GenerateClientIDFromRequest(ws.Request())
socketID, _ := shortid.Generate()
return &Client{time.Now(), 0, clientID, socketID, ws, ch, pingch, usernameChangeChannel, doneCh}
}
//GetConnection gets the connection for the client
@ -53,7 +66,7 @@ func (c *Client) Write(msg models.ChatMessage) {
case c.ch <- msg:
default:
_server.remove(c)
_server.err(fmt.Errorf("client %s is disconnected", c.id))
_server.err(fmt.Errorf("client %s is disconnected", c.clientID))
}
}
@ -79,7 +92,8 @@ func (c *Client) listenWrite() {
case msg := <-c.ch:
// log.Println("Send:", msg)
websocket.JSON.Send(c.ws, msg)
case msg := <-c.usernameChangeChannel:
websocket.JSON.Send(c.ws, msg)
// receive done request
case <-c.doneCh:
_server.remove(c)
@ -102,28 +116,59 @@ func (c *Client) listenRead() {
// read data from websocket connection
default:
var msg models.ChatMessage
id, err := shortid.Generate()
var data []byte
err := websocket.Message.Receive(c.ws, &data)
if err != nil {
log.Panicln(err)
if err == io.EOF {
c.doneCh <- true
} else {
log.Errorln(err)
}
return
}
msg.ID = id
msg.MessageType = "CHAT"
msg.Timestamp = time.Now()
msg.Visible = true
var messageTypeCheck map[string]interface{}
err = json.Unmarshal(data, &messageTypeCheck)
if err != nil {
log.Errorln(err)
}
if err := websocket.JSON.Receive(c.ws, &msg); err == io.EOF {
c.doneCh <- true
return
} else if err != nil {
_server.err(err)
} else {
c.MessageCount++
messageType := messageTypeCheck["type"]
msg.ClientID = c.id
_server.SendToAll(msg)
if messageType == CHAT {
c.chatMessageReceived(data)
} else if messageType == NAMECHANGE {
c.userChangedName(data)
}
}
}
}
func (c *Client) userChangedName(data []byte) {
var msg models.NameChangeEvent
err := json.Unmarshal(data, &msg)
if err != nil {
log.Errorln(err)
}
msg.Type = NAMECHANGE
msg.ID = shortid.MustGenerate()
_server.usernameChanged(msg)
}
func (c *Client) chatMessageReceived(data []byte) {
var msg models.ChatMessage
err := json.Unmarshal(data, &msg)
if err != nil {
log.Errorln(err)
}
id, _ := shortid.Generate()
msg.ID = id
msg.Timestamp = time.Now()
msg.Visible = true
c.MessageCount++
msg.ClientID = c.clientID
_server.SendToAll(msg)
}

View file

@ -64,17 +64,23 @@ func (s *server) sendAll(msg models.ChatMessage) {
}
func (s *server) ping() {
ping := models.PingMessage{MessageType: "PING"}
ping := models.PingMessage{MessageType: PING}
for _, c := range s.Clients {
c.pingch <- ping
}
}
func (s *server) usernameChanged(msg models.NameChangeEvent) {
for _, c := range s.Clients {
c.usernameChangeChannel <- msg
}
}
func (s *server) onConnection(ws *websocket.Conn) {
client := NewClient(ws)
defer func() {
log.Tracef("The client was connected for %s and sent %d messages (%s)", time.Since(client.ConnectedAt), client.MessageCount, client.id)
log.Tracef("The client was connected for %s and sent %d messages (%s)", time.Since(client.ConnectedAt), client.MessageCount, client.clientID)
if err := ws.Close(); err != nil {
s.errCh <- err
@ -96,15 +102,14 @@ func (s *server) Listen() {
select {
// add new a client
case c := <-s.addCh:
s.Clients[c.id] = c
s.listener.ClientAdded(c.id)
s.Clients[c.socketID] = c
s.listener.ClientAdded(c.clientID)
s.sendWelcomeMessageToClient(c)
// remove a client
case c := <-s.delCh:
delete(s.Clients, c.id)
s.listener.ClientRemoved(c.id)
delete(s.Clients, c.socketID)
s.listener.ClientRemoved(c.clientID)
// broadcast a message to all clients
case msg := <-s.sendAllCh:

10
models/nameChangeEvent.go Normal file
View file

@ -0,0 +1,10 @@
package models
//NameChangeEvent represents a user changing their name in chat
type NameChangeEvent struct {
OldName string `json:"oldName"`
NewName string `json:"newName"`
Image string `json:"image"`
Type string `json:"type"`
ID string `json:"id"`
}

View file

@ -17,7 +17,7 @@ fi
[[ -z "${VERSION}" ]] && VERSION='unknownver' || VERSION="${VERSION}"
GIT_COMMIT=$(git rev-list -1 HEAD)
GIT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
# Change to the root directory of the repository
cd $(git rev-parse --show-toplevel)
@ -35,7 +35,7 @@ build() {
VERSION=$4
GIT_COMMIT=$5
echo "Building ${NAME} (${OS}/${ARCH}) release..."
echo "Building ${NAME} (${OS}/${ARCH}) release from ${GIT_BRANCH}..."
mkdir -p dist/${NAME}
mkdir -p dist/${NAME}/webroot/static
@ -51,7 +51,7 @@ build() {
pushd dist/${NAME} >> /dev/null
CGO_ENABLED=1 ~/go/bin/xgo -ldflags "-s -w -X main.GitCommit=${GIT_COMMIT} -X main.BuildVersion=${VERSION} -X main.BuildType=${NAME}" -targets "${OS}/${ARCH}" github.com/gabek/owncast
CGO_ENABLED=1 ~/go/bin/xgo --branch ${GIT_BRANCH} -ldflags "-s -w -X main.GitCommit=${GIT_COMMIT} -X main.BuildVersion=${VERSION} -X main.BuildType=${NAME}" -targets "${OS}/${ARCH}" github.com/gabek/owncast
mv owncast-*-${ARCH} owncast
zip -r -q -8 ../owncast-$NAME-$VERSION.zip .

View file

@ -123,7 +123,8 @@
<div id="chat-container" class="bg-gray-800">
<div id="messages-container">
<div v-for="message in messages" v-cloak>
<div class="message flex">
<!-- Regular user chat message-->
<div class="message flex" v-if="message.type === 'CHAT'">
<div class="message-avatar rounded-full flex items-center justify-center" v-bind:style="{ backgroundColor: message.userColor() }">
<img
v-bind:src="message.image"
@ -134,6 +135,19 @@
<p class="message-text text-gray-400 font-thin " v-html="message.formatText()"></p>
</div>
</div>
<!-- Username change message -->
<div class="message flex" v-else-if="message.type === 'NAME_CHANGE'">
<img
class="mr-2"
width="30px"
v-bind:src="message.image"
/>
<div class="text-white text-center">
<span class="font-bold">{{ message.oldName }}</span> is now known as <span class="font-bold">{{ message.newName }}</span>.
</div>
</div>
</div>
</div>
@ -166,12 +180,12 @@
</div>
<script src="js/usercolors.js"></script>
<script src="js/utils.js"></script>
<script src="js/message.js"></script>
<script src="js/utils.js?v=2"></script>
<script src="js/message.js?v=2"></script>
<script src="js/social.js"></script>
<script src="js/components.js"></script>
<script src="js/player.js"></script>
<script src="js/app.js"></script>
<script src="js/app.js?v=2"></script>
<script>
(function () {
const app = new Owncast();

View file

@ -114,6 +114,11 @@ class Owncast {
if (this.websocketReconnectTimer) {
clearTimeout(this.websocketReconnectTimer);
}
// If we're "online" then enable the chat.
if (this.streamStatus && this.streamStatus.online) {
this.messagingInterface.enableChat();
}
};
ws.onclose = (e) => {
// connection closed, discard old websocket and create a new one in 5s
@ -124,22 +129,36 @@ class Owncast {
};
// On ws error just close the socket and let it re-connect again for now.
ws.onerror = e => {
this.handleNetworkingError(`Stream status: ${e}`);
this.handleNetworkingError(`Socket error: ${JSON.parse(e)}`);
ws.close();
};
ws.onmessage = (e) => {
const model = JSON.parse(e.data);
// Ignore non-chat messages (such as keepalive PINGs)
if (model.type !== SOCKET_MESSAGE_TYPES.CHAT) {
return;
// Send PONGs
if (model.type === SOCKET_MESSAGE_TYPES.PING) {
this.sendPong(ws);
return;
} else if (model.type === SOCKET_MESSAGE_TYPES.CHAT) {
const message = new Message(model);
this.addMessage(message);
} else if (model.type === SOCKET_MESSAGE_TYPES.NAME_CHANGE) {
this.addMessage(model);
}
const message = new Message(model);
this.addMessage(message);
};
this.websocket = ws;
this.messagingInterface.setWebsocket(this.websocket);
};
sendPong(ws) {
try {
const pong = { type: SOCKET_MESSAGE_TYPES.PONG };
ws.send(JSON.stringify(pong));
} catch (e) {
console.log('PONG error:', e);
}
}
addMessage(message) {
const existing = this.vueApp.messages.filter(function (item) {
return item.id === message.id;

View file

@ -142,6 +142,7 @@ class MessagingInterface {
}
handleUpdateUsername() {
const oldName = this.username;
var newValue = this.inputChangeUserName.value;
newValue = newValue.trim();
// do other string cleanup?
@ -154,6 +155,10 @@ class MessagingInterface {
setLocalStorage(KEY_AVATAR, this.imgUsernameAvatar.src);
}
this.handleHideChangeNameForm();
if (oldName !== newValue) {
this.sendUsernameChange(oldName, newValue, this.imgUsernameAvatar.src);
}
}
handleUsernameKeydown(event) {
@ -164,6 +169,19 @@ class MessagingInterface {
}
}
sendUsernameChange(oldName, newName, image) {
const nameChange = {
type: SOCKET_MESSAGE_TYPES.NAME_CHANGE,
oldName: oldName,
newName: newName,
image: image,
};
const jsonMessage = JSON.stringify(nameChange);
this.websocket.send(jsonMessage)
}
handleMessageInputKeydown(event) {
var okCodes = [37,38,39,40,16,91,18,46,8];
var value = this.formMessageInput.value.trim();
@ -213,6 +231,7 @@ class MessagingInterface {
body: content,
author: this.username,
image: this.imgUsernameAvatar.src,
type: SOCKET_MESSAGE_TYPES.CHAT,
});
const messageJSON = JSON.stringify(message);
if (this.websocket) {

View file

@ -84,5 +84,5 @@ function messageBubbleColorForString(str) {
g: parseInt(result[2], 16),
b: parseInt(result[3], 16),
} : null;
return 'rgba(' + rgb.r + ',' + rgb.g + ',' + rgb.b + ', 0.3)';
return 'rgba(' + rgb.r + ',' + rgb.g + ',' + rgb.b + ', 0.4)';
}

View file

@ -6,7 +6,7 @@ const URL_PREFIX = LOCAL_TEST ? 'http://localhost:8080' : '';
const URL_STATUS = `${URL_PREFIX}/status`;
const URL_CHAT_HISTORY = `${URL_PREFIX}/chat`;
const URL_STREAM = `${URL_PREFIX}/hls/stream.m3u8`;
const URL_WEBSOCKET = LOCAL_TEST
const URL_WEBSOCKET = LOCAL_TEST
? 'wss://goth.land/entry'
: `${location.protocol === 'https:' ? 'wss' : 'ws'}://${location.host}/entry`;
@ -21,7 +21,9 @@ const URL_OWNCAST = 'https://github.com/gabek/owncast'; // used in footer
// Webscoket setup
const SOCKET_MESSAGE_TYPES = {
CHAT: 'CHAT',
PING: 'PING'
PING: 'PING',
NAME_CHANGE: 'NAME_CHANGE',
PONG: 'PONG'
}
// Video setup