sx.blah.discord.api.internal.DispatchHandler.java Source code

Java tutorial

Introduction

Here is the source code for sx.blah.discord.api.internal.DispatchHandler.java

Source

/*
 *     This file is part of Discord4J.
 *
 *     Discord4J is free software: you can redistribute it and/or modify
 *     it under the terms of the GNU Lesser General Public License as published by
 *     the Free Software Foundation, either version 3 of the License, or
 *     (at your option) any later version.
 *
 *     Discord4J is distributed in the hope that it will be useful,
 *     but WITHOUT ANY WARRANTY; without even the implied warranty of
 *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *     GNU Lesser General Public License for more details.
 *
 *     You should have received a copy of the GNU Lesser General Public License
 *     along with Discord4J.  If not, see <http://www.gnu.org/licenses/>.
 */

package sx.blah.discord.api.internal;

import com.fasterxml.jackson.databind.JsonNode;
import org.apache.commons.lang3.builder.ToStringBuilder;
import sx.blah.discord.Discord4J;
import sx.blah.discord.api.internal.json.event.*;
import sx.blah.discord.api.internal.json.objects.*;
import sx.blah.discord.api.internal.json.requests.GuildMembersRequest;
import sx.blah.discord.api.internal.json.responses.ReadyResponse;
import sx.blah.discord.api.internal.json.responses.voice.VoiceUpdateResponse;
import sx.blah.discord.handle.impl.events.guild.*;
import sx.blah.discord.handle.impl.events.guild.category.CategoryCreateEvent;
import sx.blah.discord.handle.impl.events.guild.category.CategoryDeleteEvent;
import sx.blah.discord.handle.impl.events.guild.category.CategoryUpdateEvent;
import sx.blah.discord.handle.impl.events.guild.channel.*;
import sx.blah.discord.handle.impl.events.guild.channel.message.*;
import sx.blah.discord.handle.impl.events.guild.channel.message.reaction.ReactionAddEvent;
import sx.blah.discord.handle.impl.events.guild.channel.message.reaction.ReactionRemoveEvent;
import sx.blah.discord.handle.impl.events.guild.member.*;
import sx.blah.discord.handle.impl.events.guild.role.RoleCreateEvent;
import sx.blah.discord.handle.impl.events.guild.role.RoleDeleteEvent;
import sx.blah.discord.handle.impl.events.guild.role.RoleUpdateEvent;
import sx.blah.discord.handle.impl.events.guild.voice.VoiceChannelCreateEvent;
import sx.blah.discord.handle.impl.events.guild.voice.VoiceChannelDeleteEvent;
import sx.blah.discord.handle.impl.events.guild.voice.VoiceChannelUpdateEvent;
import sx.blah.discord.handle.impl.events.guild.voice.VoiceDisconnectedEvent;
import sx.blah.discord.handle.impl.events.guild.voice.user.UserVoiceChannelJoinEvent;
import sx.blah.discord.handle.impl.events.guild.voice.user.UserVoiceChannelLeaveEvent;
import sx.blah.discord.handle.impl.events.guild.voice.user.UserVoiceChannelMoveEvent;
import sx.blah.discord.handle.impl.events.shard.LoginEvent;
import sx.blah.discord.handle.impl.events.shard.ResumedEvent;
import sx.blah.discord.handle.impl.events.shard.ShardReadyEvent;
import sx.blah.discord.handle.impl.events.user.PresenceUpdateEvent;
import sx.blah.discord.handle.impl.events.user.UserUpdateEvent;
import sx.blah.discord.handle.impl.obj.*;
import sx.blah.discord.handle.obj.*;
import sx.blah.discord.util.LogMarkers;
import sx.blah.discord.util.PermissionUtils;
import sx.blah.discord.util.RequestBuilder;

import java.time.Instant;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors;

import static sx.blah.discord.api.internal.DiscordUtils.MAPPER;

/**
 * Handles {@link GatewayOps#DISPATCH} payloads on the Gateway.
 */
class DispatchHandler {
    /**
     * The associated websocket connection.
     */
    private DiscordWS ws;
    /**
     * The associated shard.
     */
    private ShardImpl shard;
    /**
     * The associated client.
     */
    private DiscordClientImpl client;
    /**
     * The thread on which every payload is handled.
     */
    private final ExecutorService dispatchExecutor = new ThreadPoolExecutor(2,
            Runtime.getRuntime().availableProcessors() * 4, 60L, TimeUnit.SECONDS, new SynchronousQueue<>(false),
            DiscordUtils.createDaemonThreadFactory("Dispatch Handler"), new ThreadPoolExecutor.CallerRunsPolicy());
    /**
     * Lock used to synchronize initialization
     */
    private final Lock startupLock = new ReentrantLock(true);

    DispatchHandler(DiscordWS ws, ShardImpl shard) {
        this.ws = ws;
        this.shard = shard;
        this.client = (DiscordClientImpl) shard.getClient();
    }

    /**
     * Deserializes the given payload and passes it to the appropriate method depending on the event name.
     *
     * @param event The json payload.
     */
    public void handle(final JsonNode event) {
        dispatchExecutor.submit(() -> {
            boolean locked = false;
            if (!client.isReady()) {
                startupLock.lock();
                locked = true;
            }
            try {
                String type = event.get("t").asText();
                JsonNode json = event.get("d");
                switch (type) {
                case "RESUMED":
                    resumed();
                    break;
                case "READY":
                    ready(MAPPER.treeToValue(json, ReadyResponse.class));
                    break;
                case "MESSAGE_CREATE":
                    messageCreate(MAPPER.treeToValue(json, MessageObject.class));
                    break;
                case "TYPING_START":
                    typingStart(MAPPER.treeToValue(json, TypingEventResponse.class));
                    break;
                case "GUILD_CREATE":
                    guildCreate(MAPPER.treeToValue(json, GuildObject.class));
                    break;
                case "GUILD_MEMBER_ADD":
                    guildMemberAdd(MAPPER.treeToValue(json, GuildMemberAddEventResponse.class));
                    break;
                case "GUILD_MEMBER_REMOVE":
                    guildMemberRemove(MAPPER.treeToValue(json, GuildMemberRemoveEventResponse.class));
                    break;
                case "GUILD_MEMBER_UPDATE":
                    guildMemberUpdate(MAPPER.treeToValue(json, GuildMemberUpdateEventResponse.class));
                    break;
                case "MESSAGE_UPDATE":
                    messageUpdate(MAPPER.treeToValue(json, MessageObject.class));
                    break;
                case "MESSAGE_DELETE":
                    messageDelete(MAPPER.treeToValue(json, MessageDeleteEventResponse.class));
                    break;
                case "MESSAGE_DELETE_BULK":
                    messageDeleteBulk(MAPPER.treeToValue(json, MessageDeleteBulkEventResponse.class));
                    break;
                case "PRESENCE_UPDATE":
                    presenceUpdate(MAPPER.treeToValue(json, PresenceUpdateEventResponse.class));
                    break;
                case "GUILD_DELETE":
                    guildDelete(MAPPER.treeToValue(json, GuildObject.class));
                    break;
                case "CHANNEL_CREATE":
                    channelCreate(MAPPER.treeToValue(json, ChannelObject.class));
                    break;
                case "CHANNEL_DELETE":
                    channelDelete(MAPPER.treeToValue(json, ChannelObject.class));
                    break;
                case "CHANNEL_PINS_UPDATE": /* Implemented in MESSAGE_UPDATE. Ignored */
                    break;
                case "CHANNEL_PINS_ACK": /* Ignored */
                    break;
                case "USER_UPDATE":
                    userUpdate(MAPPER.treeToValue(json, UserUpdateEventResponse.class));
                    break;
                case "CHANNEL_UPDATE":
                    channelUpdate(MAPPER.treeToValue(json, ChannelObject.class));
                    break;
                case "GUILD_MEMBERS_CHUNK":
                    guildMembersChunk(MAPPER.treeToValue(json, GuildMemberChunkEventResponse.class));
                    break;
                case "GUILD_UPDATE":
                    guildUpdate(MAPPER.treeToValue(json, GuildObject.class));
                    break;
                case "GUILD_ROLE_CREATE":
                    guildRoleCreate(MAPPER.treeToValue(json, GuildRoleEventResponse.class));
                    break;
                case "GUILD_ROLE_UPDATE":
                    guildRoleUpdate(MAPPER.treeToValue(json, GuildRoleEventResponse.class));
                    break;
                case "GUILD_ROLE_DELETE":
                    guildRoleDelete(MAPPER.treeToValue(json, GuildRoleDeleteEventResponse.class));
                    break;
                case "GUILD_BAN_ADD":
                    guildBanAdd(MAPPER.treeToValue(json, GuildBanEventResponse.class));
                    break;
                case "GUILD_BAN_REMOVE":
                    guildBanRemove(MAPPER.treeToValue(json, GuildBanEventResponse.class));
                    break;
                case "GUILD_EMOJIS_UPDATE":
                    guildEmojisUpdate(MAPPER.treeToValue(json, GuildEmojiUpdateResponse.class));
                    break;
                case "GUILD_INTEGRATIONS_UPDATE": /* TODO: Impl Guild integrations */
                    break;
                case "VOICE_STATE_UPDATE":
                    voiceStateUpdate(MAPPER.treeToValue(json, VoiceStateObject.class));
                    break;
                case "VOICE_SERVER_UPDATE":
                    voiceServerUpdate(MAPPER.treeToValue(json, VoiceUpdateResponse.class));
                    break;
                case "MESSAGE_REACTION_ADD":
                    reactionAdd(MAPPER.treeToValue(json, ReactionEventResponse.class));
                    break;
                case "MESSAGE_REACTION_REMOVE":
                    reactionRemove(MAPPER.treeToValue(json, ReactionEventResponse.class));
                    break;
                case "MESSAGE_REACTION_REMOVE_ALL": /* REMOVE_ALL is 204 empty but REACTION_REMOVE is sent anyway */
                    break;
                case "WEBHOOKS_UPDATE":
                    webhookUpdate(MAPPER.treeToValue(json, WebhookObject.class));
                    break;
                case "PRESENCES_REPLACE": /* Ignored. Not meant for bot accounts. */
                    break;

                default:
                    Discord4J.LOGGER.warn(LogMarkers.WEBSOCKET,
                            "Unknown message received: {}, REPORT THIS TO THE DISCORD4J DEV!", type);
                }
            } catch (Exception e) {
                Discord4J.LOGGER.error(LogMarkers.WEBSOCKET, "Unable to process JSON!", e);
            } finally {
                if (locked)
                    startupLock.unlock();
            }
        });
    }

    private void ready(ReadyResponse ready) {
        Discord4J.LOGGER.info(LogMarkers.WEBSOCKET, "Connected to Discord Gateway v{}. Receiving {} guilds.",
                ready.v, ready.guilds.length);

        ws.state = DiscordWS.State.READY;
        ws.hasReceivedReady = true; // Websocket received actual ready event
        if (client.ourUser == null)
            client.ourUser = DiscordUtils.getUserFromJSON(shard, ready.user);
        client.getDispatcher().dispatch(new LoginEvent(shard));

        new RequestBuilder(client).setAsync(true).doAction(() -> {
            ws.sessionId = ready.session_id;

            Set<UnavailableGuildObject> waitingGuilds = ConcurrentHashMap.newKeySet(ready.guilds.length);
            waitingGuilds.addAll(Arrays.asList(ready.guilds));

            final AtomicInteger loadedGuilds = new AtomicInteger(0);
            client.getDispatcher().waitFor((GuildCreateEvent e) -> {
                waitingGuilds.removeIf(g -> g.id.equals(e.getGuild().getStringID()));
                return loadedGuilds.incrementAndGet() >= ready.guilds.length;
            }, (long) Math.ceil(Math.sqrt(2 * ready.guilds.length)), TimeUnit.SECONDS);

            waitingGuilds.forEach(guild -> client.getDispatcher()
                    .dispatch(new GuildUnavailableEvent(Long.parseUnsignedLong(guild.id))));
            return true;
        }).andThen(() -> {
            if (this.shard.getInfo()[0] == 0) { // pms are only sent to shard 0
                for (ChannelObject pmObj : ready.private_channels) {
                    IPrivateChannel pm = (IPrivateChannel) DiscordUtils.getChannelFromJSON(shard, null, pmObj);
                    shard.privateChannels.put(pm);
                }
            }

            ws.isReady = true;
            client.getDispatcher().dispatch(new ShardReadyEvent(shard)); // All information for this shard has been received
            return true;
        }).execute();
    }

    private void resumed() {
        Discord4J.LOGGER.info(LogMarkers.WEBSOCKET, "Session resumed on shard " + shard.getInfo()[0]);
        ws.hasReceivedReady = true; // Technically a lie but irrelevant in the case of a resume.
        ws.isReady = true; //
        client.getDispatcher().dispatch(new ResumedEvent(shard));
    }

    private void messageCreate(MessageObject json) {
        boolean mentioned = json.mention_everyone;

        Channel channel = (Channel) client.getChannelByID(Long.parseUnsignedLong(json.channel_id));

        if (null != channel) {

            // check if our user is mentioned directly
            if (!mentioned) { //Not worth checking if already mentioned
                for (UserObject user : json.mentions) { //Check mention array for a mention
                    if (client.getOurUser().getLongID() == Long.parseUnsignedLong(user.id)) {
                        mentioned = true;
                        break;
                    }
                }
            }

            // check if our user is mentioned through role mentions
            if (!mentioned) { //Not worth checking if already mentioned
                for (String role : json.mention_roles) { //Check roles for a mention
                    if (client.getOurUser().getRolesForGuild(channel.getGuild())
                            .contains(channel.getGuild().getRoleByID(Long.parseUnsignedLong(role)))) {
                        mentioned = true;
                        break;
                    }
                }
            }

            IMessage message = DiscordUtils.getMessageFromJSON(channel, json);

            if (!channel.getMessageHistory().contains(message)) {
                Discord4J.LOGGER.debug(LogMarkers.MESSAGES, "Message from: {} ({}) in channel ID {}: {}",
                        message.getAuthor().getName(), json.author.id, json.channel_id, json.content);

                if (mentioned) {
                    client.dispatcher.dispatch(new MentionEvent(message));
                }

                channel.addToCache(message);

                if (message.getAuthor().equals(client.getOurUser())) {
                    client.dispatcher.dispatch(new MessageSendEvent(message));
                    message.getChannel().setTypingStatus(false); //Messages being sent should stop the bot from typing
                } else {
                    client.dispatcher.dispatch(new MessageReceivedEvent(message));
                    if (!message.getEmbeds().isEmpty()) {
                        client.dispatcher.dispatch(new MessageEmbedEvent(null, message, new ArrayList<>()));
                    }
                }
            }
        }
    }

    private void typingStart(TypingEventResponse event) {
        User user;
        Channel channel = (Channel) client.getChannelByID(Long.parseUnsignedLong(event.channel_id));
        if (channel != null) {
            if (channel.isPrivate()) {
                user = (User) ((IPrivateChannel) channel).getRecipient();
            } else {
                user = (User) channel.getGuild().getUserByID(Long.parseUnsignedLong(event.user_id));
            }

            if (user != null) {
                client.dispatcher.dispatch(new TypingEvent(user, channel));
            }
        }
    }

    private void guildCreate(GuildObject json) {
        if (json.unavailable) { //Guild can't be reached, so we ignore it
            Discord4J.LOGGER.warn(LogMarkers.WEBSOCKET,
                    "Guild with id {} is unavailable, ignoring it. Is there an outage?", json.id);
            return;
        }

        Guild guild = (Guild) DiscordUtils.getGuildFromJSON(shard, json);
        shard.guildCache.put(guild);

        new RequestBuilder(client).setAsync(true).doAction(() -> {
            try {
                if (json.large) {
                    shard.ws.send(GatewayOps.REQUEST_GUILD_MEMBERS, new GuildMembersRequest(json.id));
                    client.getDispatcher()
                            .waitFor((AllUsersReceivedEvent e) -> e.getGuild().getLongID() == guild.getLongID());
                }
            } catch (InterruptedException e) {
                Discord4J.LOGGER.error(LogMarkers.EVENTS,
                        "Wait for AllUsersReceivedEvent on guild create was interrupted.", e);
            }
            return true;
        }).andThen(() -> {
            guild.loadWebhooks();
            client.dispatcher.dispatch(new GuildCreateEvent(guild));
            Discord4J.LOGGER.debug(LogMarkers.EVENTS,
                    "New guild has been created/joined! \"{}\" with ID {} on shard {}.", guild.getName(),
                    guild.getStringID(), shard.getInfo()[0]);
            return true;
        }).execute();
    }

    private void guildMemberAdd(GuildMemberAddEventResponse event) {
        long guildID = Long.parseUnsignedLong(event.guild_id);
        Guild guild = (Guild) client.getGuildByID(guildID);
        if (guild != null) {
            User user = (User) DiscordUtils.getUserFromGuildMemberResponse(guild,
                    new MemberObject(event.user, event.roles));
            guild.users.put(user);
            guild.setTotalMemberCount(guild.getTotalMemberCount() + 1);
            Instant timestamp = DiscordUtils.convertFromTimestamp(event.joined_at);
            Discord4J.LOGGER.debug(LogMarkers.EVENTS, "User \"{}\" joined guild \"{}\".", user.getName(),
                    guild.getName());
            client.dispatcher.dispatch(new UserJoinEvent(guild, user, timestamp));
        }
    }

    private void guildMemberRemove(GuildMemberRemoveEventResponse event) {
        long guildID = Long.parseUnsignedLong(event.guild_id);
        Guild guild = (Guild) client.getGuildByID(guildID);
        if (guild != null) {
            User user = (User) guild.getUserByID(Long.parseUnsignedLong(event.user.id));
            if (user != null) {
                guild.users.remove(user);
                guild.joinTimes.remove(user);
                user.roles.remove(guild);
                guild.setTotalMemberCount(guild.getTotalMemberCount() - 1);
                Discord4J.LOGGER.debug(LogMarkers.EVENTS, "User \"{}\" has been removed from or left guild \"{}\".",
                        user.getName(), guild.getName());
                client.dispatcher.dispatch(new UserLeaveEvent(guild, user));
            }
        }
    }

    private void guildMemberUpdate(GuildMemberUpdateEventResponse event) {
        Guild guild = (Guild) client.getGuildByID(Long.parseUnsignedLong(event.guild_id));
        User user = (User) client.getUserByID(Long.parseUnsignedLong(event.user.id));

        if (guild != null && user != null) {
            List<IRole> oldRoles = user.getRolesForGuild(guild);
            boolean rolesChanged = oldRoles.size() != event.roles.length + 1;//Add one for the @everyone role
            if (!rolesChanged) {
                rolesChanged = oldRoles.stream().filter(role -> {
                    if (role.equals(guild.getEveryoneRole()))
                        return false;

                    for (String roleID : event.roles) {
                        if (role.getLongID() == Long.parseUnsignedLong(roleID)) {
                            return false;
                        }
                    }

                    return true;
                }).collect(Collectors.toList()).size() > 0;
            }

            if (rolesChanged) {
                user.roles.remove(guild);

                for (String role : event.roles)
                    user.addRole(guild.getLongID(), guild.getRoleByID(Long.parseUnsignedLong(role)));

                user.addRole(guild.getLongID(), guild.getEveryoneRole());

                client.dispatcher
                        .dispatch(new UserRoleUpdateEvent(guild, user, oldRoles, user.getRolesForGuild(guild)));

                if (user.equals(client.getOurUser()))
                    guild.loadWebhooks();
            }

            String oldNick = user.getNicknameForGuild(guild);
            if ((oldNick == null ^ event.nick == null) || (oldNick != null && !oldNick.equals(event.nick))
                    || event.nick != null && !event.nick.equals(oldNick)) {
                user.addNick(guild.getLongID(), event.nick);
                client.dispatcher.dispatch(new NicknameChangedEvent(guild, user, oldNick, event.nick));
            }
        }
    }

    private void messageUpdate(MessageObject json) {
        Channel channel = (Channel) client.getChannelByID(Long.parseUnsignedLong(json.channel_id));
        if (channel == null)
            return;

        IMessage toUpdate = channel.messages.get(json.id);

        if (toUpdate == null) { // Cannot resolve update type. MessageObject is incomplete, so we'll have to request for the full message.
            if (channel.isPrivate() || PermissionUtils.hasHierarchicalPermissions(channel, client.ourUser,
                    channel.getGuild().getRolesForUser(client.ourUser), Permissions.READ_MESSAGE_HISTORY))
                client.dispatcher.dispatch(
                        new MessageUpdateEvent(null, channel.fetchMessage(Long.parseUnsignedLong(json.id))));
            //         else
            //FIXME: unable to fire message update events when the user doesn't have the read message history permission
        } else {
            IMessage oldMessage = toUpdate.copy();
            IMessage updatedMessage = DiscordUtils.getUpdatedMessageFromJSON(client, toUpdate, json);
            if (json.pinned != null && oldMessage.isPinned() && !json.pinned) {
                client.dispatcher.dispatch(new MessageUnpinEvent(oldMessage, updatedMessage));
            } else if (json.pinned != null && !oldMessage.isPinned() && json.pinned) {
                client.dispatcher.dispatch(new MessagePinEvent(oldMessage, updatedMessage));
            } else if (oldMessage.getEmbeds().size() < updatedMessage.getEmbeds().size()) {
                client.dispatcher
                        .dispatch(new MessageEmbedEvent(oldMessage, updatedMessage, oldMessage.getEmbeds()));
            } else if (json.content != null && !oldMessage.getContent().equals(json.content)) {
                client.dispatcher.dispatch(new MessageEditEvent(oldMessage, updatedMessage));
            } else {
                client.dispatcher.dispatch(new MessageUpdateEvent(oldMessage, updatedMessage));
            }
        }
    }

    private void messageDelete(MessageDeleteEventResponse event) {
        long id = Long.parseUnsignedLong(event.id);
        Channel channel = (Channel) client.getChannelByID(Long.parseUnsignedLong(event.channel_id));
        IMessage message = null;

        if (channel != null) {
            message = channel.messages.get(id);
        }

        if (message == null) { // we dont have the message cached. The only thing we know about the message is its ID and its channel's ID.
            client.dispatcher.dispatch(new MessageDeleteEvent(channel, id));
        } else {
            channel.messages.remove(id);
            client.dispatcher.dispatch(new MessageDeleteEvent(message));
        }
    }

    private void messageDeleteBulk(MessageDeleteBulkEventResponse event) { //TODO: maybe add a separate event for this?
        for (String id : event.ids) {
            messageDelete(new MessageDeleteEventResponse(id, event.channel_id));
        }
    }

    private void presenceUpdate(PresenceUpdateEventResponse event) {
        IPresence presence = DiscordUtils.getPresenceFromJSON(event);
        Guild guild = event.guild_id == null ? null
                : (Guild) client.getGuildByID(Long.parseUnsignedLong(event.guild_id));
        if (guild != null) {
            User user = (User) guild.getUserByID(Long.parseUnsignedLong(event.user.id));
            if (user != null) {
                if (event.user.username != null) { //Full object was sent so there is a user change, otherwise all user fields but id would be null
                    IUser oldUser = user.copy();
                    user = DiscordUtils.getUserFromJSON(shard, event.user);
                    client.dispatcher.dispatch(new UserUpdateEvent(oldUser, user));
                }

                if (!user.getPresence().equals(presence)) {
                    IPresence oldPresence = user.getPresence();
                    user.setPresence(presence);
                    client.dispatcher.dispatch(new PresenceUpdateEvent(user, oldPresence, presence));
                    Discord4J.LOGGER.debug(LogMarkers.PRESENCES, "User \"{}\" changed presence to {}",
                            user.getName(), user.getPresence());
                }
            }
        }
    }

    private void guildDelete(GuildObject json) {
        long guildId = Long.parseUnsignedLong(json.id);
        Guild guild = (Guild) client.getGuildByID(guildId);

        // Clean up cache
        if (guild != null) {
            ((ShardImpl) guild.getShard()).guildCache.remove(guild);
            ((User) client.getOurUser()).voiceStates.remove(guild.getLongID());
            DiscordVoiceWS vWS = shard.voiceWebSockets.get(guildId);
            if (vWS != null) {
                vWS.disconnect(VoiceDisconnectedEvent.Reason.LEFT_CHANNEL);
                shard.voiceWebSockets.remove(guildId);
            }
        }

        if (json.unavailable) { //Guild can't be reached
            Discord4J.LOGGER.warn(LogMarkers.WEBSOCKET, "Guild with id {} is unavailable, is there an outage?",
                    json.id);
            client.dispatcher.dispatch(new GuildUnavailableEvent(guildId));
        } else {
            Discord4J.LOGGER.debug(LogMarkers.EVENTS, "You have been kicked from or left \"{}\"! :O",
                    guild.getName());
            client.dispatcher.dispatch(new GuildLeaveEvent(guild));
        }
    }

    private void channelCreate(ChannelObject json) {
        if (json.type == ChannelObject.Type.PRIVATE) {
            if (!shard.privateChannels.containsKey(json.id)) {
                shard.privateChannels.put((IPrivateChannel) DiscordUtils.getChannelFromJSON(shard, null, json));
            }
        } else {
            Guild guild = (Guild) shard.getGuildByID(Long.parseUnsignedLong(json.guild_id));
            if (guild != null) {
                IChannel channel = DiscordUtils.getChannelFromJSON(shard, guild, json);
                if (json.type == ChannelObject.Type.GUILD_TEXT) {
                    guild.channels.put(channel);
                    client.dispatcher.dispatch(new ChannelCreateEvent(channel));
                } else if (json.type == ChannelObject.Type.GUILD_VOICE) {
                    guild.voiceChannels.put((IVoiceChannel) channel);
                    client.dispatcher.dispatch(new VoiceChannelCreateEvent((IVoiceChannel) channel));
                } else if (json.type == ChannelObject.Type.GUILD_CATEGORY) {
                    ICategory category = DiscordUtils.getCategoryFromJSON(shard, guild, json);
                    guild.categories.put(category);
                    client.dispatcher.dispatch(new CategoryCreateEvent(category));
                }
            }
        }
    }

    private void channelDelete(ChannelObject json) {
        if (json.type == ChannelObject.Type.GUILD_TEXT) {
            Channel channel = (Channel) client.getChannelByID(Long.parseUnsignedLong(json.id));
            if (channel != null) {
                if (!channel.isPrivate())
                    ((Guild) channel.getGuild()).channels.remove(channel);
                else
                    shard.privateChannels.remove(channel);
                client.dispatcher.dispatch(new ChannelDeleteEvent(channel));
            }
        } else if (json.type == ChannelObject.Type.GUILD_VOICE) {
            VoiceChannel channel = (VoiceChannel) client.getVoiceChannelByID(Long.parseUnsignedLong(json.id));
            if (channel != null) {
                ((Guild) channel.getGuild()).voiceChannels.remove(channel);
                client.dispatcher.dispatch(new VoiceChannelDeleteEvent(channel));
            }
        } else if (json.type == ChannelObject.Type.GUILD_CATEGORY) {
            ICategory category = client.getCategoryByID(Long.parseUnsignedLong(json.id));
            if (category != null) {
                ((Guild) category.getGuild()).categories.remove(category);
                client.dispatcher.dispatch(new CategoryDeleteEvent(category));
            }
        }
    }

    private void userUpdate(UserUpdateEventResponse event) {
        User newUser = (User) client.getUserByID(Long.parseUnsignedLong(event.id));
        if (newUser != null) {
            IUser oldUser = newUser.copy();
            newUser = DiscordUtils.getUserFromJSON(shard, event);
            client.dispatcher.dispatch(new UserUpdateEvent(oldUser, newUser));
        }
    }

    private void channelUpdate(ChannelObject json) {
        if (json.type == ChannelObject.Type.GUILD_TEXT) {
            Channel toUpdate = (Channel) shard.getChannelByID(Long.parseUnsignedLong(json.id));
            if (toUpdate != null) {
                IChannel oldChannel = toUpdate.copy();
                toUpdate = (Channel) DiscordUtils.getChannelFromJSON(shard, toUpdate.getGuild(), json);
                toUpdate.loadWebhooks();

                if (!Objects.equals(oldChannel.getCategory(), toUpdate.getCategory())) {
                    client.dispatcher.dispatch(new ChannelCategoryUpdateEvent(oldChannel, toUpdate,
                            oldChannel.getCategory(), toUpdate.getCategory()));
                } else {
                    client.dispatcher.dispatch(new ChannelUpdateEvent(oldChannel, toUpdate));
                }
            }
        } else if (json.type == ChannelObject.Type.GUILD_VOICE) {
            IVoiceChannel toUpdate = shard.getVoiceChannelByID(Long.parseUnsignedLong(json.id));
            if (toUpdate != null) {
                IVoiceChannel oldChannel = toUpdate.copy();
                toUpdate = (IVoiceChannel) DiscordUtils.getChannelFromJSON(shard, toUpdate.getGuild(), json);

                if (!Objects.equals(oldChannel.getCategory(), toUpdate.getCategory())) {
                    client.dispatcher.dispatch(new ChannelCategoryUpdateEvent(oldChannel, toUpdate,
                            oldChannel.getCategory(), toUpdate.getCategory()));
                } else {
                    client.dispatcher.dispatch(new VoiceChannelUpdateEvent(oldChannel, toUpdate));
                }
            }
        } else if (json.type == ChannelObject.Type.GUILD_CATEGORY) {
            ICategory toUpdate = shard.getCategoryByID(Long.parseUnsignedLong(json.id));
            if (toUpdate != null) {
                ICategory oldCategory = toUpdate.copy();
                toUpdate = DiscordUtils.getCategoryFromJSON(shard, toUpdate.getGuild(), json);
                client.dispatcher.dispatch(new CategoryUpdateEvent(oldCategory, toUpdate));
            }
        }
    }

    private void guildMembersChunk(GuildMemberChunkEventResponse event) {
        Guild guildToUpdate = (Guild) client.getGuildByID(Long.parseUnsignedLong(event.guild_id));
        if (guildToUpdate == null) {
            Discord4J.LOGGER.warn(LogMarkers.WEBSOCKET,
                    "Can't receive guild members chunk for guild id {}, the guild is null!", event.guild_id);
            return;
        }

        for (MemberObject member : event.members) {
            IUser user = DiscordUtils.getUserFromGuildMemberResponse(guildToUpdate, member);
            guildToUpdate.users.put(user);
        }
        if (guildToUpdate.getUsers().size() >= guildToUpdate.getTotalMemberCount()) {
            client.getDispatcher().dispatch(new AllUsersReceivedEvent(guildToUpdate));
        }
    }

    private void guildUpdate(GuildObject json) {
        Guild toUpdate = (Guild) client.getGuildByID(Long.parseUnsignedLong(json.id));

        if (toUpdate != null) {
            IGuild oldGuild = toUpdate.copy();

            toUpdate = (Guild) DiscordUtils.getGuildFromJSON(shard, json);

            if (toUpdate.getOwnerLongID() != oldGuild.getOwnerLongID()) {
                client.dispatcher.dispatch(
                        new GuildTransferOwnershipEvent(oldGuild.getOwner(), toUpdate.getOwner(), toUpdate));
            } else {
                client.dispatcher.dispatch(new GuildUpdateEvent(oldGuild, toUpdate));
            }
        }
    }

    private void guildRoleCreate(GuildRoleEventResponse event) {
        IGuild guild = client.getGuildByID(Long.parseUnsignedLong(event.guild_id));
        if (guild != null) {
            IRole role = DiscordUtils.getRoleFromJSON(guild, event.role);
            client.dispatcher.dispatch(new RoleCreateEvent(role));
        }
    }

    private void guildRoleUpdate(GuildRoleEventResponse event) {
        IGuild guild = client.getGuildByID(Long.parseUnsignedLong(event.guild_id));
        if (guild != null) {
            IRole toUpdate = guild.getRoleByID(Long.parseUnsignedLong(event.role.id));
            if (toUpdate != null) {
                IRole oldRole = toUpdate.copy();
                toUpdate = DiscordUtils.getRoleFromJSON(guild, event.role);
                client.dispatcher.dispatch(new RoleUpdateEvent(oldRole, toUpdate));

                if (guild.getRolesForUser(client.getOurUser()).contains(toUpdate))
                    ((Guild) guild).loadWebhooks();
            }
        }
    }

    private void guildRoleDelete(GuildRoleDeleteEventResponse event) {
        Guild guild = (Guild) client.getGuildByID(Long.parseUnsignedLong(event.guild_id));
        if (guild != null) {
            IRole role = guild.getRoleByID(Long.parseUnsignedLong(event.role_id));
            if (role != null) {
                guild.roles.remove(role);
                client.dispatcher.dispatch(new RoleDeleteEvent(role));
            }
        }
    }

    private void guildBanAdd(GuildBanEventResponse event) {
        Guild guild = (Guild) client.getGuildByID(Long.parseUnsignedLong(event.guild_id));
        if (guild != null) {
            IUser user = DiscordUtils.getUserFromJSON(shard, event.user);
            if (guild.getUserByID(user.getLongID()) != null) {
                guild.users.remove(user);
                guild.joinTimes.remove(user);
            }

            client.dispatcher.dispatch(new UserBanEvent(guild, user));
        }
    }

    private void guildBanRemove(GuildBanEventResponse event) {
        IGuild guild = client.getGuildByID(Long.parseUnsignedLong(event.guild_id));
        if (guild != null) {
            IUser user = DiscordUtils.getUserFromJSON(shard, event.user);

            client.dispatcher.dispatch(new UserPardonEvent(guild, user));
        }
    }

    private void voiceStateUpdate(VoiceStateObject json) {
        User user = (User) shard.getUserByID(Long.parseUnsignedLong(json.user_id));

        if (user != null) {
            IVoiceState curVoiceState = user.voiceStates.get(json.guild_id);

            IVoiceChannel channel = json.channel_id != null
                    ? shard.getVoiceChannelByID(Long.parseUnsignedLong(json.channel_id))
                    : null;
            IVoiceChannel oldChannel = curVoiceState == null ? null : curVoiceState.getChannel();

            user.voiceStates.put(DiscordUtils
                    .getVoiceStateFromJson(shard.getGuildByID(Long.parseUnsignedLong(json.guild_id)), json));

            if (oldChannel != channel) {
                if (channel == null) {
                    client.getDispatcher().dispatch(new UserVoiceChannelLeaveEvent(oldChannel, user));
                } else if (oldChannel == null) {
                    ((Guild) channel.getGuild()).connectingVoiceChannelID = 0;
                    client.getDispatcher().dispatch(new UserVoiceChannelJoinEvent(channel, user));
                } else if (oldChannel.getGuild().equals(channel.getGuild())) {
                    client.getDispatcher().dispatch(new UserVoiceChannelMoveEvent(user, oldChannel, channel));
                }
            }
        }
    }

    private void voiceServerUpdate(VoiceUpdateResponse event) {
        DiscordVoiceWS oldWS = shard.voiceWebSockets.get(event.guild_id);
        if (oldWS != null) {
            oldWS.disconnect(VoiceDisconnectedEvent.Reason.SERVER_UPDATE);
        }

        if (event.endpoint == null) {
            Discord4J.LOGGER.debug(LogMarkers.VOICE, "Awaiting endpoint to join voice channel in guild id {}...",
                    event.guild_id);
        } else {
            Discord4J.LOGGER.trace(LogMarkers.VOICE, "Voice endpoint received! Connecting to {}...",
                    event.endpoint);
            DiscordVoiceWS vWS = new DiscordVoiceWS(shard, event);
            shard.voiceWebSockets.put(vWS);
            vWS.connect();
        }
    }

    private void guildEmojisUpdate(GuildEmojiUpdateResponse event) {
        Guild guild = (Guild) shard.getGuildByID(Long.parseUnsignedLong(event.guild_id));

        if (guild != null) {
            List<IEmoji> oldEmoji = guild.getEmojis();
            List<IEmoji> newEmoji = Arrays.stream(event.emojis).map(e -> DiscordUtils.getEmojiFromJSON(guild, e))
                    .collect(Collectors.toList());

            guild.emojis.clear();
            guild.emojis.putAll(newEmoji);

            client.dispatcher.dispatch(new GuildEmojisUpdateEvent(guild, oldEmoji, newEmoji));
        }
    }

    private void reactionAdd(ReactionEventResponse event) {
        IChannel channel = shard.getChannelByID(Long.parseUnsignedLong(event.channel_id));
        if (channel == null)
            return;
        if (!PermissionUtils.hasPermissions(channel, client.ourUser, Permissions.READ_MESSAGES,
                Permissions.READ_MESSAGE_HISTORY))
            return; // Discord sends this event no matter our permissions for some reason.

        boolean cached = ((Channel) channel).messages.containsKey(Long.parseUnsignedLong(event.message_id));
        IMessage message = channel.fetchMessage(Long.parseUnsignedLong(event.message_id));
        if (message == null) {
            Discord4J.LOGGER.debug("Unable to fetch the message specified by a reaction add event\nObject={}",
                    ToStringBuilder.reflectionToString(event));
            return;
        }
        IReaction reaction = event.emoji.id == null ? message.getReactionByUnicode(event.emoji.name)
                : message.getReactionByID(Long.parseUnsignedLong(event.emoji.id));
        message.getReactions().remove(reaction);

        if (reaction == null) { // Only happens in the case of a cached message with a new reaction
            long id = event.emoji.id == null ? 0 : Long.parseUnsignedLong(event.emoji.id);
            reaction = new Reaction(message, 1, ReactionEmoji.of(event.emoji.name, id));
        } else if (cached) {
            reaction = new Reaction(message, reaction.getCount() + 1, reaction.getEmoji());
        }
        message.getReactions().add(reaction);

        IUser user;
        if (channel.isPrivate()) {
            user = channel.getUsersHere().get(
                    channel.getUsersHere().get(0).getLongID() == Long.parseUnsignedLong(event.user_id) ? 0 : 1);
        } else {
            user = channel.getGuild().getUserByID(Long.parseUnsignedLong(event.user_id));
        }

        client.dispatcher.dispatch(new ReactionAddEvent(message, reaction, user));
    }

    private void reactionRemove(ReactionEventResponse event) {
        IChannel channel = shard.getChannelByID(Long.parseUnsignedLong(event.channel_id));
        if (channel == null)
            return;
        if (!PermissionUtils.hasPermissions(channel, client.ourUser, Permissions.READ_MESSAGES,
                Permissions.READ_MESSAGE_HISTORY))
            return; // Discord sends this event no matter our permissions for some reason.

        boolean cached = ((Channel) channel).messages.containsKey(Long.parseUnsignedLong(event.message_id));
        IMessage message = channel.fetchMessage(Long.parseUnsignedLong(event.message_id));
        if (message == null) {
            Discord4J.LOGGER.debug("Unable to fetch the message specified by a reaction remove event\nObject={}",
                    ToStringBuilder.reflectionToString(event));
            return;
        }
        IReaction reaction = event.emoji.id == null ? message.getReactionByUnicode(event.emoji.name)
                : message.getReactionByID(Long.parseUnsignedLong(event.emoji.id));
        message.getReactions().remove(reaction);

        if (reaction == null) { // the last reaction of the emoji was removed
            long id = event.emoji.id == null ? 0 : Long.parseUnsignedLong(event.emoji.id);
            reaction = new Reaction(message, 0, ReactionEmoji.of(event.emoji.name, id));
        } else {
            reaction = new Reaction(message, !cached ? reaction.getCount() : reaction.getCount() - 1,
                    reaction.getEmoji());
        }

        if (reaction.getCount() > 0) {
            message.getReactions().add(reaction);
        }

        IUser user;
        if (channel.isPrivate()) {
            user = channel.getUsersHere().get(
                    channel.getUsersHere().get(0).getLongID() == Long.parseUnsignedLong(event.user_id) ? 0 : 1);
        } else {
            user = channel.getGuild().getUserByID(Long.parseUnsignedLong(event.user_id));
        }

        client.dispatcher.dispatch(new ReactionRemoveEvent(message, reaction, user));
    }

    private void webhookUpdate(WebhookObject event) {
        Channel channel = (Channel) client.getChannelByID(Long.parseUnsignedLong(event.channel_id));
        if (channel != null)
            channel.loadWebhooks();
    }
}