Files
discordeno/module/shardingManager.ts
2020-05-21 23:22:09 -04:00

595 lines
18 KiB
TypeScript

import {
DiscordBotGatewayData,
DiscordPayload,
GatewayOpcode,
PresenceUpdatePayload,
TypingStartPayload,
VoiceStateUpdatePayload,
WebhookUpdatePayload,
} from "../types/discord.ts";
import {
eventHandlers,
botGatewayData,
identifyPayload,
botID,
} from "./client.ts";
import { delay } from "https://deno.land/std@0.50.0/async/delay.ts";
import {
handleInternalChannelCreate,
handleInternalChannelUpdate,
handleInternalChannelDelete,
} from "../events/channels.ts";
import { ChannelCreatePayload } from "../types/channel.ts";
import { createGuild } from "../structures/guild.ts";
import {
CreateGuildPayload,
GuildDeletePayload,
GuildBanPayload,
GuildEmojisUpdatePayload,
GuildMemberAddPayload,
GuildMemberUpdatePayload,
GuildMemberChunkPayload,
GuildRolePayload,
UserPayload,
FetchMembersOptions,
GuildRoleDeletePayload,
} from "../types/guild.ts";
import {
handleInternalGuildCreate,
handleInternalGuildUpdate,
handleInternalGuildDelete,
} from "../events/guilds.ts";
import { cache } from "../utils/cache.ts";
import { createUser } from "../structures/user.ts";
import { createMember } from "../structures/member.ts";
import { createRole } from "../structures/role.ts";
import {
MessageCreateOptions,
MessageDeletePayload,
MessageDeleteBulkPayload,
MessageUpdatePayload,
MessageReactionPayload,
BaseMessageReactionPayload,
MessageReactionRemoveEmojiPayload,
} from "../types/message.ts";
import { createMessage } from "../structures/message.ts";
let shardCounter = 0;
export interface FetchAllMembersRequest {
resolve: Function;
requestedMax: number;
receivedAmount: number;
}
const fetchAllMembersProcessingRequests = new Map<
string,
FetchAllMembersRequest
>();
const shards: Worker[] = [];
let createNextShard = true;
export function createShardWorker(shardID?: number) {
const path = new URL("./shard.ts", import.meta.url).toString();
const shard = new Worker(path, { type: "module", deno: true });
shard.onmessage = (message) => {
if (message.data.type === "REQUEST_CLIENT_OPTIONS") {
identifyPayload.shard = [
shardID || shardCounter++,
botGatewayData.shards,
];
shard.postMessage(
{
type: "CREATE_SHARD",
botGatewayData,
identifyPayload,
},
);
} else if (message.data.type === "HANDLE_DISCORD_PAYLOAD") {
handleDiscordPayload(JSON.parse(message.data.payload));
}
};
shards.push(shard);
}
export const spawnShards = async (
data: DiscordBotGatewayData,
payload: unknown,
id = 1,
) => {
if (id < data.shards) {
if (createNextShard) {
createNextShard = false;
createShardWorker();
spawnShards(data, payload, id + 1);
} else {
await delay(1000);
spawnShards(data, payload, id);
}
}
};
async function handleDiscordPayload(data: DiscordPayload) {
eventHandlers.raw?.(data);
switch (data.op) {
case GatewayOpcode.HeartbeatACK:
// Incase the user wants to listen to heartbeat responses
return eventHandlers.heartbeat?.();
case GatewayOpcode.Dispatch:
if (data.t === "READY") {
// Triggered on each shard
eventHandlers.ready?.();
// Wait 5 seconds to spawn next shard
await delay(5000);
createNextShard = true;
}
if (data.t === "CHANNEL_CREATE") {
return handleInternalChannelCreate(data.d as ChannelCreatePayload);
}
if (data.t === "CHANNEL_UPDATE") {
return handleInternalChannelUpdate(data.d as ChannelCreatePayload);
}
if (data.t === "CHANNEL_DELETE") {
return handleInternalChannelDelete(data.d as ChannelCreatePayload);
}
if (data.t === "GUILD_CREATE") {
const guild = createGuild(data.d as CreateGuildPayload);
handleInternalGuildCreate(guild);
if (cache.unavailableGuilds.get(guild.id)) {
return cache.unavailableGuilds.delete(guild.id);
}
return eventHandlers.guildCreate?.(guild);
}
if (data.t === "GUILD_UPDATE") {
const options = data.d as CreateGuildPayload;
const cachedGuild = cache.guilds.get(options.id);
const guild = createGuild(options);
handleInternalGuildUpdate(guild);
if (!cachedGuild) return;
return eventHandlers.guildUpdate?.(guild, cachedGuild);
}
if (data.t === "GUILD_DELETE") {
const options = data.d as GuildDeletePayload;
const guild = cache.guilds.get(options.id);
if (!guild) return;
guild.channels.forEach((channel) => cache.channels.delete(channel.id));
if (options.unavailable) {
return cache.unavailableGuilds.set(options.id, Date.now());
}
handleInternalGuildDelete(guild);
return eventHandlers.guildDelete?.(guild);
}
if (data.t && ["GUILD_BAN_ADD", "GUILD_BAN_REMOVE"].includes(data.t)) {
const options = data.d as GuildBanPayload;
const guild = cache.guilds.get(options.guild_id);
if (!guild) return;
const user = createUser(options.user);
return data.t === "GUILD_BAN_ADD"
? eventHandlers.guildBanAdd?.(guild, user)
: eventHandlers.guildBanRemove?.(guild, user);
}
if (data.t === "GUILD_EMOJIS_UPDATE") {
const options = data.d as GuildEmojisUpdatePayload;
const guild = cache.guilds.get(options.guild_id);
if (!guild) return;
const cachedEmojis = guild.emojis;
guild.emojis = options.emojis;
return eventHandlers.guildEmojisUpdate?.(
guild,
options.emojis,
cachedEmojis,
);
}
if (data.t === "GUILD_MEMBER_ADD") {
const options = data.d as GuildMemberAddPayload;
const guild = cache.guilds.get(options.guild_id);
if (!guild) return;
const memberCount = guild.memberCount + 1;
guild.memberCount = memberCount;
const member = createMember(
options,
guild,
);
guild.members.set(options.user.id, member);
cache.users.set(options.user.id, createUser(member.user));
return eventHandlers.guildMemberAdd?.(guild, member);
}
if (data.t === "GUILD_MEMBER_REMOVE") {
const options = data.d as GuildBanPayload;
const guild = cache.guilds.get(options.guild_id);
if (!guild) return;
const memberCount = guild.memberCount - 1;
guild.memberCount = memberCount;
const member = guild.members.get(options.user.id);
return eventHandlers.guildMemberRemove?.(
guild,
member || createUser(options.user),
);
}
if (data.t === "GUILD_MEMBER_UPDATE") {
const options = data.d as GuildMemberUpdatePayload;
const guild = cache.guilds.get(options.guild_id);
if (!guild) return;
const cachedMember = guild.members.get(options.user.id);
const newMemberData = {
...options,
premium_since: options.premium_since || undefined,
joined_at: new Date(cachedMember?.joined_at || Date.now())
.toISOString(),
deaf: cachedMember?.deaf || false,
mute: cachedMember?.mute || false,
};
const member = createMember(
newMemberData,
guild,
);
guild.members.set(options.user.id, member);
cache.users.set(options.user.id, createUser(member.user));
if (cachedMember?.nick !== options.nick) {
eventHandlers.nicknameUpdate?.(
guild,
member,
options.nick,
cachedMember?.nick,
);
}
const roleIDs = cachedMember?.roles || [];
roleIDs.forEach((id) => {
if (!options.roles.includes(id)) {
eventHandlers.roleLost?.(guild, member, id);
}
});
options.roles.forEach((id) => {
if (!roleIDs.includes(id)) {
eventHandlers.roleGained?.(guild, member, id);
}
});
return eventHandlers.guildMemberUpdate?.(guild, member, cachedMember);
}
if (data.t === "GUILD_MEMBERS_CHUNK") {
const options = data.d as GuildMemberChunkPayload;
const guild = cache.guilds.get(options.guild_id);
if (!guild) return;
options.members.forEach((member) => {
guild.members.set(
member.user.id,
createMember(
member,
guild,
),
);
cache.users.set(member.user.id, createUser(member.user));
});
// Check if its necessary to resolve the fetchmembers promise for this chunk or if more chunks will be coming
if (
options.nonce
) {
const request = fetchAllMembersProcessingRequests.get(options.nonce);
if (!request) return;
if (options.chunk_index + 1 === options.chunk_count) {
fetchAllMembersProcessingRequests.delete(options.nonce);
request.resolve();
}
}
}
if (data.t === "GUILD_ROLE_DELETE") {
const options = data.d as GuildRoleDeletePayload;
const guild = cache.guilds.get(options.guild_id);
if (!guild) return;
const cachedRole = guild.roles.get(options.role_id)!;
guild.roles.delete(options.role_id);
return eventHandlers.roleDelete?.(guild, cachedRole);
}
if (
data.t &&
["GUILD_ROLE_CREATE", "GUILD_ROLE_UPDATE"]
.includes(data.t)
) {
const options = data.d as GuildRolePayload;
const guild = cache.guilds.get(options.guild_id);
if (!guild) return;
if (data.t === "GUILD_ROLE_CREATE") {
const role = createRole(options.role);
const roles = guild.roles.set(options.role.id, role);
guild.roles = roles;
return eventHandlers.roleCreate?.(guild, role);
}
const cachedRole = guild.roles.get(options.role.id);
if (!cachedRole) return;
if (data.t === "GUILD_ROLE_UPDATE") {
const role = createRole(options.role);
return eventHandlers.roleUpdate?.(guild, role, cachedRole);
}
}
if (data.t === "MESSAGE_CREATE") {
const options = data.d as MessageCreateOptions;
const channel = cache.channels.get(options.channel_id);
if (channel) channel.last_message_id = options.id;
// Cache the message author themself
cache.users.set(options.author.id, createUser(options.author));
const message = createMessage(options);
// Cache the message
cache.messages.set(options.id, message);
const guild = options.guild_id
? cache.guilds.get(options.guild_id)
: undefined;
if (options.member) {
// If in a guild cache the author as a member
guild?.members.set(
options.author.id,
createMember(
{ ...options.member, user: options.author },
guild,
),
);
}
options.mentions.forEach((mention) => {
// For each mention cache the user
cache.users.set(mention.id, createUser(mention));
// Cache the member if its a valid member
if (mention.member) {
guild?.members.set(
mention.id,
createMember({ ...mention.member, user: mention }, guild),
);
}
});
return eventHandlers.messageCreate?.(message);
}
if (
data.t && ["MESSAGE_DELETE", "MESSAGE_DELETE_BULK"].includes(data.t)
) {
const options = data.d as MessageDeletePayload;
const deletedMessages = data.t === "MESSAGE_DELETE"
? [options.id]
: (data.d as MessageDeleteBulkPayload).ids;
const channel = cache.channels.get(options.channel_id);
if (!channel) return;
deletedMessages.forEach((id) => {
const message = cache.messages.get(id);
if (!message) return;
eventHandlers.messageDelete?.(message || { id, channel });
cache.messages.delete(id);
});
}
if (data.t === "MESSAGE_UPDATE") {
const options = data.d as MessageUpdatePayload;
const channel = cache.channels.get(options.channel_id);
if (!channel) return;
// const cachedMessage = channel.messages().get(options.id)
// return eventHandlers.message_update?.(message, cachedMessage)
}
if (
data.t &&
["MESSAGE_REACTION_ADD", "MESSAGE_REACTION_REMOVE"].includes(data.t)
) {
const options = data.d as MessageReactionPayload;
const message = cache.messages.get(options.message_id);
const isAdd = data.t === "MESSAGE_REACTION_ADD";
if (message) {
const previousReactions = message.reactions;
const reactionExisted = previousReactions?.find(
(reaction) =>
reaction.emoji.id === options.emoji.id &&
reaction.emoji.name === options.emoji.name,
);
if (reactionExisted) {
reactionExisted.count = isAdd
? reactionExisted.count + 1
: reactionExisted.count - 1;
} else {
const newReaction = {
count: 1,
me: options.user_id === botID,
emoji: { ...options.emoji, id: options.emoji.id || undefined },
};
message.reactions = message.reactions
? [...message.reactions, newReaction]
: [newReaction];
}
cache.messages.set(options.message_id, message);
}
if (options.member && options.guild_id) {
const guild = cache.guilds.get(options.guild_id);
if (guild) {
const member = createMember(
options.member,
guild,
);
guild.members.set(
options.member.user.id,
member,
);
cache.users.set(options.member.user.id, createUser(member.user));
}
}
return isAdd
? eventHandlers.reactionAdd?.(
message || options,
options.emoji,
options.user_id,
)
: eventHandlers.reactionRemove?.(
message || options,
options.emoji,
options.user_id,
);
}
if (data.t === "MESSAGE_REACTION_REMOVE_ALL") {
return eventHandlers.reactionRemoveAll?.(
data.d as BaseMessageReactionPayload,
);
}
if (data.t === "MESSAGE_REACTION_REMOVE_EMOJI") {
return eventHandlers.reactionRemoveEmoji?.(
data.d as MessageReactionRemoveEmojiPayload,
);
}
if (data.t === "PRESENCE_UPDATE") {
return eventHandlers.presenceUpdate?.(data.d as PresenceUpdatePayload);
}
if (data.t === "TYPING_START") {
return eventHandlers.typingStart?.(data.d as TypingStartPayload);
}
if (data.t === "USER_UPDATE") {
const userData = data.d as UserPayload;
const cachedUser = cache.users.get(botID);
const user = createUser(userData);
cache.users.set(userData.id, user);
return eventHandlers.botUpdate?.(user, cachedUser);
}
if (data.t === "VOICE_STATE_UPDATE") {
const payload = data.d as VoiceStateUpdatePayload;
if (!payload.guild_id) return;
const guild = cache.guilds.get(payload.guild_id);
if (!guild) return;
const member = guild.members.get(payload.user_id);
if (!member) return;
// No cached state before so lets make one for em
const cachedState = guild.voiceStates.get(payload.user_id);
if (!cachedState) {
guild.voiceStates.set(payload.user_id, {
...payload,
guildID: payload.guild_id,
channelID: payload.channel_id,
userID: payload.user_id,
sessionID: payload.session_id,
selfDeaf: payload.self_deaf,
selfMute: payload.self_mute,
selfStream: payload.self_stream,
});
return;
}
if (cachedState.channelID !== payload.channel_id) {
// Either joined or moved channels
if (payload.channel_id) {
cachedState.channelID
? // Was in a channel before
eventHandlers.voiceChannelSwitch?.(
member,
payload.channel_id,
cachedState.channelID,
)
: // Was not in a channel before so user just joined
eventHandlers.voiceChannelJoin?.(member, payload.channel_id);
} // Left the channel
else if (cachedState.channelID) {
eventHandlers.voiceChannelLeave?.(member, cachedState.channelID);
}
}
return eventHandlers.voiceStateUpdate?.(member, payload);
}
if (data.t === "WEBHOOKS_UPDATE") {
const options = data.d as WebhookUpdatePayload;
return eventHandlers.webhooksUpdate?.(
options.channel_id,
options.guild_id,
);
}
return;
default:
return;
}
}
export function requestAllMembers(
guildID: string,
resolve: Function,
memberCount: number,
options?: FetchMembersOptions,
) {
const payload = {
resolve,
requestedMax: options?.query
? 100
: options?.userIDs?.length || options?.limit || memberCount,
receivedAmount: 0,
};
const nonce = Math.random().toString();
fetchAllMembersProcessingRequests.set(nonce, payload);
return shards[0].postMessage({
type: "FETCH_MEMBERS",
guildID,
nonce,
options,
});
}
export function sendGatewayCommand(type: "EDIT_BOTS_STATUS", payload: object) {
shards.forEach((shard) => {
shard.postMessage({
type,
...payload,
});
});
}