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:
parent
87636a4183
commit
d9509f5606
@ -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)
|
||||
}
|
||||
|
@ -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
10
models/nameChangeEvent.go
Normal 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"`
|
||||
}
|
@ -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 .
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
|
@ -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) {
|
||||
|
@ -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)';
|
||||
}
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user