Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OF-2918 Clear MUC Chat History #2627

Open
wants to merge 24 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
e8ac57a
OF-2918: Add option to clear history for a given MUC
Nov 29, 2024
2df54ce
OF-2918: Add option to clear history for a given MUC
Nov 29, 2024
4afca51
OF-2918: Add option to clear history for a given MUC
Nov 29, 2024
7bd86d3
OF-2918: Add option to clear history for a given MUC
Nov 29, 2024
196e6ee
OF-2918: Add option to clear history for a given MUC
Nov 29, 2024
f8343f6
OF-2918: Add option to clear history for a given MUC
Nov 29, 2024
5f3573a
OF-2918: Add option to clear history for a given MUC
Dec 3, 2024
156f9ce
OF-2918: Add option to clear history for a given MUC
Dec 3, 2024
01af2dd
OF-2918: Add option to clear history for a given MUC
Dec 3, 2024
d5a9754
OF-2918: Add option to clear history for a given MUC
Dec 6, 2024
7be30bd
OF-2918: Add option to clear history for a given MUC
Dec 6, 2024
94f083d
OF-2918: Add option to clear history for a given MUC
Dec 6, 2024
4e83456
OF-2918: Add option to clear history for a given MUC
Dec 6, 2024
4ed359c
OF-2918: Add option to clear history for a given MUC
Dec 6, 2024
5d673d2
#369 and #370 Clear Chat Room History
Dec 11, 2024
4ae3b94
#369 and #370 Clear Chat Room History
Dec 11, 2024
82fc000
#369 and #370 Clear Chat Room History
Dec 11, 2024
b7f21ae
#369 and #370 Clear Chat Room History
Dec 11, 2024
c03e7e0
#369 and #370 Clear Chat Room History
Dec 11, 2024
db1bc7d
OF-2918: Add option to clear history for a given MUC
Dec 12, 2024
2e8dabf
OF-2918: Add option to clear history for a given MUC
Dec 16, 2024
9de6408
OF-2918: Add option to clear history for a given MUC
Dec 16, 2024
79f01f0
OF-2918: Add option to clear history for a given MUC
Dec 16, 2024
2c0f292
OF-2918: Add option to clear history for a given MUC
Dec 16, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions i18n/src/main/resources/openfire_i18n.properties
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,8 @@ tab.tab-groupchat.descr=Click to manage group chat settings
sidebar.muc-room-affiliations.descr=Click to edit room permissions (affiliations) for users and groups
sidebar.muc-room-federation=Federation
sidebar.muc-room-federation.descr=Click to edit the room's FMUC settings
sidebar.muc-room-clear-chat=Clear Chat History
sidebar.muc-room-clear-chat.descr=Click to clear chat history
sidebar.muc-room-delete=Delete Room
sidebar.muc-room-delete.descr=Click to delete the room
sidebar.muc-room-create=Create New Room
Expand Down Expand Up @@ -825,6 +827,15 @@ muc.room.affiliations.room_admin=Room Admins
muc.room.affiliations.room_member=Room Members
muc.room.affiliations.room_outcast=Room Outcasts


# Muc clear room chat history

muc.room.clear_chat.title=Clear Chat History
muc.room.clear_chat.info=Are you sure you want to clear the current chat history for the room
muc.room.clear_chat.detail=from the system?
muc.room.clear_chat.clear_command=Clear Chat History
muc.room.clear_chat.retraction_fallback_msg=A request to retract a previous message to clear chat history was received, but your client does not support this.

# Muc room delete Page

muc.room.delete.title=Destroy Room
Expand Down Expand Up @@ -921,6 +932,7 @@ muc.room.summary.persistent=Persistent
muc.room.summary.users=Users
muc.room.summary.edit=Edit
muc.room.summary.destroy=Destroy
muc.room.summary.cleared_chat=Cleared chat history
muc.room.summary.no_room_in_group=No rooms in the Group Chat service.
muc.room.summary.alt_persistent=Room is persistent
muc.room.summary.alt_temporary=Room is temporary
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,16 @@ public static void roomDestroyed(JID roomJID) {
}
}

public static void roomClearChatHistory(JID roomJID) {
for (MUCEventListener listener : listeners) {
try {
listener.roomClearChatHistory(roomJID);
} catch (Exception e) {
Log.warn("An exception occurred while dispatching a 'roomClearChatHistory' event for room {}!", roomJID, e);
}
}
}

public static void roomSubjectChanged(JID roomJID, JID user, String newSubject) {
for (MUCEventListener listener : listeners) {
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package org.jivesoftware.openfire.muc;

import org.eclipse.jetty.ee8.servlet.listener.IntrospectorCleaner;
import org.xmpp.packet.JID;
import org.xmpp.packet.Message;

Expand All @@ -41,6 +42,13 @@ public interface MUCEventListener {
*/
void roomDestroyed(JID roomJID);

/**
* Event triggered when a clear chat history command was issued.
*
* @param roomJID JID of the room to clear chat history.
*/
void roomClearChatHistory(JID roomJID);

/**
* Event triggered when a new occupant joins a room.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,6 @@
*/
@JiveID(JiveConstants.MUC_ROOM)
public class MUCRoom implements GroupEventListener, UserEventListener, Externalizable, Result, Cacheable {

private static final Logger Log = LoggerFactory.getLogger(MUCRoom.class);

public static final SystemProperty<Boolean> JOIN_PRESENCE_ENABLE = SystemProperty.Builder.ofType(Boolean.class)
Expand All @@ -87,6 +86,12 @@ public class MUCRoom implements GroupEventListener, UserEventListener, Externali
.setDynamic(true)
.build();

public static final SystemProperty<Boolean> BULK_MSG_RETRACTION_ENABLED = SystemProperty.Builder.ofType(Boolean.class)
.setKey("xmpp.muc.bulkretraction")
guusdk marked this conversation as resolved.
Show resolved Hide resolved
.setDynamic(true)
.setDefaultValue(false)
.build();

/**
* The service hosting the room.
*/
Expand Down Expand Up @@ -1308,6 +1313,55 @@ public void removeOccupant(@Nonnull final MUCOccupant occupant) {
MUCEventDispatcher.occupantLeft(occupant.getOccupantJID(), occupant.getUserAddress(), occupant.getNickname());
}

/**
* Asynchronously remove the chat history of a room from run-time memory and database storage.
* If bulk message retraction is enabled, it sends message retraction stanzas to all occupants.
*/
public CompletableFuture<Void> clearChatHistory() {
return CompletableFuture.runAsync(() -> {
if(BULK_MSG_RETRACTION_ENABLED.getValue()) {
ListIterator<Message> reverseMessageHistory = roomHistory.getReverseMessageHistory();
while (reverseMessageHistory.hasPrevious()) {
Message originalMsg = reverseMessageHistory.previous();
Message retractionMsg = new Message();

// Sets a unique ID for the retraction message by prefixing the original message ID with "retract-"
retractionMsg.setID("retract-" + originalMsg.getID());
// Sets the sender of the retraction message to be the chat room itself
retractionMsg.setFrom(selfOccupantData.getOccupantJID());
// Sets the recipient of the retraction message to be the chat room itself to send to all occupants
retractionMsg.setTo(new JID(getName(), getMUCService().getServiceDomain(), null).toBareJID());
retractionMsg.setType(Message.Type.groupchat);
// An XML element is added to the message to indicate that it is a retraction, with an attribute specifying
// the ID of the message being retracted
retractionMsg.addChildElement("retract", "urn:xmpp:message-retract:1").addAttribute(
"id",
new JID(getName(), getMUCService().getServiceDomain(), null).toBareJID()
);
// A fallback element is added to provide a fallback message for clients that do not support message retraction
retractionMsg.addChildElement("fallback", "urn:xmpp:fallback:0").addAttribute(
"for",
"urn:xmpp:message-retract:1"
);
retractionMsg.setBody(LocaleUtils.getLocalizedString("muc.room.clear_chat.retraction_fallback_msg"));
// Finally, a hint is added to the message to indicate that it should be stored by the client.
// This ensures that the retraction event is recorded and can be referenced later.
retractionMsg.addChildElement("store", "urn:xmpp:hints");

// Broadcast the retraction message but don't store it in the history
broadcast(retractionMsg, false);
}
}

// Clear the history of the room from the DB if the room was persistent
MUCPersistenceManager.clearRoomChatFromDB(this);
// Remove the history of the room from memory (preventing it to pop up in a new room by the same name).
roomHistory.purge();
// Fire event to clear chat room history
MUCEventDispatcher.roomClearChatHistory(getSelfRepresentation().getOccupantJID());
guusdk marked this conversation as resolved.
Show resolved Hide resolved
});
}

/**
* Destroys the room. Each occupant will be removed and will receive a presence stanza of type
* "unavailable" whose "from" attribute will be the occupant's nickname that the user knows he
Expand Down Expand Up @@ -1384,8 +1438,12 @@ public void destroyRoom(JID alternateJID, String password, String reason) {
MUCPersistenceManager.deleteFromDB(this);
// Remove the history of the room from memory (preventing it to pop up in a new room by the same name).
roomHistory.purge();
// If we are not preserving room history on deletion, fire event to clear chat room history
if(!preserveHistOnRoomDeletion) {
MUCEventDispatcher.roomClearChatHistory(getJID());
}
// Fire event that the room has been destroyed
MUCEventDispatcher.roomDestroyed(getSelfRepresentation().getOccupantJID());
MUCEventDispatcher.roomDestroyed(getJID());
}

/**
Expand Down Expand Up @@ -1756,6 +1814,11 @@ private void broadcast(@Nonnull final Message message, @Nonnull final MUCOccupan
* @param message The message stanza
*/
public void broadcast(@Nonnull final Message message)
{
broadcast(message, true);
}

private void broadcast(@Nonnull final Message message, final boolean storeMsgInRoomHistory)
{
Log.debug("Broadcasting message in room {} for occupant {}", this.getName(), message.getFrom() );

Expand All @@ -1768,7 +1831,7 @@ public void broadcast(@Nonnull final Message message)
}

// Add message to the room history
roomHistory.addMessage(message);
if (storeMsgInRoomHistory) roomHistory.addMessage(message);
// Send message to occupants connected to this JVM

// Create a defensive copy of the message that will be broadcast, as the broadcast will modify it ('to' addresses
Expand All @@ -1789,7 +1852,7 @@ public void broadcast(@Nonnull final Message message)
Log.warn("An unexpected exception prevented a message from {} to be broadcast to {}.", message.getFrom(), occupant.getUserAddress(), e);
}
}
if (isLogEnabled()) {
if (isLogEnabled() && storeMsgInRoomHistory) {
JID senderAddress = getSelfRepresentation().getOccupantJID(); // default to the room being the sender of the message.

// convert the MUC nickname/role JID back into a real user JID
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,9 @@ public class MUCPersistenceManager {
"INSERT INTO ofMucConversationLog (roomID,messageID,sender,nickname,logTime,subject,body,stanza) VALUES (?,?,?,?,?,?,?,?)";
private static final String DELETE_ROOM_HISTORY =
"DELETE FROM ofMucConversationLog WHERE roomID=?";
// Clear the chat history for a room but don't clear the messages that set the room's subject
guusdk marked this conversation as resolved.
Show resolved Hide resolved
private static final String CLEAR_ROOM_CHAT_HISTORY =
"DELETE FROM ofMucConversationLog WHERE roomID=? AND subject IS NULL";

/* Map of subdomains to their associated properties */
private static ConcurrentHashMap<String,MUCServiceProperties> propertyMaps = new ConcurrentHashMap<>();
Expand Down Expand Up @@ -559,6 +562,36 @@ public static void deleteFromDB(MUCRoom room) {
}
}

/**
* Clears the chat history for a room
*
* @param room the room to cleaer chat history from
*/
public static void clearRoomChatFromDB(MUCRoom room) {
Log.debug("Attempting to clear the chat history of room '{}' from the database.", room.getName());

if (!room.isPersistent() || !room.wasSavedToDB()) {
return;
}
Connection con = null;
PreparedStatement pstmt = null;
boolean abortTransaction = false;
try {
con = DbConnectionManager.getTransactionConnection();
pstmt = con.prepareStatement(CLEAR_ROOM_CHAT_HISTORY);
pstmt.setLong(1, room.getID());
pstmt.executeUpdate();
}
catch (SQLException sqle) {
Log.error("A database error occurred while trying to delete room: {}", room.getName(), sqle);
abortTransaction = true;
}
finally {
DbConnectionManager.closeStatement(pstmt);
DbConnectionManager.closeTransactionConnection(con, abortTransaction);
}
}

/**
* Loads the name of all the rooms that are in the database.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -960,6 +960,11 @@ public void roomDestroyed(@Nonnull final JID roomJID)
}
}

@Override
public void roomClearChatHistory(JID roomJID) {
// Not used.
}

@Override
public void messageReceived(JID roomJID, JID user, String nickname, Message message) {
// Not used.
Expand Down
5 changes: 5 additions & 0 deletions xmppserver/src/main/resources/admin-sidebar.xml
Original file line number Diff line number Diff line change
Expand Up @@ -505,6 +505,11 @@
url="muc-room-federation.jsp"
description="${sidebar.muc-room-federation.descr}"/>

<!-- Clear Chat History -->
<item id="muc-room-clear-chat" name="${sidebar.muc-room-clear-chat}"
url="muc-room-clear-chat.jsp"
description="${sidebar.muc-room-clear-chat.descr}"/>

<!-- Delete Room -->
<item id="muc-room-delete" name="${sidebar.muc-room-delete}"
url="muc-room-delete.jsp"
Expand Down
100 changes: 100 additions & 0 deletions xmppserver/src/main/webapp/muc-room-clear-chat.jsp
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
<%@ page contentType="text/html; charset=UTF-8" %>
<%--
-
- Copyright (C) 2024 Ignite Realtime Foundation. All rights reserved.
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.

--%>

<%@ page import="org.jivesoftware.util.*,
org.jivesoftware.openfire.muc.MUCRoom,
java.net.URLEncoder"
errorPage="error.jsp"
%>
<%@ page import="org.xmpp.packet.JID" %>

<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt" %>
<jsp:useBean id="webManager" class="org.jivesoftware.util.WebManager" />
<% webManager.init(request, response, session, application, out ); %>

<% // Get parameters //
boolean cancel = request.getParameter("cancel") != null;
boolean clear = request.getParameter("clear") != null;
Cookie csrfCookie = CookieUtils.getCookie(request, "csrf");
String csrfParam = ParamUtils.getParameter(request, "csrf");

if (clear) {
if (csrfCookie == null || csrfParam == null || !csrfCookie.getValue().equals(csrfParam)) {
clear = false;
}
}
csrfParam = StringUtils.randomString(15);
CookieUtils.setCookie(request, response, "csrf", csrfParam, -1);
pageContext.setAttribute("csrf", csrfParam);

JID roomJID = new JID(ParamUtils.getParameter(request,"roomJID"));
String roomName = roomJID.getNode();

// Handle a cancel
if (cancel) {
response.sendRedirect("muc-room-edit-form.jsp?roomJID="+URLEncoder.encode(roomJID.toBareJID(), "UTF-8"));
return;
}

// Load the room object
MUCRoom room = webManager.getMultiUserChatManager().getMultiUserChatService(roomJID).getChatRoom(roomName);

// Handle a room clear:
if (clear) {
// Clear the room
if (room != null) {
webManager.logEvent("Making a request to clear chat history of MUC room ", roomJID.toString());
guusdk marked this conversation as resolved.
Show resolved Hide resolved
// finalWebManager is a final variable that references webManager
// allowing webManager to be used inside the lambda expression without causing a compilation error
final WebManager finalWebManager = webManager;
room.clearChatHistory().thenRun(() -> {
finalWebManager.logEvent("Cleared the chat history of MUC room ", roomJID.toString());
});
}
// Done, so redirect to the room edit form
response.sendRedirect("muc-room-edit-form.jsp?roomJID="+URLEncoder.encode(roomJID.toBareJID(), "UTF-8")+"&clearchatsuccess=true");
return;
}
%>

<html>
<head>
<title><fmt:message key="muc.room.clear_chat.title"/></title>
<meta name="subPageID" content="muc-room-clear-chat"/>
<meta name="extraParams" content="<%= "roomJID="+URLEncoder.encode(roomJID.toBareJID(), "UTF-8") %>"/>
</head>
<body>
<p>
<fmt:message key="muc.room.clear_chat.info" />
<b><a href="muc-room-edit-form.jsp?roomJID=<%= URLEncoder.encode(room.getJID().toBareJID(), "UTF-8") %>"><%= StringUtils.escapeHTMLTags(room.getJID().toBareJID()) %></a></b>
<fmt:message key="muc.room.clear_chat.detail" />
</p>

<form action="muc-room-clear-chat.jsp">
<input type="hidden" name="csrf" value="${csrf}">
<input type="hidden" name="roomJID" value="<%= StringUtils.escapeForXML(roomJID.toBareJID()) %>">

<br>

<input type="submit" name="clear" value="<fmt:message key="muc.room.clear_chat.clear_command" />">
<input type="submit" name="cancel" value="<fmt:message key="global.cancel" />">
</form>
</body>
</html>
6 changes: 6 additions & 0 deletions xmppserver/src/main/webapp/muc-room-edit-form.jsp
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,12 @@
</admin:infobox>
</c:if>

<% if (request.getParameter("clearchatsuccess") != null) { %>
<admin:infoBox type="success">
<fmt:message key="muc.room.summary.cleared_chat" />
</admin:infoBox>
<% } %>

<c:choose>
<c:when test="${not create}">
<p>
Expand Down
Loading