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 23 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
13 changes: 13 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 Expand Up @@ -1320,6 +1332,7 @@ system_property.adminConsole.perUsernameAttemptResetInterval=Time frame before A
system_property.xmpp.muc.muclumbus.v1-0.enabled=Determine is the multi-user chat "muclumbus" (v1.0) search feature is enabled.
system_property.xmpp.muc.join.presence=Setting the presence send of participants joining in MUC rooms.
system_property.xmpp.muc.join.self-presence-timeout=Maximum duration to wait for presence to be broadcast while joining a MUC room.
system_property.xmpp.muc.bulkretraction=Enable or disable the bulk retraction of messages in MUC rooms.
system_property.ldap.pagedResultsSize=The maximum number of records to retrieve from LDAP in a single page. \
The default value of -1 means rely on the paging of the LDAP server itself. \
Note that if using ActiveDirectory, this should not be left at the default, and should not be set to more than the value of the ActiveDirectory MaxPageSize; 1,000 by default.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import java.sql.Statement;
import java.util.Arrays;

import org.jivesoftware.database.bugfix.Issue369and370;
import org.jivesoftware.database.bugfix.OF1515;
import org.jivesoftware.database.bugfix.OF33;
import org.jivesoftware.openfire.XMPPServer;
Expand Down Expand Up @@ -275,6 +276,9 @@ else if (DbConnectionManager.getDatabaseType() == DbConnectionManager.DatabaseTy
if (i == 28 && schemaKey.equals("openfire")) {
OF1515.executeFix();
}
if (i == 9 && schemaKey.equals("monitoring")) {
Issue369and370.executeMigration();
}
} catch (Exception e) {
Log.error(e.getMessage(), e);
return false;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
package org.jivesoftware.database.bugfix;

import org.jivesoftware.database.DbConnectionManager;
import org.jivesoftware.database.SchemaManager;
import org.jivesoftware.database.SequenceManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
* This class to handle the migration of data for the monitoring plugin to obtain roomIDs so chat history can be cleared.
* This is for the features identified as issue 369 and 370 for the Monitoring plugin.
* <p>
* The code in this class is intended to be executed only once, under very
* strict circumstances. The only class responsible for calling this code should
* be an instance of {@link SchemaManager}. The Monitoring database update version
* corresponding to this fix is 9.
*
* @author Huy Vu
* @see <a href="https://github.com/igniterealtime/openfire-monitoring-plugin/issues/369">Monitoring plugin issue 369</a>
* @see <a href="https://github.com/igniterealtime/openfire-monitoring-plugin/issues/370">Monitoring plugin issue 370</a>
*/
public class Issue369and370 {
private static final Logger Log = LoggerFactory.getLogger(Issue369and370.class);
private static final SequenceManager roomIDSequenceManager = new SequenceManager(655, 50);

public static void executeMigration() {
Log.info("Migrating data to obtain roomIDs so chat can be removed.");

Map<String, Long> roomJIDToRoomIDMap = new HashMap<>();
Map<Long, Long> conversationIDToRoomIDMap = new HashMap<>();

// For conversations that are not external, assign a unique roomID based on roomJID using information from the ofConversation table
try (Connection con = DbConnectionManager.getConnection();
PreparedStatement pstmt = con.prepareStatement("SELECT room, conversationID FROM ofConversation WHERE isExternal = 0 ORDER BY startDate ASC", ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY);
ResultSet rs = pstmt.executeQuery()) {

pstmt.setFetchSize(250);
pstmt.setFetchDirection(ResultSet.FETCH_FORWARD);
while (rs.next()) {
String roomJID = rs.getString("room");
long conversationID = rs.getLong("conversationID");
long roomID = roomJIDToRoomIDMap.computeIfAbsent(roomJID, k -> roomIDSequenceManager.nextUniqueID());
conversationIDToRoomIDMap.put(conversationID, roomID);
}

pstmt.setFetchSize(250);
guusdk marked this conversation as resolved.
Show resolved Hide resolved
pstmt.setFetchDirection(ResultSet.FETCH_FORWARD);
Log.debug("1 of 7. Generated room IDs for unique rooms");
} catch (SQLException e) {
Log.error("Error querying ofConversation to generate room IDs for unique rooms", e);
}

// Update ofConversation table with roomIDs from conversationIDToRoomIDMap
updateTable("UPDATE ofConversation SET roomID = ? WHERE conversationID = ?", conversationIDToRoomIDMap);
Log.debug("2 of 7. Updated room IDs in ofConversation table");
// Update the roomIDs in ofMessageArchive using conversationIDToRoomIDMap
updateTable("UPDATE ofMessageArchive SET roomID = ? WHERE conversationID = ?", conversationIDToRoomIDMap);
Log.debug("3 of 7. Updated room IDs in ofMessageArchive table");
// Update the roomIDs in ofConParticipant using conversationIDToRoomIDMap
updateTable("UPDATE ofConParticipant SET roomID = ? WHERE conversationID = ?", conversationIDToRoomIDMap);
Log.debug("4 of 7. Updated room IDs in ofConParticipant table");

// Insert room information into ofMucRoomStatus table from roomJIDToRoomIDMap
try (Connection con = DbConnectionManager.getConnection();
PreparedStatement pstmt = con.prepareStatement("INSERT INTO ofMucRoomStatus (roomID, roomJID, roomDestroyed) VALUES (?, ?, 0)")) {

for (Map.Entry<String, Long> entry : roomJIDToRoomIDMap.entrySet()) {
pstmt.setLong(1, entry.getValue());
pstmt.setString(2, entry.getKey());
pstmt.addBatch();
}
pstmt.executeBatch();
Log.debug("5 of 7. Inserted room information into ofMucRoomStatus table");
} catch (SQLException e) {
Log.error("Error inserting room information into ofMucRoomStatus table", e);
}

// Create an activeMUCs list of the roomJID@subdomain format from the ofMucRoom and ofMucService tables
List<String> activeMUCs = new ArrayList<>();
// Join the tables ofMucRoom and ofMucService based on serviceID so we can get the 'name' and 'subdomain' fields
try (Connection con = DbConnectionManager.getConnection();
PreparedStatement pstmt = con.prepareStatement("SELECT ofMucRoom.name, ofMucService.subdomain FROM ofMucRoom JOIN ofMucService ON ofMucRoom.serviceID = ofMucService.serviceID", ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY);
ResultSet rs = pstmt.executeQuery()) {

pstmt.setFetchSize(250);
pstmt.setFetchDirection(ResultSet.FETCH_FORWARD);
while (rs.next()) {
String roomName = rs.getString("name");
String subdomain = rs.getString("subdomain");

activeMUCs.add(roomName + "@" + subdomain);
Log.debug("Active MUC: {}", roomName + "@" + subdomain);
}
Log.debug("6 of 7. Created list of active MUCs so we can determine which room is destroyed");
} catch (SQLException e) {
Log.error("Error joining ofMucRoom and ofMucService tables", e);
}

// if roomJID from roomJIDToRoomIDMap is not in activeMUCs, update ofMucRoomStatus to set roomDestroyed to 1
try (Connection con = DbConnectionManager.getConnection();
PreparedStatement pstmt = con.prepareStatement("UPDATE ofMucRoomStatus SET roomDestroyed = 1 WHERE roomID = ?")) {
for (String roomJID : roomJIDToRoomIDMap.keySet()) {
boolean roomDestroyed = true;
for (String activeMUCID : activeMUCs) {
if (roomJID.startsWith(activeMUCID)) {
roomDestroyed = false;
break;
}
}
Log.debug("roomJID={} destroyed={}", roomJID, roomDestroyed);
if (roomDestroyed) {
long roomID = roomJIDToRoomIDMap.get(roomJID);
pstmt.setLong(1, roomID);
pstmt.addBatch();
}
}
pstmt.executeBatch();
Log.debug("7 of 7. Updated roomDestroyed status in ofMucRoomStatus table");
} catch (SQLException e) {
Log.error("Error updating roomDestroyed status in ofMucRoomStatus table", e);
}
}

private static void updateTable(String query, Map<Long, Long> conversationIDToRoomIDMap) {
try (Connection con = DbConnectionManager.getConnection();
PreparedStatement pstmt = con.prepareStatement(query)) {

for (Map.Entry<Long, Long> entry : conversationIDToRoomIDMap.entrySet()) {
pstmt.setLong(1, entry.getValue());
pstmt.setLong(2, entry.getKey());
pstmt.addBatch();
}
pstmt.executeBatch();
Log.debug("Updated table with query: {}", query);
} catch (SQLException e) {
Log.error("Error updating table with query: {} ", query, e);
}
}
}
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(getJID());
});
}

/**
* 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
Loading
Loading