diff --git a/src/main/java/net/dv8tion/jda/api/JDA.java b/src/main/java/net/dv8tion/jda/api/JDA.java index 8be30903d9..a6bb20e2d2 100644 --- a/src/main/java/net/dv8tion/jda/api/JDA.java +++ b/src/main/java/net/dv8tion/jda/api/JDA.java @@ -55,6 +55,7 @@ import java.util.function.BooleanSupplier; import java.util.function.Consumer; import java.util.regex.Matcher; +import java.util.stream.Collectors; /** * The core of JDA. Acts as a registry system of JDA. All parts of the the API can be accessed starting from this class. @@ -1412,6 +1413,8 @@ default GuildChannel getGuildChannelById(@Nonnull ChannelType type, long id) return getTextChannelById(id); case VOICE: return getVoiceChannelById(id); + case STAGE: + return getStageChannelById(id); case STORE: return getStoreChannelById(id); case CATEGORY: @@ -1420,6 +1423,81 @@ default GuildChannel getGuildChannelById(@Nonnull ChannelType type, long id) return null; } + /** + * An unmodifiable list of all {@link net.dv8tion.jda.api.entities.StageChannel StageChannels} that have the same name as the one provided. + *
If there are no {@link net.dv8tion.jda.api.entities.StageChannel StageChannels} with the provided name, then this returns an empty list. + * + * @param name + * The name of the requested {@link net.dv8tion.jda.api.entities.StageChannel StageChannels}. + * @param ignoreCase + * Whether to ignore case or not when comparing the provided name to each {@link net.dv8tion.jda.api.entities.StageChannel#getName()}. + * + * @return Possibly-empty list of all the {@link net.dv8tion.jda.api.entities.StageChannel StageChannels} that all have the + * same name as the provided name. + */ + @Nonnull + default List getStageChannelsByName(@Nonnull String name, boolean ignoreCase) + { + return getVoiceChannelsByName(name, ignoreCase) + .stream() + .filter(StageChannel.class::isInstance) + .map(StageChannel.class::cast) + .collect(Collectors.toList()); + } + + /** + * This returns the {@link net.dv8tion.jda.api.entities.StageChannel StageChannel} which has the same id as the one provided. + *
If there is no known {@link net.dv8tion.jda.api.entities.StageChannel StageChannel} with an id that matches the provided + * one, then this returns {@code null}. + * + * @param id + * The id of the {@link net.dv8tion.jda.api.entities.StageChannel StageChannel}. + * @throws java.lang.NumberFormatException + * If the provided {@code id} cannot be parsed by {@link Long#parseLong(String)} + * + * @return Possibly-null {@link net.dv8tion.jda.api.entities.StageChannel StageChannel} with matching id. + */ + @Nullable + default StageChannel getStageChannelById(@Nonnull String id) + { + return getStageChannelById(MiscUtil.parseSnowflake(id)); + } + + /** + * This returns the {@link net.dv8tion.jda.api.entities.StageChannel StageChannel} which has the same id as the one provided. + *
If there is no known {@link net.dv8tion.jda.api.entities.StageChannel StageChannel} with an id that matches the provided + * one, then this returns {@code null}. + * + * @param id + * The id of the {@link net.dv8tion.jda.api.entities.StageChannel StageChannel}. + * + * @return Possibly-null {@link net.dv8tion.jda.api.entities.StageChannel StageChannel} with matching id. + */ + @Nullable + default StageChannel getStageChannelById(long id) + { + VoiceChannel channel = getVoiceChannelById(id); + return channel instanceof StageChannel ? (StageChannel) channel : null; + } + + /** + * An unmodifiable list of all {@link net.dv8tion.jda.api.entities.StageChannel StageChannels} of all connected + * {@link net.dv8tion.jda.api.entities.Guild Guilds}. + * + *

This copies the backing store into a list. This means every call + * creates a new list with O(n) complexity. + * + * @return Possible-empty list of all known {@link net.dv8tion.jda.api.entities.StageChannel StageChannels}. + */ + @Nonnull + default List getStageChannels() + { + return getVoiceChannels().stream() + .filter(StageChannel.class::isInstance) + .map(StageChannel.class::cast) + .collect(Collectors.toList()); + } + /** * {@link net.dv8tion.jda.api.utils.cache.SnowflakeCacheView SnowflakeCacheView} of * all cached {@link net.dv8tion.jda.api.entities.Category Categories} visible to this JDA session. @@ -1685,6 +1763,8 @@ default List getTextChannelsByName(@Nonnull String name, boolean ig * {@link net.dv8tion.jda.api.utils.cache.SnowflakeCacheView SnowflakeCacheView} of * all cached {@link net.dv8tion.jda.api.entities.VoiceChannel VoiceChannels} visible to this JDA session. * + *

This may also contain {@link StageChannel StageChannels}! + * * @return {@link net.dv8tion.jda.api.utils.cache.SnowflakeCacheView SnowflakeCacheView} */ @Nonnull @@ -1699,6 +1779,8 @@ default List getTextChannelsByName(@Nonnull String name, boolean ig * a local variable or use {@link #getVoiceChannelCache()} and use its more efficient * versions of handling these values. * + *

This may also contain {@link StageChannel StageChannels}! + * * @return Possible-empty list of all known {@link net.dv8tion.jda.api.entities.VoiceChannel VoiceChannels}. */ @Nonnull @@ -1712,6 +1794,8 @@ default List getVoiceChannels() *
If there is no known {@link net.dv8tion.jda.api.entities.VoiceChannel VoiceChannel} with an id that matches the provided * one, then this returns {@code null}. * + *

This may also contain {@link StageChannel StageChannels}! + * * @param id * The id of the {@link net.dv8tion.jda.api.entities.VoiceChannel VoiceChannel}. * @throws java.lang.NumberFormatException @@ -1730,6 +1814,8 @@ default VoiceChannel getVoiceChannelById(@Nonnull String id) *
If there is no known {@link net.dv8tion.jda.api.entities.VoiceChannel VoiceChannel} with an id that matches the provided * one, then this returns {@code null}. * + *

This may also contain {@link StageChannel StageChannels}! + * * @param id * The id of the {@link net.dv8tion.jda.api.entities.VoiceChannel VoiceChannel}. * @@ -1745,6 +1831,8 @@ default VoiceChannel getVoiceChannelById(long id) * An unmodifiable list of all {@link net.dv8tion.jda.api.entities.VoiceChannel VoiceChannels} that have the same name as the one provided. *
If there are no {@link net.dv8tion.jda.api.entities.VoiceChannel VoiceChannels} with the provided name, then this returns an empty list. * + *

This may also contain {@link StageChannel StageChannels}! + * * @param name * The name of the requested {@link net.dv8tion.jda.api.entities.VoiceChannel VoiceChannels}. * @param ignoreCase diff --git a/src/main/java/net/dv8tion/jda/api/Permission.java b/src/main/java/net/dv8tion/jda/api/Permission.java index 3b7051f808..4cee18a9de 100644 --- a/src/main/java/net/dv8tion/jda/api/Permission.java +++ b/src/main/java/net/dv8tion/jda/api/Permission.java @@ -71,6 +71,8 @@ public enum Permission MANAGE_WEBHOOKS( 29, true, true, "Manage Webhooks"), MANAGE_EMOTES( 30, true, false, "Manage Emojis"), + REQUEST_TO_SPEAK( 32, true, true, "Request to Speak"), + UNKNOWN(-1, false, false, "Unknown"); /** @@ -109,7 +111,7 @@ public enum Permission */ public static final long ALL_VOICE_PERMISSIONS = Permission.getRaw(VOICE_STREAM, VOICE_CONNECT, VOICE_SPEAK, VOICE_MUTE_OTHERS, - VOICE_DEAF_OTHERS, VOICE_MOVE_OTHERS, VOICE_USE_VAD, PRIORITY_SPEAKER); + VOICE_DEAF_OTHERS, VOICE_MOVE_OTHERS, VOICE_USE_VAD, PRIORITY_SPEAKER, REQUEST_TO_SPEAK); private final int offset; private final long raw; diff --git a/src/main/java/net/dv8tion/jda/api/audit/ActionType.java b/src/main/java/net/dv8tion/jda/api/audit/ActionType.java index d045eb9895..9b548f463c 100644 --- a/src/main/java/net/dv8tion/jda/api/audit/ActionType.java +++ b/src/main/java/net/dv8tion/jda/api/audit/ActionType.java @@ -16,6 +16,8 @@ package net.dv8tion.jda.api.audit; +import net.dv8tion.jda.api.entities.Member; + /** * ActionTypes for {@link net.dv8tion.jda.api.audit.AuditLogEntry AuditLogEntry} instances *
Found via {@link net.dv8tion.jda.api.audit.AuditLogEntry#getType() AuditLogEntry.getType()} @@ -444,6 +446,54 @@ public enum ActionType */ INTEGRATION_DELETE(82, TargetType.INTEGRATION), + /** + * A {@link net.dv8tion.jda.api.entities.StageInstance StageInstance} was started by a {@link net.dv8tion.jda.api.entities.StageChannel#isModerator(Member) Stage Moderator}. + * + *

Possible Options

+ * + * + *

Possible Keys

+ * + */ + STAGE_INSTANCE_CREATE(83, TargetType.STAGE_INSTANCE), + + /** + * A {@link net.dv8tion.jda.api.entities.StageInstance StageInstance} was updated by a {@link net.dv8tion.jda.api.entities.StageChannel#isModerator(Member) Stage Moderator}. + * + *

Possible Options

+ * + * + *

Possible Keys

+ * + */ + STAGE_INSTANCE_UPDATE(84, TargetType.STAGE_INSTANCE), + + /** + * A {@link net.dv8tion.jda.api.entities.StageInstance StageInstance} was deleted by a {@link net.dv8tion.jda.api.entities.StageChannel#isModerator(Member) Stage Moderator}. + * + *

Possible Options

+ * + * + *

Possible Keys

+ * + */ + STAGE_INSTANCE_DELETE(85, TargetType.STAGE_INSTANCE), + UNKNOWN(-1, TargetType.UNKNOWN); private final int key; diff --git a/src/main/java/net/dv8tion/jda/api/audit/AuditLogKey.java b/src/main/java/net/dv8tion/jda/api/audit/AuditLogKey.java index 33bd84dc8a..114f264a74 100644 --- a/src/main/java/net/dv8tion/jda/api/audit/AuditLogKey.java +++ b/src/main/java/net/dv8tion/jda/api/audit/AuditLogKey.java @@ -262,6 +262,16 @@ public enum AuditLogKey */ CHANNEL_OVERRIDES("permission_overwrites"), + // STAGE_INSTANCE + + /** + * Change of the {@link net.dv8tion.jda.api.entities.StageInstance#getPrivacyLevel() StageInstance.getPrivacyLevel()} value + *
Use with {@link net.dv8tion.jda.api.entities.StageInstance.PrivacyLevel#fromKey(int) StageInstance.PrivacyLevel.fromKey(int)} + * + *

Expected type: Integer + */ + PRIVACY_LEVEL("privacy_level"), + // MEMBER /** diff --git a/src/main/java/net/dv8tion/jda/api/audit/TargetType.java b/src/main/java/net/dv8tion/jda/api/audit/TargetType.java index f0694ece72..c6f3cc3c31 100644 --- a/src/main/java/net/dv8tion/jda/api/audit/TargetType.java +++ b/src/main/java/net/dv8tion/jda/api/audit/TargetType.java @@ -39,5 +39,6 @@ public enum TargetType WEBHOOK, EMOTE, INTEGRATION, + STAGE_INSTANCE, UNKNOWN } diff --git a/src/main/java/net/dv8tion/jda/api/entities/Category.java b/src/main/java/net/dv8tion/jda/api/entities/Category.java index 8e95072e3d..384ac1c99f 100644 --- a/src/main/java/net/dv8tion/jda/api/entities/Category.java +++ b/src/main/java/net/dv8tion/jda/api/entities/Category.java @@ -88,7 +88,7 @@ public interface Category extends GuildChannel /** * Creates a new {@link net.dv8tion.jda.api.entities.TextChannel TextChannel} with this Category as parent. * For this to be successful, the logged in account has to have the - * {@link net.dv8tion.jda.api.Permission#MANAGE_CHANNEL MANAGE_CHANNEL} Permission in the {@link net.dv8tion.jda.api.entities.Guild Guild}. + * {@link net.dv8tion.jda.api.Permission#MANAGE_CHANNEL MANAGE_CHANNEL} Permission in this Category. * *

This will copy all {@link net.dv8tion.jda.api.entities.PermissionOverride PermissionOverrides} of this Category! * Unless the bot is unable to sync it with this category due to permission escalation. @@ -125,7 +125,7 @@ public interface Category extends GuildChannel /** * Creates a new {@link net.dv8tion.jda.api.entities.VoiceChannel VoiceChannel} with this Category as parent. * For this to be successful, the logged in account has to have the - * {@link net.dv8tion.jda.api.Permission#MANAGE_CHANNEL MANAGE_CHANNEL} Permission in the {@link net.dv8tion.jda.api.entities.Guild Guild}. + * {@link net.dv8tion.jda.api.Permission#MANAGE_CHANNEL MANAGE_CHANNEL} Permission in this Category. * *

This will copy all {@link net.dv8tion.jda.api.entities.PermissionOverride PermissionOverrides} of this Category! * Unless the bot is unable to sync it with this category due to permission escalation. @@ -159,6 +159,43 @@ public interface Category extends GuildChannel @CheckReturnValue ChannelAction createVoiceChannel(@Nonnull String name); + /** + * Creates a new {@link net.dv8tion.jda.api.entities.StageChannel StageChannel} with this Category as parent. + * For this to be successful, the logged in account has to have the + * {@link net.dv8tion.jda.api.Permission#MANAGE_CHANNEL MANAGE_CHANNEL} Permission in this Category. + * + *

This will copy all {@link net.dv8tion.jda.api.entities.PermissionOverride PermissionOverrides} of this Category! + * Unless the bot is unable to sync it with this category due to permission escalation. + * See {@link IPermissionHolder#canSync(GuildChannel, GuildChannel)} for details. + * + *

Possible {@link net.dv8tion.jda.api.requests.ErrorResponse ErrorResponses} caused by + * the returned {@link net.dv8tion.jda.api.requests.RestAction RestAction} include the following: + *

+ * + * @param name + * The name of the StageChannel to create + * + * @throws net.dv8tion.jda.api.exceptions.InsufficientPermissionException + * If the logged in account does not have the {@link net.dv8tion.jda.api.Permission#MANAGE_CHANNEL} permission + * @throws IllegalArgumentException + * If the provided name is {@code null} or empty or greater than 100 characters in length + * + * @return A specific {@link ChannelAction ChannelAction} + *
This action allows to set fields for the new StageChannel before creating it + */ + @Nonnull + @CheckReturnValue + ChannelAction createStageChannel(@Nonnull String name); + /** * Modifies the positional order of this Category's nested {@link #getTextChannels() TextChannels} and {@link #getStoreChannels() StoreChannels}. *
This uses an extension of {@link ChannelOrderAction ChannelOrderAction} diff --git a/src/main/java/net/dv8tion/jda/api/entities/ChannelType.java b/src/main/java/net/dv8tion/jda/api/entities/ChannelType.java index 8c097dafe5..465111d734 100644 --- a/src/main/java/net/dv8tion/jda/api/entities/ChannelType.java +++ b/src/main/java/net/dv8tion/jda/api/entities/ChannelType.java @@ -47,6 +47,10 @@ public enum ChannelType * A {@link net.dv8tion.jda.api.entities.StoreChannel StoreChannel}, Guild-Only. */ STORE(6, 0, true), + /** + * A {@link StageChannel StageChannel}, Guild-Only. + */ + STAGE(13, 1, true), /** * Unknown Discord channel type. Should never happen and would only possibly happen if Discord implemented a new * channel type and JDA had yet to implement support for it. @@ -99,6 +103,42 @@ public boolean isGuild() return isGuild; } + /** + * Whether channels of this type support audio connections. + * + * @return True, if channels of this type support audio + */ + public boolean isAudio() + { + switch (this) + { + case VOICE: + case STAGE: + return true; + default: + return false; + } + } + + /** + * Whether channels of this type support message sending. + * + * @return True, if channels of this type support messages + */ + public boolean isMessage() + { + switch (this) + { + //case NEWS: TODO + case TEXT: + case PRIVATE: + case GROUP: + return true; + default: + return false; + } + } + /** * Static accessor for retrieving a channel type based on its Discord id key. * diff --git a/src/main/java/net/dv8tion/jda/api/entities/Guild.java b/src/main/java/net/dv8tion/jda/api/entities/Guild.java index 796b7ec518..973387a7c9 100644 --- a/src/main/java/net/dv8tion/jda/api/entities/Guild.java +++ b/src/main/java/net/dv8tion/jda/api/entities/Guild.java @@ -57,6 +57,7 @@ import java.util.concurrent.Future; import java.util.function.Consumer; import java.util.function.Predicate; +import java.util.stream.Collectors; /** * Represents a Discord {@link net.dv8tion.jda.api.entities.Guild Guild}. @@ -1525,6 +1526,8 @@ default GuildChannel getGuildChannelById(@Nonnull ChannelType type, long id) return getTextChannelById(id); case VOICE: return getVoiceChannelById(id); + case STAGE: + return getStageChannelById(id); case STORE: return getStoreChannelById(id); case CATEGORY: @@ -1533,6 +1536,87 @@ default GuildChannel getGuildChannelById(@Nonnull ChannelType type, long id) return null; } + /** + * Gets a list of all {@link net.dv8tion.jda.api.entities.StageChannel StageChannel} in this Guild that have the same + * name as the one provided. + *
If there are no {@link net.dv8tion.jda.api.entities.StageChannel StageChannels} with the provided name, then this returns an empty list. + * + * @param name + * The name used to filter the returned {@link net.dv8tion.jda.api.entities.StageChannel StageChannels}. + * @param ignoreCase + * Determines if the comparison ignores case when comparing. True - case insensitive. + * + * @return Possibly-empty immutable list of all StageChannel names that match the provided name. + */ + @Nonnull + default List getStageChannelsByName(@Nonnull String name, boolean ignoreCase) + { + return getVoiceChannelsByName(name, ignoreCase) + .stream() + .filter(StageChannel.class::isInstance) + .map(StageChannel.class::cast) + .collect(Collectors.toList()); + } + + /** + * Gets a {@link net.dv8tion.jda.api.entities.StageChannel StageChannel} from this guild that has the same id as the + * one provided. This method is similar to {@link net.dv8tion.jda.api.JDA#getStageChannelById(String)}, but it only + * checks this specific Guild for a StageChannel. + *
If there is no {@link net.dv8tion.jda.api.entities.StageChannel StageChannel} with an id that matches the provided + * one, then this returns {@code null}. + * + * @param id + * The id of the {@link net.dv8tion.jda.api.entities.StageChannel StageChannel}. + * + * @throws java.lang.NumberFormatException + * If the provided {@code id} cannot be parsed by {@link Long#parseLong(String)} + * + * @return Possibly-null {@link net.dv8tion.jda.api.entities.StageChannel StageChannel} with matching id. + */ + @Nullable + default StageChannel getStageChannelById(@Nonnull String id) + { + return getStageChannelById(MiscUtil.parseSnowflake(id)); + } + + /** + * Gets a {@link net.dv8tion.jda.api.entities.StageChannel StageChannel} from this guild that has the same id as the + * one provided. This method is similar to {@link net.dv8tion.jda.api.JDA#getStageChannelById(long)}, but it only + * checks this specific Guild for a StageChannel. + *
If there is no {@link net.dv8tion.jda.api.entities.StageChannel StageChannel} with an id that matches the provided + * one, then this returns {@code null}. + * + * @param id + * The id of the {@link net.dv8tion.jda.api.entities.StageChannel StageChannel}. + * + * @return Possibly-null {@link net.dv8tion.jda.api.entities.StageChannel StageChannel} with matching id. + */ + @Nullable + default StageChannel getStageChannelById(long id) + { + VoiceChannel channel = getVoiceChannelById(id); + return channel instanceof StageChannel ? (StageChannel) channel : null; + } + + /** + * Gets all {@link net.dv8tion.jda.api.entities.StageChannel StageChannel} in this {@link net.dv8tion.jda.api.entities.Guild Guild}. + *
The channels returned will be sorted according to their position. + * + *

This copies the backing store into a list. This means every call + * creates a new list with O(n) complexity. + * + * @return An immutable List of {@link net.dv8tion.jda.api.entities.StageChannel StageChannels}. + */ + @Nonnull + default List getStageChannels() + { + return getVoiceChannels() + .stream() + .filter(StageChannel.class::isInstance) + .map(StageChannel.class::cast) + .collect(Collectors.toList()); + } + /** * Gets the {@link net.dv8tion.jda.api.entities.Category Category} from this guild that matches the provided id. * This method is similar to {@link net.dv8tion.jda.api.JDA#getCategoryById(String)}, but it only checks in this @@ -1802,6 +1886,8 @@ default List getTextChannelsByName(@Nonnull String name, boolean ig *
If there is no {@link net.dv8tion.jda.api.entities.VoiceChannel VoiceChannel} with an id that matches the provided * one, then this returns {@code null}. * + *

This may also contain {@link StageChannel StageChannels}! + * * @param id * The id of the {@link net.dv8tion.jda.api.entities.VoiceChannel VoiceChannel}. * @@ -1823,6 +1909,8 @@ default VoiceChannel getVoiceChannelById(@Nonnull String id) *
If there is no {@link net.dv8tion.jda.api.entities.VoiceChannel VoiceChannel} with an id that matches the provided * one, then this returns {@code null}. * + *

This may also contain {@link StageChannel StageChannels}! + * * @param id * The id of the {@link net.dv8tion.jda.api.entities.VoiceChannel VoiceChannel}. * @@ -1843,6 +1931,8 @@ default VoiceChannel getVoiceChannelById(long id) * a local variable or use {@link #getVoiceChannelCache()} and use its more efficient * versions of handling these values. * + *

This may also contain {@link StageChannel StageChannels}! + * * @return An immutable List of {@link net.dv8tion.jda.api.entities.VoiceChannel VoiceChannels}. */ @Nonnull @@ -1856,6 +1946,8 @@ default List getVoiceChannels() * name as the one provided. *
If there are no {@link net.dv8tion.jda.api.entities.VoiceChannel VoiceChannels} with the provided name, then this returns an empty list. * + *

This may also contain {@link StageChannel StageChannels}! + * * @param name * The name used to filter the returned {@link net.dv8tion.jda.api.entities.VoiceChannel VoiceChannels}. * @param ignoreCase @@ -1874,6 +1966,8 @@ default List getVoiceChannelsByName(@Nonnull String name, boolean * all cached {@link net.dv8tion.jda.api.entities.VoiceChannel VoiceChannels} of this Guild. *
VoiceChannels are sorted according to their position. * + *

This may also contain {@link StageChannel StageChannels}! + * * @return {@link net.dv8tion.jda.api.utils.cache.SortedSnowflakeCacheView SortedSnowflakeCacheView} */ @Nonnull @@ -2631,6 +2725,44 @@ default RestAction retrieveBan(@Nonnull User bannedUser) @Nonnull AudioManager getAudioManager(); + /** + * Once the currently logged in account is connected to a {@link StageChannel} with an active {@link StageInstance}, + * this will trigger a {@link GuildVoiceState#getRequestToSpeakTimestamp() Request-to-Speak} (aka raise your hand). + * + *

This will set an internal flag to automatically request to speak once the bot joins a stage channel. + *
You can use {@link #cancelRequestToSpeak()} to move back to the audience or cancel your pending request. + * + *

If the self member has {@link Permission#VOICE_MUTE_OTHERS} this will immediately promote them to speaker. + * + *

Example: + *

{@code
+     * stageChannel.createStageInstance("Talent Show").queue()
+     * guild.requestToSpeak(); // Set request to speak flag
+     * guild.getAudioManager().openAudioConnection(stageChannel); // join the channel
+     * }
+ * + * @return {@link Task} representing the request to speak. + * Calling {@link Task#get()} can result in deadlocks and should be avoided at all times. + * + * @see #cancelRequestToSpeak() + */ + @Nonnull + Task requestToSpeak(); + + /** + * Cancels the {@link #requestToSpeak() Request-to-Speak}. + *
This can also be used to move back to the audience if you are currently a speaker. + * + *

If there is no request to speak or the member is not currently connected to an active {@link StageInstance}, this does nothing. + * + * @return {@link Task} representing the request to speak cancellation. + * Calling {@link Task#get()} can result in deadlocks and should be avoided at all times. + * + * @see #requestToSpeak() + */ + @Nonnull + Task cancelRequestToSpeak(); + /** * Returns the {@link net.dv8tion.jda.api.JDA JDA} instance of this Guild * @@ -5038,6 +5170,70 @@ default ChannelAction createVoiceChannel(@Nonnull String name) @CheckReturnValue ChannelAction createVoiceChannel(@Nonnull String name, @Nullable Category parent); + /** + * Creates a new {@link net.dv8tion.jda.api.entities.StageChannel StageChannel} in this Guild. + * For this to be successful, the logged in account has to have the {@link net.dv8tion.jda.api.Permission#MANAGE_CHANNEL MANAGE_CHANNEL} Permission. + * + *

Possible {@link net.dv8tion.jda.api.requests.ErrorResponse ErrorResponses} caused by + * the returned {@link net.dv8tion.jda.api.requests.RestAction RestAction} include the following: + *

    + *
  • {@link net.dv8tion.jda.api.requests.ErrorResponse#MISSING_PERMISSIONS MISSING_PERMISSIONS} + *
    The channel could not be created due to a permission discrepancy
  • + * + *
  • {@link net.dv8tion.jda.api.requests.ErrorResponse#MAX_CHANNELS MAX_CHANNELS} + *
    The maximum number of channels were exceeded
  • + *
+ * + * @param name + * The name of the StageChannel to create + * + * @throws net.dv8tion.jda.api.exceptions.InsufficientPermissionException + * If the logged in account does not have the {@link net.dv8tion.jda.api.Permission#MANAGE_CHANNEL} permission + * @throws IllegalArgumentException + * If the provided name is {@code null} or empty or greater than 100 characters in length + * + * @return A specific {@link ChannelAction ChannelAction} + *
This action allows to set fields for the new StageChannel before creating it + */ + @Nonnull + @CheckReturnValue + default ChannelAction createStageChannel(@Nonnull String name) + { + return createStageChannel(name, null); + } + + /** + * Creates a new {@link net.dv8tion.jda.api.entities.StageChannel StageChannel} in this Guild. + * For this to be successful, the logged in account has to have the {@link net.dv8tion.jda.api.Permission#MANAGE_CHANNEL MANAGE_CHANNEL} Permission. + * + *

Possible {@link net.dv8tion.jda.api.requests.ErrorResponse ErrorResponses} caused by + * the returned {@link net.dv8tion.jda.api.requests.RestAction RestAction} include the following: + *

    + *
  • {@link net.dv8tion.jda.api.requests.ErrorResponse#MISSING_PERMISSIONS MISSING_PERMISSIONS} + *
    The channel could not be created due to a permission discrepancy
  • + * + *
  • {@link net.dv8tion.jda.api.requests.ErrorResponse#MAX_CHANNELS MAX_CHANNELS} + *
    The maximum number of channels were exceeded
  • + *
+ * + * @param name + * The name of the StageChannel to create + * @param parent + * The optional parent category for this channel, or null + * + * @throws net.dv8tion.jda.api.exceptions.InsufficientPermissionException + * If the logged in account does not have the {@link net.dv8tion.jda.api.Permission#MANAGE_CHANNEL} permission + * @throws IllegalArgumentException + * If the provided name is {@code null} or empty or greater than 100 characters in length; + * or the provided parent is not in the same guild. + * + * @return A specific {@link ChannelAction ChannelAction} + *
This action allows to set fields for the new StageChannel before creating it + */ + @Nonnull + @CheckReturnValue + ChannelAction createStageChannel(@Nonnull String name, @Nullable Category parent); + /** * Creates a new {@link net.dv8tion.jda.api.entities.Category Category} in this Guild. * For this to be successful, the logged in account has to have the {@link net.dv8tion.jda.api.Permission#MANAGE_CHANNEL MANAGE_CHANNEL} Permission. diff --git a/src/main/java/net/dv8tion/jda/api/entities/GuildVoiceState.java b/src/main/java/net/dv8tion/jda/api/entities/GuildVoiceState.java index 069e37f2d4..8004c3ff54 100644 --- a/src/main/java/net/dv8tion/jda/api/entities/GuildVoiceState.java +++ b/src/main/java/net/dv8tion/jda/api/entities/GuildVoiceState.java @@ -17,9 +17,12 @@ package net.dv8tion.jda.api.entities; import net.dv8tion.jda.api.JDA; +import net.dv8tion.jda.api.requests.RestAction; +import javax.annotation.CheckReturnValue; import javax.annotation.Nonnull; import javax.annotation.Nullable; +import java.time.OffsetDateTime; /** * Represents the voice state of a {@link net.dv8tion.jda.api.entities.Member Member} in a @@ -27,7 +30,7 @@ * * @see Member#getVoiceState() */ -public interface GuildVoiceState +public interface GuildVoiceState extends ISnowflake { /** * Returns the {@link net.dv8tion.jda.api.JDA JDA} instance of this VoiceState @@ -83,12 +86,15 @@ public interface GuildVoiceState /** * Returns true if this {@link net.dv8tion.jda.api.entities.Member Member} is unable to speak because the - * channel is actively suppressing audio communication. This occurs only in + * channel is actively suppressing audio communication. This occurs in * {@link net.dv8tion.jda.api.entities.VoiceChannel VoiceChannels} where the Member either doesn't have * {@link net.dv8tion.jda.api.Permission#VOICE_SPEAK Permission#VOICE_SPEAK} or if the channel is the * designated AFK channel. + *
This is also used by {@link StageChannel StageChannels} for listeners without speaker approval. * * @return True, if this {@link net.dv8tion.jda.api.entities.Member Member's} audio is being suppressed. + * + * @see #getRequestToSpeakTimestamp() */ boolean isSuppressed(); @@ -145,4 +151,62 @@ public interface GuildVoiceState */ @Nullable String getSessionId(); + + /** + * The time at which the user requested to speak. + *
This is used for {@link StageChannel StageChannels} and can only be approved by members with {@link net.dv8tion.jda.api.Permission#VOICE_MUTE_OTHERS Permission.VOICE_MUTE_OTHERS} on the channel. + * + * @return The request to speak timestamp, or null if this user didn't request to speak + */ + @Nullable + OffsetDateTime getRequestToSpeakTimestamp(); + + /** + * Promote the member to speaker. + *

This requires a non-null {@link #getRequestToSpeakTimestamp()}. + * You can use {@link #inviteSpeaker()} to invite the member to become a speaker if they haven't requested to speak. + * + *

This does nothing if the member is not connected to a {@link StageChannel}. + * + * @throws net.dv8tion.jda.api.exceptions.InsufficientPermissionException + * If the currently logged in account does not have {@link net.dv8tion.jda.api.Permission#VOICE_MUTE_OTHERS Permission.VOICE_MUTE_OTHERS} + * in the associated {@link StageChannel} + * + * @return {@link RestAction} + */ + @Nonnull + @CheckReturnValue + RestAction approveSpeaker(); + + /** + * Reject this members {@link #getRequestToSpeakTimestamp() request to speak}. + *

This requires a non-null {@link #getRequestToSpeakTimestamp()}. + * The member will have to request to speak again. + * + *

This does nothing if the member is not connected to a {@link StageChannel}. + * + * @throws net.dv8tion.jda.api.exceptions.InsufficientPermissionException + * If the currently logged in account does not have {@link net.dv8tion.jda.api.Permission#VOICE_MUTE_OTHERS Permission.VOICE_MUTE_OTHERS} + * in the associated {@link StageChannel} + * + * @return {@link RestAction} + */ + @Nonnull + @CheckReturnValue + RestAction declineSpeaker(); + + /** + * Invite this member to become a speaker. + * + *

This does nothing if the member is not connected to a {@link StageChannel}. + * + * @throws net.dv8tion.jda.api.exceptions.InsufficientPermissionException + * If the currently logged in account does not have {@link net.dv8tion.jda.api.Permission#VOICE_MUTE_OTHERS Permission.VOICE_MUTE_OTHERS} + * in the associated {@link StageChannel} + * + * @return {@link RestAction} + */ + @Nonnull + @CheckReturnValue + RestAction inviteSpeaker(); } diff --git a/src/main/java/net/dv8tion/jda/api/entities/IPermissionHolder.java b/src/main/java/net/dv8tion/jda/api/entities/IPermissionHolder.java index 3a52ffa2f4..1dcba93e77 100644 --- a/src/main/java/net/dv8tion/jda/api/entities/IPermissionHolder.java +++ b/src/main/java/net/dv8tion/jda/api/entities/IPermissionHolder.java @@ -173,7 +173,7 @@ public interface IPermissionHolder extends ISnowflake default boolean hasAccess(@Nonnull GuildChannel channel) { Checks.notNull(channel, "Channel"); - return channel.getType() == ChannelType.VOICE + return channel.getType() == ChannelType.VOICE || channel.getType() == ChannelType.STAGE ? hasPermission(channel, Permission.VOICE_CONNECT, Permission.VIEW_CHANNEL) : hasPermission(channel, Permission.VIEW_CHANNEL); } diff --git a/src/main/java/net/dv8tion/jda/api/entities/StageChannel.java b/src/main/java/net/dv8tion/jda/api/entities/StageChannel.java new file mode 100644 index 0000000000..0364e88aa1 --- /dev/null +++ b/src/main/java/net/dv8tion/jda/api/entities/StageChannel.java @@ -0,0 +1,96 @@ +/* + * Copyright 2015 Austin Keener, Michael Ritter, Florian Spieß, and the JDA contributors + * + * 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. + */ + +package net.dv8tion.jda.api.entities; + +import net.dv8tion.jda.api.Permission; +import net.dv8tion.jda.api.requests.restaction.StageInstanceAction; +import net.dv8tion.jda.internal.utils.Checks; + +import javax.annotation.CheckReturnValue; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * Represents a Stage Channel. + * + *

This is a more advanced version of a {@link VoiceChannel} + * that can be used to host events with speakers and listeners. + */ +public interface StageChannel extends VoiceChannel +{ + /** + * {@link StageInstance} attached to this stage channel. + * + *

This indicates whether a stage channel is currently "live". + * + * @return The {@link StageInstance} or {@code null} if this stage is not live + */ + @Nullable + StageInstance getStageInstance(); + + /** + * Create a new {@link StageInstance} for this stage channel. + * + *

Possible {@link net.dv8tion.jda.api.requests.ErrorResponse ErrorResponses} include: + *

    + *
  • {@link net.dv8tion.jda.api.requests.ErrorResponse#STAGE_ALREADY_OPEN STAGE_ALREADY_OPEN} + *
    If there already is an active {@link StageInstance} for this channel
  • + *
  • {@link net.dv8tion.jda.api.requests.ErrorResponse#UNKNOWN_CHANNEL UNKNOWN_CHANNEL} + *
    If the channel was deleted
  • + *
+ * + * @param topic + * The topic of this stage instance, must be 1-120 characters long + * + * @throws net.dv8tion.jda.api.exceptions.InsufficientPermissionException + * If the self member is not a stage moderator. (See {@link #isModerator(Member)}) + * @throws IllegalArgumentException + * If the topic is null, empty, or longer than 120 characters + * + * @return {@link StageInstanceAction} + */ + @Nonnull + @CheckReturnValue + StageInstanceAction createStageInstance(@Nonnull String topic); + + /** + * Whether this member is considered a moderator for this stage channel. + *
Moderators can modify the {@link #getStageInstance() Stage Instance} and promote speakers. + * To promote a speaker you can use {@link GuildVoiceState#inviteSpeaker()} or {@link GuildVoiceState#approveSpeaker()} if they have already raised their hand (indicated by {@link GuildVoiceState#getRequestToSpeakTimestamp()}). + * A stage moderator can move between speaker and audience without raising their hand. This can be done with {@link Guild#requestToSpeak()} and {@link Guild#cancelRequestToSpeak()} respectively. + * + *

A member is considered a stage moderator if they have these permissions in the stage channel: + *

    + *
  • {@link Permission#MANAGE_CHANNEL}
  • + *
  • {@link Permission#VOICE_MUTE_OTHERS}
  • + *
  • {@link Permission#VOICE_MOVE_OTHERS}
  • + *
+ * + * @param member + * The member to check + * + * @throws IllegalArgumentException + * If the provided member is null or not from this guild + * + * @return True, if the provided member is a stage moderator + */ + default boolean isModerator(@Nonnull Member member) + { + Checks.notNull(member, "Member"); + return member.hasPermission(this, Permission.MANAGE_CHANNEL, Permission.VOICE_MUTE_OTHERS, Permission.VOICE_MOVE_OTHERS); + } +} diff --git a/src/main/java/net/dv8tion/jda/api/entities/StageInstance.java b/src/main/java/net/dv8tion/jda/api/entities/StageInstance.java new file mode 100644 index 0000000000..f1797aca11 --- /dev/null +++ b/src/main/java/net/dv8tion/jda/api/entities/StageInstance.java @@ -0,0 +1,234 @@ +/* + * Copyright 2015 Austin Keener, Michael Ritter, Florian Spieß, and the JDA contributors + * + * 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. + */ + +package net.dv8tion.jda.api.entities; + +import net.dv8tion.jda.api.Permission; +import net.dv8tion.jda.api.managers.StageInstanceManager; +import net.dv8tion.jda.api.requests.RestAction; + +import javax.annotation.CheckReturnValue; +import javax.annotation.Nonnull; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +/** + * A Stage Instance holds information about a live stage. + * + *

This instance indicates an active stage channel with speakers, usually to host events such as presentations or meetings. + */ +public interface StageInstance extends ISnowflake +{ + /** + * The {@link Guild} this stage instance is in + * + * @return The {@link Guild} + */ + @Nonnull + Guild getGuild(); + + /** + * The {@link StageChannel} for this stage instance + * + * @return The {@link StageChannel} + */ + @Nonnull + StageChannel getChannel(); + + /** + * The topic of this stage instance + * + * @return The topic + */ + @Nonnull + String getTopic(); + + /** + * The {@link PrivacyLevel} of this stage instance + * + * @return The {@link PrivacyLevel} + */ + @Nonnull + PrivacyLevel getPrivacyLevel(); + + /** + * Whether this stage instance can be found in stage discovery. + * + * @return True if this is a public stage that can be found in stage discovery + */ + boolean isDiscoverable(); + + /** + * All current speakers of this stage instance. + * + *

A member is considered a speaker when they are currently connected to the stage channel + * and their voice state is not {@link GuildVoiceState#isSuppressed() suppressed}. + * When a member is not a speaker, they are part of the {@link #getAudience() audience}. + * + *

Only {@link StageChannel#isModerator(Member) stage moderators} can promote or invite speakers. + * A stage moderator can move between speaker and audience at any time. + * + * @return {@link List} of {@link Member Members} which can speak in this stage instance + */ + @Nonnull + default List getSpeakers() + { + return Collections.unmodifiableList(getChannel().getMembers() + .stream() + .filter(member -> !member.getVoiceState().isSuppressed()) // voice states should not be null since getMembers() checks only for connected members in the channel + .collect(Collectors.toList())); + } + + /** + * All current audience members of this stage instance. + * + *

A member is considered part of the audience when they are currently connected to the stage channel + * and their voice state is {@link GuildVoiceState#isSuppressed() suppressed}. + * When a member is not part of the audience, they are considered a {@link #getSpeakers() speaker}. + * + *

Only {@link StageChannel#isModerator(Member) stage moderators} can promote or invite speakers. + * A stage moderator can move between speaker and audience at any time. + * + * @return {@link List} of {@link Member Members} which cannot speak in this stage instance + */ + @Nonnull + default List getAudience() + { + return Collections.unmodifiableList(getChannel().getMembers() + .stream() + .filter(member -> member.getVoiceState().isSuppressed()) // voice states should not be null since getMembers() checks only for connected members in the channel + .collect(Collectors.toList())); + } + + /** + * Deletes this stage instance + * + *

Possible {@link net.dv8tion.jda.api.requests.ErrorResponse ErrorResponses} include: + *

    + *
  • {@link net.dv8tion.jda.api.requests.ErrorResponse#UNKNOWN_STAGE_INSTANCE UNKNOWN_STAGE_INSTANCE} + *
    If this stage instance is already deleted
  • + *
  • {@link net.dv8tion.jda.api.requests.ErrorResponse#UNKNOWN_CHANNEL UNKNOWN_CHANNEL} + *
    If the channel was deleted
  • + *
+ * + * @throws net.dv8tion.jda.api.exceptions.InsufficientPermissionException + * If the self member is not a {@link StageChannel#isModerator(Member) stage moderator} + * + * @return {@link RestAction} + */ + @Nonnull + @CheckReturnValue + RestAction delete(); + + /** + * Sends a {@link GuildVoiceState#getRequestToSpeakTimestamp() request-to-speak} indicator to the stage instance moderators. + *

If the self member has {@link Permission#VOICE_MUTE_OTHERS} this will immediately promote them to speaker. + * + * @throws IllegalStateException + * If the self member is not currently connected to the channel of this stage instance + * + * @return {@link RestAction} + * + * @see #cancelRequestToSpeak() + */ + @Nonnull + @CheckReturnValue + RestAction requestToSpeak(); + + /** + * Cancels the {@link #requestToSpeak() Request-to-Speak}. + *
This can also be used to move back to the audience if you are currently a speaker. + * + *

If there is no request to speak or the member is not currently connected to an active {@link StageInstance}, this does nothing. + * + * @throws IllegalStateException + * If the self member is not currently connected to the channel of this stage instance + * + * @return {@link RestAction} + * + * @see #requestToSpeak() + */ + @Nonnull + @CheckReturnValue + RestAction cancelRequestToSpeak(); + + /** + * The {@link StageInstanceManager} used to update this stage instance. + *

This can be used to update multiple fields such as topic and privacy level in one request + * + *

If this stage instance is already deleted, this will fail with {@link net.dv8tion.jda.api.requests.ErrorResponse#UNKNOWN_STAGE_INSTANCE ErrorResponse.UNKNOWN_STAGE_INSTANCE}. + * + * @throws net.dv8tion.jda.api.exceptions.InsufficientPermissionException + * If the self member is not a {@link StageChannel#isModerator(Member) stage moderator} + * + * @return The {@link StageInstanceManager} + */ + @Nonnull + @CheckReturnValue + StageInstanceManager getManager(); + + /** + * The privacy level for a stage instance. + * + *

This indicates from where people can join the stage instance. + */ + enum PrivacyLevel + { + /** Placeholder for future privacy levels, indicates that this version of JDA does not support this privacy level yet */ + UNKNOWN(-1), + /** This stage instance can be accessed by lurkers, meaning users that are not active members of the guild */ + PUBLIC(1), + /** This stage instance can only be accessed by guild members */ + GUILD_ONLY(2); + + private final int key; + + PrivacyLevel(int key) + { + this.key = key; + } + + /** + * The raw API key for this privacy level + * + * @return The raw API value or {@code -1} if this is {@link #UNKNOWN} + */ + public int getKey() + { + return key; + } + + /** + * Converts the raw API key into the respective enum value + * + * @param key + * The API key + * + * @return The enum value or {@link #UNKNOWN} + */ + @Nonnull + public static PrivacyLevel fromKey(int key) + { + for (PrivacyLevel level : values()) + { + if (level.key == key) + return level; + } + return UNKNOWN; + } + } +} diff --git a/src/main/java/net/dv8tion/jda/api/entities/VoiceChannel.java b/src/main/java/net/dv8tion/jda/api/entities/VoiceChannel.java index 9d5fb9be4b..b997e833fc 100644 --- a/src/main/java/net/dv8tion/jda/api/entities/VoiceChannel.java +++ b/src/main/java/net/dv8tion/jda/api/entities/VoiceChannel.java @@ -47,6 +47,8 @@ public interface VoiceChannel extends GuildChannel * {@link net.dv8tion.jda.api.entities.VoiceChannel VoiceChannel} at once. *
0 - No limit * + *

This is meaningless for {@link StageChannel StageChannels}. + * * @return The maximum amount of members allowed in this channel at once. */ int getUserLimit(); diff --git a/src/main/java/net/dv8tion/jda/api/events/guild/invite/GenericGuildInviteEvent.java b/src/main/java/net/dv8tion/jda/api/events/guild/invite/GenericGuildInviteEvent.java index 4db3af934e..df42bb9c0e 100644 --- a/src/main/java/net/dv8tion/jda/api/events/guild/invite/GenericGuildInviteEvent.java +++ b/src/main/java/net/dv8tion/jda/api/events/guild/invite/GenericGuildInviteEvent.java @@ -114,7 +114,7 @@ public TextChannel getTextChannel() * The {@link VoiceChannel} this invite points to. * * @throws IllegalStateException - * If this did not happen in a channel of type {@link ChannelType#VOICE ChannelType.VOICE} + * If this did not happen in a voice channel or stage channel * * @return {@link VoiceChannel} * @@ -124,11 +124,30 @@ public TextChannel getTextChannel() @Nonnull public VoiceChannel getVoiceChannel() { - if (getChannelType() != ChannelType.VOICE) - throw new IllegalStateException("The channel is not of type VOICE"); + if (!(channel instanceof VoiceChannel)) + throw new IllegalStateException("The channel is not of type VOICE or STAGE"); return (VoiceChannel) getChannel(); } + /** + * The {@link StageChannel} this invite points to. + * + * @throws IllegalStateException + * If this did not happen in a channel of type {@link ChannelType#STAGE ChannelType.STAGE} + * + * @return {@link StageChannel} + * + * @see #getChannel() + * @see #getChannelType() + */ + @Nonnull + public StageChannel getStageChannel() + { + if (getChannelType() != ChannelType.STAGE) + throw new IllegalStateException("The channel is not of type STAGE"); + return (StageChannel) getChannel(); + } + /** * The {@link StoreChannel} this invite points to. * diff --git a/src/main/java/net/dv8tion/jda/api/events/guild/voice/GuildVoiceRequestToSpeakEvent.java b/src/main/java/net/dv8tion/jda/api/events/guild/voice/GuildVoiceRequestToSpeakEvent.java new file mode 100644 index 0000000000..5263939464 --- /dev/null +++ b/src/main/java/net/dv8tion/jda/api/events/guild/voice/GuildVoiceRequestToSpeakEvent.java @@ -0,0 +1,121 @@ +/* + * Copyright 2015 Austin Keener, Michael Ritter, Florian Spieß, and the JDA contributors + * + * 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. + */ + +package net.dv8tion.jda.api.events.guild.voice; + +import net.dv8tion.jda.api.JDA; +import net.dv8tion.jda.api.entities.GuildVoiceState; +import net.dv8tion.jda.api.entities.Member; +import net.dv8tion.jda.api.entities.StageChannel; +import net.dv8tion.jda.api.requests.RestAction; + +import javax.annotation.CheckReturnValue; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.time.OffsetDateTime; + +/** + * Indicates that a guild member has updated their {@link GuildVoiceState#getRequestToSpeakTimestamp() Request-to-Speak}. + * + *

If {@link #getNewTime()} is non-null, this means the member has raised their hand and wants to speak. + * You can use {@link #approveSpeaker()} or {@link #declineSpeaker()} to handle this request if you have {@link net.dv8tion.jda.api.Permission#VOICE_MUTE_OTHERS Permission.VOICE_MUTE_OTHERS}. + * + *

Requirements

+ * + *

These events require the {@link net.dv8tion.jda.api.utils.cache.CacheFlag#VOICE_STATE VOICE_STATE} CacheFlag to be enabled, which requires + * the {@link net.dv8tion.jda.api.requests.GatewayIntent#GUILD_VOICE_STATES GUILD_VOICE_STATES} intent. + * + *
{@link net.dv8tion.jda.api.JDABuilder#createLight(String) createLight(String)} disables that CacheFlag by default! + * + *

Additionally, these events require the {@link net.dv8tion.jda.api.utils.MemberCachePolicy MemberCachePolicy} + * to cache the updated members. Discord does not specifically tell us about the updates, but merely tells us the + * member was updated and gives us the updated member object. In order to fire specific events like these we + * need to have the old member cached to compare against. + */ +public class GuildVoiceRequestToSpeakEvent extends GenericGuildVoiceEvent +{ + private final OffsetDateTime oldTime, newTime; + + public GuildVoiceRequestToSpeakEvent(@Nonnull JDA api, long responseNumber, @Nonnull Member member, + @Nullable OffsetDateTime oldTime, @Nullable OffsetDateTime newTime) + { + super(api, responseNumber, member); + this.oldTime = oldTime; + this.newTime = newTime; + } + + /** + * The old {@link GuildVoiceState#getRequestToSpeakTimestamp()} + * + * @return The old timestamp, or null if this member did not request to speak before + */ + @Nullable + public OffsetDateTime getOldTime() + { + return oldTime; + } + + /** + * The new {@link GuildVoiceState#getRequestToSpeakTimestamp()} + * + * @return The new timestamp, or null if the request to speak was declined or cancelled + */ + @Nullable + public OffsetDateTime getNewTime() + { + return newTime; + } + + /** + * Promote the member to speaker. + *

This requires a non-null {@link #getNewTime()}. + * You can use {@link GuildVoiceState#inviteSpeaker()} to invite the member to become a speaker if they haven't requested to speak. + * + *

This does nothing if the member is not connected to a {@link StageChannel}. + * + * @throws net.dv8tion.jda.api.exceptions.InsufficientPermissionException + * If the currently logged in account does not have {@link net.dv8tion.jda.api.Permission#VOICE_MUTE_OTHERS Permission.VOICE_MUTE_OTHERS} + * in the associated {@link StageChannel} + * + * @return {@link RestAction} + */ + @Nonnull + @CheckReturnValue + public RestAction approveSpeaker() + { + return getVoiceState().approveSpeaker(); + } + + /** + * Reject this members {@link GuildVoiceState#getRequestToSpeakTimestamp() request to speak}. + *

This requires a non-null {@link #getNewTime()}. + * The member will have to request to speak again. + * + *

This does nothing if the member is not connected to a {@link StageChannel}. + * + * @throws net.dv8tion.jda.api.exceptions.InsufficientPermissionException + * If the currently logged in account does not have {@link net.dv8tion.jda.api.Permission#VOICE_MUTE_OTHERS Permission.VOICE_MUTE_OTHERS} + * in the associated {@link StageChannel} + * + * @return {@link RestAction} + */ + @Nonnull + @CheckReturnValue + public RestAction declineSpeaker() + { + return getVoiceState().declineSpeaker(); + } +} diff --git a/src/main/java/net/dv8tion/jda/api/events/guild/voice/GuildVoiceUpdateEvent.java b/src/main/java/net/dv8tion/jda/api/events/guild/voice/GuildVoiceUpdateEvent.java index 90945a60cf..13cac6b0dc 100644 --- a/src/main/java/net/dv8tion/jda/api/events/guild/voice/GuildVoiceUpdateEvent.java +++ b/src/main/java/net/dv8tion/jda/api/events/guild/voice/GuildVoiceUpdateEvent.java @@ -16,10 +16,12 @@ package net.dv8tion.jda.api.events.guild.voice; +import net.dv8tion.jda.api.entities.Guild; import net.dv8tion.jda.api.entities.Member; import net.dv8tion.jda.api.entities.VoiceChannel; import net.dv8tion.jda.api.events.UpdateEvent; +import javax.annotation.Nonnull; import javax.annotation.Nullable; /** @@ -49,6 +51,22 @@ public interface GuildVoiceUpdateEvent extends UpdateEvent { String IDENTIFIER = "voice-channel"; + /** + * The affected {@link net.dv8tion.jda.api.entities.Member Member} + * + * @return The affected Member + */ + @Nonnull + Member getMember(); + + /** + * The {@link net.dv8tion.jda.api.entities.Guild Guild} + * + * @return The Guild + */ + @Nonnull + Guild getGuild(); + /** * The {@link net.dv8tion.jda.api.entities.VoiceChannel VoiceChannel} that the {@link net.dv8tion.jda.api.entities.Member Member} is moved from * diff --git a/src/main/java/net/dv8tion/jda/api/events/stage/GenericStageInstanceEvent.java b/src/main/java/net/dv8tion/jda/api/events/stage/GenericStageInstanceEvent.java new file mode 100644 index 0000000000..bad85e368b --- /dev/null +++ b/src/main/java/net/dv8tion/jda/api/events/stage/GenericStageInstanceEvent.java @@ -0,0 +1,63 @@ +/* + * Copyright 2015 Austin Keener, Michael Ritter, Florian Spieß, and the JDA contributors + * + * 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. + */ + +package net.dv8tion.jda.api.events.stage; + +import net.dv8tion.jda.api.JDA; +import net.dv8tion.jda.api.entities.StageChannel; +import net.dv8tion.jda.api.entities.StageInstance; +import net.dv8tion.jda.api.events.guild.GenericGuildEvent; + +import javax.annotation.Nonnull; + +/** + * Indicates that a {@link net.dv8tion.jda.api.entities.StageInstance StageInstance} was created/deleted/changed. + *
Every StageInstanceEvent is derived from this event and can be casted. + * + *

Can be used to detect any StageInstanceEvent. + */ +public abstract class GenericStageInstanceEvent extends GenericGuildEvent +{ + protected final StageInstance instance; + + public GenericStageInstanceEvent(@Nonnull JDA api, long responseNumber, @Nonnull StageInstance stageInstance) + { + super(api, responseNumber, stageInstance.getGuild()); + this.instance = stageInstance; + } + + /** + * The affected {@link StageInstance} + * + * @return The {@link StageInstance} + */ + @Nonnull + public StageInstance getInstance() + { + return instance; + } + + /** + * The {@link StageChannel} this instance belongs to + * + * @return The StageChannel + */ + @Nonnull + public StageChannel getChannel() + { + return instance.getChannel(); + } +} diff --git a/src/main/java/net/dv8tion/jda/api/events/stage/StageInstanceCreateEvent.java b/src/main/java/net/dv8tion/jda/api/events/stage/StageInstanceCreateEvent.java new file mode 100644 index 0000000000..f4192f8e2b --- /dev/null +++ b/src/main/java/net/dv8tion/jda/api/events/stage/StageInstanceCreateEvent.java @@ -0,0 +1,35 @@ +/* + * Copyright 2015 Austin Keener, Michael Ritter, Florian Spieß, and the JDA contributors + * + * 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. + */ + +package net.dv8tion.jda.api.events.stage; + +import net.dv8tion.jda.api.JDA; +import net.dv8tion.jda.api.entities.StageInstance; + +import javax.annotation.Nonnull; + +/** + * Indicates that a {@link net.dv8tion.jda.api.entities.StageInstance StageInstance} was created. + * + *

Can be used to retrieve the created StageInstance and its Guild. + */ +public class StageInstanceCreateEvent extends GenericStageInstanceEvent +{ + public StageInstanceCreateEvent(@Nonnull JDA api, long responseNumber, @Nonnull StageInstance stageInstance) + { + super(api, responseNumber, stageInstance); + } +} diff --git a/src/main/java/net/dv8tion/jda/api/events/stage/StageInstanceDeleteEvent.java b/src/main/java/net/dv8tion/jda/api/events/stage/StageInstanceDeleteEvent.java new file mode 100644 index 0000000000..ca80135f37 --- /dev/null +++ b/src/main/java/net/dv8tion/jda/api/events/stage/StageInstanceDeleteEvent.java @@ -0,0 +1,35 @@ +/* + * Copyright 2015 Austin Keener, Michael Ritter, Florian Spieß, and the JDA contributors + * + * 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. + */ + +package net.dv8tion.jda.api.events.stage; + +import net.dv8tion.jda.api.JDA; +import net.dv8tion.jda.api.entities.StageInstance; + +import javax.annotation.Nonnull; + +/** + * Indicates that a {@link net.dv8tion.jda.api.entities.StageInstance StageInstance} was deleted. + * + *

Can be used to retrieve the deleted StageInstance and its Guild. + */ +public class StageInstanceDeleteEvent extends GenericStageInstanceEvent +{ + public StageInstanceDeleteEvent(@Nonnull JDA api, long responseNumber, @Nonnull StageInstance stageInstance) + { + super(api, responseNumber, stageInstance); + } +} diff --git a/src/main/java/net/dv8tion/jda/api/events/stage/update/GenericStageInstanceUpdateEvent.java b/src/main/java/net/dv8tion/jda/api/events/stage/update/GenericStageInstanceUpdateEvent.java new file mode 100644 index 0000000000..62be4e64e2 --- /dev/null +++ b/src/main/java/net/dv8tion/jda/api/events/stage/update/GenericStageInstanceUpdateEvent.java @@ -0,0 +1,74 @@ +/* + * Copyright 2015 Austin Keener, Michael Ritter, Florian Spieß, and the JDA contributors + * + * 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. + */ + +package net.dv8tion.jda.api.events.stage.update; + +import net.dv8tion.jda.api.JDA; +import net.dv8tion.jda.api.entities.StageInstance; +import net.dv8tion.jda.api.events.UpdateEvent; +import net.dv8tion.jda.api.events.stage.GenericStageInstanceEvent; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * Indicates that a {@link net.dv8tion.jda.api.entities.StageInstance StageInstance} was updated. + *
Every StageInstanceUpdateEvent is derived from this event and can be casted. + * + *

Can be used to detect any StageInstanceUpdateEvent. + */ +public abstract class GenericStageInstanceUpdateEvent extends GenericStageInstanceEvent implements UpdateEvent +{ + protected final T previous; + protected final T next; + protected final String identifier; + + public GenericStageInstanceUpdateEvent(@Nonnull JDA api, long responseNumber, @Nonnull StageInstance stageInstance, T previous, T next, String identifier) + { + super(api, responseNumber, stageInstance); + this.previous = previous; + this.next = next; + this.identifier = identifier; + } + + @Nonnull + @Override + public String getPropertyIdentifier() + { + return identifier; + } + + @Nonnull + @Override + public StageInstance getEntity() + { + return getInstance(); + } + + @Nullable + @Override + public T getOldValue() + { + return previous; + } + + @Nullable + @Override + public T getNewValue() + { + return next; + } +} diff --git a/src/main/java/net/dv8tion/jda/api/events/stage/update/StageInstanceUpdatePrivacyLevelEvent.java b/src/main/java/net/dv8tion/jda/api/events/stage/update/StageInstanceUpdatePrivacyLevelEvent.java new file mode 100644 index 0000000000..2a00e860ef --- /dev/null +++ b/src/main/java/net/dv8tion/jda/api/events/stage/update/StageInstanceUpdatePrivacyLevelEvent.java @@ -0,0 +1,54 @@ +/* + * Copyright 2015 Austin Keener, Michael Ritter, Florian Spieß, and the JDA contributors + * + * 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. + */ + +package net.dv8tion.jda.api.events.stage.update; + +import net.dv8tion.jda.api.JDA; +import net.dv8tion.jda.api.entities.StageInstance; + +import javax.annotation.Nonnull; + +/** + * Indicates that a {@link net.dv8tion.jda.api.entities.StageInstance StageInstance} updated its {@link net.dv8tion.jda.api.entities.StageInstance.PrivacyLevel PrivacyLevel}. + * + *

Can be used to retrieve the privacy level. + * + *

Identifier: {@code privacy_level} + */ +@SuppressWarnings("ConstantConditions") +public class StageInstanceUpdatePrivacyLevelEvent extends GenericStageInstanceUpdateEvent +{ + public static final String IDENTIFIER = "privacy_level"; + + public StageInstanceUpdatePrivacyLevelEvent(@Nonnull JDA api, long responseNumber, @Nonnull StageInstance stageInstance, @Nonnull StageInstance.PrivacyLevel previous) + { + super(api, responseNumber, stageInstance, previous, stageInstance.getPrivacyLevel(), IDENTIFIER); + } + + @Nonnull + @Override + public StageInstance.PrivacyLevel getOldValue() + { + return super.getOldValue(); + } + + @Nonnull + @Override + public StageInstance.PrivacyLevel getNewValue() + { + return super.getNewValue(); + } +} diff --git a/src/main/java/net/dv8tion/jda/api/events/stage/update/StageInstanceUpdateTopicEvent.java b/src/main/java/net/dv8tion/jda/api/events/stage/update/StageInstanceUpdateTopicEvent.java new file mode 100644 index 0000000000..0be4d8d130 --- /dev/null +++ b/src/main/java/net/dv8tion/jda/api/events/stage/update/StageInstanceUpdateTopicEvent.java @@ -0,0 +1,54 @@ +/* + * Copyright 2015 Austin Keener, Michael Ritter, Florian Spieß, and the JDA contributors + * + * 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. + */ + +package net.dv8tion.jda.api.events.stage.update; + +import net.dv8tion.jda.api.JDA; +import net.dv8tion.jda.api.entities.StageInstance; + +import javax.annotation.Nonnull; + +/** + * Indicates that a {@link net.dv8tion.jda.api.entities.StageInstance StageInstance} updated its {@code topic}. + * + *

Can be used to retrieve the topic. + * + *

Identifier: {@code topic} + */ +@SuppressWarnings("ConstantConditions") +public class StageInstanceUpdateTopicEvent extends GenericStageInstanceUpdateEvent +{ + public static final String IDENTIFIER = "topic"; + + public StageInstanceUpdateTopicEvent(@Nonnull JDA api, long responseNumber, @Nonnull StageInstance stageInstance, String previous) + { + super(api, responseNumber, stageInstance, previous, stageInstance.getTopic(), IDENTIFIER); + } + + @Nonnull + @Override + public String getOldValue() + { + return super.getOldValue(); + } + + @Nonnull + @Override + public String getNewValue() + { + return super.getNewValue(); + } +} diff --git a/src/main/java/net/dv8tion/jda/api/hooks/ListenerAdapter.java b/src/main/java/net/dv8tion/jda/api/hooks/ListenerAdapter.java index 3e59676691..b9bed943ac 100644 --- a/src/main/java/net/dv8tion/jda/api/hooks/ListenerAdapter.java +++ b/src/main/java/net/dv8tion/jda/api/hooks/ListenerAdapter.java @@ -85,6 +85,12 @@ import net.dv8tion.jda.api.events.role.RoleDeleteEvent; import net.dv8tion.jda.api.events.role.update.*; import net.dv8tion.jda.api.events.self.*; +import net.dv8tion.jda.api.events.stage.GenericStageInstanceEvent; +import net.dv8tion.jda.api.events.stage.StageInstanceCreateEvent; +import net.dv8tion.jda.api.events.stage.StageInstanceDeleteEvent; +import net.dv8tion.jda.api.events.stage.update.GenericStageInstanceUpdateEvent; +import net.dv8tion.jda.api.events.stage.update.StageInstanceUpdatePrivacyLevelEvent; +import net.dv8tion.jda.api.events.stage.update.StageInstanceUpdateTopicEvent; import net.dv8tion.jda.api.events.user.GenericUserEvent; import net.dv8tion.jda.api.events.user.UserActivityEndEvent; import net.dv8tion.jda.api.events.user.UserActivityStartEvent; @@ -295,6 +301,12 @@ public void onPrivateChannelCreate(@Nonnull PrivateChannelCreateEvent event) {} @DeprecatedSince("4.3.0") public void onPrivateChannelDelete(@Nonnull PrivateChannelDeleteEvent event) {} + //StageInstance Event + public void onStageInstanceDelete(@Nonnull StageInstanceDeleteEvent event) {} + public void onStageInstanceUpdateTopic(@Nonnull StageInstanceUpdateTopicEvent event) {} + public void onStageInstanceUpdatePrivacyLevel(@Nonnull StageInstanceUpdatePrivacyLevelEvent event) {} + public void onStageInstanceCreate(@Nonnull StageInstanceCreateEvent event) {} + //Guild Events public void onGuildReady(@Nonnull GuildReadyEvent event) {} public void onGuildTimeout(@Nonnull GuildTimeoutEvent event) {} @@ -367,6 +379,7 @@ public void onGuildVoiceSelfMute(@Nonnull GuildVoiceSelfMuteEvent event) {} public void onGuildVoiceSelfDeafen(@Nonnull GuildVoiceSelfDeafenEvent event) {} public void onGuildVoiceSuppress(@Nonnull GuildVoiceSuppressEvent event) {} public void onGuildVoiceStream(@Nonnull GuildVoiceStreamEvent event) {} + public void onGuildVoiceRequestToSpeak(@Nonnull GuildVoiceRequestToSpeakEvent event) {} //Role events public void onRoleCreate(@Nonnull RoleCreateEvent event) {} @@ -411,6 +424,8 @@ public void onGenericVoiceChannel(@Nonnull GenericVoiceChannelEvent event) {} public void onGenericVoiceChannelUpdate(@Nonnull GenericVoiceChannelUpdateEvent event) {} public void onGenericCategory(@Nonnull GenericCategoryEvent event) {} public void onGenericCategoryUpdate(@Nonnull GenericCategoryUpdateEvent event) {} + public void onGenericStageInstance(@Nonnull GenericStageInstanceEvent event) {} + public void onGenericStageInstanceUpdate(@Nonnull GenericStageInstanceUpdateEvent event) {} public void onGenericGuild(@Nonnull GenericGuildEvent event) {} public void onGenericGuildUpdate(@Nonnull GenericGuildUpdateEvent event) {} public void onGenericGuildInvite(@Nonnull GenericGuildInviteEvent event) {} diff --git a/src/main/java/net/dv8tion/jda/api/managers/ChannelManager.java b/src/main/java/net/dv8tion/jda/api/managers/ChannelManager.java index 30a9c4a7af..3c5cd25a73 100644 --- a/src/main/java/net/dv8tion/jda/api/managers/ChannelManager.java +++ b/src/main/java/net/dv8tion/jda/api/managers/ChannelManager.java @@ -370,13 +370,14 @@ default ChannelManager sync() ChannelManager setPosition(int position); /** - * Sets the topic of the selected {@link net.dv8tion.jda.api.entities.TextChannel TextChannel}. + * Sets the topic of the selected + * {@link net.dv8tion.jda.api.entities.TextChannel TextChannel} or {@link StageChannel StageChannel}. * *

A channel topic must not be more than {@code 1024} characters long! *
This is only available to {@link net.dv8tion.jda.api.entities.TextChannel TextChannels} * * @param topic - * The new topic for the selected {@link net.dv8tion.jda.api.entities.TextChannel TextChannel}, + * The new topic for the selected channel, * {@code null} or empty String to reset * * @throws UnsupportedOperationException diff --git a/src/main/java/net/dv8tion/jda/api/managers/StageInstanceManager.java b/src/main/java/net/dv8tion/jda/api/managers/StageInstanceManager.java new file mode 100644 index 0000000000..99be2e1a30 --- /dev/null +++ b/src/main/java/net/dv8tion/jda/api/managers/StageInstanceManager.java @@ -0,0 +1,126 @@ +/* + * Copyright 2015 Austin Keener, Michael Ritter, Florian Spieß, and the JDA contributors + * + * 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. + */ + +package net.dv8tion.jda.api.managers; + +import net.dv8tion.jda.api.entities.StageInstance; + +import javax.annotation.CheckReturnValue; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * Manager providing functionality to update one or more fields for a {@link net.dv8tion.jda.api.entities.StageInstance StageInstance}. + * + *

Example + *

{@code
+ * manager.setTopic("LMAO JOIN FOR FREE NITRO")
+ *        .setPrivacyLevel(PrivacyLevel.PUBLIC)
+ *        .queue();
+ * manager.reset(ChannelManager.TOPIC | ChannelManager.PRIVACY_LEVEL)
+ *        .setTopic("Talent Show | WINNER GETS FREE NITRO")
+ *        .setPrivacyLevel(PrivacyLevel.GUILD_ONLY)
+ *        .queue();
+ * }
+ * + * @see net.dv8tion.jda.api.entities.StageInstance#getManager() + */ +public interface StageInstanceManager extends Manager +{ + /** Used to reset the topic field */ + long TOPIC = 1 << 0; + /** Used to reset the privacy level field */ + long PRIVACY_LEVEL = 1 << 1; + + /** + * Resets the fields specified by the provided bit-flag pattern. + * You can specify a combination by using a bitwise OR concat of the flag constants. + *
Example: {@code manager.reset(ChannelManager.TOPIC | ChannelManager.PRIVACY_LEVEL);} + * + *

Flag Constants: + *

    + *
  • {@link #TOPIC}
  • + *
  • {@link #PRIVACY_LEVEL}
  • + *
+ * + * @param fields + * Integer value containing the flags to reset. + * + * @return StageInstanceManager for chaining convenience + */ + @Nonnull + @Override + StageInstanceManager reset(long fields); + + /** + * Resets the fields specified by the provided bit-flag patterns. + *
Example: {@code manager.reset(ChannelManager.TOPIC, ChannelManager.PRIVACY_LEVEL);} + * + *

Flag Constants: + *

    + *
  • {@link #TOPIC}
  • + *
  • {@link #PRIVACY_LEVEL}
  • + *
+ * + * @param fields + * Integer values containing the flags to reset. + * + * @return StageInstanceManager for chaining convenience + */ + @Nonnull + @Override + StageInstanceManager reset(long... fields); + + /** + * The associated {@link StageInstance} + * + * @return The {@link StageInstance} + */ + @Nonnull + StageInstance getStageInstance(); + + /** + * Sets the topic for this stage instance. + *
This shows up in stage discovery and in the stage view. + * + * @param topic + * The topic or null to reset, must be 1-120 characters long + * + * @throws IllegalArgumentException + * If the topic is longer than 120 characters + * + * @return StageInstanceManager for chaining convenience + */ + @Nonnull + @CheckReturnValue + StageInstanceManager setTopic(@Nullable String topic); + + /** + * Sets the {@link net.dv8tion.jda.api.entities.StageInstance.PrivacyLevel PrivacyLevel} for this stage instance. + *
This indicates whether guild lurkers are allowed to join the stage instance or only guild members. + * + * @param level + * The privacy level + * + * @throws IllegalArgumentException + * If the privacy level is null or {@link net.dv8tion.jda.api.entities.StageInstance.PrivacyLevel#UNKNOWN UNKNOWN} + * + * @return StageInstanceManager for chaining convenience + */ + @Nonnull + @CheckReturnValue + StageInstanceManager setPrivacyLevel(@Nonnull StageInstance.PrivacyLevel level); +} diff --git a/src/main/java/net/dv8tion/jda/api/requests/ErrorResponse.java b/src/main/java/net/dv8tion/jda/api/requests/ErrorResponse.java index c772fc86a8..d7ae49f29b 100644 --- a/src/main/java/net/dv8tion/jda/api/requests/ErrorResponse.java +++ b/src/main/java/net/dv8tion/jda/api/requests/ErrorResponse.java @@ -64,6 +64,7 @@ public enum ErrorResponse UNKNOWN_INTERACTION( 10062, "Unknown Interaction"), UNKNOWN_COMMAND( 10063, "Unknown application command"), UNKNOWN_COMMAND_PERMISSIONS( 10066, "Unknown application command permissions"), + UNKNOWN_STAGE_INSTANCE( 10067, "Unknown Stage Instance"), BOTS_NOT_ALLOWED( 20001, "Bots cannot use this endpoint"), ONLY_BOTS_ALLOWED( 20002, "Only bots can use this endpoint"), MAX_GUILDS( 30001, "Maximum number of Guilds reached (100)"), @@ -110,6 +111,7 @@ public enum ErrorResponse MFA_NOT_ENABLED( 60003, "MFA auth required but not enabled"), REACTION_BLOCKED( 90001, "Reaction Blocked"), RESOURCES_OVERLOADED( 130000, "Resource overloaded"), + STAGE_ALREADY_OPEN( 150006, "The Stage is already open"), SERVER_ERROR( 0, "Discord encountered an internal server error! Not good!"); diff --git a/src/main/java/net/dv8tion/jda/api/requests/restaction/GuildAction.java b/src/main/java/net/dv8tion/jda/api/requests/restaction/GuildAction.java index a8b4d4cc1f..4b1f14880b 100644 --- a/src/main/java/net/dv8tion/jda/api/requests/restaction/GuildAction.java +++ b/src/main/java/net/dv8tion/jda/api/requests/restaction/GuildAction.java @@ -546,9 +546,12 @@ class ChannelData implements SerializableData public ChannelData(ChannelType type, String name) { Checks.notBlank(name, "Name"); - Checks.check(type == ChannelType.TEXT || type == ChannelType.VOICE, "Can only create channels of type TEXT or VOICE in GuildAction!"); - Checks.check(name.length() >= 2 && name.length() <= 100, "Channel name has to be between 2-100 characters long!"); - Checks.check(type == ChannelType.VOICE || name.matches("[a-zA-Z0-9-_]+"), "Channels of type TEXT must have a name in alphanumeric with underscores!"); + Checks.check(type == ChannelType.TEXT || type == ChannelType.VOICE || type == ChannelType.STAGE, + "Can only create channels of type TEXT, STAGE, or VOICE in GuildAction!"); + Checks.check(name.length() >= 2 && name.length() <= 100, + "Channel name has to be between 2-100 characters long!"); + Checks.check(type == ChannelType.VOICE || type == ChannelType.STAGE || name.matches("[a-zA-Z0-9-_]+"), + "Channels of type TEXT must have a name in alphanumeric with underscores!"); this.type = type; this.name = name; diff --git a/src/main/java/net/dv8tion/jda/api/requests/restaction/StageInstanceAction.java b/src/main/java/net/dv8tion/jda/api/requests/restaction/StageInstanceAction.java new file mode 100644 index 0000000000..6564149a33 --- /dev/null +++ b/src/main/java/net/dv8tion/jda/api/requests/restaction/StageInstanceAction.java @@ -0,0 +1,78 @@ +/* + * Copyright 2015 Austin Keener, Michael Ritter, Florian Spieß, and the JDA contributors + * + * 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. + */ + +package net.dv8tion.jda.api.requests.restaction; + +import net.dv8tion.jda.api.entities.StageInstance; +import net.dv8tion.jda.api.requests.RestAction; + +import javax.annotation.CheckReturnValue; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.concurrent.TimeUnit; +import java.util.function.BooleanSupplier; + +/** + * Specialized {@link RestAction} used to create a {@link StageInstance} + * + * @see net.dv8tion.jda.api.entities.StageChannel#createStageInstance(String) + */ +public interface StageInstanceAction extends RestAction +{ + @Nonnull + @Override + StageInstanceAction setCheck(@Nullable BooleanSupplier checks); + + @Nonnull + @Override + StageInstanceAction timeout(long timeout, @Nonnull TimeUnit unit); + + @Nonnull + @Override + StageInstanceAction deadline(long timestamp); + + /** + * Sets the topic for the stage instance. + *
This shows up in stage discovery and in the stage view. + * + * @param topic + * The topic, must be 1-120 characters long + * + * @throws IllegalArgumentException + * If the topic is null, empty, or longer than 120 characters + * + * @return The StageInstanceAction for chaining + */ + @Nonnull + @CheckReturnValue + StageInstanceAction setTopic(@Nonnull String topic); + + /** + * Sets the {@link net.dv8tion.jda.api.entities.StageInstance.PrivacyLevel PrivacyLevel} for the stage instance. + *
This indicates whether guild lurkers are allowed to join the stage instance or only guild members. + * + * @param level + * The {@link net.dv8tion.jda.api.entities.StageInstance.PrivacyLevel} + * + * @throws IllegalArgumentException + * If the provided level is null or {@link net.dv8tion.jda.api.entities.StageInstance.PrivacyLevel#UNKNOWN UNKNOWN} + * + * @return The StageInstanceAction for chaining + */ + @Nonnull + @CheckReturnValue + StageInstanceAction setPrivacyLevel(@Nonnull StageInstance.PrivacyLevel level); +} diff --git a/src/main/java/net/dv8tion/jda/internal/entities/CategoryImpl.java b/src/main/java/net/dv8tion/jda/internal/entities/CategoryImpl.java index 5a1a2d0655..7589beca3e 100644 --- a/src/main/java/net/dv8tion/jda/internal/entities/CategoryImpl.java +++ b/src/main/java/net/dv8tion/jda/internal/entities/CategoryImpl.java @@ -171,6 +171,14 @@ public ChannelAction createVoiceChannel(@Nonnull String name) return trySync(action); } + @Nonnull + @Override + public ChannelAction createStageChannel(@Nonnull String name) + { + ChannelAction action = getGuild().createStageChannel(name, this); + return trySync(action); + } + private ChannelAction trySync(ChannelAction action) { Member selfMember = getGuild().getSelfMember(); 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 24d2766adc..1d2f0cbb69 100644 --- a/src/main/java/net/dv8tion/jda/internal/entities/EntityBuilder.java +++ b/src/main/java/net/dv8tion/jda/internal/entities/EntityBuilder.java @@ -279,6 +279,9 @@ public GuildImpl createGuild(long guildId, DataObject guildJson, TLongObjectMap< createGuildEmotePass(guildObj, emotesArray); + guildJson.optArray("stage_instances") + .map(arr -> arr.stream(DataArray::getObject)) + .ifPresent(list -> list.forEach(it -> createStageInstance(guildObj, it))); guildObj.setAfkChannel(guildObj.getVoiceChannelById(afkChannelId)) .setSystemChannel(guildObj.getTextChannelById(systemChannelId)) @@ -296,6 +299,7 @@ private void createGuildChannel(GuildImpl guildObj, DataObject channelData) case TEXT: createTextChannel(guildObj, channelData, guildObj.getIdLong()); break; + case STAGE: case VOICE: createVoiceChannel(guildObj, channelData, guildObj.getIdLong()); break; @@ -536,6 +540,11 @@ private void createVoiceState(GuildImpl guild, DataObject voiceStateJson, User u LOG.error("Received a GuildVoiceState with a channel ID for a non-existent channel! ChannelId: {} GuildId: {} UserId: {}", channelId, guild.getId(), user.getId()); + String requestToSpeak = voiceStateJson.getString("request_to_speak_timestamp", null); + OffsetDateTime timestamp = null; + if (requestToSpeak != null) + timestamp = OffsetDateTime.parse(requestToSpeak); + // VoiceState is considered volatile so we don't expect anything to actually exist voiceState.setSelfMuted(voiceStateJson.getBoolean("self_mute")) .setSelfDeafened(voiceStateJson.getBoolean("self_deaf")) @@ -544,6 +553,7 @@ private void createVoiceState(GuildImpl guild, DataObject voiceStateJson, User u .setSuppressed(voiceStateJson.getBoolean("suppress")) .setSessionId(voiceStateJson.getString("session_id")) .setStream(voiceStateJson.getBoolean("self_stream")) + .setRequestToSpeak(timestamp) .setConnectedChannel(voiceChannel); } @@ -974,7 +984,10 @@ public VoiceChannel createVoiceChannel(GuildImpl guild, DataObject json, long gu UnlockHook vlock = guildVoiceView.writeLock(); UnlockHook jlock = voiceView.writeLock()) { - channel = new VoiceChannelImpl(id, guild); + if (json.getInt("type") == ChannelType.STAGE.getId()) + channel = new StageChannelImpl(id, guild); + else + channel = new VoiceChannelImpl(id, guild); guildVoiceView.getMap().put(id, channel); playbackCache = voiceView.getMap().put(id, channel) == null; } @@ -1034,6 +1047,33 @@ public PrivateChannel createPrivateChannel(DataObject json, UserImpl user) return priv; } + @Nullable + public StageInstance createStageInstance(GuildImpl guild, DataObject json) + { + long channelId = json.getUnsignedLong("channel_id"); + StageChannelImpl channel = (StageChannelImpl) guild.getStageChannelById(channelId); + if (channel == null) + return null; + + long id = json.getUnsignedLong("id"); + String topic = json.getString("topic"); + boolean discoverable = !json.getBoolean("discoverable_disabled"); + StageInstance.PrivacyLevel level = StageInstance.PrivacyLevel.fromKey(json.getInt("privacy_level", -1)); + + + StageInstanceImpl instance = (StageInstanceImpl) channel.getStageInstance(); + if (instance == null) + { + instance = new StageInstanceImpl(id, channel); + channel.setStageInstance(instance); + } + + return instance + .setPrivacyLevel(level) + .setDiscoverable(discoverable) + .setTopic(topic); + } + public void createOverridesPass(AbstractChannelImpl channel, DataArray overrides) { for (int i = 0; i < overrides.length(); i++) diff --git a/src/main/java/net/dv8tion/jda/internal/entities/GuildImpl.java b/src/main/java/net/dv8tion/jda/internal/entities/GuildImpl.java index 7680db110b..adf3217268 100644 --- a/src/main/java/net/dv8tion/jda/internal/entities/GuildImpl.java +++ b/src/main/java/net/dv8tion/jda/internal/entities/GuildImpl.java @@ -84,6 +84,7 @@ public class GuildImpl implements Guild private final CacheView.SimpleCacheView memberPresences; private GuildManager manager; + private CompletableFuture pendingRequestToSpeak; private Member owner; private String name; @@ -886,6 +887,36 @@ public AudioManager getAudioManager() return mng; } + @Nonnull + @Override + public synchronized Task requestToSpeak() + { + if (!isRequestToSpeakPending()) + pendingRequestToSpeak = new CompletableFuture<>(); + + Task task = new GatewayTask<>(pendingRequestToSpeak, this::cancelRequestToSpeak); + updateRequestToSpeak(); + return task; + } + + @Nonnull + @Override + public synchronized Task cancelRequestToSpeak() + { + if (isRequestToSpeakPending()) + { + pendingRequestToSpeak.cancel(false); + pendingRequestToSpeak = null; + } + + VoiceChannel channel = getSelfMember().getVoiceState().getChannel(); + StageInstance instance = channel instanceof StageChannel ? ((StageChannel) channel).getStageInstance() : null; + if (instance == null) + return new GatewayTask<>(CompletableFuture.completedFuture(null), () -> {}); + CompletableFuture future = instance.cancelRequestToSpeak().submit(); + return new GatewayTask<>(future, () -> future.cancel(false)); + } + @Nonnull @Override public JDAImpl getJDA() @@ -1551,7 +1582,6 @@ public ChannelAction createTextChannel(@Nonnull String name, Catego Checks.notBlank(name, "Name"); name = name.trim(); - Checks.notEmpty(name, "Name"); Checks.notLonger(name, 100, "Name"); return new ChannelActionImpl<>(TextChannel.class, name, this, ChannelType.TEXT).setParent(parent); } @@ -1573,11 +1603,31 @@ public ChannelAction createVoiceChannel(@Nonnull String name, Cate Checks.notBlank(name, "Name"); name = name.trim(); - Checks.notEmpty(name, "Name"); Checks.notLonger(name, 100, "Name"); return new ChannelActionImpl<>(VoiceChannel.class, name, this, ChannelType.VOICE).setParent(parent); } + @Nonnull + @Override + public ChannelAction createStageChannel(@Nonnull String name, Category parent) + { + if (parent != null) + { + Checks.check(parent.getGuild().equals(this), "Category is not from the same guild!"); + if (!getSelfMember().hasPermission(parent, Permission.MANAGE_CHANNEL)) + throw new InsufficientPermissionException(parent, Permission.MANAGE_CHANNEL); + } + else + { + checkPermission(Permission.MANAGE_CHANNEL); + } + + Checks.notBlank(name, "Name"); + name = name.trim(); + Checks.notLonger(name, 100, "Name"); + return new ChannelActionImpl<>(StageChannel.class, name, this, ChannelType.STAGE).setParent(parent); + } + @Nonnull @Override public ChannelAction createCategory(@Nonnull String name) @@ -1703,6 +1753,29 @@ private void checkRoles(Collection roles, String type, String preposition) }); } + private synchronized boolean isRequestToSpeakPending() + { + return pendingRequestToSpeak != null && !pendingRequestToSpeak.isDone(); + } + + public synchronized void updateRequestToSpeak() + { + if (!isRequestToSpeakPending()) + return; + VoiceChannel connectedChannel = getSelfMember().getVoiceState().getChannel(); + if (!(connectedChannel instanceof StageChannel)) + return; + StageChannel stage = (StageChannel) connectedChannel; + StageInstance instance = stage.getStageInstance(); + if (instance == null) + return; + + CompletableFuture future = pendingRequestToSpeak; + pendingRequestToSpeak = null; + + instance.requestToSpeak().queue((v) -> future.complete(null), future::completeExceptionally); + } + // ---- Setters ----- public GuildImpl setAvailable(boolean available) diff --git a/src/main/java/net/dv8tion/jda/internal/entities/GuildVoiceStateImpl.java b/src/main/java/net/dv8tion/jda/internal/entities/GuildVoiceStateImpl.java index 1883c894bb..c0936b5299 100644 --- a/src/main/java/net/dv8tion/jda/internal/entities/GuildVoiceStateImpl.java +++ b/src/main/java/net/dv8tion/jda/internal/entities/GuildVoiceStateImpl.java @@ -17,12 +17,18 @@ package net.dv8tion.jda.internal.entities; import net.dv8tion.jda.api.JDA; -import net.dv8tion.jda.api.entities.Guild; -import net.dv8tion.jda.api.entities.GuildVoiceState; -import net.dv8tion.jda.api.entities.Member; -import net.dv8tion.jda.api.entities.VoiceChannel; +import net.dv8tion.jda.api.Permission; +import net.dv8tion.jda.api.entities.*; +import net.dv8tion.jda.api.exceptions.InsufficientPermissionException; +import net.dv8tion.jda.api.requests.RestAction; +import net.dv8tion.jda.api.utils.data.DataObject; +import net.dv8tion.jda.internal.requests.CompletedRestAction; +import net.dv8tion.jda.internal.requests.RestActionImpl; +import net.dv8tion.jda.internal.requests.Route; +import net.dv8tion.jda.internal.utils.Helpers; import javax.annotation.Nonnull; +import java.time.OffsetDateTime; public class GuildVoiceStateImpl implements GuildVoiceState { @@ -32,6 +38,7 @@ public class GuildVoiceStateImpl implements GuildVoiceState private VoiceChannel connectedChannel; private String sessionId; + private long requestToSpeak; private boolean selfMuted = false; private boolean selfDeafened = false; private boolean guildMuted = false; @@ -42,8 +49,7 @@ public class GuildVoiceStateImpl implements GuildVoiceState public GuildVoiceStateImpl(Member member) { this.api = member.getJDA(); - this.guild = member.getGuild(); - this.member = member; + setMember(member); } @Override @@ -71,6 +77,64 @@ public String getSessionId() return sessionId; } + public long getRequestToSpeak() + { + return requestToSpeak; + } + + @Override + public OffsetDateTime getRequestToSpeakTimestamp() + { + return requestToSpeak == 0 ? null : Helpers.toOffset(requestToSpeak); + } + + @Nonnull + @Override + public RestAction approveSpeaker() + { + return update(false); + } + + @Nonnull + @Override + public RestAction declineSpeaker() + { + return update(true); + } + + private RestAction update(boolean suppress) + { + if (requestToSpeak == 0L || !(connectedChannel instanceof StageChannel)) + return new CompletedRestAction<>(api, null); + Member selfMember = getGuild().getSelfMember(); + boolean isSelf = selfMember.equals(member); + if (!isSelf && !selfMember.hasPermission(connectedChannel, Permission.VOICE_MUTE_OTHERS)) + throw new InsufficientPermissionException(connectedChannel, Permission.VOICE_MUTE_OTHERS); + + Route.CompiledRoute route = Route.Guilds.UPDATE_VOICE_STATE.compile(guild.getId(), isSelf ? "@me" : getId()); + DataObject body = DataObject.empty() + .put("channel_id", connectedChannel.getId()) + .put("suppress", suppress); + return new RestActionImpl<>(getJDA(), route, body); + } + + @Nonnull + @Override + public RestAction inviteSpeaker() + { + if (!(connectedChannel instanceof StageChannel)) + return new CompletedRestAction<>(api, null); + if (!getGuild().getSelfMember().hasPermission(connectedChannel, Permission.VOICE_MUTE_OTHERS)) + throw new InsufficientPermissionException(connectedChannel, Permission.VOICE_MUTE_OTHERS); + + Route.CompiledRoute route = Route.Guilds.UPDATE_VOICE_STATE.compile(guild.getId(), getId()); + DataObject body = DataObject.empty() + .put("channel_id", connectedChannel.getId()) + .put("suppress", false) + .put("request_to_speak_timestamp", OffsetDateTime.now().toString()); + return new RestActionImpl<>(getJDA(), route, body); + } + @Override public boolean isMuted() { @@ -139,10 +203,16 @@ public boolean inVoiceChannel() return getChannel() != null; } + @Override + public long getIdLong() + { + return member.getIdLong(); + } + @Override public int hashCode() { - return getMember().hashCode(); + return member.hashCode(); } @Override @@ -153,17 +223,23 @@ public boolean equals(Object obj) if (!(obj instanceof GuildVoiceState)) return false; GuildVoiceState oStatus = (GuildVoiceState) obj; - return this.getMember().equals(oStatus.getMember()); + return member.equals(oStatus.getMember()); } @Override public String toString() { - return "VS:" + getGuild().getName() + ':' + getMember().getEffectiveName(); + return "VS:" + getGuild().getName() + '(' + getId() + ')'; } // -- Setters -- + public GuildVoiceStateImpl setMember(Member member) + { + this.member = member; + return this; + } + public GuildVoiceStateImpl setConnectedChannel(VoiceChannel connectedChannel) { this.connectedChannel = connectedChannel; @@ -211,4 +287,10 @@ public GuildVoiceStateImpl setStream(boolean stream) this.stream = stream; return this; } + + public GuildVoiceStateImpl setRequestToSpeak(OffsetDateTime timestamp) + { + this.requestToSpeak = timestamp == null ? 0L : timestamp.toInstant().toEpochMilli(); + return this; + } } diff --git a/src/main/java/net/dv8tion/jda/internal/entities/MemberImpl.java b/src/main/java/net/dv8tion/jda/internal/entities/MemberImpl.java index b0068a730b..660b4cc265 100644 --- a/src/main/java/net/dv8tion/jda/internal/entities/MemberImpl.java +++ b/src/main/java/net/dv8tion/jda/internal/entities/MemberImpl.java @@ -31,16 +31,13 @@ import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.awt.*; -import java.time.Instant; import java.time.OffsetDateTime; -import java.time.ZoneOffset; import java.util.List; import java.util.*; import java.util.concurrent.ConcurrentHashMap; public class MemberImpl implements Member { - private static final ZoneOffset OFFSET = ZoneOffset.of("+00:00"); private final JDAImpl api; private final Set roles = ConcurrentHashMap.newKeySet(); private final GuildVoiceState voiceState; @@ -100,7 +97,7 @@ public JDA getJDA() public OffsetDateTime getTimeJoined() { if (hasTimeJoined()) - return OffsetDateTime.ofInstant(Instant.ofEpochMilli(joinDate), OFFSET); + return Helpers.toOffset(joinDate); return getGuild().getTimeCreated(); } @@ -114,7 +111,7 @@ public boolean hasTimeJoined() @Override public OffsetDateTime getTimeBoosted() { - return boostDate != 0 ? OffsetDateTime.ofInstant(Instant.ofEpochMilli(boostDate), OFFSET) : null; + return boostDate != 0 ? Helpers.toOffset(boostDate) : null; } @Override diff --git a/src/main/java/net/dv8tion/jda/internal/entities/StageChannelImpl.java b/src/main/java/net/dv8tion/jda/internal/entities/StageChannelImpl.java new file mode 100644 index 0000000000..fa155a5569 --- /dev/null +++ b/src/main/java/net/dv8tion/jda/internal/entities/StageChannelImpl.java @@ -0,0 +1,74 @@ +/* + * Copyright 2015 Austin Keener, Michael Ritter, Florian Spieß, and the JDA contributors + * + * 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. + */ + +package net.dv8tion.jda.internal.entities; + +import net.dv8tion.jda.api.Permission; +import net.dv8tion.jda.api.entities.ChannelType; +import net.dv8tion.jda.api.entities.StageChannel; +import net.dv8tion.jda.api.entities.StageInstance; +import net.dv8tion.jda.api.exceptions.InsufficientPermissionException; +import net.dv8tion.jda.api.requests.restaction.StageInstanceAction; +import net.dv8tion.jda.internal.requests.restaction.StageInstanceActionImpl; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.EnumSet; + +public class StageChannelImpl extends VoiceChannelImpl implements StageChannel +{ + private StageInstance instance; + + public StageChannelImpl(long id, GuildImpl guild) + { + super(id, guild); + } + + @Nonnull + @Override + public ChannelType getType() + { + return ChannelType.STAGE; + } + + @Nullable + @Override + public StageInstance getStageInstance() + { + return instance; + } + + @Nonnull + @Override + public StageInstanceAction createStageInstance(@Nonnull String topic) + { + EnumSet permissions = getGuild().getSelfMember().getPermissions(this); + EnumSet required = EnumSet.of(Permission.MANAGE_CHANNEL, Permission.VOICE_MUTE_OTHERS, Permission.VOICE_MOVE_OTHERS); + for (Permission perm : required) + { + if (!permissions.contains(perm)) + throw new InsufficientPermissionException(this, perm, "You must be a stage moderator to create a stage instance! Missing Permission: " + perm); + } + + return new StageInstanceActionImpl(this).setTopic(topic); + } + + public StageChannelImpl setStageInstance(StageInstance instance) + { + this.instance = instance; + return this; + } +} diff --git a/src/main/java/net/dv8tion/jda/internal/entities/StageInstanceImpl.java b/src/main/java/net/dv8tion/jda/internal/entities/StageInstanceImpl.java new file mode 100644 index 0000000000..dcaceb36a0 --- /dev/null +++ b/src/main/java/net/dv8tion/jda/internal/entities/StageInstanceImpl.java @@ -0,0 +1,175 @@ +/* + * Copyright 2015 Austin Keener, Michael Ritter, Florian Spieß, and the JDA contributors + * + * 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. + */ + +package net.dv8tion.jda.internal.entities; + +import net.dv8tion.jda.api.Permission; +import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.entities.StageChannel; +import net.dv8tion.jda.api.entities.StageInstance; +import net.dv8tion.jda.api.exceptions.InsufficientPermissionException; +import net.dv8tion.jda.api.managers.StageInstanceManager; +import net.dv8tion.jda.api.requests.RestAction; +import net.dv8tion.jda.api.utils.data.DataObject; +import net.dv8tion.jda.internal.managers.StageInstanceManagerImpl; +import net.dv8tion.jda.internal.requests.RestActionImpl; +import net.dv8tion.jda.internal.requests.Route; + +import javax.annotation.Nonnull; +import java.time.OffsetDateTime; +import java.util.EnumSet; + +public class StageInstanceImpl implements StageInstance +{ + private final long id; + private StageChannel channel; + private StageInstanceManager manager; + + private String topic; + private PrivacyLevel privacyLevel; + private boolean discoverable; + + public StageInstanceImpl(long id, StageChannel channel) + { + this.id = id; + this.channel = channel; + } + + @Override + public long getIdLong() + { + return id; + } + + @Nonnull + @Override + public Guild getGuild() + { + return getChannel().getGuild(); + } + + @Nonnull + @Override + public StageChannel getChannel() + { + StageChannel real = channel.getJDA().getStageChannelById(channel.getIdLong()); + if (real != null) + channel = real; + return channel; + } + + @Nonnull + @Override + public String getTopic() + { + return topic; + } + + @Nonnull + @Override + public PrivacyLevel getPrivacyLevel() + { + return privacyLevel; + } + + @Override + public boolean isDiscoverable() + { + return discoverable; + } + + @Nonnull + @Override + public RestAction delete() + { + checkPermissions(); + Route.CompiledRoute route = Route.StageInstances.DELETE_INSTANCE.compile(channel.getId()); + return new RestActionImpl<>(channel.getJDA(), route); + } + + @Nonnull + @Override + public RestAction requestToSpeak() + { + Guild guild = getGuild(); + Route.CompiledRoute route = Route.Guilds.UPDATE_VOICE_STATE.compile(guild.getId(), "@me"); + DataObject body = DataObject.empty().put("channel_id", channel.getId()); + // Stage moderators can bypass the request queue by just unsuppressing + if (guild.getSelfMember().hasPermission(getChannel(), Permission.VOICE_MUTE_OTHERS)) + body.putNull("request_to_speak_timestamp").put("suppress", false); + else + body.put("request_to_speak_timestamp", OffsetDateTime.now().toString()); + + if (!channel.equals(guild.getSelfMember().getVoiceState().getChannel())) + throw new IllegalStateException("Cannot request to speak without being connected to the stage channel!"); + return new RestActionImpl<>(channel.getJDA(), route, body); + } + + @Nonnull + @Override + public RestAction cancelRequestToSpeak() + { + Guild guild = getGuild(); + Route.CompiledRoute route = Route.Guilds.UPDATE_VOICE_STATE.compile(guild.getId(), "@me"); + DataObject body = DataObject.empty() + .putNull("request_to_speak_timestamp") + .put("suppress", true) + .put("channel_id", channel.getId()); + + if (!channel.equals(guild.getSelfMember().getVoiceState().getChannel())) + throw new IllegalStateException("Cannot cancel request to speak without being connected to the stage channel!"); + return new RestActionImpl<>(channel.getJDA(), route, body); + } + + @Nonnull + @Override + public StageInstanceManager getManager() + { + checkPermissions(); + if (manager == null) + manager = new StageInstanceManagerImpl(this); + return manager; + } + + public StageInstanceImpl setTopic(String topic) + { + this.topic = topic; + return this; + } + + public StageInstanceImpl setPrivacyLevel(PrivacyLevel privacyLevel) + { + this.privacyLevel = privacyLevel; + return this; + } + + public StageInstanceImpl setDiscoverable(boolean discoverable) + { + this.discoverable = discoverable; + return this; + } + + private void checkPermissions() + { + EnumSet permissions = getGuild().getSelfMember().getPermissions(getChannel()); + EnumSet required = EnumSet.of(Permission.MANAGE_CHANNEL, Permission.VOICE_MUTE_OTHERS, Permission.VOICE_MOVE_OTHERS); + for (Permission perm : required) + { + if (!permissions.contains(perm)) + throw new InsufficientPermissionException(getChannel(), perm, "You must be a stage moderator to manage a stage instance! Missing Permission: " + perm); + } + } +} diff --git a/src/main/java/net/dv8tion/jda/internal/handle/ChannelCreateHandler.java b/src/main/java/net/dv8tion/jda/internal/handle/ChannelCreateHandler.java index 1a8cb2a00f..76d19abe65 100644 --- a/src/main/java/net/dv8tion/jda/internal/handle/ChannelCreateHandler.java +++ b/src/main/java/net/dv8tion/jda/internal/handle/ChannelCreateHandler.java @@ -67,6 +67,7 @@ protected Long handleInternally(DataObject content) builder.createTextChannel(content, guildId))); break; } + case STAGE: case VOICE: { jda.handleEvent( diff --git a/src/main/java/net/dv8tion/jda/internal/handle/ChannelDeleteHandler.java b/src/main/java/net/dv8tion/jda/internal/handle/ChannelDeleteHandler.java index ddfa50052b..c361c51af6 100644 --- a/src/main/java/net/dv8tion/jda/internal/handle/ChannelDeleteHandler.java +++ b/src/main/java/net/dv8tion/jda/internal/handle/ChannelDeleteHandler.java @@ -84,6 +84,7 @@ protected Long handleInternally(DataObject content) channel)); break; } + case STAGE: case VOICE: { VoiceChannel channel = getJDA().getVoiceChannelsView().remove(channelId); diff --git a/src/main/java/net/dv8tion/jda/internal/handle/ChannelUpdateHandler.java b/src/main/java/net/dv8tion/jda/internal/handle/ChannelUpdateHandler.java index aca2646285..f195669177 100644 --- a/src/main/java/net/dv8tion/jda/internal/handle/ChannelUpdateHandler.java +++ b/src/main/java/net/dv8tion/jda/internal/handle/ChannelUpdateHandler.java @@ -183,6 +183,7 @@ protected Long handleInternally(DataObject content) applyPermissions(textChannel, permOverwrites); break; //Finish the TextChannelUpdate case } + case STAGE: case VOICE: { VoiceChannelImpl voiceChannel = (VoiceChannelImpl) getJDA().getVoiceChannelsView().get(channelId); @@ -333,6 +334,7 @@ private void applyPermissions(AbstractChannelImpl channel, DataArray permOv api, responseNumber, (StoreChannel) channel, changed)); break; + case STAGE: case VOICE: api.handleEvent( new VoiceChannelUpdatePermissionsEvent( diff --git a/src/main/java/net/dv8tion/jda/internal/handle/StageInstanceCreateHandler.java b/src/main/java/net/dv8tion/jda/internal/handle/StageInstanceCreateHandler.java new file mode 100644 index 0000000000..4424cd6bf3 --- /dev/null +++ b/src/main/java/net/dv8tion/jda/internal/handle/StageInstanceCreateHandler.java @@ -0,0 +1,55 @@ +/* + * Copyright 2015 Austin Keener, Michael Ritter, Florian Spieß, and the JDA contributors + * + * 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. + */ + +package net.dv8tion.jda.internal.handle; + +import net.dv8tion.jda.api.entities.StageInstance; +import net.dv8tion.jda.api.events.stage.StageInstanceCreateEvent; +import net.dv8tion.jda.api.utils.data.DataObject; +import net.dv8tion.jda.internal.JDAImpl; +import net.dv8tion.jda.internal.entities.GuildImpl; + +public class StageInstanceCreateHandler extends SocketHandler +{ + public StageInstanceCreateHandler(JDAImpl api) + { + super(api); + } + + @Override + protected Long handleInternally(DataObject content) + { + long guildId = content.getUnsignedLong("guild_id", 0L); + if (getJDA().getGuildSetupController().isLocked(guildId)) + return guildId; + + GuildImpl guild = (GuildImpl) getJDA().getGuildById(guildId); + if (guild == null) + { + EventCache.LOG.debug("Caching STAGE_INSTANCE_CREATE for uncached guild with id {}", guildId); + getJDA().getEventCache().cache(EventCache.Type.GUILD, guildId, responseNumber, allContent, this::handle); + return null; + } + + StageInstance instance = getJDA().getEntityBuilder().createStageInstance(guild, content); + if (instance != null) + { + getJDA().handleEvent(new StageInstanceCreateEvent(getJDA(), responseNumber, instance)); + guild.updateRequestToSpeak(); + } + return null; + } +} diff --git a/src/main/java/net/dv8tion/jda/internal/handle/StageInstanceDeleteHandler.java b/src/main/java/net/dv8tion/jda/internal/handle/StageInstanceDeleteHandler.java new file mode 100644 index 0000000000..3fbbe98f0c --- /dev/null +++ b/src/main/java/net/dv8tion/jda/internal/handle/StageInstanceDeleteHandler.java @@ -0,0 +1,58 @@ +/* + * Copyright 2015 Austin Keener, Michael Ritter, Florian Spieß, and the JDA contributors + * + * 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. + */ + +package net.dv8tion.jda.internal.handle; + +import net.dv8tion.jda.api.entities.StageInstance; +import net.dv8tion.jda.api.events.stage.StageInstanceDeleteEvent; +import net.dv8tion.jda.api.utils.data.DataObject; +import net.dv8tion.jda.internal.JDAImpl; +import net.dv8tion.jda.internal.entities.GuildImpl; +import net.dv8tion.jda.internal.entities.StageChannelImpl; + +public class StageInstanceDeleteHandler extends SocketHandler +{ + public StageInstanceDeleteHandler(JDAImpl api) + { + super(api); + } + + @Override + protected Long handleInternally(DataObject content) + { + long guildId = content.getUnsignedLong("guild_id", 0L); + if (getJDA().getGuildSetupController().isLocked(guildId)) + return guildId; + + GuildImpl guild = (GuildImpl) getJDA().getGuildById(guildId); + if (guild == null) + { + EventCache.LOG.debug("Caching STAGE_INSTANCE_DELETE for uncached guild with id {}", guildId); + getJDA().getEventCache().cache(EventCache.Type.GUILD, guildId, responseNumber, allContent, this::handle); + return null; + } + + long channelId = content.getUnsignedLong("channel_id", 0L); + StageChannelImpl channel = (StageChannelImpl) guild.getStageChannelById(channelId); + if (channel == null) + return null; + StageInstance instance = channel.getStageInstance(); + channel.setStageInstance(null); + if (instance != null) + getJDA().handleEvent(new StageInstanceDeleteEvent(getJDA(), responseNumber, instance)); + return null; + } +} diff --git a/src/main/java/net/dv8tion/jda/internal/handle/StageInstanceUpdateHandler.java b/src/main/java/net/dv8tion/jda/internal/handle/StageInstanceUpdateHandler.java new file mode 100644 index 0000000000..5113467b6e --- /dev/null +++ b/src/main/java/net/dv8tion/jda/internal/handle/StageInstanceUpdateHandler.java @@ -0,0 +1,70 @@ +/* + * Copyright 2015 Austin Keener, Michael Ritter, Florian Spieß, and the JDA contributors + * + * 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. + */ + +package net.dv8tion.jda.internal.handle; + +import net.dv8tion.jda.api.entities.StageChannel; +import net.dv8tion.jda.api.entities.StageInstance; +import net.dv8tion.jda.api.events.stage.update.StageInstanceUpdatePrivacyLevelEvent; +import net.dv8tion.jda.api.events.stage.update.StageInstanceUpdateTopicEvent; +import net.dv8tion.jda.api.utils.data.DataObject; +import net.dv8tion.jda.internal.JDAImpl; +import net.dv8tion.jda.internal.entities.GuildImpl; + +import java.util.Objects; + +public class StageInstanceUpdateHandler extends SocketHandler +{ + public StageInstanceUpdateHandler(JDAImpl api) + { + super(api); + } + + @Override + protected Long handleInternally(DataObject content) + { + long guildId = content.getUnsignedLong("guild_id", 0L); + if (getJDA().getGuildSetupController().isLocked(guildId)) + return guildId; + + GuildImpl guild = (GuildImpl) getJDA().getGuildById(guildId); + if (guild == null) + { + EventCache.LOG.debug("Caching STAGE_INSTANCE_UPDATE for uncached guild with id {}", guildId); + getJDA().getEventCache().cache(EventCache.Type.GUILD, guildId, responseNumber, allContent, this::handle); + return null; + } + + StageChannel channel = getJDA().getStageChannelById(content.getUnsignedLong("channel_id")); + if (channel == null) + return null; + StageInstance oldInstance = channel.getStageInstance(); + if (oldInstance == null) + return null; + + String oldTopic = oldInstance.getTopic(); + StageInstance.PrivacyLevel oldLevel = oldInstance.getPrivacyLevel(); + StageInstance newInstance = getJDA().getEntityBuilder().createStageInstance(guild, content); + if (newInstance == null) + return null; + + if (!Objects.equals(oldTopic, newInstance.getTopic())) + getJDA().handleEvent(new StageInstanceUpdateTopicEvent(getJDA(), responseNumber, newInstance, oldTopic)); + if (oldLevel != newInstance.getPrivacyLevel()) + getJDA().handleEvent(new StageInstanceUpdatePrivacyLevelEvent(getJDA(), responseNumber, newInstance, oldLevel)); + return null; + } +} diff --git a/src/main/java/net/dv8tion/jda/internal/handle/VoiceStateUpdateHandler.java b/src/main/java/net/dv8tion/jda/internal/handle/VoiceStateUpdateHandler.java index 3c1a65e908..b0b12b157f 100644 --- a/src/main/java/net/dv8tion/jda/internal/handle/VoiceStateUpdateHandler.java +++ b/src/main/java/net/dv8tion/jda/internal/handle/VoiceStateUpdateHandler.java @@ -26,7 +26,9 @@ import net.dv8tion.jda.internal.entities.MemberImpl; import net.dv8tion.jda.internal.entities.VoiceChannelImpl; import net.dv8tion.jda.internal.managers.AudioManagerImpl; +import net.dv8tion.jda.internal.requests.WebSocketClient; +import java.time.OffsetDateTime; import java.util.Objects; public class VoiceStateUpdateHandler extends SocketHandler @@ -44,6 +46,14 @@ protected Long handleInternally(DataObject content) return null; //unhandled for calls if (getJDA().getGuildSetupController().isLocked(guildId)) return guildId; + + // TODO: Handle these voice states properly + if (content.isNull("member")) + { + WebSocketClient.LOG.debug("Discarding VOICE_STATE_UPDATE with missing member. JSON: {}", content); + return null; + } + handleGuildVoiceState(content); return null; } @@ -60,6 +70,14 @@ private void handleGuildVoiceState(DataObject content) boolean guildDeafened = content.getBoolean("deaf"); boolean suppressed = content.getBoolean("suppress"); boolean stream = content.getBoolean("self_stream"); + String requestToSpeak = content.getString("request_to_speak_timestamp", null); + OffsetDateTime requestToSpeakTime = null; + long requestToSpeakTimestamp = 0L; + if (requestToSpeak != null) + { + requestToSpeakTime = OffsetDateTime.parse(requestToSpeak); + requestToSpeakTimestamp = requestToSpeakTime.toInstant().toEpochMilli(); + } Guild guild = getJDA().getGuildById(guildId); if (guild == null) @@ -132,6 +150,12 @@ private void handleGuildVoiceState(DataObject content) getJDA().handleEvent(new GuildVoiceMuteEvent(getJDA(), responseNumber, member)); if (wasDeaf != vState.isDeafened()) getJDA().handleEvent(new GuildVoiceDeafenEvent(getJDA(), responseNumber, member)); + if (requestToSpeakTimestamp != vState.getRequestToSpeak()) + { + OffsetDateTime oldRequestToSpeak = vState.getRequestToSpeakTimestamp(); + vState.setRequestToSpeak(requestToSpeakTime); + getJDA().handleEvent(new GuildVoiceRequestToSpeakEvent(getJDA(), responseNumber, member, oldRequestToSpeak, requestToSpeakTime)); + } if (!Objects.equals(channel, vState.getChannel())) { @@ -152,7 +176,7 @@ else if (channel == null) oldChannel.getConnectedMembersMap().remove(userId); if (isSelf) getJDA().getDirectAudioController().update(guild, null); - getJDA().getEntityBuilder().updateMemberCache(member); + getJDA().getEntityBuilder().updateMemberCache(member, memberJson.isNull("joined_at")); getJDA().handleEvent( new GuildVoiceLeaveEvent( getJDA(), responseNumber, @@ -193,5 +217,7 @@ else if (channel == null) if (voiceInterceptor.onVoiceStateUpdate(new VoiceDispatchInterceptor.VoiceStateUpdate(channel, vState, allContent))) getJDA().getDirectAudioController().update(guild, channel); } + + ((GuildImpl) guild).updateRequestToSpeak(); } } diff --git a/src/main/java/net/dv8tion/jda/internal/managers/ChannelManagerImpl.java b/src/main/java/net/dv8tion/jda/internal/managers/ChannelManagerImpl.java index 18c2622432..620e5468f4 100644 --- a/src/main/java/net/dv8tion/jda/internal/managers/ChannelManagerImpl.java +++ b/src/main/java/net/dv8tion/jda/internal/managers/ChannelManagerImpl.java @@ -297,7 +297,7 @@ public ChannelManagerImpl setName(@Nonnull String name) public ChannelManagerImpl setRegion(@Nonnull Region region) { Checks.notNull(region, "Region"); - if (getType() != ChannelType.VOICE) + if (!getType().isAudio()) throw new IllegalStateException("Can only change region on voice channels!"); Checks.check(Region.VOICE_CHANNEL_REGIONS.contains(region), "Region is not usable for VoiceChannel region overrides!"); this.region = region == Region.AUTOMATIC ? null : region.getKey(); @@ -389,7 +389,7 @@ public ChannelManagerImpl setUserLimit(int userLimit) @CheckReturnValue public ChannelManagerImpl setBitrate(int bitrate) { - if (getType() != ChannelType.VOICE) + if (!getType().isAudio()) throw new IllegalStateException("Can only set bitrate on voice channels"); final int maxBitrate = getGuild().getMaxBitrate(); Checks.check(bitrate >= 8000, "Bitrate must be greater or equal to 8000"); diff --git a/src/main/java/net/dv8tion/jda/internal/managers/StageInstanceManagerImpl.java b/src/main/java/net/dv8tion/jda/internal/managers/StageInstanceManagerImpl.java new file mode 100644 index 0000000000..f04dd60564 --- /dev/null +++ b/src/main/java/net/dv8tion/jda/internal/managers/StageInstanceManagerImpl.java @@ -0,0 +1,86 @@ +/* + * Copyright 2015 Austin Keener, Michael Ritter, Florian Spieß, and the JDA contributors + * + * 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. + */ + +package net.dv8tion.jda.internal.managers; + +import net.dv8tion.jda.api.entities.StageInstance; +import net.dv8tion.jda.api.managers.StageInstanceManager; +import net.dv8tion.jda.api.utils.data.DataObject; +import net.dv8tion.jda.internal.requests.Route; +import net.dv8tion.jda.internal.utils.Checks; +import okhttp3.RequestBody; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +public class StageInstanceManagerImpl extends ManagerBase implements StageInstanceManager +{ + private final StageInstance instance; + + private String topic; + private StageInstance.PrivacyLevel privacyLevel; + + public StageInstanceManagerImpl(StageInstance instance) + { + super(instance.getChannel().getJDA(), Route.StageInstances.UPDATE_INSTANCE.compile(instance.getChannel().getId())); + this.instance = instance; + } + + @Nonnull + @Override + public StageInstance getStageInstance() + { + return instance; + } + + @Nonnull + @Override + public StageInstanceManager setTopic(@Nullable String topic) + { + if (topic != null) + { + topic = topic.trim(); + Checks.notLonger(topic, 120, "Topic"); + if (topic.isEmpty()) + topic = null; + } + this.topic = topic; + set |= TOPIC; + return this; + } + + @Nonnull + @Override + public StageInstanceManager setPrivacyLevel(@Nonnull StageInstance.PrivacyLevel level) + { + Checks.notNull(level, "PrivacyLevel"); + Checks.check(level != StageInstance.PrivacyLevel.UNKNOWN, "PrivacyLevel must not be UNKNOWN!"); + this.privacyLevel = level; + set |= PRIVACY_LEVEL; + return this; + } + + @Override + protected RequestBody finalizeData() + { + DataObject body = DataObject.empty(); + if (shouldUpdate(TOPIC) && topic != null) + body.put("topic", topic); + if (shouldUpdate(PRIVACY_LEVEL)) + body.put("privacy_level", privacyLevel.getKey()); + return getRequestBody(body); + } +} 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 2448e8a9aa..6680ff9a2e 100644 --- a/src/main/java/net/dv8tion/jda/internal/requests/Route.java +++ b/src/main/java/net/dv8tion/jda/internal/requests/Route.java @@ -145,6 +145,7 @@ public static class Guilds public static final Route GET_GUILD_EMOTES = new Route(GET, "guilds/{guild_id}/emojis"); public static final Route GET_AUDIT_LOGS = new Route(GET, "guilds/{guild_id}/audit-logs"); public static final Route GET_VOICE_REGIONS = new Route(GET, "guilds/{guild_id}/regions"); + public static final Route UPDATE_VOICE_STATE = new Route(PATCH, "guilds/{guild_id}/voice-states/{user_id}"); public static final Route GET_INTEGRATIONS = new Route(GET, "guilds/{guild_id}/integrations"); public static final Route CREATE_INTEGRATION = new Route(POST, "guilds/{guild_id}/integrations"); @@ -225,6 +226,14 @@ public static class Channels public static final Route STOP_CALL = new Route(POST, "channels/{channel_id}/call/stop_ringing"); // aka deny or end call } + public static class StageInstances + { + public static final Route GET_INSTANCE = new Route(GET, "stage-instances/{channel_id}"); + public static final Route DELETE_INSTANCE = new Route(DELETE, "stage-instances/{channel_id}"); + public static final Route UPDATE_INSTANCE = new Route(PATCH, "stage-instances/{channel_id}"); + public static final Route CREATE_INSTANCE = new Route(POST, "stage-instances"); + } + public static class Messages { public static final Route EDIT_MESSAGE = new Route(PATCH, "channels/{channel_id}/messages/{message_id}"); // requires special handling, same bucket but different endpoints diff --git a/src/main/java/net/dv8tion/jda/internal/requests/WebSocketClient.java b/src/main/java/net/dv8tion/jda/internal/requests/WebSocketClient.java index 94db588cd1..da68ce1f09 100644 --- a/src/main/java/net/dv8tion/jda/internal/requests/WebSocketClient.java +++ b/src/main/java/net/dv8tion/jda/internal/requests/WebSocketClient.java @@ -1348,6 +1348,9 @@ protected void setupHandlers() handlers.put("MESSAGE_REACTION_REMOVE_EMOTE", new MessageReactionClearEmoteHandler(api)); handlers.put("MESSAGE_UPDATE", new MessageUpdateHandler(api)); handlers.put("READY", new ReadyHandler(api)); + handlers.put("STAGE_INSTANCE_CREATE", new StageInstanceCreateHandler(api)); + handlers.put("STAGE_INSTANCE_DELETE", new StageInstanceDeleteHandler(api)); + handlers.put("STAGE_INSTANCE_UPDATE", new StageInstanceUpdateHandler(api)); handlers.put("USER_UPDATE", new UserUpdateHandler(api)); handlers.put("VOICE_SERVER_UPDATE", new VoiceServerUpdateHandler(api)); handlers.put("VOICE_STATE_UPDATE", new VoiceStateUpdateHandler(api)); diff --git a/src/main/java/net/dv8tion/jda/internal/requests/restaction/ChannelActionImpl.java b/src/main/java/net/dv8tion/jda/internal/requests/restaction/ChannelActionImpl.java index faa264435a..f954db5f99 100644 --- a/src/main/java/net/dv8tion/jda/internal/requests/restaction/ChannelActionImpl.java +++ b/src/main/java/net/dv8tion/jda/internal/requests/restaction/ChannelActionImpl.java @@ -282,8 +282,8 @@ private ChannelActionImpl addOverride(long targetId, int type, long allow, lo @CheckReturnValue public ChannelActionImpl setBitrate(Integer bitrate) { - if (type != ChannelType.VOICE) - throw new UnsupportedOperationException("Can only set the bitrate for a VoiceChannel!"); + if (!type.isAudio()) + throw new UnsupportedOperationException("Can only set the bitrate for an Audio Channel!"); if (bitrate != null) { int maxBitrate = getGuild().getMaxBitrate(); @@ -328,14 +328,17 @@ protected RequestBody finalizeData() object.put("user_limit", userlimit); break; case TEXT: - if (topic != null && !topic.isEmpty()) - object.put("topic", topic); if (nsfw != null) object.put("nsfw", nsfw); if (slowmode != null) object.put("rate_limit_per_user", slowmode); if (news != null) object.put("type", news ? 5 : 0); + break; + case STAGE: + if (bitrate != null) + object.put("bitrate", bitrate); + break; } if (type != ChannelType.CATEGORY && parent != null) object.put("parent_id", parent.getId()); @@ -350,6 +353,7 @@ protected void handleSuccess(Response response, Request request) GuildChannel channel; switch (type) { + case STAGE: case VOICE: channel = builder.createVoiceChannel(response.getObject(), guild.getIdLong()); break; diff --git a/src/main/java/net/dv8tion/jda/internal/requests/restaction/StageInstanceActionImpl.java b/src/main/java/net/dv8tion/jda/internal/requests/restaction/StageInstanceActionImpl.java new file mode 100644 index 0000000000..f74453e7fb --- /dev/null +++ b/src/main/java/net/dv8tion/jda/internal/requests/restaction/StageInstanceActionImpl.java @@ -0,0 +1,104 @@ +/* + * Copyright 2015 Austin Keener, Michael Ritter, Florian Spieß, and the JDA contributors + * + * 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. + */ + +package net.dv8tion.jda.internal.requests.restaction; + +import net.dv8tion.jda.api.entities.StageChannel; +import net.dv8tion.jda.api.entities.StageInstance; +import net.dv8tion.jda.api.requests.Request; +import net.dv8tion.jda.api.requests.Response; +import net.dv8tion.jda.api.requests.restaction.StageInstanceAction; +import net.dv8tion.jda.api.utils.data.DataObject; +import net.dv8tion.jda.internal.entities.GuildImpl; +import net.dv8tion.jda.internal.requests.RestActionImpl; +import net.dv8tion.jda.internal.requests.Route; +import net.dv8tion.jda.internal.utils.Checks; +import okhttp3.RequestBody; + +import javax.annotation.Nonnull; +import java.util.concurrent.TimeUnit; +import java.util.function.BooleanSupplier; + +public class StageInstanceActionImpl extends RestActionImpl implements StageInstanceAction +{ + private final StageChannel channel; + private String topic; + private StageInstance.PrivacyLevel level = StageInstance.PrivacyLevel.GUILD_ONLY; + + public StageInstanceActionImpl(StageChannel channel) + { + super(channel.getJDA(), Route.StageInstances.CREATE_INSTANCE.compile()); + this.channel = channel; + } + + @Nonnull + @Override + public StageInstanceAction setCheck(BooleanSupplier checks) + { + return (StageInstanceAction) super.setCheck(checks); + } + + @Nonnull + @Override + public StageInstanceAction timeout(long timeout, @Nonnull TimeUnit unit) + { + return (StageInstanceAction) super.timeout(timeout, unit); + } + + @Nonnull + @Override + public StageInstanceAction deadline(long timestamp) + { + return (StageInstanceAction) super.deadline(timestamp); + } + + @Nonnull + @Override + public StageInstanceAction setTopic(@Nonnull String topic) + { + Checks.notEmpty(topic, "Topic"); + Checks.notLonger(topic, 120, "Topic"); + this.topic = topic; + return this; + } + + @Nonnull + @Override + public StageInstanceAction setPrivacyLevel(@Nonnull StageInstance.PrivacyLevel level) + { + Checks.notNull(level, "PrivacyLevel"); + Checks.check(level != StageInstance.PrivacyLevel.UNKNOWN, "The PrivacyLevel must not be UNKNOWN!"); + this.level = level; + return this; + } + + @Override + protected RequestBody finalizeData() + { + DataObject body = DataObject.empty(); + body.put("channel_id", channel.getId()); + body.put("topic", topic); + body.put("privacy_level", level.getKey()); + return getRequestBody(body); + } + + @Override + protected void handleSuccess(Response response, Request request) + { + StageInstance instance = api.getEntityBuilder().createStageInstance((GuildImpl) channel.getGuild(), response.getObject()); + request.onSuccess(instance); + } +} diff --git a/src/main/java/net/dv8tion/jda/internal/utils/Helpers.java b/src/main/java/net/dv8tion/jda/internal/utils/Helpers.java index bc6dd3b766..727b1af520 100644 --- a/src/main/java/net/dv8tion/jda/internal/utils/Helpers.java +++ b/src/main/java/net/dv8tion/jda/internal/utils/Helpers.java @@ -16,6 +16,9 @@ package net.dv8tion.jda.internal.utils; +import java.time.Instant; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; import java.util.*; import java.util.function.Consumer; @@ -26,6 +29,7 @@ */ public final class Helpers { + private static final ZoneOffset OFFSET = ZoneOffset.of("+00:00"); @SuppressWarnings("rawtypes") private static final Consumer EMPTY_CONSUMER = (v) -> {}; @@ -35,6 +39,11 @@ public static Consumer emptyConsumer() return (Consumer) EMPTY_CONSUMER; } + public static OffsetDateTime toOffset(long instant) + { + return OffsetDateTime.ofInstant(Instant.ofEpochMilli(instant), OFFSET); + } + // locale-safe String#format public static String format(String format, Object... args) diff --git a/src/main/java/net/dv8tion/jda/internal/utils/PermissionUtil.java b/src/main/java/net/dv8tion/jda/internal/utils/PermissionUtil.java index 4591060b2a..85ac63949f 100644 --- a/src/main/java/net/dv8tion/jda/internal/utils/PermissionUtil.java +++ b/src/main/java/net/dv8tion/jda/internal/utils/PermissionUtil.java @@ -368,7 +368,7 @@ public static long getEffectivePermission(GuildChannel channel, Member member) //When the permission to view the channel or to connect to the channel is not applied it is not granted // This means that we have no access to this channel at all - final boolean hasConnect = channel.getType() != ChannelType.VOICE || isApplied(permission, connectChannel); + final boolean hasConnect = (channel.getType() != ChannelType.VOICE && channel.getType() != ChannelType.STAGE) || isApplied(permission, connectChannel); final boolean hasView = isApplied(permission, viewChannel); return hasView && hasConnect ? permission : 0; }