Message Takebacks
This commit is contained in:
parent
a797bc45da
commit
b19a4821e4
30
Protocol.md
30
Protocol.md
|
@ -123,12 +123,16 @@ The server sends a similar message to push chats to the client:
|
|||
"action": "message",
|
||||
"channel": "lobby",
|
||||
"username": "senderName",
|
||||
"message": "Hello!"
|
||||
"message": "Hello!",
|
||||
"msgID": 123
|
||||
}
|
||||
```
|
||||
|
||||
If the message is a DM, the channel will be the username prepended by an @ symbol and the ChatClient will add it to the appropriate DM thread (creating a new DM thread if needed).
|
||||
|
||||
Every message or file share originated from a user has a "msgID" attached
|
||||
which is useful for [takebacks](#takeback).
|
||||
|
||||
## File
|
||||
|
||||
Sent by: Client.
|
||||
|
@ -146,6 +150,30 @@ The client is posting an image to share in chat.
|
|||
|
||||
The server will massage and validate the image data and then send it to others in the chat via a normal `message` containing an `<img>` tag with a data: URL - directly passing the image data to other chatters without needing to store it somewhere with a public URL.
|
||||
|
||||
## Takeback
|
||||
|
||||
Sent by: Client, Server.
|
||||
|
||||
The takeback message is how a user can delete their previous message from
|
||||
everybody else's display. Operators may also take back messages sent by
|
||||
other users.
|
||||
|
||||
```javascript
|
||||
{
|
||||
"action": "takeback",
|
||||
"msgID": 123
|
||||
}
|
||||
```
|
||||
|
||||
Every message or file share initiated by a real user (not ChatClient or
|
||||
ChatServer) is assigned an auto-incrementing message ID, and the chat
|
||||
server records which message IDs "belong" to which user (so that a
|
||||
modded chat client or bot can't takeback other peoples' messages without
|
||||
operator rights).
|
||||
|
||||
When the front-end receives a takeback, it searches all channels to
|
||||
delete the message with that ID.
|
||||
|
||||
## Presence
|
||||
|
||||
Sent by: Server.
|
||||
|
|
|
@ -106,12 +106,20 @@ func (s *Server) OnMessage(sub *Subscriber, msg Message) {
|
|||
// Detect and expand media such as YouTube videos.
|
||||
markdown = s.ExpandMedia(markdown)
|
||||
|
||||
// Assign a message ID and own it to the sender.
|
||||
MessageID++
|
||||
var mid = MessageID
|
||||
sub.midMu.Lock()
|
||||
sub.messageIDs[mid] = struct{}{}
|
||||
sub.midMu.Unlock()
|
||||
|
||||
// Message to be echoed to the channel.
|
||||
var message = Message{
|
||||
Action: ActionMessage,
|
||||
Channel: msg.Channel,
|
||||
Username: sub.Username,
|
||||
Message: markdown,
|
||||
MessageID: mid,
|
||||
}
|
||||
|
||||
// Is this a DM?
|
||||
|
@ -143,6 +151,25 @@ func (s *Server) OnMessage(sub *Subscriber, msg Message) {
|
|||
s.Broadcast(message)
|
||||
}
|
||||
|
||||
// OnTakeback handles takebacks (delete your message for everybody)
|
||||
func (s *Server) OnTakeback(sub *Subscriber, msg Message) {
|
||||
// Permission check.
|
||||
if sub.JWTClaims == nil || !sub.JWTClaims.IsAdmin {
|
||||
sub.midMu.Lock()
|
||||
defer sub.midMu.Unlock()
|
||||
if _, ok := sub.messageIDs[msg.MessageID]; !ok {
|
||||
sub.ChatServer("That is not your message to take back.")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Broadcast to everybody to remove this message.
|
||||
s.Broadcast(Message{
|
||||
Action: ActionTakeback,
|
||||
MessageID: msg.MessageID,
|
||||
})
|
||||
}
|
||||
|
||||
// OnFile handles a picture shared in chat with a channel.
|
||||
func (s *Server) OnFile(sub *Subscriber, msg Message) {
|
||||
if sub.Username == "" {
|
||||
|
@ -172,11 +199,19 @@ func (s *Server) OnFile(sub *Subscriber, msg Message) {
|
|||
img, pvWidth, pvHeight := ProcessImage(filetype, msg.Bytes)
|
||||
var dataURL = fmt.Sprintf("data:%s;base64,%s", filetype, base64.StdEncoding.EncodeToString(img))
|
||||
|
||||
// Assign a message ID and own it to the sender.
|
||||
MessageID++
|
||||
var mid = MessageID
|
||||
sub.midMu.Lock()
|
||||
sub.messageIDs[mid] = struct{}{}
|
||||
sub.midMu.Unlock()
|
||||
|
||||
// Message to be echoed to the channel.
|
||||
var message = Message{
|
||||
Action: ActionMessage,
|
||||
Channel: msg.Channel,
|
||||
Username: sub.Username,
|
||||
MessageID: mid,
|
||||
|
||||
// Their image embedded via a data: URI - no server storage needed!
|
||||
Message: fmt.Sprintf(
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
package barertc
|
||||
|
||||
// Auto incrementing Message ID for anything pushed out by the server.
|
||||
var MessageID int
|
||||
|
||||
/*
|
||||
Message is the basic carrier of WebSocket chat protocol actions.
|
||||
|
||||
|
@ -26,6 +29,9 @@ type Message struct {
|
|||
ChatStatus string `json:"status,omitempty"` // online vs. away
|
||||
NSFW bool `json:"nsfw,omitempty"` // user tags their video NSFW
|
||||
|
||||
// Message ID to support takebacks/local deletions
|
||||
MessageID int `json:"msgID,omitempty"`
|
||||
|
||||
// Sent on `open` actions along with the (other) Username.
|
||||
OpenSecret string `json:"openSecret,omitempty"`
|
||||
|
||||
|
@ -53,6 +59,7 @@ const (
|
|||
ActionWatch = "watch" // user has received video and is watching you
|
||||
ActionUnwatch = "unwatch" // user has closed your video
|
||||
ActionFile = "file" // image sharing in chat
|
||||
ActionTakeback = "takeback" // user takes back (deletes) their message for everybody
|
||||
|
||||
// Actions sent by server only
|
||||
ActionPing = "ping"
|
||||
|
|
|
@ -39,6 +39,10 @@ type Subscriber struct {
|
|||
muteMu sync.RWMutex
|
||||
booted map[string]struct{} // usernames booted off your camera
|
||||
muted map[string]struct{} // usernames you muted
|
||||
|
||||
// Record which message IDs belong to this user.
|
||||
midMu sync.Mutex
|
||||
messageIDs map[int]struct{}
|
||||
}
|
||||
|
||||
// ReadLoop spawns a goroutine that reads from the websocket connection.
|
||||
|
@ -102,6 +106,8 @@ func (sub *Subscriber) ReadLoop(s *Server) {
|
|||
s.OnWatch(sub, msg)
|
||||
case ActionUnwatch:
|
||||
s.OnUnwatch(sub, msg)
|
||||
case ActionTakeback:
|
||||
s.OnTakeback(sub, msg)
|
||||
default:
|
||||
sub.ChatServer("Unsupported message type.")
|
||||
}
|
||||
|
@ -172,6 +178,7 @@ func (s *Server) WebSocket() http.HandlerFunc {
|
|||
},
|
||||
booted: make(map[string]struct{}),
|
||||
muted: make(map[string]struct{}),
|
||||
messageIDs: make(map[int]struct{}),
|
||||
ChatStatus: "online",
|
||||
}
|
||||
|
||||
|
|
|
@ -311,6 +311,10 @@ const app = Vue.createApp({
|
|||
// Is the current channel a DM?
|
||||
return this.channel.indexOf("@") === 0;
|
||||
},
|
||||
isOp() {
|
||||
// Returns if the current user has operator rights
|
||||
return this.jwt.claims.op;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
// Load user prefs from localStorage, called on startup
|
||||
|
@ -503,9 +507,26 @@ const app = Vue.createApp({
|
|||
channel: msg.channel,
|
||||
username: msg.username,
|
||||
message: msg.message,
|
||||
messageID: msg.msgID,
|
||||
});
|
||||
},
|
||||
|
||||
// A user deletes their message for everybody
|
||||
onTakeback(msg) {
|
||||
// Search all channels for this message ID and remove it.
|
||||
for (let channel of Object.keys(this.channels)) {
|
||||
for (let i = 0; i < this.channels[channel].history.length; i++) {
|
||||
let cmp = this.channels[channel].history[i];
|
||||
if (cmp.msgID === msg.msgID) {
|
||||
this.channels[channel].history.splice(i, 1);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.error("Got a takeback for msgID %d but did not find it!", msg.msgID);
|
||||
},
|
||||
|
||||
// User logged in or out.
|
||||
onPresence(msg) {
|
||||
// TODO: make a dedicated leave event
|
||||
|
@ -608,6 +629,9 @@ const app = Vue.createApp({
|
|||
case "message":
|
||||
this.onMessage(msg);
|
||||
break;
|
||||
case "takeback":
|
||||
this.onTakeback(msg);
|
||||
break;
|
||||
case "presence":
|
||||
this.onPresence(msg);
|
||||
break;
|
||||
|
@ -947,6 +971,27 @@ const app = Vue.createApp({
|
|||
delete(this.channels[channel]);
|
||||
},
|
||||
|
||||
/* Take back messages (for everyone) or remove locally */
|
||||
takeback(msg) {
|
||||
if (!window.confirm(
|
||||
"Do you want to take this message back? Doing so will remove this message from everybody's view in the chat room."
|
||||
)) return;
|
||||
|
||||
this.ws.conn.send(JSON.stringify({
|
||||
action: "takeback",
|
||||
msgID: msg.msgID,
|
||||
}));
|
||||
},
|
||||
removeMessage(msg) {
|
||||
if (!window.confirm(
|
||||
"Do you want to remove this message from your view? This will delete the message only for you, but others in this chat thread may still see it."
|
||||
)) return;
|
||||
|
||||
this.onTakeback({
|
||||
msgID: msg.msgID,
|
||||
})
|
||||
},
|
||||
|
||||
activeChannels() {
|
||||
// List of current channels, unread indicators etc.
|
||||
let result = [];
|
||||
|
@ -1357,7 +1402,7 @@ const app = Vue.createApp({
|
|||
};
|
||||
}
|
||||
},
|
||||
pushHistory({ channel, username, message, action = "message", isChatServer, isChatClient }) {
|
||||
pushHistory({ channel, username, message, action = "message", isChatServer, isChatClient, messageID }) {
|
||||
// Default channel = your current channel.
|
||||
if (!channel) {
|
||||
channel = this.channel;
|
||||
|
@ -1372,6 +1417,7 @@ const app = Vue.createApp({
|
|||
action: action,
|
||||
username: username,
|
||||
message: message,
|
||||
msgID: messageID,
|
||||
at: new Date(),
|
||||
isChatServer,
|
||||
isChatClient,
|
||||
|
|
|
@ -821,26 +821,44 @@
|
|||
<small v-else class="has-text-grey">internal</small>
|
||||
</div>
|
||||
|
||||
<div class="column is-narrow px-1 pt-0"
|
||||
v-if="!(msg.username === username || isDM)">
|
||||
<div class="column is-narrow pl-1 pt-0">
|
||||
<!-- DMs button -->
|
||||
<button type="button"
|
||||
v-if="!(msg.username === username || isDM)"
|
||||
class="button is-grey is-outlined is-small px-2"
|
||||
@click="openDMs({username: msg.username})">
|
||||
@click="openDMs({username: msg.username})"
|
||||
title="Open a Direct Message (DM) thread">
|
||||
<i class="fa fa-message"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="column is-narrow pl-1 pt-0"
|
||||
v-if="!(msg.username === username)">
|
||||
|
||||
<!-- Mute button -->
|
||||
<button type="button"
|
||||
class="button is-grey is-outlined is-small px-2"
|
||||
v-if="!(msg.username === username)"
|
||||
class="button is-grey is-outlined is-small px-2 ml-1"
|
||||
@click="muteUser(msg.username)"
|
||||
title="Mute user">
|
||||
<i class="fa fa-comment-slash"
|
||||
:class="{'has-text-success': isMutedUser(msg.username),
|
||||
'has-text-danger': !isMutedUser(msg.username)}"></i>
|
||||
</button>
|
||||
|
||||
<!-- Owner or admin: take back the message -->
|
||||
<button type="button"
|
||||
v-if="msg.username === username || isOp"
|
||||
class="button is-grey is-outlined is-small px-2 ml-1"
|
||||
title="Take back this message (delete it for everybody)"
|
||||
@click="takeback(msg)">
|
||||
<i class="fa fa-rotate-left has-text-danger"></i>
|
||||
</button>
|
||||
|
||||
<!-- Everyone else: can hide it locally -->
|
||||
<button type="button"
|
||||
v-if="msg.username !== username"
|
||||
class="button is-grey is-outlined is-small px-2 ml-1"
|
||||
title="Hide this message (delete it only for your view)"
|
||||
@click="removeMessage(msg)">
|
||||
<i class="fa fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
Loading…
Reference in New Issue
Block a user