Repository: javacord/javacord
Issue: Message cache can be outdated

In some cases there might be messages in the cache which are no longer up to date:
- Message gets send in a channel with read-access
- Bot loses read-access for this channel
- Message gets edited
- Bot gains read-access again

=> The message content is out of date, but still in the cache

This can cause some issues with methods like `MessageEditEvent#getOldContent()`.

**Possible solution:**
- Remove a message from the cache when the bot loses read-access

Changed files:
javacord-core/src/main/java/org/javacord/core/DiscordApiImpl.java
javacord-core/src/main/java/org/javacord/core/util/handler/guild/GuildMemberUpdateHandler.java
javacord-core/src/main/java/org/javacord/core/util/handler/channel/ChannelUpdateHandler.java
javacord-core/src/main/java/org/javacord/core/util/handler/guild/GuildMemberRemoveHandler.java
javacord-core/src/main/java/org/javacord/core/util/handler/channel/ChannelDeleteHandler.java
javacord-core/src/main/java/org/javacord/core/util/handler/guild/role/GuildRoleUpdateHandler.java
javacord-core/src/main/java/org/javacord/core/util/handler/guild/GuildDeleteHandler.java
javacord-core/src/main/java/org/javacord/core/util/handler/ResumedHandler.java

diff --git a/javacord-core/src/main/java/org/javacord/core/DiscordApiImpl.java b/javacord-core/src/main/java/org/javacord/core/DiscordApiImpl.java
index 374da79a..70f5d20e 100644
--- a/javacord-core/src/main/java/org/javacord/core/DiscordApiImpl.java
+++ b/javacord-core/src/main/java/org/javacord/core/DiscordApiImpl.java
@@ -78,7 +78,9 @@ import java.util.WeakHashMap;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
 import java.util.function.Function;
+import java.util.function.Predicate;
 import java.util.stream.Collectors;
 
 /**
@@ -1330,6 +1332,38 @@ public class DiscordApiImpl implements DiscordApi, DispatchQueueSelector {
         }
     }
 
+    /**
+     * Get messages from the cache that satisfy a given condition.
+     *
+     * @param filter The filter for messages to be included.
+     * @return A set of cached messages satisfying the condition.
+     */
+    public MessageSet getCachedMessagesWhere(Predicate<Message> filter) {
+        synchronized (messages) {
+            return messages.values().stream()
+                    .map(Reference::get)
+                    .filter(Objects::nonNull)
+                    .filter(filter)
+                    .collect(Collectors.toCollection(MessageSetImpl::new));
+        }
+    }
+
+    /**
+     * Execute a task for every message in cache that satisfied a given condition.
+     *
+     * @param filter The condition on which to execute the code.
+     * @param action The action to be applied to the messages.
+     */
+    public void forEachCachedMessageWhere(Predicate<Message> filter, Consumer<Message> action) {
+        synchronized (messages) {
+            messages.values().stream()
+                    .map(Reference::get)
+                    .filter(Objects::nonNull)
+                    .filter(filter)
+                    .forEach(action);
+        }
+    }
+
     @Override
     public Optional<Message> getCachedMessageById(long id) {
         return Optional.ofNullable(messages.get(id)).map(Reference::get);
diff --git a/javacord-core/src/main/java/org/javacord/core/util/handler/ResumedHandler.java b/javacord-core/src/main/java/org/javacord/core/util/handler/ResumedHandler.java
index 02b6603b..110bf4ce 100644
--- a/javacord-core/src/main/java/org/javacord/core/util/handler/ResumedHandler.java
+++ b/javacord-core/src/main/java/org/javacord/core/util/handler/ResumedHandler.java
@@ -23,4 +23,4 @@ public class ResumedHandler extends PacketHandler {
         // NOP
     }
 
-}
\\ No newline at end of file
+}
diff --git a/javacord-core/src/main/java/org/javacord/core/util/handler/channel/ChannelDeleteHandler.java b/javacord-core/src/main/java/org/javacord/core/util/handler/channel/ChannelDeleteHandler.java
index ac241439..6f85c83e 100644
--- a/javacord-core/src/main/java/org/javacord/core/util/handler/channel/ChannelDeleteHandler.java
+++ b/javacord-core/src/main/java/org/javacord/core/util/handler/channel/ChannelDeleteHandler.java
@@ -108,6 +108,10 @@ public class ChannelDeleteHandler extends PacketHandler {
                 .ifPresent(server -> server.getTextChannelById(channelId).ifPresent(channel -> {
                     dispatchServerChannelDeleteEvent(channel);
                     ((ServerImpl) server).removeChannelFromCache(channel.getId());
+                    api.forEachCachedMessageWhere(
+                            msg -> msg.getChannel().getId() == channelId,
+                            msg -> api.removeMessageFromCache(msg.getId())
+                    );
                 }));
         api.removeObjectListeners(ServerTextChannel.class, channelId);
         api.removeObjectListeners(ServerChannel.class, channelId);
@@ -150,7 +154,10 @@ public class ChannelDeleteHandler extends PacketHandler {
             api.removeObjectListeners(VoiceChannel.class, channelId);
             api.removeObjectListeners(TextChannel.class, channelId);
             api.removeObjectListeners(Channel.class, channelId);
-
+            api.forEachCachedMessageWhere(
+                    msg -> msg.getChannel().getId() == privateChannel.getId(),
+                    msg -> api.removeMessageFromCache(msg.getId())
+            );
             recipient.setChannel(null);
         });
     }
@@ -172,6 +179,10 @@ public class ChannelDeleteHandler extends PacketHandler {
             api.removeObjectListeners(VoiceChannel.class, channelId);
             api.removeObjectListeners(TextChannel.class, channelId);
             api.removeObjectListeners(Channel.class, channelId);
+            api.forEachCachedMessageWhere(
+                    msg -> msg.getChannel().getId() == groupChannel.getId(),
+                    msg -> api.removeMessageFromCache(msg.getId())
+            );
         });
     }
 
diff --git a/javacord-core/src/main/java/org/javacord/core/util/handler/channel/ChannelUpdateHandler.java b/javacord-core/src/main/java/org/javacord/core/util/handler/channel/ChannelUpdateHandler.java
index b6ae10bb..f077ea24 100644
--- a/javacord-core/src/main/java/org/javacord/core/util/handler/channel/ChannelUpdateHandler.java
+++ b/javacord-core/src/main/java/org/javacord/core/util/handler/channel/ChannelUpdateHandler.java
@@ -47,6 +47,7 @@ import java.util.Iterator;
 import java.util.Map;
 import java.util.Objects;
 import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicBoolean;
 
 /**
  * Handles the channel update packet.
@@ -128,6 +129,7 @@ public class ChannelUpdateHandler extends PacketHandler {
             }
         }
 
+        final AtomicBoolean areYouAffected = new AtomicBoolean(false);
         ChannelCategory oldCategory = channel.asCategorizable().flatMap(Categorizable::getCategory).orElse(null);
         ChannelCategory newCategory = jsonChannel.hasNonNull("parent_id")
                 ? channel.getServer().getChannelCategoryById(jsonChannel.get("parent_id").asLong(-1)).orElse(null)
@@ -142,7 +144,6 @@ public class ChannelUpdateHandler extends PacketHandler {
                 ((ServerVoiceChannelImpl) channel).setParentId(newCategory == null ? -1 : newCategory.getId());
             }
             channel.setPosition(newRawPosition);
-
             int newPosition = channel.getPosition();
 
             ServerChannelChangePositionEvent event = new ServerChannelChangePositionEventImpl(
@@ -188,6 +189,9 @@ public class ChannelUpdateHandler extends PacketHandler {
                     if (server.isReady()) {
                         dispatchServerChannelChangeOverwrittenPermissionsEvent(
                                 channel, newOverwrittenPermissions, oldOverwrittenPermissions, entity);
+                        areYouAffected.compareAndSet(false, entity instanceof User && ((User) entity).isYourself());
+                        areYouAffected.compareAndSet(false, entity instanceof Role
+                                && ((Role) entity).getUsers().stream().anyMatch(User::isYourself));
                     }
                 }
             }
@@ -209,6 +213,7 @@ public class ChannelUpdateHandler extends PacketHandler {
                 if (server.isReady()) {
                     dispatchServerChannelChangeOverwrittenPermissionsEvent(
                             channel, PermissionsImpl.EMPTY_PERMISSIONS, oldPermissions, user);
+                    areYouAffected.compareAndSet(false, user.isYourself());
                 }
             });
         }
@@ -225,9 +230,17 @@ public class ChannelUpdateHandler extends PacketHandler {
                 if (server.isReady()) {
                     dispatchServerChannelChangeOverwrittenPermissionsEvent(
                             channel, PermissionsImpl.EMPTY_PERMISSIONS, oldPermissions, role);
+                    areYouAffected.compareAndSet(false, role.getUsers().stream().anyMatch(User::isYourself));
                 }
             });
         }
+
+        if (areYouAffected.get() && !channel.canYouSee()) {
+            api.forEachCachedMessageWhere(
+                    msg -> msg.getChannel().getId() == channelId,
+                    msg -> api.removeMessageFromCache(msg.getId())
+            );
+        }
     }
 
     /**
diff --git a/javacord-core/src/main/java/org/javacord/core/util/handler/guild/GuildDeleteHandler.java b/javacord-core/src/main/java/org/javacord/core/util/handler/guild/GuildDeleteHandler.java
index 7aa0b4d1..95f50a11 100644
--- a/javacord-core/src/main/java/org/javacord/core/util/handler/guild/GuildDeleteHandler.java
+++ b/javacord-core/src/main/java/org/javacord/core/util/handler/guild/GuildDeleteHandler.java
@@ -34,6 +34,10 @@ public class GuildDeleteHandler extends PacketHandler {
 
                 api.getEventDispatcher().dispatchServerBecomesUnavailableEvent(
                         (DispatchQueueSelector) server, server, event);
+                api.forEachCachedMessageWhere(
+                        msg -> msg.getServer().map(s -> s.getId() == serverId).orElse(false),
+                        msg -> api.removeMessageFromCache(msg.getId())
+                );
             });
             api.removeServerFromCache(serverId);
             return;
@@ -44,6 +48,10 @@ public class GuildDeleteHandler extends PacketHandler {
             api.getEventDispatcher().dispatchServerLeaveEvent((DispatchQueueSelector) server, server, event);
         });
         api.removeObjectListeners(Server.class, serverId);
+        api.forEachCachedMessageWhere(
+                msg -> msg.getServer().map(s -> s.getId() == serverId).orElse(false),
+                msg -> api.removeMessageFromCache(msg.getId())
+        );
         api.removeServerFromCache(serverId);
     }
 
diff --git a/javacord-core/src/main/java/org/javacord/core/util/handler/guild/GuildMemberRemoveHandler.java b/javacord-core/src/main/java/org/javacord/core/util/handler/guild/GuildMemberRemoveHandler.java
index e126bc4c..64138c72 100644
--- a/javacord-core/src/main/java/org/javacord/core/util/handler/guild/GuildMemberRemoveHandler.java
+++ b/javacord-core/src/main/java/org/javacord/core/util/handler/guild/GuildMemberRemoveHandler.java
@@ -34,7 +34,8 @@ public class GuildMemberRemoveHandler extends PacketHandler {
                     ServerMemberLeaveEvent event = new ServerMemberLeaveEventImpl(server, user);
 
                     api.getEventDispatcher().dispatchServerMemberLeaveEvent(server, server, user, event);
+                    
                 });
     }
 
-}
\\ No newline at end of file
+}
diff --git a/javacord-core/src/main/java/org/javacord/core/util/handler/guild/GuildMemberUpdateHandler.java b/javacord-core/src/main/java/org/javacord/core/util/handler/guild/GuildMemberUpdateHandler.java
index 94128c3a..ee952570 100644
--- a/javacord-core/src/main/java/org/javacord/core/util/handler/guild/GuildMemberUpdateHandler.java
+++ b/javacord-core/src/main/java/org/javacord/core/util/handler/guild/GuildMemberUpdateHandler.java
@@ -2,6 +2,7 @@ package org.javacord.core.util.handler.guild;
 
 import com.fasterxml.jackson.databind.JsonNode;
 import org.javacord.api.DiscordApi;
+import org.javacord.api.entity.channel.ServerTextChannel;
 import org.javacord.api.entity.permission.Role;
 import org.javacord.api.entity.user.User;
 import org.javacord.api.event.server.role.UserRoleAddEvent;
@@ -19,6 +20,9 @@ import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashSet;
 import java.util.Objects;
+import java.util.Set;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
 
 /**
  * Handles the guild member update packet.
@@ -95,6 +99,18 @@ public class GuildMemberUpdateHandler extends PacketHandler {
                                     (DispatchQueueSelector) role.getServer(), role, role.getServer(), user, event);
                         }
                     }
+
+                    if (user.isYourself()) {
+                        Set<Long> unreadableChannels = server.getTextChannels().stream()
+                                .filter(((Predicate<ServerTextChannel>)ServerTextChannel::canYouSee).negate())
+                                .map(ServerTextChannel::getId)
+                                .collect(Collectors.toSet());
+                        api.forEachCachedMessageWhere(
+                                msg -> unreadableChannels.contains(msg.getChannel().getId()),
+                                msg -> api.removeMessageFromCache(msg.getId())
+                        );
+                    }
+
                 });
     }
 
diff --git a/javacord-core/src/main/java/org/javacord/core/util/handler/guild/role/GuildRoleUpdateHandler.java b/javacord-core/src/main/java/org/javacord/core/util/handler/guild/role/GuildRoleUpdateHandler.java
index 6767d318..0d7696fa 100644
--- a/javacord-core/src/main/java/org/javacord/core/util/handler/guild/role/GuildRoleUpdateHandler.java
+++ b/javacord-core/src/main/java/org/javacord/core/util/handler/guild/role/GuildRoleUpdateHandler.java
@@ -2,7 +2,9 @@ package org.javacord.core.util.handler.guild.role;
 
 import com.fasterxml.jackson.databind.JsonNode;
 import org.javacord.api.DiscordApi;
+import org.javacord.api.entity.channel.ServerTextChannel;
 import org.javacord.api.entity.permission.Permissions;
+import org.javacord.api.entity.user.User;
 import org.javacord.api.event.server.role.RoleChangeColorEvent;
 import org.javacord.api.event.server.role.RoleChangeHoistEvent;
 import org.javacord.api.event.server.role.RoleChangeMentionableEvent;
@@ -21,6 +23,9 @@ import org.javacord.core.util.event.DispatchQueueSelector;
 import org.javacord.core.util.gateway.PacketHandler;
 
 import java.awt.Color;
+import java.util.Set;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
 
 /**
  * Handles the guild role create packet.
@@ -97,6 +102,17 @@ public class GuildRoleUpdateHandler extends PacketHandler {
 
                 api.getEventDispatcher().dispatchRoleChangePermissionsEvent(
                         (DispatchQueueSelector) role.getServer(), role, role.getServer(), event);
+                // If bot is affected remove messages from cache that are no longer visible
+                if (role.getUsers().stream().anyMatch(User::isYourself)) {
+                    Set<Long> unreadableChannels = role.getServer().getTextChannels().stream()
+                            .filter(((Predicate<ServerTextChannel>)ServerTextChannel::canYouSee).negate())
+                            .map(ServerTextChannel::getId)
+                            .collect(Collectors.toSet());
+                    api.forEachCachedMessageWhere(
+                            msg -> unreadableChannels.contains(msg.getChannel().getId()),
+                            msg -> api.removeMessageFromCache(msg.getId())
+                    );
+                }
             }
 
             int oldPosition = role.getPosition();