All Downloads are FREE. Search and download functionalities are using the official Maven repository.

net.dv8tion.jda.internal.entities.GuildImpl Maven / Gradle / Ivy

Go to download

Java wrapper for the popular chat & VOIP service: Discord https://discord.com

There is a newer version: 5.1.0
Show newest version
/*
 * 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 gnu.trove.map.TLongObjectMap;
import gnu.trove.map.hash.TLongObjectHashMap;
import gnu.trove.set.TLongSet;
import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.Region;
import net.dv8tion.jda.api.audio.hooks.ConnectionStatus;
import net.dv8tion.jda.api.entities.*;
import net.dv8tion.jda.api.entities.automod.AutoModRule;
import net.dv8tion.jda.api.entities.automod.build.AutoModRuleData;
import net.dv8tion.jda.api.entities.channel.Channel;
import net.dv8tion.jda.api.entities.channel.ChannelType;
import net.dv8tion.jda.api.entities.channel.attribute.ICategorizableChannel;
import net.dv8tion.jda.api.entities.channel.attribute.IThreadContainer;
import net.dv8tion.jda.api.entities.channel.concrete.*;
import net.dv8tion.jda.api.entities.channel.middleman.AudioChannel;
import net.dv8tion.jda.api.entities.channel.middleman.GuildChannel;
import net.dv8tion.jda.api.entities.channel.unions.DefaultGuildChannelUnion;
import net.dv8tion.jda.api.entities.emoji.RichCustomEmoji;
import net.dv8tion.jda.api.entities.sticker.GuildSticker;
import net.dv8tion.jda.api.entities.sticker.StandardSticker;
import net.dv8tion.jda.api.entities.sticker.StickerSnowflake;
import net.dv8tion.jda.api.entities.templates.Template;
import net.dv8tion.jda.api.exceptions.HierarchyException;
import net.dv8tion.jda.api.exceptions.InsufficientPermissionException;
import net.dv8tion.jda.api.exceptions.ParsingException;
import net.dv8tion.jda.api.exceptions.PermissionException;
import net.dv8tion.jda.api.interactions.DiscordLocale;
import net.dv8tion.jda.api.interactions.commands.Command;
import net.dv8tion.jda.api.interactions.commands.PrivilegeConfig;
import net.dv8tion.jda.api.interactions.commands.build.CommandData;
import net.dv8tion.jda.api.interactions.commands.privileges.IntegrationPrivilege;
import net.dv8tion.jda.api.managers.*;
import net.dv8tion.jda.api.requests.GatewayIntent;
import net.dv8tion.jda.api.requests.RestAction;
import net.dv8tion.jda.api.requests.Route;
import net.dv8tion.jda.api.requests.restaction.*;
import net.dv8tion.jda.api.requests.restaction.order.CategoryOrderAction;
import net.dv8tion.jda.api.requests.restaction.order.ChannelOrderAction;
import net.dv8tion.jda.api.requests.restaction.order.RoleOrderAction;
import net.dv8tion.jda.api.requests.restaction.pagination.AuditLogPaginationAction;
import net.dv8tion.jda.api.utils.FileUpload;
import net.dv8tion.jda.api.utils.cache.*;
import net.dv8tion.jda.api.utils.concurrent.Task;
import net.dv8tion.jda.api.utils.data.DataArray;
import net.dv8tion.jda.api.utils.data.DataObject;
import net.dv8tion.jda.internal.JDAImpl;
import net.dv8tion.jda.internal.entities.automod.AutoModRuleImpl;
import net.dv8tion.jda.internal.handle.EventCache;
import net.dv8tion.jda.internal.interactions.CommandDataImpl;
import net.dv8tion.jda.internal.interactions.command.CommandImpl;
import net.dv8tion.jda.internal.managers.*;
import net.dv8tion.jda.internal.requests.*;
import net.dv8tion.jda.internal.requests.restaction.*;
import net.dv8tion.jda.internal.requests.restaction.order.CategoryOrderActionImpl;
import net.dv8tion.jda.internal.requests.restaction.order.ChannelOrderActionImpl;
import net.dv8tion.jda.internal.requests.restaction.order.RoleOrderActionImpl;
import net.dv8tion.jda.internal.requests.restaction.pagination.AuditLogPaginationActionImpl;
import net.dv8tion.jda.internal.requests.restaction.pagination.BanPaginationActionImpl;
import net.dv8tion.jda.internal.utils.Checks;
import net.dv8tion.jda.internal.utils.EntityString;
import net.dv8tion.jda.internal.utils.Helpers;
import net.dv8tion.jda.internal.utils.UnlockHook;
import net.dv8tion.jda.internal.utils.cache.*;
import net.dv8tion.jda.internal.utils.concurrent.task.GatewayTask;
import okhttp3.MediaType;
import okhttp3.MultipartBody;

import javax.annotation.CheckReturnValue;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.time.Duration;
import java.time.OffsetDateTime;
import java.time.temporal.TemporalAccessor;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class GuildImpl implements Guild
{
    private final long id;
    private final JDAImpl api;

    private final SortedSnowflakeCacheViewImpl scheduledEventCache = new SortedSnowflakeCacheViewImpl<>(ScheduledEvent.class, ScheduledEvent::getName, Comparator.naturalOrder());
    private final SortedChannelCacheViewImpl channelCache = new SortedChannelCacheViewImpl<>(GuildChannel.class);
    private final SortedSnowflakeCacheViewImpl roleCache = new SortedSnowflakeCacheViewImpl<>(Role.class, Role::getName, Comparator.reverseOrder());
    private final SnowflakeCacheViewImpl emojicache = new SnowflakeCacheViewImpl<>(RichCustomEmoji.class, RichCustomEmoji::getName);
    private final SnowflakeCacheViewImpl stickerCache = new SnowflakeCacheViewImpl<>(GuildSticker.class, GuildSticker::getName);
    private final MemberCacheViewImpl memberCache = new MemberCacheViewImpl();
    private final CacheView.SimpleCacheView memberPresences;

    private CompletableFuture pendingRequestToSpeak;

    private Member owner;
    private String name;
    private String iconId, splashId;
    private String vanityCode;
    private String description, banner;
    private int maxPresences, maxMembers;
    private int boostCount;
    private long ownerId;
    private Set features;
    private VoiceChannel afkChannel;
    private TextChannel systemChannel;
    private TextChannel rulesChannel;
    private TextChannel communityUpdatesChannel;
    private Role publicRole;
    private VerificationLevel verificationLevel = VerificationLevel.UNKNOWN;
    private NotificationLevel defaultNotificationLevel = NotificationLevel.UNKNOWN;
    private MFALevel mfaLevel = MFALevel.UNKNOWN;
    private ExplicitContentLevel explicitContentLevel = ExplicitContentLevel.UNKNOWN;
    private NSFWLevel nsfwLevel = NSFWLevel.UNKNOWN;
    private Timeout afkTimeout;
    private BoostTier boostTier = BoostTier.NONE;
    private DiscordLocale preferredLocale = DiscordLocale.ENGLISH_US;
    private int memberCount;
    private boolean boostProgressBarEnabled;

    public GuildImpl(JDAImpl api, long id)
    {
        this.id = id;
        this.api = api;
        if (api.getCacheFlags().stream().anyMatch(CacheFlag::isPresence))
            memberPresences = new CacheView.SimpleCacheView<>(MemberPresenceImpl.class, null);
        else
            memberPresences = null;
    }

    public void invalidate()
    {
        //Remove everything from global cache
        // this prevents some race-conditions for getting audio managers from guilds
        getJDA().getGuildsView().remove(id);

        ChannelCacheViewImpl channelsView = getJDA().getChannelsView();
        try (UnlockHook hook = channelsView.writeLock())
        {
            getChannels().forEach(channel -> channelsView.remove(channel.getType(), channel.getIdLong()));
        }

        // Clear audio connection
        getJDA().getClient().removeAudioConnection(id);
        final AbstractCacheView audioManagerView = getJDA().getAudioManagersView();
        final AudioManagerImpl manager = (AudioManagerImpl) audioManagerView.get(id); //read-lock access/release
        if (manager != null)
            manager.closeAudioConnection(ConnectionStatus.DISCONNECTED_REMOVED_FROM_GUILD); //connection-lock access/release
        audioManagerView.remove(id); //write-lock access/release

        //cleaning up all users that we do not share a guild with anymore
        // Anything left in memberIds will be removed from the main userMap
        //Use a new HashSet so that we don't actually modify the Member map so it doesn't affect Guild#getMembers for the leave event.
        TLongSet memberIds = getMembersView().keySet(); // copies keys
        getJDA().getGuildCache().stream()
                .map(GuildImpl.class::cast)
                .forEach(g -> memberIds.removeAll(g.getMembersView().keySet()));
        // Remember, everything left in memberIds is removed from the userMap
        SnowflakeCacheViewImpl userView = getJDA().getUsersView();
        try (UnlockHook hook = userView.writeLock())
        {
            long selfId = getJDA().getSelfUser().getIdLong();
            memberIds.forEach(memberId -> {
                if (memberId == selfId)
                    return true; // don't remove selfUser from cache
                userView.remove(memberId);
                getJDA().getEventCache().clear(EventCache.Type.USER, memberId);
                return true;
            });
        }
    }

    public void uncacheChannel(GuildChannel channel, boolean keepThreads)
    {
        long id = channel.getIdLong();

        // Enforce idempotence by checking the channel was in cache
        // If the channel was not in cache, there is no reason to cleanup anything else.
        // This idempotency makes sure that new cache is never affected by old cache
        if (channelCache.remove(channel.getType(), id) == null)
            return;

        api.getChannelsView().remove(channel.getType(), id);

        if (!keepThreads && channel instanceof IThreadContainer)
        {
            // Remove dangling threads
            SortedChannelCacheViewImpl localView = this.getChannelView();
            ChannelCacheViewImpl globalView = api.getChannelsView();
            Predicate predicate = thread -> channel.equals(thread.getParentChannel());

            try (UnlockHook hook1 = localView.writeLock(); UnlockHook hook2 = globalView.writeLock())
            {
                localView.removeIf(ThreadChannel.class, predicate);
                globalView.removeIf(ThreadChannel.class, predicate);
            }
        }
    }

    @Nonnull
    @Override
    public RestAction> retrieveCommands(boolean withLocalizations)
    {
        Route.CompiledRoute route = Route.Interactions.GET_GUILD_COMMANDS
                .compile(getJDA().getSelfUser().getApplicationId(), getId())
                .withQueryParams("with_localizations", String.valueOf(withLocalizations));

        return new RestActionImpl<>(getJDA(), route,
                (response, request) ->
                        response.getArray()
                                .stream(DataArray::getObject)
                                .map(json -> new CommandImpl(getJDA(), this, json))
                                .collect(Collectors.toList()));
    }

    @Nonnull
    @Override
    public RestAction retrieveCommandById(@Nonnull String id)
    {
        Checks.isSnowflake(id);
        Route.CompiledRoute route = Route.Interactions.GET_GUILD_COMMAND.compile(getJDA().getSelfUser().getApplicationId(), getId(), id);
        return new RestActionImpl<>(getJDA(), route, (response, request) -> new CommandImpl(getJDA(), this, response.getObject()));
    }

    @Nonnull
    @Override
    public CommandCreateAction upsertCommand(@Nonnull CommandData command)
    {
        Checks.notNull(command, "CommandData");
        return new CommandCreateActionImpl(this, (CommandDataImpl) command);
    }

    @Nonnull
    @Override
    public CommandListUpdateAction updateCommands()
    {
        Route.CompiledRoute route = Route.Interactions.UPDATE_GUILD_COMMANDS.compile(getJDA().getSelfUser().getApplicationId(), getId());
        return new CommandListUpdateActionImpl(getJDA(), this, route);
    }

    @Nonnull
    @Override
    public CommandEditAction editCommandById(@Nonnull String id)
    {
        Checks.isSnowflake(id);
        return new CommandEditActionImpl(this, id);
    }

    @Nonnull
    @Override
    public RestAction deleteCommandById(@Nonnull String commandId)
    {
        Checks.isSnowflake(commandId);
        Route.CompiledRoute route = Route.Interactions.DELETE_GUILD_COMMAND.compile(getJDA().getSelfUser().getApplicationId(), getId(), commandId);
        return new RestActionImpl<>(getJDA(), route);
    }

    @Nonnull
    @Override
    public RestAction> retrieveIntegrationPrivilegesById(@Nonnull String targetId)
    {
        Checks.isSnowflake(targetId, "ID");
        Route.CompiledRoute route = Route.Interactions.GET_COMMAND_PERMISSIONS.compile(getJDA().getSelfUser().getApplicationId(), getId(), targetId);
        return new RestActionImpl<>(getJDA(), route, (response, request) -> parsePrivilegesList(response.getObject()));
    }

    @Nonnull
    @Override
    public RestAction retrieveCommandPrivileges()
    {
        Route.CompiledRoute route = Route.Interactions.GET_ALL_COMMAND_PERMISSIONS.compile(getJDA().getSelfUser().getApplicationId(), getId());
        return new RestActionImpl<>(getJDA(), route, (response, request) -> {
            Map> privileges = new HashMap<>();
            response.getArray().stream(DataArray::getObject).forEach(obj -> {
                String id = obj.getString("id");
                List list = Collections.unmodifiableList(parsePrivilegesList(obj));
                privileges.put(id, list);
            });
            return new PrivilegeConfig(this, privileges);
        });
    }

    private List parsePrivilegesList(DataObject obj)
    {
        return obj.getArray("permissions")
                .stream(DataArray::getObject)
                .map(this::parsePrivilege)
                .collect(Collectors.toList());
    }

    private IntegrationPrivilege parsePrivilege(DataObject data)
    {
        IntegrationPrivilege.Type type = IntegrationPrivilege.Type.fromKey(data.getInt("type", 1));
        boolean enabled = data.getBoolean("permission");
        return new IntegrationPrivilege(this, type, enabled, data.getUnsignedLong("id"));
    }

    @Nonnull
    @Override
    public RestAction> retrieveRegions(boolean includeDeprecated)
    {
        Route.CompiledRoute route = Route.Guilds.GET_VOICE_REGIONS.compile(getId());
        return new RestActionImpl<>(getJDA(), route, (response, request) ->
        {
            EnumSet set = EnumSet.noneOf(Region.class);
            DataArray arr = response.getArray();
            for (int i = 0; i < arr.length(); i++)
            {
                DataObject obj = arr.getObject(i);
                if (!includeDeprecated && obj.getBoolean("deprecated"))
                    continue;
                String id = obj.getString("id", "");
                Region region = Region.fromKey(id);
                if (region != Region.UNKNOWN)
                    set.add(region);
            }
            return set;
        });
    }

    @Nonnull
    @Override
    public RestAction> retrieveAutoModRules()
    {
        checkPermission(Permission.MANAGE_SERVER);
        Route.CompiledRoute route = Route.AutoModeration.LIST_RULES.compile(getId());
        return new RestActionImpl<>(api, route, (response, request) ->
        {
            DataArray array = response.getArray();
            List rules = new ArrayList<>(array.length());
            for (int i = 0; i < array.length(); i++)
            {
                try
                {
                    DataObject obj = array.getObject(i);
                    rules.add(AutoModRuleImpl.fromData(this, obj));
                }
                catch (ParsingException exception)
                {
                    EntityBuilder.LOG.error("Failed to parse AutoModRule", exception);
                }
            }
            return Collections.unmodifiableList(rules);
        });
    }

    @Nonnull
    @Override
    public RestAction retrieveAutoModRuleById(@Nonnull String id)
    {
        Checks.isSnowflake(id);
        checkPermission(Permission.MANAGE_SERVER);
        Route.CompiledRoute route = Route.AutoModeration.GET_RULE.compile(getId(), id);
        return new RestActionImpl<>(api, route, (response, request) -> AutoModRuleImpl.fromData(this, response.getObject()));
    }

    @Nonnull
    @Override
    public AuditableRestAction createAutoModRule(@Nonnull AutoModRuleData rule)
    {
        Checks.notNull(rule, "AutoMod Rule");
        rule.getRequiredPermissions().forEach(this::checkPermission);
        Route.CompiledRoute route = Route.AutoModeration.CREATE_RULE.compile(getId());
        return new AuditableRestActionImpl<>(api, route, rule.toData(), (response, request) -> AutoModRuleImpl.fromData(this, response.getObject()));
    }

    @Nonnull
    @Override
    public AutoModRuleManager modifyAutoModRuleById(@Nonnull String id)
    {
        Checks.isSnowflake(id);
        checkPermission(Permission.MANAGE_SERVER);
        return new AutoModRuleManagerImpl(this, id);
    }

    @Nonnull
    @Override
    public AuditableRestAction deleteAutoModRuleById(@Nonnull String id)
    {
        Checks.isSnowflake(id);
        checkPermission(Permission.MANAGE_SERVER);
        Route.CompiledRoute route = Route.AutoModeration.DELETE_RULE.compile(getId(), id);
        return new AuditableRestActionImpl<>(api, route);
    }

    @Nonnull
    @Override
    public MemberAction addMember(@Nonnull String accessToken, @Nonnull UserSnowflake user)
    {
        Checks.notBlank(accessToken, "Access-Token");
        Checks.notNull(user, "User");
        Checks.check(!isMember(user), "User is already in this guild");
        if (!getSelfMember().hasPermission(Permission.CREATE_INSTANT_INVITE))
            throw new InsufficientPermissionException(this, Permission.CREATE_INSTANT_INVITE);
        return new MemberActionImpl(getJDA(), this, user.getId(), accessToken);
    }

    @Override
    public boolean isLoaded()
    {
        // Only works with GUILD_MEMBERS intent
        return getJDA().isIntent(GatewayIntent.GUILD_MEMBERS)
                && (long) getMemberCount() <= getMemberCache().size();
    }

    @Override
    public void pruneMemberCache()
    {
        try (UnlockHook h = memberCache.writeLock())
        {
            EntityBuilder builder = getJDA().getEntityBuilder();
            Set members = memberCache.asSet();
            members.forEach(m -> builder.updateMemberCache((MemberImpl) m));
        }
    }

    @Override
    public boolean unloadMember(long userId)
    {
        if (userId == api.getSelfUser().getIdLong())
            return false;
        MemberImpl member = (MemberImpl) getMemberById(userId);
        if (member == null)
            return false;
        api.getEntityBuilder().updateMemberCache(member, true);
        return true;
    }

    @Override
    public int getMemberCount()
    {
        return memberCount;
    }

    @Nonnull
    @Override
    public String getName()
    {
        return name;
    }

    @Override
    public String getIconId()
    {
        return iconId;
    }

    @Nonnull
    @Override
    public Set getFeatures()
    {
        return features;
    }

    @Override
    public String getSplashId()
    {
        return splashId;
    }

    @Nullable
    @Override
    public String getVanityCode()
    {
        return vanityCode;
    }

    @Override
    @Nonnull
    public RestAction retrieveVanityInvite()
    {
        checkPermission(Permission.MANAGE_SERVER);
        JDAImpl api = getJDA();
        Route.CompiledRoute route = Route.Guilds.GET_VANITY_URL.compile(getId());
        return new RestActionImpl<>(api, route,
            (response, request) -> new VanityInvite(vanityCode, response.getObject().getInt("uses")));
    }

    @Nullable
    @Override
    public String getDescription()
    {
        return description;
    }

    @Nonnull
    @Override
    public DiscordLocale getLocale()
    {
        return preferredLocale;
    }

    @Nullable
    @Override
    public String getBannerId()
    {
        return banner;
    }

    @Nonnull
    @Override
    public BoostTier getBoostTier()
    {
        return boostTier;
    }

    @Override
    public int getBoostCount()
    {
        return boostCount;
    }

    @Nonnull
    @Override
    @SuppressWarnings("ConstantConditions") // can't be null here
    public List getBoosters()
    {
        return memberCache.applyStream((members) ->
            members.filter(m -> m.getTimeBoosted() != null)
                   .sorted(Comparator.comparing(Member::getTimeBoosted))
                   .collect(Helpers.toUnmodifiableList()));
    }

    @Override
    public int getMaxMembers()
    {
        return maxMembers;
    }

    @Override
    public int getMaxPresences()
    {
        return maxPresences;
    }

    @Nonnull
    @Override
    public RestAction retrieveMetaData()
    {
        Route.CompiledRoute route = Route.Guilds.GET_GUILD.compile(getId());
        route = route.withQueryParams("with_counts", "true");
        return new RestActionImpl<>(getJDA(), route, (response, request) -> {
            DataObject json = response.getObject();
            int memberLimit = json.getInt("max_members", 0);
            int presenceLimit = json.getInt("max_presences", 5000);
            this.maxMembers = memberLimit;
            this.maxPresences = presenceLimit;
            int approxMembers = json.getInt("approximate_member_count", this.memberCount);
            int approxPresence = json.getInt("approximate_presence_count", 0);
            return new MetaData(memberLimit, presenceLimit, approxPresence, approxMembers);
        });
    }

    @Override
    public VoiceChannel getAfkChannel()
    {
        return afkChannel;
    }

    @Override
    public TextChannel getSystemChannel()
    {
        return systemChannel;
    }

    @Override
    public TextChannel getRulesChannel()
    {
        return rulesChannel;
    }

    @Nonnull
    @Override
    public CacheRestAction retrieveScheduledEventById(@Nonnull String id)
    {
        Checks.isSnowflake(id);
        return new DeferredRestAction<>(getJDA(), ScheduledEvent.class,
                () -> getScheduledEventById(id),
                () ->
                {
                    Route.CompiledRoute route = Route.Guilds.GET_SCHEDULED_EVENT.compile(getId(), id);
                    return new RestActionImpl<>(getJDA(), route, (response, request) -> api.getEntityBuilder().createScheduledEvent(this, response.getObject()));
                });
    }

    @Nonnull
    @Override
    public CacheRestAction retrieveScheduledEventById(long id)
    {
        return retrieveScheduledEventById(Long.toUnsignedString(id));
    }

    @Nonnull
    @Override
    public ScheduledEventAction createScheduledEvent(@Nonnull String name, @Nonnull String location, @Nonnull OffsetDateTime startTime, @Nonnull OffsetDateTime endTime)
    {
        checkPermission(Permission.MANAGE_EVENTS);
        return new ScheduledEventActionImpl(name, location, startTime, endTime, this);
    }

    @Nonnull
    @Override
    public ScheduledEventAction createScheduledEvent(@Nonnull String name, @Nonnull GuildChannel channel, @Nonnull OffsetDateTime startTime)
    {
        checkPermission(Permission.MANAGE_EVENTS);
        return new ScheduledEventActionImpl(name, channel, startTime, this);
    }


    @Override
    public TextChannel getCommunityUpdatesChannel()
    {
        return communityUpdatesChannel;
    }

    @Nonnull
    @Override
    public RestAction> retrieveWebhooks()
    {
        if (!getSelfMember().hasPermission(Permission.MANAGE_WEBHOOKS))
            throw new InsufficientPermissionException(this, Permission.MANAGE_WEBHOOKS);

        Route.CompiledRoute route = Route.Guilds.GET_WEBHOOKS.compile(getId());

        return new RestActionImpl<>(getJDA(), route, (response, request) ->
        {
            DataArray array = response.getArray();
            List webhooks = new ArrayList<>(array.length());
            EntityBuilder builder = api.getEntityBuilder();

            for (int i = 0; i < array.length(); i++)
            {
                try
                {
                    webhooks.add(builder.createWebhook(array.getObject(i)));
                }
                catch (Exception e)
                {
                    JDAImpl.LOG.error("Error creating webhook from json", e);
                }
            }

            return Collections.unmodifiableList(webhooks);
        });
    }

    @Override
    public Member getOwner()
    {
        return owner;
    }

    @Override
    public long getOwnerIdLong()
    {
        return ownerId;
    }

    @Nonnull
    @Override
    public Timeout getAfkTimeout()
    {
        return afkTimeout;
    }

    @Override
    public boolean isMember(@Nonnull UserSnowflake user)
    {
        return memberCache.get(user.getIdLong()) != null;
    }

    @Nonnull
    @Override
    public Member getSelfMember()
    {
        Member member = getMember(getJDA().getSelfUser());
        if (member == null)
            throw new IllegalStateException("Guild does not have a self member");
        return member;
    }

    @Override
    public Member getMember(@Nonnull UserSnowflake user)
    {
        Checks.notNull(user, "User");
        return getMemberById(user.getIdLong());
    }

    @Nonnull
    @Override
    public MemberCacheView getMemberCache()
    {
        return memberCache;
    }

    @Nonnull
    @Override
    public SortedSnowflakeCacheView getScheduledEventCache()
    {
        return scheduledEventCache;
    }

    @Nonnull
    @Override
    public SortedSnowflakeCacheView getCategoryCache()
    {
        return channelCache.ofType(Category.class);
    }

    @Nonnull
    @Override
    public SortedSnowflakeCacheView getTextChannelCache()
    {
        return channelCache.ofType(TextChannel.class);
    }

    @Nonnull
    @Override
    public SortedSnowflakeCacheView getNewsChannelCache()
    {
        return channelCache.ofType(NewsChannel.class);
    }

    @Nonnull
    @Override
    public SortedSnowflakeCacheView getVoiceChannelCache()
    {
        return channelCache.ofType(VoiceChannel.class);
    }

    @Nonnull
    @Override
    public SortedSnowflakeCacheView getForumChannelCache()
    {
        return channelCache.ofType(ForumChannel.class);
    }

    @Nonnull
    @Override
    public SnowflakeCacheView getMediaChannelCache()
    {
        return channelCache.ofType(MediaChannel.class);
    }

    @Nonnull
    @Override
    public SortedSnowflakeCacheView getStageChannelCache()
    {
        return channelCache.ofType(StageChannel.class);
    }

    @Nonnull
    @Override
    public SortedSnowflakeCacheView getThreadChannelCache()
    {
        return channelCache.ofType(ThreadChannel.class);
    }

    @Nonnull
    @Override
    public SortedChannelCacheViewImpl getChannelCache()
    {
        return channelCache;
    }

    @Nullable
    @Override
    public GuildChannel getGuildChannelById(long id)
    {
        return channelCache.getElementById(id);
    }

    @Override
    public GuildChannel getGuildChannelById(@Nonnull ChannelType type, long id)
    {
        return channelCache.getElementById(type, id);
    }

    @Nonnull
    @Override
    public SortedSnowflakeCacheView getRoleCache()
    {
        return roleCache;
    }

    @Nonnull
    @Override
    public SnowflakeCacheView getEmojiCache()
    {
        return emojicache;
    }

    @Nonnull
    @Override
    public SnowflakeCacheView getStickerCache()
    {
        return stickerCache;
    }

    @Nonnull
    @Override
    public List getChannels(boolean includeHidden)
    {
        if (includeHidden)
        {
            return channelCache.applyStream(stream ->
                stream.filter(it -> !it.getType().isThread())
                      .sorted()
                      .collect(Helpers.toUnmodifiableList())
            );
        }

        // When we remove hidden channels there are 2 considerations to account for:
        //
        // 1. A channel is not visible if we don't have VIEW_CHANNEL permissions
        // 2. A category is not visible if we don't see any channels within it
        //
        // In our implementation we iterate all applicable channels and only add categories,
        // when a member of the category is added too.
        //
        // Note: We avoid using Category#getChannels because it would iterate the entire cache each time.
        // This is an optimization to avoid many unnecessary iterations.

        Member self = getSelfMember();

        SortedSet channels = new TreeSet<>();
        channelCache.ofType(ICategorizableChannel.class).forEachUnordered(channel ->
        {
            // Hide threads and inaccessible channels
            if (channel.getType().isThread() || !self.hasPermission(channel, Permission.VIEW_CHANNEL)) return;

            Category category = channel.getParentCategory();
            channels.add(channel);

            // Empty categories will never show up here,
            // since no categorizable channel will add them to this group
            if (category != null)
                channels.add(category);
        });

        return Collections.unmodifiableList(new ArrayList<>(channels));
    }

    @Nonnull
    @Override
    public RestAction> retrieveEmojis()
    {
        Route.CompiledRoute route = Route.Emojis.GET_EMOJIS.compile(getId());
        return new RestActionImpl<>(getJDA(), route, (response, request) ->
        {
            EntityBuilder builder = GuildImpl.this.getJDA().getEntityBuilder();
            DataArray emojis = response.getArray();
            List list = new ArrayList<>(emojis.length());
            for (int i = 0; i < emojis.length(); i++)
            {
                DataObject emoji = emojis.getObject(i);
                list.add(builder.createEmoji(GuildImpl.this, emoji));
            }

            return Collections.unmodifiableList(list);
        });
    }

    @Nonnull
    @Override
    public RestAction retrieveEmojiById(@Nonnull String id)
    {
        Checks.isSnowflake(id, "Emoji ID");

        JDAImpl jda = getJDA();
        return new DeferredRestAction<>(jda, RichCustomEmoji.class,
        () -> {
            RichCustomEmoji emoji = getEmojiById(id);
            if (emoji != null)
            {
                if (emoji.getOwner() != null || !getSelfMember().hasPermission(Permission.MANAGE_GUILD_EXPRESSIONS))
                    return emoji;
            }
            return null;
        }, () -> {
            Route.CompiledRoute route = Route.Emojis.GET_EMOJI.compile(getId(), id);
            return new AuditableRestActionImpl<>(jda, route, (response, request) ->
            {
                EntityBuilder builder = GuildImpl.this.getJDA().getEntityBuilder();
                return builder.createEmoji(GuildImpl.this, response.getObject());
            });
        });
    }

    @Nonnull
    @Override
    public RestAction> retrieveStickers()
    {
        Route.CompiledRoute route = Route.Stickers.GET_GUILD_STICKERS.compile(getId());
        return new RestActionImpl<>(getJDA(), route, (response, request) -> {
            DataArray array = response.getArray();
            List stickers = new ArrayList<>(array.length());
            EntityBuilder builder = api.getEntityBuilder();
            for (int i = 0; i < array.length(); i++)
            {
                DataObject object = null;
                try
                {
                    object = array.getObject(i);
                    GuildSticker sticker = (GuildSticker) builder.createRichSticker(object);
                    stickers.add(sticker);
                }
                catch (ParsingException | ClassCastException ex)
                {
                    EntityBuilder.LOG.error("Failed to parse sticker for JSON: {}", object, ex);
                }
            }

            return Collections.unmodifiableList(stickers);
        });
    }

    @Nonnull
    @Override
    public RestAction retrieveSticker(@Nonnull StickerSnowflake sticker)
    {
        Checks.notNull(sticker, "Sticker");
        Route.CompiledRoute route = Route.Stickers.GET_GUILD_STICKER.compile(getId(), sticker.getId());
        return new RestActionImpl<>(getJDA(), route, (response, request) -> {
            DataObject object = response.getObject();
            EntityBuilder builder = api.getEntityBuilder();
            return (GuildSticker) builder.createRichSticker(object);
        });
    }

    @Nonnull
    @Override
    public GuildStickerManager editSticker(@Nonnull StickerSnowflake sticker)
    {
        Checks.notNull(sticker, "Sticker");
        if (sticker instanceof GuildSticker)
            Checks.check(((GuildSticker) sticker).getGuildIdLong() == id, "Cannot edit a sticker from another guild!");
        Checks.check(!(sticker instanceof StandardSticker), "Cannot edit a standard sticker.");
        return new GuildStickerManagerImpl(this, id, sticker);
    }

    @Nonnull
    @Override
    public BanPaginationActionImpl retrieveBanList()
    {
        if (!getSelfMember().hasPermission(Permission.BAN_MEMBERS))
            throw new InsufficientPermissionException(this, Permission.BAN_MEMBERS);

        return new BanPaginationActionImpl(this);
    }

    @Nonnull
    @Override
    public RestAction retrieveBan(@Nonnull UserSnowflake user)
    {
        if (!getSelfMember().hasPermission(Permission.BAN_MEMBERS))
            throw new InsufficientPermissionException(this, Permission.BAN_MEMBERS);

        Checks.notNull(user, "User");

        Route.CompiledRoute route = Route.Guilds.GET_BAN.compile(getId(), user.getId());
        return new RestActionImpl<>(getJDA(), route, (response, request) ->
        {
            EntityBuilder builder = api.getEntityBuilder();
            DataObject bannedObj = response.getObject();
            DataObject userJson = bannedObj.getObject("user");
            return new Ban(builder.createUser(userJson), bannedObj.getString("reason", null));
        });
    }

    @Nonnull
    @Override
    public RestAction retrievePrunableMemberCount(int days)
    {
        if (!getSelfMember().hasPermission(Permission.KICK_MEMBERS))
            throw new InsufficientPermissionException(this, Permission.KICK_MEMBERS);

        Checks.check(days >= 1 && days <= 30, "Provided %d days must be between 1 and 30.", days);

        Route.CompiledRoute route = Route.Guilds.PRUNABLE_COUNT.compile(getId()).withQueryParams("days", Integer.toString(days));
        return new RestActionImpl<>(getJDA(), route, (response, request) -> response.getObject().getInt("pruned"));
    }

    @Nonnull
    @Override
    public Role getPublicRole()
    {
        return publicRole;
    }

    @Nullable
    @Override
    public DefaultGuildChannelUnion getDefaultChannel()
    {
        final Role role = getPublicRole();
        return (DefaultGuildChannelUnion) Stream.concat(getTextChannelCache().stream(), getNewsChannelCache().stream())
                .filter(c -> role.hasPermission(c, Permission.VIEW_CHANNEL))
                .min(Comparator.naturalOrder())
                .orElse(null);
    }

    @Nonnull
    @Override
    public GuildManager getManager()
    {
        return new GuildManagerImpl(this);
    }

    @Override
    public boolean isBoostProgressBarEnabled()
    {
        return boostProgressBarEnabled;
    }

    @Nonnull
    @Override
    public AuditLogPaginationAction retrieveAuditLogs()
    {
        return new AuditLogPaginationActionImpl(this);
    }

    @Nonnull
    @Override
    public RestAction leave()
    {
        if (getSelfMember().isOwner())
            throw new IllegalStateException("Cannot leave a guild that you are the owner of! Transfer guild ownership first!");

        Route.CompiledRoute route = Route.Self.LEAVE_GUILD.compile(getId());
        return new RestActionImpl<>(getJDA(), route);
    }

    @Nonnull
    @Override
    public RestAction delete()
    {
        if (!getJDA().getSelfUser().isBot() && getJDA().getSelfUser().isMfaEnabled())
            throw new IllegalStateException("Cannot delete a guild without providing MFA code. Use Guild#delete(String)");

        return delete(null);
    }

    @Nonnull
    @Override
    public RestAction delete(String mfaCode)
    {
        if (!getSelfMember().isOwner())
            throw new PermissionException("Cannot delete a guild that you do not own!");

        DataObject mfaBody = null;
        if (!getJDA().getSelfUser().isBot() && getJDA().getSelfUser().isMfaEnabled())
        {
            Checks.notEmpty(mfaCode, "Provided MultiFactor Auth code");
            mfaBody = DataObject.empty().put("code", mfaCode);
        }

        Route.CompiledRoute route = Route.Guilds.DELETE_GUILD.compile(getId());
        return new RestActionImpl<>(getJDA(), route, mfaBody);
    }

    @Nonnull
    @Override
    public AudioManager getAudioManager()
    {
        if (!getJDA().isIntent(GatewayIntent.GUILD_VOICE_STATES))
            throw new IllegalStateException("Cannot use audio features with disabled GUILD_VOICE_STATES intent!");
        final AbstractCacheView managerMap = getJDA().getAudioManagersView();
        AudioManager mng = managerMap.get(id);
        if (mng == null)
        {
            // No previous manager found -> create one
            try (UnlockHook hook = managerMap.writeLock())
            {
                GuildImpl cachedGuild = (GuildImpl) getJDA().getGuildById(id);
                if (cachedGuild == null)
                    throw new IllegalStateException("Cannot get an AudioManager instance on an uncached Guild");
                mng = managerMap.get(id);
                if (mng == null)
                {
                    mng = new AudioManagerImpl(cachedGuild);
                    managerMap.getMap().put(id, mng);
                }
            }
        }
        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;
        }

        AudioChannel channel = getSelfMember().getVoiceState().getChannel();
        if (channel instanceof StageChannel)
        {
            CompletableFuture future = ((StageChannel) channel).cancelRequestToSpeak().submit();
            return new GatewayTask<>(future, () -> future.cancel(false));
        }

        return new GatewayTask<>(CompletableFuture.completedFuture(null), () -> {});
    }

    @Nonnull
    @Override
    public JDAImpl getJDA()
    {
        return api;
    }

    @Nonnull
    @Override
    public List getVoiceStates()
    {
        return getMembersView().stream()
                .map(Member::getVoiceState)
                .filter(Objects::nonNull)
                .collect(Helpers.toUnmodifiableList());
    }

    @Nonnull
    @Override
    public VerificationLevel getVerificationLevel()
    {
        return verificationLevel;
    }

    @Nonnull
    @Override
    public NotificationLevel getDefaultNotificationLevel()
    {
        return defaultNotificationLevel;
    }

    @Nonnull
    @Override
    public MFALevel getRequiredMFALevel()
    {
        return mfaLevel;
    }

    @Nonnull
    @Override
    public ExplicitContentLevel getExplicitContentLevel()
    {
        return explicitContentLevel;
    }

    @Nonnull
    @Override
    public Task loadMembers(@Nonnull Consumer callback)
    {
        Checks.notNull(callback, "Callback");
        if (!getJDA().isIntent(GatewayIntent.GUILD_MEMBERS))
            throw new IllegalStateException("Cannot use loadMembers without GatewayIntent.GUILD_MEMBERS!");
        if (isLoaded())
        {
            memberCache.forEachUnordered(callback);
            return new GatewayTask<>(CompletableFuture.completedFuture(null), () -> {});
        }

        MemberChunkManager chunkManager = getJDA().getClient().getChunkManager();
        boolean includePresences = getJDA().isIntent(GatewayIntent.GUILD_PRESENCES);
        MemberChunkManager.ChunkRequest handler = chunkManager.chunkGuild(this, includePresences, (last, list) -> list.forEach(callback));
        handler.exceptionally(ex -> {
            WebSocketClient.LOG.error("Encountered exception trying to handle member chunk response", ex);
            return null;
        });
        return new GatewayTask<>(handler, () -> handler.cancel(false)).onSetTimeout(handler::setTimeout);
    }

    @Nonnull
    @Override
    public CacheRestAction retrieveMemberById(long id)
    {
        JDAImpl jda = getJDA();
        return new DeferredRestAction<>(jda, Member.class,
                () -> getMemberById(id),
                () -> {
                    if (id == jda.getSelfUser().getIdLong())
                        return new CompletedRestAction<>(jda, getSelfMember());
                    Route.CompiledRoute route = Route.Guilds.GET_MEMBER.compile(getId(), Long.toUnsignedString(id));
                    return new RestActionImpl<>(jda, route, (resp, req) -> {
                        MemberImpl member = jda.getEntityBuilder().createMember(this, resp.getObject());
                        jda.getEntityBuilder().updateMemberCache(member);
                        return member;
                    });
                }).useCache(jda.isIntent(GatewayIntent.GUILD_MEMBERS));
    }

    @Nonnull
    @Override
    public Task> retrieveMembersByIds(boolean includePresence, @Nonnull long... ids)
    {
        Checks.notNull(ids, "ID Array");
        Checks.check(!includePresence || api.isIntent(GatewayIntent.GUILD_PRESENCES),
                "Cannot retrieve presences of members without GUILD_PRESENCES intent!");

        if (ids.length == 0)
            return new GatewayTask<>(CompletableFuture.completedFuture(Collections.emptyList()), () -> {});
        Checks.check(ids.length <= 100, "You can only request 100 members at once");
        MemberChunkManager chunkManager = api.getClient().getChunkManager();
        List collect = new ArrayList<>(ids.length);
        CompletableFuture> result = new CompletableFuture<>();
        MemberChunkManager.ChunkRequest handle = chunkManager.chunkGuild(this, includePresence, ids, (last, list) -> {
            collect.addAll(list);
            if (last)
                result.complete(collect);
        });

        handle.exceptionally(ex -> {
            WebSocketClient.LOG.error("Encountered exception trying to handle member chunk response", ex);
            result.completeExceptionally(ex);
            return null;
        });

        return new GatewayTask<>(result, () -> handle.cancel(false)).onSetTimeout(handle::setTimeout);
    }

    @Nonnull
    @Override
    @CheckReturnValue
    public Task> retrieveMembersByPrefix(@Nonnull String prefix, int limit)
    {
        Checks.notEmpty(prefix, "Prefix");
        Checks.positive(limit, "Limit");
        Checks.check(limit <= 100, "Limit must not be greater than 100");
        MemberChunkManager chunkManager = api.getClient().getChunkManager();

        List collect = new ArrayList<>(limit);
        CompletableFuture> result = new CompletableFuture<>();
        MemberChunkManager.ChunkRequest handle = chunkManager.chunkGuild(this, prefix, limit, (last, list) -> {
            collect.addAll(list);
            if (last)
                result.complete(collect);
        });

        handle.exceptionally(ex -> {
            WebSocketClient.LOG.error("Encountered exception trying to handle member chunk response", ex);
            result.completeExceptionally(ex);
            return null;
        });

        return new GatewayTask<>(result, () -> handle.cancel(false)).onSetTimeout(handle::setTimeout);
    }

    @Nonnull
    @Override
    public RestAction> retrieveActiveThreads()
    {
        Route.CompiledRoute route = Route.Guilds.LIST_ACTIVE_THREADS.compile(getId());
        return new RestActionImpl<>(api, route, (response, request) ->
        {
            DataObject obj = response.getObject();
            DataArray selfThreadMembers = obj.getArray("members");
            DataArray threads = obj.getArray("threads");

            List list = new ArrayList<>(threads.length());
            EntityBuilder builder = api.getEntityBuilder();

            TLongObjectMap selfThreadMemberMap = new TLongObjectHashMap<>();
            for (int i = 0; i < selfThreadMembers.length(); i++)
            {
                DataObject selfThreadMember = selfThreadMembers.getObject(i);

                //Store the thread member based on the "id" which is the _thread's_ id, not the member's id (which would be our id)
                selfThreadMemberMap.put(selfThreadMember.getLong("id"), selfThreadMember);
            }

            for (int i = 0; i < threads.length(); i++)
            {
                DataObject threadObj = threads.getObject(i);
                DataObject selfThreadMemberObj = selfThreadMemberMap.get(threadObj.getLong("id", 0));

                if (selfThreadMemberObj != null)
                {
                    //Combine the thread and self thread-member into a single object to model what we get from
                    // thread payloads (like from Gateway, etc)
                    threadObj.put("member", selfThreadMemberObj);
                }

                try
                {
                    ThreadChannel thread = builder.createThreadChannel(threadObj, this.getIdLong());
                    list.add(thread);
                }
                catch (Exception e)
                {
                    if (EntityBuilder.MISSING_CHANNEL.equals(e.getMessage()))
                        EntityBuilder.LOG.debug("Discarding thread without cached parent channel. JSON: {}", threadObj);
                    else
                        EntityBuilder.LOG.warn("Failed to create thread channel. JSON: {}", threadObj, e);
                }
            }

            return Collections.unmodifiableList(list);
        });
    }

    @Override
    public long getIdLong()
    {
        return id;
    }

    @Nonnull
    @Override
    public RestAction> retrieveInvites()
    {
        if (!this.getSelfMember().hasPermission(Permission.MANAGE_SERVER))
            throw new InsufficientPermissionException(this, Permission.MANAGE_SERVER);

        final Route.CompiledRoute route = Route.Invites.GET_GUILD_INVITES.compile(getId());
        return new RestActionImpl<>(getJDA(), route, (response, request) ->
        {
            EntityBuilder entityBuilder = api.getEntityBuilder();
            DataArray array = response.getArray();
            List invites = new ArrayList<>(array.length());
            for (int i = 0; i < array.length(); i++)
                invites.add(entityBuilder.createInvite(array.getObject(i)));
            return Collections.unmodifiableList(invites);
        });
    }

    @Nonnull
    @Override
    public RestAction> retrieveTemplates()
    {
        if (!this.getSelfMember().hasPermission(Permission.MANAGE_SERVER))
            throw new InsufficientPermissionException(this, Permission.MANAGE_SERVER);

        final Route.CompiledRoute route = Route.Templates.GET_GUILD_TEMPLATES.compile(getId());
        return new RestActionImpl<>(getJDA(), route, (response, request) ->
        {
            EntityBuilder entityBuilder = api.getEntityBuilder();
            DataArray array = response.getArray();
            List