Clear DMs history button
This commit is contained in:
parent
d510ac791f
commit
3424be2f4d
31
docs/API.md
31
docs/API.md
|
@ -278,3 +278,34 @@ The response JSON given to the chat page from /api/profile looks like:
|
||||||
The "Remaining" integer in the result shows how many older messages still
|
The "Remaining" integer in the result shows how many older messages still
|
||||||
remain to be retrieved, and tells the front-end page that it can request
|
remain to be retrieved, and tells the front-end page that it can request
|
||||||
another page.
|
another page.
|
||||||
|
|
||||||
|
## POST /api/message/clear
|
||||||
|
|
||||||
|
Clear stored direct message history for a user.
|
||||||
|
|
||||||
|
This endpoint can be called by the user themself (using JWT token authorization),
|
||||||
|
or by your website (using your admin APIKey) so your site can also clear chat
|
||||||
|
history remotely (e.g., for when your user deleted their account).
|
||||||
|
|
||||||
|
The request body payload looks like:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
// when called from the BareRTC frontend for the current user
|
||||||
|
"JWTToken": "the caller's chat jwt token",
|
||||||
|
|
||||||
|
// when called from your website
|
||||||
|
"APIKey": "your AdminAPIKey from settings.toml",
|
||||||
|
"Username": "soandso"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The response JSON given to the chat page looks like:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
"OK": true,
|
||||||
|
"Error": "only on error messages",
|
||||||
|
"MessagesErased": 42
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
122
pkg/api.go
122
pkg/api.go
|
@ -881,6 +881,128 @@ func (s *Server) MessageHistory() http.HandlerFunc {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ClearMessages (/api/message/clear) deletes all the stored direct messages for a user.
|
||||||
|
//
|
||||||
|
// It can be called by the authenticated user themself (with JWTToken), or from your website
|
||||||
|
// (with APIKey) in which case you can remotely clear history for a user.
|
||||||
|
//
|
||||||
|
// It is a POST request with a json body containing the following schema:
|
||||||
|
//
|
||||||
|
// {
|
||||||
|
// "JWTToken": "the caller's jwt token",
|
||||||
|
// "APIKey": "your website's admin API key"
|
||||||
|
// "Username": "if using your APIKey to specify a user to delete",
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// The response JSON will look like the following:
|
||||||
|
//
|
||||||
|
// {
|
||||||
|
// "OK": true,
|
||||||
|
// "Error": "only on error responses",
|
||||||
|
// "MessagesErased": 123,
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// The Remaining value is how many older messages still exist to be loaded.
|
||||||
|
func (s *Server) ClearMessages() http.HandlerFunc {
|
||||||
|
type request struct {
|
||||||
|
JWTToken string
|
||||||
|
APIKey string
|
||||||
|
Username string
|
||||||
|
}
|
||||||
|
|
||||||
|
type result struct {
|
||||||
|
OK bool
|
||||||
|
Error string `json:",omitempty"`
|
||||||
|
MessagesErased int `json:""`
|
||||||
|
}
|
||||||
|
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// JSON writer for the response.
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
enc := json.NewEncoder(w)
|
||||||
|
enc.SetIndent("", " ")
|
||||||
|
|
||||||
|
// Parse the request.
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
enc.Encode(result{
|
||||||
|
Error: "Only POST methods allowed",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
} else if r.Header.Get("Content-Type") != "application/json" {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
enc.Encode(result{
|
||||||
|
Error: "Only application/json content-types allowed",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
defer r.Body.Close()
|
||||||
|
|
||||||
|
// Parse the request payload.
|
||||||
|
var (
|
||||||
|
params request
|
||||||
|
dec = json.NewDecoder(r.Body)
|
||||||
|
)
|
||||||
|
if err := dec.Decode(¶ms); err != nil {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
enc.Encode(result{
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Authenticate this request.
|
||||||
|
if params.APIKey != "" {
|
||||||
|
// By admin API key.
|
||||||
|
if params.APIKey != config.Current.AdminAPIKey {
|
||||||
|
w.WriteHeader(http.StatusUnauthorized)
|
||||||
|
enc.Encode(result{
|
||||||
|
Error: "Authentication denied.",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Are JWT tokens enabled on the server?
|
||||||
|
if !config.Current.JWT.Enabled || params.JWTToken == "" {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
enc.Encode(result{
|
||||||
|
Error: "JWT authentication is not available.",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate the user's JWT token.
|
||||||
|
claims, _, err := jwt.ParseAndValidate(params.JWTToken)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
enc.Encode(result{
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the username to clear.
|
||||||
|
params.Username = claims.Subject
|
||||||
|
}
|
||||||
|
|
||||||
|
// Erase their message history.
|
||||||
|
count, err := (models.DirectMessage{}).ClearMessages(params.Username)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
enc.Encode(result{
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
enc.Encode(result{
|
||||||
|
OK: true,
|
||||||
|
MessagesErased: count,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Blocklist cache sent over from your website.
|
// Blocklist cache sent over from your website.
|
||||||
var (
|
var (
|
||||||
// Map of username to the list of usernames they block.
|
// Map of username to the list of usernames they block.
|
||||||
|
|
|
@ -82,6 +82,42 @@ func (dm DirectMessage) LogMessage(fromUsername, toUsername string, msg messages
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ClearMessages clears all stored DMs that the username as a participant in.
|
||||||
|
func (dm DirectMessage) ClearMessages(username string) (int, error) {
|
||||||
|
if DB == nil {
|
||||||
|
return 0, ErrNotInitialized
|
||||||
|
}
|
||||||
|
|
||||||
|
var placeholders = []interface{}{
|
||||||
|
fmt.Sprintf("@%s:%%", username), // `@alice:%`
|
||||||
|
fmt.Sprintf("%%:@%s", username), // `%:@alice`
|
||||||
|
username,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Count all the messages we'll delete.
|
||||||
|
var (
|
||||||
|
count int
|
||||||
|
row = DB.QueryRow(`
|
||||||
|
SELECT COUNT(message_id)
|
||||||
|
FROM direct_messages
|
||||||
|
WHERE (channel_id LIKE ? OR channel_id LIKE ?)
|
||||||
|
OR username = ?
|
||||||
|
`, placeholders...)
|
||||||
|
)
|
||||||
|
if err := row.Scan(&count); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete them all.
|
||||||
|
_, err := DB.Exec(`
|
||||||
|
DELETE FROM direct_messages
|
||||||
|
WHERE (channel_id LIKE ? OR channel_id LIKE ?)
|
||||||
|
OR username = ?
|
||||||
|
`, placeholders...)
|
||||||
|
|
||||||
|
return count, err
|
||||||
|
}
|
||||||
|
|
||||||
// TakebackMessage removes a message by its MID from the DM history.
|
// TakebackMessage removes a message by its MID from the DM history.
|
||||||
//
|
//
|
||||||
// Because the MessageID may have been from a previous chat session, the server can't immediately
|
// Because the MessageID may have been from a previous chat session, the server can't immediately
|
||||||
|
|
|
@ -58,6 +58,7 @@ func (s *Server) Setup() error {
|
||||||
mux.Handle("/api/shutdown", s.ShutdownAPI())
|
mux.Handle("/api/shutdown", s.ShutdownAPI())
|
||||||
mux.Handle("/api/profile", s.UserProfile())
|
mux.Handle("/api/profile", s.UserProfile())
|
||||||
mux.Handle("/api/message/history", s.MessageHistory())
|
mux.Handle("/api/message/history", s.MessageHistory())
|
||||||
|
mux.Handle("/api/message/clear", s.ClearMessages())
|
||||||
mux.Handle("/assets/", http.StripPrefix("/assets/", http.FileServer(http.Dir("dist/assets"))))
|
mux.Handle("/assets/", http.StripPrefix("/assets/", http.FileServer(http.Dir("dist/assets"))))
|
||||||
mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("dist/static"))))
|
mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("dist/static"))))
|
||||||
|
|
||||||
|
|
104
src/App.vue
104
src/App.vue
|
@ -282,6 +282,12 @@ export default {
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
},
|
},
|
||||||
|
clearDirectMessages: {
|
||||||
|
busy: false,
|
||||||
|
ok: false,
|
||||||
|
messagesErased: 0,
|
||||||
|
timeout: null,
|
||||||
|
},
|
||||||
|
|
||||||
// Responsive CSS controls for mobile.
|
// Responsive CSS controls for mobile.
|
||||||
responsive: {
|
responsive: {
|
||||||
|
@ -1037,6 +1043,13 @@ export default {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clear user chat history.
|
||||||
|
if (this.message.toLowerCase().indexOf("/clear-history") === 0) {
|
||||||
|
this.clearMessageHistory();
|
||||||
|
this.message = "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// console.debug("Send message: %s", this.message);
|
// console.debug("Send message: %s", this.message);
|
||||||
this.client.send({
|
this.client.send({
|
||||||
action: "message",
|
action: "message",
|
||||||
|
@ -3277,6 +3290,65 @@ export default {
|
||||||
this.directMessageHistory[channel].busy = false;
|
this.directMessageHistory[channel].busy = false;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
async clearMessageHistory(prompt = false) {
|
||||||
|
if (!this.jwt.valid || this.clearDirectMessages.busy) return;
|
||||||
|
|
||||||
|
if (prompt) {
|
||||||
|
if (!window.confirm(
|
||||||
|
"This will delete all of your DMs history stored on the server. People you have " +
|
||||||
|
"chatted with will have their past messages sent to you erased as well.\n\n" +
|
||||||
|
"Note: messages that are currently displayed on your chat partner's screen will " +
|
||||||
|
"NOT be removed by this action -- if this is a concern and you want to 'take back' " +
|
||||||
|
"a message from their screen, use the 'take back' button (red arrow circle) on the " +
|
||||||
|
"message you sent to them. The 'clear history' button only clears the database, but " +
|
||||||
|
"does not send takebacks to pull the message from everybody else's screen.\n\n" +
|
||||||
|
"Are you sure you want to clear your stored DMs history on the server?",
|
||||||
|
)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.clearDirectMessages.timeout !== null) {
|
||||||
|
clearTimeout(this.clearDirectMessages.timeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.clearDirectMessages.busy = true;
|
||||||
|
return fetch("/api/message/clear", {
|
||||||
|
method: "POST",
|
||||||
|
mode: "same-origin",
|
||||||
|
cache: "no-cache",
|
||||||
|
credentials: "same-origin",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
"JWTToken": this.jwt.token,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.then((response) => response.json())
|
||||||
|
.then((data) => {
|
||||||
|
if (data.Error) {
|
||||||
|
console.error("ClearMessageHistory: ", data.Error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.clearDirectMessages.ok = true;
|
||||||
|
this.clearDirectMessages.messagesErased = data.MessagesErased;
|
||||||
|
this.clearDirectMessages.timeout = setTimeout(() => {
|
||||||
|
this.clearDirectMessages.ok = false;
|
||||||
|
}, 15000);
|
||||||
|
|
||||||
|
this.ChatClient(
|
||||||
|
"Your direct message history has been cleared from the server's database. "+
|
||||||
|
"(" + data.MessagesErased + " messages erased)",
|
||||||
|
);
|
||||||
|
}).catch(resp => {
|
||||||
|
console.error("DirectMessageHistory: ", resp);
|
||||||
|
this.ChatClient("Error clearing your chat history: " + resp);
|
||||||
|
}).finally(() => {
|
||||||
|
this.clearDirectMessages.busy = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Webhook methods
|
* Webhook methods
|
||||||
|
@ -3387,6 +3459,11 @@ export default {
|
||||||
Misc
|
Misc
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li :class="{ 'is-active': settingsModal.tab === 'advanced' }">
|
||||||
|
<a href="#" @click.prevent="settingsModal.tab = 'advanced'">
|
||||||
|
Advanced
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -3760,7 +3837,7 @@ export default {
|
||||||
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label mb-0">Direct Messages</label>
|
<label class="label mb-0">Direct Messages</label>
|
||||||
<label class="checkbox">
|
<label class="checkbox mb-0">
|
||||||
<input type="checkbox" v-model="prefs.closeDMs" :value="true">
|
<input type="checkbox" v-model="prefs.closeDMs" :value="true">
|
||||||
Ignore unsolicited DMs from others
|
Ignore unsolicited DMs from others
|
||||||
</label>
|
</label>
|
||||||
|
@ -3771,6 +3848,27 @@ export default {
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Clear DMs history on server -->
|
||||||
|
<div class="field" v-if="this.jwt.valid">
|
||||||
|
<a href="#" @click.prevent="clearMessageHistory(true)" class="button is-small has-text-danger">
|
||||||
|
<i class="fa fa-trash mr-1"></i> Clear direct message history
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div v-if="clearDirectMessages.busy" class="has-text-success mt-2 is-size-7">
|
||||||
|
<i class="fa fa-spinner fa-spin mr-1"></i>
|
||||||
|
Working...
|
||||||
|
</div>
|
||||||
|
<div v-else-if="clearDirectMessages.ok" class="has-text-success mt-2 is-size-7">
|
||||||
|
<i class="fa fa-check mr-1"></i>
|
||||||
|
History cleared ({{ clearDirectMessages.messagesErased }} message{{ clearDirectMessages.messagesErased === 1 ? '' : 's' }} erased)
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Advanced preferences -->
|
||||||
|
<div v-if="settingsModal.tab === 'advanced'">
|
||||||
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label mb-0">
|
<label class="label mb-0">
|
||||||
Server Connection Method
|
Server Connection Method
|
||||||
|
@ -3798,13 +3896,13 @@ export default {
|
||||||
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label mb-0">
|
<label class="label mb-0">
|
||||||
Advanced
|
Apple compatibility mode
|
||||||
</label>
|
</label>
|
||||||
<label class="checkbox">
|
<label class="checkbox">
|
||||||
<input type="checkbox"
|
<input type="checkbox"
|
||||||
v-model="prefs.appleCompat"
|
v-model="prefs.appleCompat"
|
||||||
:value="true">
|
:value="true">
|
||||||
Apple compatibility mode (iPad, iPhone, Safari)
|
Check this box if you are on an iPad, iPhone, or Safari browser
|
||||||
</label>
|
</label>
|
||||||
<p class="help">
|
<p class="help">
|
||||||
If you experience difficulty opening cameras and you are on an Apple device (iPad,
|
If you experience difficulty opening cameras and you are on an Apple device (iPad,
|
||||||
|
|
Loading…
Reference in New Issue
Block a user