Clear DMs history button

master
Noah 2024-04-11 23:28:35 -07:00
parent d510ac791f
commit 3424be2f4d
5 changed files with 292 additions and 4 deletions

View File

@ -277,4 +277,35 @@ 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
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
}
```

View File

@ -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(&params); 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.
var (
// Map of username to the list of usernames they block.

View File

@ -82,6 +82,42 @@ func (dm DirectMessage) LogMessage(fromUsername, toUsername string, msg messages
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.
//
// Because the MessageID may have been from a previous chat session, the server can't immediately

View File

@ -58,6 +58,7 @@ func (s *Server) Setup() error {
mux.Handle("/api/shutdown", s.ShutdownAPI())
mux.Handle("/api/profile", s.UserProfile())
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("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("dist/static"))))

View File

@ -282,6 +282,12 @@ export default {
}
*/
},
clearDirectMessages: {
busy: false,
ok: false,
messagesErased: 0,
timeout: null,
},
// Responsive CSS controls for mobile.
responsive: {
@ -1037,6 +1043,13 @@ export default {
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);
this.client.send({
action: "message",
@ -3277,6 +3290,65 @@ export default {
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
@ -3387,6 +3459,11 @@ export default {
Misc
</a>
</li>
<li :class="{ 'is-active': settingsModal.tab === 'advanced' }">
<a href="#" @click.prevent="settingsModal.tab = 'advanced'">
Advanced
</a>
</li>
</ul>
</div>
@ -3760,7 +3837,7 @@ export default {
<div class="field">
<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">
Ignore unsolicited DMs from others
</label>
@ -3771,6 +3848,27 @@ export default {
</p>
</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">
<label class="label mb-0">
Server Connection Method
@ -3798,13 +3896,13 @@ export default {
<div class="field">
<label class="label mb-0">
Advanced
Apple compatibility mode
</label>
<label class="checkbox">
<input type="checkbox"
v-model="prefs.appleCompat"
:value="true">
Apple compatibility mode (iPad, iPhone, Safari)
Check this box if you are on an iPad, iPhone, or Safari browser
</label>
<p class="help">
If you experience difficulty opening cameras and you are on an Apple device (iPad,