diff --git a/src/main/java/net/dv8tion/jda/api/entities/PrivateChannel.java b/src/main/java/net/dv8tion/jda/api/entities/PrivateChannel.java
index ec6f0d1a0a..f6778f7754 100644
--- a/src/main/java/net/dv8tion/jda/api/entities/PrivateChannel.java
+++ b/src/main/java/net/dv8tion/jda/api/entities/PrivateChannel.java
@@ -15,7 +15,11 @@
*/
package net.dv8tion.jda.api.entities;
+import net.dv8tion.jda.api.requests.RestAction;
+
+import javax.annotation.CheckReturnValue;
import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
/**
* Represents the connection used for direct messaging.
@@ -27,8 +31,51 @@ public interface PrivateChannel extends MessageChannel
/**
* The {@link net.dv8tion.jda.api.entities.User User} that this {@link net.dv8tion.jda.api.entities.PrivateChannel PrivateChannel} communicates with.
*
- * @return A non-null {@link net.dv8tion.jda.api.entities.User User}.
+ *
This user is only null if this channel is currently uncached, and one the following occur:
+ *
+ * - A reaction is removed
+ * - A reaction is added
+ * - A message is deleted
+ * - This account sends a message to a user from another shard (not shard 0)
+ *
+ * The consequence of this is that for any message this bot receives from a guild or from other users, the user will not be null.
+ *
+ *
In order to retrieve a user that is null, use {@link #retrieveUser()}
+ *
+ * @return Possibly-null {@link net.dv8tion.jda.api.entities.User User}.
+ *
+ * @see #retrieveUser()
*/
- @Nonnull
+ @Nullable
User getUser();
+
+ /**
+ * Retrieves the {@link User User} that this {@link net.dv8tion.jda.api.entities.PrivateChannel PrivateChannel} communicates with.
+ *
+ *
This method fetches the channel from the API and retrieves the User from that.
+ *
+ * @return A {@link RestAction RestAction} to retrieve the {@link User User} that this {@link PrivateChannel PrivateChannel} communicates with.
+ */
+ @Nonnull
+ @CheckReturnValue
+ RestAction retrieveUser();
+
+ /**
+ * The human-readable name of this channel.
+ *
+ * If getUser returns null, this method will return an empty String.
+ * This happens when JDA does not have enough information to populate the channel name.
+ *
+ * This will occur only when {@link #getUser()} is null, and the reasons are given in {@link #getUser()}
+ *
+ * If the channel name is important, {@link #retrieveUser()} should be used, instead.
+ *
+ * @return The name of this channel
+ *
+ * @see #retrieveUser()
+ * @see #getUser()
+ */
+ @Nonnull
+ @Override
+ String getName();
}
diff --git a/src/main/java/net/dv8tion/jda/api/events/message/react/GenericMessageReactionEvent.java b/src/main/java/net/dv8tion/jda/api/events/message/react/GenericMessageReactionEvent.java
index 3d93e5227d..c801982f66 100644
--- a/src/main/java/net/dv8tion/jda/api/events/message/react/GenericMessageReactionEvent.java
+++ b/src/main/java/net/dv8tion/jda/api/events/message/react/GenericMessageReactionEvent.java
@@ -175,6 +175,8 @@ public RestAction retrieveMember()
{
if (member != null)
return new CompletedRestAction<>(getJDA(), member);
+ if (!getChannel().getType().isGuild())
+ throw new IllegalStateException("Cannot retrieve member for a private reaction not from a guild");
return getGuild().retrieveMemberById(getUserIdLong());
}
diff --git a/src/main/java/net/dv8tion/jda/api/events/message/react/MessageReactionAddEvent.java b/src/main/java/net/dv8tion/jda/api/events/message/react/MessageReactionAddEvent.java
index 50db94409f..22f056357a 100644
--- a/src/main/java/net/dv8tion/jda/api/events/message/react/MessageReactionAddEvent.java
+++ b/src/main/java/net/dv8tion/jda/api/events/message/react/MessageReactionAddEvent.java
@@ -40,7 +40,7 @@
*/
public class MessageReactionAddEvent extends GenericMessageReactionEvent
{
- public MessageReactionAddEvent(@Nonnull JDA api, long responseNumber, @Nonnull User user,
+ public MessageReactionAddEvent(@Nonnull JDA api, long responseNumber, @Nullable User user,
@Nullable Member member, @Nonnull MessageReaction reaction, long userId)
{
super(api, responseNumber, user, member, reaction, userId);
diff --git a/src/main/java/net/dv8tion/jda/internal/entities/EntityBuilder.java b/src/main/java/net/dv8tion/jda/internal/entities/EntityBuilder.java
index 75abf4c321..2b73301363 100644
--- a/src/main/java/net/dv8tion/jda/internal/entities/EntityBuilder.java
+++ b/src/main/java/net/dv8tion/jda/internal/entities/EntityBuilder.java
@@ -1227,42 +1227,59 @@ public ThreadMember createThreadMember(ThreadChannelImpl threadChannel, Member m
public PrivateChannel createPrivateChannel(DataObject json)
{
- final long channelId = json.getUnsignedLong("id");
- PrivateChannel channel = api.getPrivateChannelById(channelId);
- api.usedPrivateChannel(channelId);
- if (channel != null)
- return channel;
-
- DataObject recipient = json.hasKey("recipients") ?
- json.getArray("recipients").getObject(0) :
- json.getObject("recipient");
- final long userId = recipient.getLong("id");
- UserImpl user = (UserImpl) getJDA().getUserById(userId);
- if (user == null)
- { //The getJDA() can give us private channels connected to Users that we can no longer communicate with.
- // As such, make a fake user and fake private channel.
- user = createUser(recipient);
- }
-
- return createPrivateChannel(json, user);
+ return createPrivateChannel(json, null);
}
public PrivateChannel createPrivateChannel(DataObject json, UserImpl user)
{
- final long channelId = json.getLong("id");
- PrivateChannelImpl priv = new PrivateChannelImpl(channelId, user)
- .setLatestMessageIdLong(json.getLong("last_message_id", 0));
- user.setPrivateChannel(priv);
+ final long channelId = json.getUnsignedLong("id");
+ PrivateChannelImpl channel = (PrivateChannelImpl) api.getPrivateChannelById(channelId);
+ if (channel == null)
+ {
+ channel = new PrivateChannelImpl(getJDA(), channelId, user)
+ .setLatestMessageIdLong(json.getLong("last_message_id", 0));
+ }
+ UserImpl recipient = user;
+ if (channel.getUser() == null)
+ {
+ if (recipient == null && (json.hasKey("recipients") || json.hasKey("recipient")))
+ {
+ //if we don't know the recipient, and we have information on them, we can use that
+ DataObject recipientJson = json.hasKey("recipients") ?
+ json.getArray("recipients").getObject(0) :
+ json.getObject("recipient");
+ final long userId = recipientJson.getUnsignedLong("id");
+ recipient = (UserImpl) getJDA().getUserById(userId);
+ if (recipient == null)
+ {
+ recipient = createUser(recipientJson);
+ }
+ }
+ if (recipient != null)
+ {
+ //update the channel if we have found the user
+ channel.setUser(user);
+ }
+ }
+ if (recipient != null)
+ {
+ recipient.setPrivateChannel(channel);
+ }
+ // only add channels to the cache when they come from an event, otherwise we would never remove the channel
+ cachePrivateChannel(channel);
+ api.usedPrivateChannel(channelId);
+ return channel;
+ }
- // only add channels to cache when they come from an event, otherwise we would never remove the channel
+ private void cachePrivateChannel(PrivateChannelImpl priv)
+ {
SnowflakeCacheViewImpl privateView = getJDA().getPrivateChannelsView();
try (UnlockHook hook = privateView.writeLock())
{
- privateView.getMap().put(channelId, priv);
+ privateView.getMap().put(priv.getIdLong(), priv);
}
- api.usedPrivateChannel(channelId);
- getJDA().getEventCache().playbackCache(EventCache.Type.CHANNEL, channelId);
- return priv;
+ api.usedPrivateChannel(priv.getIdLong());
+ getJDA().getEventCache().playbackCache(EventCache.Type.CHANNEL, priv.getIdLong());
}
@Nullable
@@ -1387,11 +1404,15 @@ public ReceivedMessage createMessage(DataObject jsonObject, @Nullable MessageCha
final long authorId = author.getLong("id");
MemberImpl member = null;
- if (channel == null && jsonObject.isNull("guild_id") && authorId != getJDA().getSelfUser().getIdLong())
+ if (channel == null && jsonObject.isNull("guild_id"))
{
DataObject channelData = DataObject.empty()
- .put("id", channelId)
- .put("recipient", author);
+ .put("id", channelId);
+ //if we see an author that isn't us, we can assume that is the other side of this private channel
+ //if the author is us, we learn no information about the user at the other end
+ if (authorId != getJDA().getSelfUser().getIdLong())
+ channelData.put("recipient", author);
+ //even without knowing the user at the other end, we can still construct a minimal channel
channel = createPrivateChannel(channelData);
}
else if (channel == null)
diff --git a/src/main/java/net/dv8tion/jda/internal/entities/PrivateChannelImpl.java b/src/main/java/net/dv8tion/jda/internal/entities/PrivateChannelImpl.java
index 568e1133f8..8b6d331510 100644
--- a/src/main/java/net/dv8tion/jda/internal/entities/PrivateChannelImpl.java
+++ b/src/main/java/net/dv8tion/jda/internal/entities/PrivateChannelImpl.java
@@ -16,26 +16,28 @@
package net.dv8tion.jda.internal.entities;
-import net.dv8tion.jda.api.AccountType;
import net.dv8tion.jda.api.JDA;
import net.dv8tion.jda.api.entities.ChannelType;
import net.dv8tion.jda.api.entities.PrivateChannel;
import net.dv8tion.jda.api.entities.User;
import net.dv8tion.jda.api.requests.RestAction;
+import net.dv8tion.jda.internal.JDAImpl;
import net.dv8tion.jda.internal.entities.mixin.channel.middleman.MessageChannelMixin;
+import net.dv8tion.jda.internal.requests.CompletedRestAction;
import net.dv8tion.jda.internal.requests.RestActionImpl;
import net.dv8tion.jda.internal.requests.Route;
import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
public class PrivateChannelImpl extends AbstractChannelImpl implements PrivateChannel, MessageChannelMixin
{
private User user;
private long latestMessageId;
- public PrivateChannelImpl(long id, User user)
+ public PrivateChannelImpl(JDA api, long id, @Nullable User user)
{
- super(id, user.getJDA());
+ super(id, api);
this.user = user;
}
@@ -46,7 +48,7 @@ public ChannelType getType()
return ChannelType.PRIVATE;
}
- @Nonnull
+ @Nullable
@Override
public User getUser()
{
@@ -56,16 +58,34 @@ public User getUser()
@Nonnull
@Override
- public String getName()
+ public RestAction retrieveUser()
{
- return getUser().getName();
+ User user = getUser();
+ if (user != null)
+ return new CompletedRestAction<>(getJDA(), user);
+ //even if the user blocks the bot, this does not fail.
+ return retrievePrivateChannel()
+ .map(PrivateChannel::getUser);
}
@Nonnull
@Override
- public JDA getJDA()
+ public String getName()
{
- return user.getJDA();
+ User user = getUser();
+ if (user == null)
+ {
+ //don't break or override the contract of @NonNull
+ return "";
+ }
+ return user.getName();
+ }
+
+ @Nonnull
+ private RestAction retrievePrivateChannel()
+ {
+ Route.CompiledRoute route = Route.Channels.GET_CHANNEL.compile(getId());
+ return new RestActionImpl<>(getJDA(), route, (response, request) -> ((JDAImpl) getJDA()).getEntityBuilder().createPrivateChannel(response.getObject()));
}
@Nonnull
@@ -85,7 +105,12 @@ public long getLatestMessageIdLong()
@Override
public boolean canTalk()
{
- return !user.isBot();
+ //The only way user is null is when an event is dispatched that doesn't give us enough information to build the recipient user,
+ // which only happens if this bot sends a message (or otherwise triggers an event) from a shard other than shard 0.
+ // The event will be received on shard 0 and not have enough information to build the recipient user.
+ //As such, since events will only happen in this channel if it is between the bot and the user, a null user is a valid channel state.
+ // Events cannot happen between a bot and another bot, so the user would never be null in that case.
+ return user == null || !user.isBot();
}
@Override
@@ -120,6 +145,11 @@ public boolean canDeleteOtherUsersMessages()
return false;
}
+ public void setUser(User user)
+ {
+ this.user = user;
+ }
+
@Override
public PrivateChannelImpl setLatestMessageIdLong(long latestMessageId)
{
@@ -149,7 +179,7 @@ public boolean equals(Object obj)
@Override
public String toString()
{
- return "PC:" + getUser().getName() + '(' + getId() + ')';
+ return "PC:" + getName() + '(' + getId() + ')';
}
private void updateUser()
@@ -162,7 +192,12 @@ private void updateUser()
private void checkBot()
{
- if (getUser().isBot() && getJDA().getAccountType() == AccountType.BOT)
+ //The only way user is null is when an event is dispatched that doesn't give us enough information to build the recipient user,
+ // which only happens if this bot sends a message (or otherwise triggers an event) from a shard other than shard 0.
+ // The event will be received on shard 0 and not have enough information to build the recipient user.
+ //As such, since events will only happen in this channel if it is between the bot and the user, a null user is a valid channel state.
+ // Events cannot happen between a bot and another bot, so the user would never be null in that case.
+ if (getUser() != null && getUser().isBot())
throw new UnsupportedOperationException("Cannot send a private message between bots.");
}
}
diff --git a/src/main/java/net/dv8tion/jda/internal/entities/UserImpl.java b/src/main/java/net/dv8tion/jda/internal/entities/UserImpl.java
index ba42b3e590..b50bbd291b 100644
--- a/src/main/java/net/dv8tion/jda/internal/entities/UserImpl.java
+++ b/src/main/java/net/dv8tion/jda/internal/entities/UserImpl.java
@@ -42,7 +42,7 @@ public class UserImpl extends UserById implements User
protected String name;
protected String avatarId;
protected Profile profile;
- protected long privateChannel = 0L;
+ protected long privateChannelId = 0L;
protected boolean bot;
protected boolean system;
protected boolean fake = false;
@@ -113,7 +113,7 @@ public String getAsTag()
@Override
public boolean hasPrivateChannel()
{
- return privateChannel != 0;
+ return privateChannelId != 0;
}
@Nonnull
@@ -126,7 +126,7 @@ public RestAction openPrivateChannel()
return new RestActionImpl<>(getJDA(), route, body, (response, request) ->
{
PrivateChannel priv = api.getEntityBuilder().createPrivateChannel(response.getObject(), this);
- UserImpl.this.privateChannel = priv.getIdLong();
+ UserImpl.this.privateChannelId = priv.getIdLong();
return priv;
});
});
@@ -136,8 +136,8 @@ public PrivateChannel getPrivateChannel()
{
if (!hasPrivateChannel())
return null;
- PrivateChannel channel = getJDA().getPrivateChannelById(privateChannel);
- return channel != null ? channel : new PrivateChannelImpl(privateChannel, this);
+ PrivateChannel channel = getJDA().getPrivateChannelById(privateChannelId);
+ return channel != null ? channel : new PrivateChannelImpl(getJDA(), privateChannelId, this);
}
@Nonnull
@@ -214,7 +214,7 @@ public UserImpl setProfile(Profile profile)
public UserImpl setPrivateChannel(PrivateChannel privateChannel)
{
if (privateChannel != null)
- this.privateChannel = privateChannel.getIdLong();
+ this.privateChannelId = privateChannel.getIdLong();
return this;
}
diff --git a/src/main/java/net/dv8tion/jda/internal/handle/MessageReactionHandler.java b/src/main/java/net/dv8tion/jda/internal/handle/MessageReactionHandler.java
index e85c4107f6..7f0a423467 100644
--- a/src/main/java/net/dv8tion/jda/internal/handle/MessageReactionHandler.java
+++ b/src/main/java/net/dv8tion/jda/internal/handle/MessageReactionHandler.java
@@ -70,8 +70,8 @@ protected Long handleInternally(DataObject content)
JDALogger.getLazyString(() -> add ? "add" : "remove"), content);
return null;
}
-
- Guild guild = api.getGuildById(content.getUnsignedLong("guild_id", 0));
+ final long guildId = content.getUnsignedLong("guild_id", 0);
+ Guild guild = api.getGuildById(guildId);
MemberImpl member = null;
if (guild != null)
{
@@ -105,8 +105,12 @@ protected Long handleInternally(DataObject content)
User user = api.getUserById(userId);
if (user == null && member != null)
user = member.getUser(); // this happens when we have guild subscriptions disabled
+
if (user == null)
{
+ // We expect there to be a user object already cached when we are in a guild and adding a new reaction as the user should be a member cached in the guild.
+ // The event in the context of a guild will also provide a member object, if the required intents are present.
+ // The only time we can receive a reaction add but not have the user cached would be if we receive the event in an uncached or partially built PrivateChannel.
if (add && guild != null)
{
api.getEventCache().cache(EventCache.Type.USER, userId, responseNumber, allContent, this::handle);
@@ -126,11 +130,18 @@ protected Long handleInternally(DataObject content)
channel = api.getPrivateChannelById(channelId);
if (channel == null)
{
- api.getEventCache().cache(EventCache.Type.CHANNEL, channelId, responseNumber, allContent, this::handle);
- EventCache.LOG.debug("Received a reaction for a channel that JDA does not currently have cached");
- return null;
+ if (guildId != 0)
+ {
+ api.getEventCache().cache(EventCache.Type.CHANNEL, channelId, responseNumber, allContent, this::handle);
+ EventCache.LOG.debug("Received a reaction for a channel that JDA does not currently have cached");
+ return null;
+ }
+ //create a new private channel with minimal information for this event
+ channel = getJDA().getEntityBuilder().createPrivateChannel(
+ DataObject.empty()
+ .put("id", channelId)
+ );
}
-
MessageReaction.ReactionEmote rEmote;
if (emojiId != null)
{
diff --git a/src/main/java/net/dv8tion/jda/internal/requests/Route.java b/src/main/java/net/dv8tion/jda/internal/requests/Route.java
index 7589810251..e504180a8c 100644
--- a/src/main/java/net/dv8tion/jda/internal/requests/Route.java
+++ b/src/main/java/net/dv8tion/jda/internal/requests/Route.java
@@ -207,6 +207,7 @@ public static class Channels
{
public static final Route DELETE_CHANNEL = new Route(DELETE, "channels/{channel_id}");
public static final Route MODIFY_CHANNEL = new Route(PATCH, "channels/{channel_id}");
+ public static final Route GET_CHANNEL = new Route(GET, "channels/{channel_id}");
public static final Route GET_WEBHOOKS = new Route(GET, "channels/{channel_id}/webhooks");
public static final Route CREATE_WEBHOOK = new Route(POST, "channels/{channel_id}/webhooks");
public static final Route CREATE_PERM_OVERRIDE = new Route(PUT, "channels/{channel_id}/permissions/{permoverride_id}");