sharding bots starting up now

This commit is contained in:
Skillz
2020-05-10 13:18:46 -04:00
parent 51756175ad
commit 2889cc130e
6 changed files with 455 additions and 316 deletions
+30 -30
View File
@@ -1,20 +1,20 @@
import { endpoints } from "../constants/discord.ts"
import { DiscordBotGatewayData } from "../types/discord.ts"
import { ClientOptions, EventHandlers } from "../types/options.ts"
import { RequestManager } from "./requestManager.ts"
import { Channel } from "../structures/channel.ts"
import { spawnShards } from "./shardingManager.ts"
import { cache } from "../utils/cache.ts"
import { endpoints } from "../constants/discord.ts";
import { DiscordBotGatewayData } from "../types/discord.ts";
import { ClientOptions, EventHandlers } from "../types/options.ts";
import { RequestManager } from "./requestManager.ts";
import { Channel } from "../structures/channel.ts";
import { spawnShards } from "./shardingManager.ts";
import { cache } from "../utils/cache.ts";
// import { connectWebSocket } from "https://deno.land/std@0.50.0/ws/mod.ts";
export let authorization = ""
export let botID = ""
export let authorization = "";
export let botID = "";
/** The bot's token. This should never be used by end users. It is meant to be used internally to make requests to the Discord API. */
export let token = ""
export let token = "";
export let eventHandlers: EventHandlers = {}
export let botGatewayData: DiscordBotGatewayData
export let eventHandlers: EventHandlers = {};
export let botGatewayData: DiscordBotGatewayData;
export const identifyPayload = {
token: "",
@@ -25,29 +25,29 @@ export const identifyPayload = {
$device: "Discordeno",
},
intents: 0,
shards: [0, 0],
}
shard: [0, 0],
};
export const createClient = async (data: ClientOptions) => {
// Assign some defaults to the options to make them fulfilled / not annoying to use.
botID = data.botID
token = data.token
if (data.eventHandlers) eventHandlers = data.eventHandlers
authorization = `Bot ${data.token}`
botID = data.botID;
token = data.token;
if (data.eventHandlers) eventHandlers = data.eventHandlers;
authorization = `Bot ${data.token}`;
// Initial API connection to get info about bots connection
botGatewayData = await RequestManager.get(endpoints.GATEWAY_BOT)
botGatewayData = await RequestManager.get(endpoints.GATEWAY_BOT);
identifyPayload.token = data.token
identifyPayload.intents = data.intents.reduce((bits, next) => (bits |= next), 0)
identifyPayload.token = data.token;
identifyPayload.intents = data.intents.reduce(
(bits, next) => (bits |= next),
0,
);
spawnShards(botGatewayData, identifyPayload)
}
spawnShards(botGatewayData, identifyPayload);
};
export default createClient
export default createClient;
export const updateChannelCache = (key: string, value: Channel) => {
cache.channels.set(key, value)
}
cache.channels.set(key, value);
};
+16 -11
View File
@@ -1,17 +1,22 @@
import { WebSocket } from "https://deno.land/std@v1.0.0-rc1/ws/mod.ts"
import { GatewayOpcode } from "../types/discord.ts"
import { delay } from "https://deno.land/std@v1.0.0-rc1/util/async.ts"
import { WebSocket } from "https://deno.land/std@0.50.0/ws/mod.ts";
import { GatewayOpcode } from "../types/discord.ts";
import { delay } from "https://deno.land/std@0.50.0/async/mod.ts";
// Discord requests null if no number has yet been sent by discord
export let previousSequenceNumber: number | null = null
export let previousSequenceNumber: number | null = null;
// TODO: If a client does not receive a heartbeat ack between its attempts at sending heartbeats, it should immediately terminate the connection with a non-1000 close code, reconnect, and attempt to resume.
export const sendConstantHeartbeats = async (socket: WebSocket, interval: number) => {
await delay(interval)
socket.send(JSON.stringify({ op: GatewayOpcode.Heartbeat, d: previousSequenceNumber }))
sendConstantHeartbeats(socket, interval)
}
export const sendConstantHeartbeats = async (
socket: WebSocket,
interval: number,
) => {
await delay(interval);
socket.send(
JSON.stringify({ op: GatewayOpcode.Heartbeat, d: previousSequenceNumber }),
);
sendConstantHeartbeats(socket, interval);
};
export const updatePreviousSequenceNumber = (sequence: number) => {
previousSequenceNumber = sequence
}
previousSequenceNumber = sequence;
};
+57 -47
View File
@@ -1,108 +1,118 @@
import { RequestMethod } from "../types/fetch.ts"
import { authorization } from "./client.ts"
import { sleep } from "../utils/utils.ts"
import { RequestMethod } from "../types/fetch.ts";
import { authorization } from "./client.ts";
import { sleep } from "../utils/utils.ts";
const ratelimitedPaths = new Map<string, RateLimitedPath>()
const ratelimitedPaths = new Map<string, RateLimitedPath>();
export interface RateLimitedPath {
url: string
resetTimestamp: number
url: string;
resetTimestamp: number;
}
setInterval(() => {
const now = Date.now()
const now = Date.now();
ratelimitedPaths.forEach((value, key) => {
if (value.resetTimestamp > now) return
ratelimitedPaths.delete(key)
})
}, 1000)
if (value.resetTimestamp > now) return;
ratelimitedPaths.delete(key);
});
}, 1000);
export const RequestManager = {
// Something off about using runMethod with get breaks when using fetch
get: async (url: string, body?: unknown) => {
await checkRatelimits(url)
const result = await fetch(url, createRequestBody(body))
processHeaders(url, result.headers)
await checkRatelimits(url);
return result.json()
const result = await fetch(url, createRequestBody(body));
processHeaders(url, result.headers);
return result.json();
},
post: (url: string, body?: unknown) => {
return runMethod(RequestMethod.Post, url, body)
return runMethod(RequestMethod.Post, url, body);
},
delete: (url: string, body?: unknown) => {
return runMethod(RequestMethod.Delete, url, body)
return runMethod(RequestMethod.Delete, url, body);
},
patch: (url: string, body?: unknown) => {
return runMethod(RequestMethod.Patch, url, body)
return runMethod(RequestMethod.Patch, url, body);
},
put: (url: string, body?: unknown) => {
return runMethod(RequestMethod.Put, url, body)
return runMethod(RequestMethod.Put, url, body);
},
}
};
const createRequestBody = (body: any, method?: RequestMethod) => {
return {
headers: {
Authorization: authorization,
"User-Agent": `DiscordBot (https://github.com/skillz4killz/discordeno, 0.0.1)`,
"User-Agent":
`DiscordBot (https://github.com/skillz4killz/discordeno, 0.0.1)`,
"Content-Type": "application/json",
"X-Audit-Log-Reason": body ? encodeURIComponent(body.reason) : "",
},
body: JSON.stringify(body),
method: method?.toUpperCase(),
}
}
};
};
const runMethod = async (method: RequestMethod, url: string, body?: unknown) => {
await checkRatelimits(url)
const response = await fetch(url, createRequestBody(body, method))
processHeaders(url, response.headers)
const runMethod = async (
method: RequestMethod,
url: string,
body?: unknown,
) => {
await checkRatelimits(url);
const response = await fetch(url, createRequestBody(body, method));
processHeaders(url, response.headers);
// Sometimes Discord returns an empty 204 response that can't be made to JSON.
if (response.status === 204) return
if (response.status === 204) return;
return await response.json()
}
return await response.json();
};
const checkRatelimits = async (url: string) => {
const ratelimited = ratelimitedPaths.get(url)
const global = ratelimitedPaths.get("global")
const ratelimited = ratelimitedPaths.get(url);
const global = ratelimitedPaths.get("global");
const now = Date.now()
if (ratelimited && now < ratelimited.resetTimestamp) await sleep(now - ratelimited.resetTimestamp)
if (global && now < global.resetTimestamp) await sleep(now - global.resetTimestamp)
}
const now = Date.now();
if (ratelimited && now < ratelimited.resetTimestamp) {
await sleep(now - ratelimited.resetTimestamp);
}
if (global && now < global.resetTimestamp) {
await sleep(now - global.resetTimestamp);
}
};
const processHeaders = (url: string, headers: Headers) => {
// If a rate limit response is encountered this will become true and returned
let ratelimited = false
let ratelimited = false;
// Get all useful headers
const remaining = headers.get("x-ratelimit-remaining")
const resetTimestamp = headers.get("x-ratelimit-reset")
const retryAfter = headers.get("retry-after")
const global = headers.get("x-ratelimit-global")
const remaining = headers.get("x-ratelimit-remaining");
const resetTimestamp = headers.get("x-ratelimit-reset");
const retryAfter = headers.get("retry-after");
const global = headers.get("x-ratelimit-global");
// If there is no remaining rate limit for this endpoint, we save it in cache
if (remaining && remaining === "0") {
ratelimited = true
ratelimited = true;
ratelimitedPaths.set(url, {
url,
resetTimestamp: Number(resetTimestamp),
})
});
}
// If there is no remaining global limit, we save it in cache
if (global) {
ratelimited = true
ratelimited = true;
ratelimitedPaths.set("global", {
url: "global",
resetTimestamp: Date.now() + Number(retryAfter),
})
});
}
// Returns a boolean to check if we need to request again once the rate limit resets
return ratelimited
}
return ratelimited;
};
+32 -17
View File
@@ -1,20 +1,35 @@
import { connectWebSocket, isWebSocketCloseEvent } from "https://deno.land/std@v1.0.0-rc1/ws/mod.ts"
import { botGatewayData, identifyPayload } from "./client.ts"
import { GatewayOpcode } from "../types/discord.ts"
import { logRed } from "../utils/logger.ts"
import { handleDiscordPayload, resumeConnection } from "./shardingManager.ts"
import {
connectWebSocket,
isWebSocketCloseEvent,
} from "https://deno.land/std@0.50.0/ws/mod.ts";
import { GatewayOpcode, DiscordBotGatewayData } from "../types/discord.ts";
import { logRed } from "../utils/logger.ts";
import { handleDiscordPayload, resumeConnection } from "./shardingManager.ts";
let shardSocket = await connectWebSocket(botGatewayData.url)
let resumeInterval = 0
export const createShard = async (
botGatewayData: DiscordBotGatewayData,
identifyPayload: object,
) => {
const shardSocket = await connectWebSocket(botGatewayData.url);
let resumeInterval = 0;
// Intial identify with the gateway
await shardSocket.send(JSON.stringify({ op: GatewayOpcode.Identify, d: identifyPayload }))
for await (const message of shardSocket) {
if (typeof message === "string") {
handleDiscordPayload(JSON.parse(message), shardSocket, resumeInterval)
} else if (isWebSocketCloseEvent(message)) {
logRed(`Close :( ${JSON.stringify(message)}`)
resumeInterval = await resumeConnection(identifyPayload, shardSocket)
// Intial identify with the gateway
await shardSocket.send(
JSON.stringify({ op: GatewayOpcode.Identify, d: identifyPayload }),
);
for await (const message of shardSocket) {
if (typeof message === "string") {
handleDiscordPayload(JSON.parse(message), shardSocket, resumeInterval);
} else if (isWebSocketCloseEvent(message)) {
logRed(`Close :( ${JSON.stringify(message)}`);
resumeInterval = await resumeConnection(identifyPayload, shardSocket);
}
}
}
};
postMessage({ type: "REQUEST_CLIENT_OPTIONS" });
onmessage = (message) => {
if (message.data.type === "CREATE_SHARD") {
createShard(message.data.botGatewayData, message.data.identifyPayload);
}
};
+295 -193
View File
@@ -8,202 +8,266 @@ import {
TypingStartPayload,
VoiceStateUpdatePayload,
WebhookUpdatePayload,
} from "../types/discord.ts"
import { eventHandlers, botID, botGatewayData, identifyPayload } from "./client.ts"
import { updatePreviousSequenceNumber, sendConstantHeartbeats, previousSequenceNumber } from "./gateway.ts"
import { WebSocket, connectWebSocket } from "https://deno.land/std@v1.0.0-rc1/ws/mod.ts"
import { handleInternalChannelCreate } from "../events/channels.ts"
import { handleInternalChannelUpdate } from "../events/channels.ts"
import { handleInternalChannelDelete } from "../events/channels.ts"
import { createGuild } from "../structures/guild.ts"
import { handleInternalGuildCreate } from "../events/guilds.ts"
import { cache } from "../utils/cache.ts"
import { handleInternalGuildUpdate } from "../events/guilds.ts"
import { CreateGuildPayload, GuildMemberChunkPayload, GuildRolePayload, UserPayload } from "../types/guild.ts"
import { GuildDeletePayload } from "../types/guild.ts"
import { handleInternalGuildDelete } from "../events/guilds.ts"
import { GuildBanPayload } from "../types/guild.ts"
import { createUser } from "../structures/user.ts"
import { GuildEmojisUpdatePayload } from "../types/guild.ts"
import { GuildMemberAddPayload } from "../types/guild.ts"
import { createMember } from "../structures/member.ts"
import { GuildMemberUpdatePayload } from "../types/guild.ts"
import { ChannelCreatePayload } from "../types/channel.ts"
import { createRole } from "../structures/role.ts"
import { MessageCreateOptions } from "../types/message.ts"
import { createMessage } from "../structures/message.ts"
import { MessageDeletePayload } from "../types/message.ts"
import { MessageDeleteBulkPayload } from "../types/message.ts"
import { MessageUpdatePayload } from "../types/message.ts"
import { MessageReactionPayload } from "../types/message.ts"
import { BaseMessageReactionPayload } from "../types/message.ts"
import { MessageReactionRemoveEmojiPayload } from "../types/message.ts"
} from "../types/discord.ts";
import {
eventHandlers,
botID,
botGatewayData,
identifyPayload,
} from "./client.ts";
import {
updatePreviousSequenceNumber,
sendConstantHeartbeats,
previousSequenceNumber,
} from "./gateway.ts";
import {
WebSocket,
connectWebSocket,
} from "https://deno.land/std@0.50.0/ws/mod.ts";
import { handleInternalChannelCreate } from "../events/channels.ts";
import { handleInternalChannelUpdate } from "../events/channels.ts";
import { handleInternalChannelDelete } from "../events/channels.ts";
import { createGuild } from "../structures/guild.ts";
import { handleInternalGuildCreate } from "../events/guilds.ts";
import { cache } from "../utils/cache.ts";
import { handleInternalGuildUpdate } from "../events/guilds.ts";
import {
CreateGuildPayload,
GuildMemberChunkPayload,
GuildRolePayload,
UserPayload,
} from "../types/guild.ts";
import { GuildDeletePayload } from "../types/guild.ts";
import { handleInternalGuildDelete } from "../events/guilds.ts";
import { GuildBanPayload } from "../types/guild.ts";
import { createUser } from "../structures/user.ts";
import { GuildEmojisUpdatePayload } from "../types/guild.ts";
import { GuildMemberAddPayload } from "../types/guild.ts";
import { createMember } from "../structures/member.ts";
import { GuildMemberUpdatePayload } from "../types/guild.ts";
import { ChannelCreatePayload } from "../types/channel.ts";
import { createRole } from "../structures/role.ts";
import { MessageCreateOptions } from "../types/message.ts";
import { createMessage } from "../structures/message.ts";
import { MessageDeletePayload } from "../types/message.ts";
import { MessageDeleteBulkPayload } from "../types/message.ts";
import { MessageUpdatePayload } from "../types/message.ts";
import { MessageReactionPayload } from "../types/message.ts";
import { BaseMessageReactionPayload } from "../types/message.ts";
import { MessageReactionRemoveEmojiPayload } from "../types/message.ts";
/** The session id is needed for RESUME functionality when discord disconnects randomly. */
let sessionID = ""
let sessionID = "";
let shardCounter = 0;
function createShardWorker() {
new Worker("./module/shard.ts", { type: "module" })
const shard = new Worker("./module/shard.ts", { type: "module", deno: true });
shard.onmessage = (message) => {
if (message.data.type === "REQUEST_CLIENT_OPTIONS") {
identifyPayload.shard = [shardCounter++, botGatewayData.shards];
shard.postMessage(
{ type: "CREATE_SHARD", botGatewayData, identifyPayload },
);
}
};
}
export const spawnShards = (data: DiscordBotGatewayData, payload: unknown, id = 0) => {
identifyPayload.shards = [id, data.shards]
createShardWorker()
if (id < data.shards) spawnShards(data, payload, id + 1)
}
export const spawnShards = async (
data: DiscordBotGatewayData,
payload: unknown,
id = 1,
) => {
createShardWorker();
if (id < data.shards) spawnShards(data, payload, id + 1);
};
export function handleDiscordPayload(data: DiscordPayload, socket: WebSocket, resumeInterval: number) {
export function handleDiscordPayload(
data: DiscordPayload,
socket: WebSocket,
resumeInterval: number,
) {
// Update the sequence number if it is present
if (data.s) updatePreviousSequenceNumber(data.s)
eventHandlers.raw?.(data)
if (data.s) updatePreviousSequenceNumber(data.s);
eventHandlers.raw?.(data);
switch (data.op) {
case GatewayOpcode.Hello:
sendConstantHeartbeats(socket, (data.d as DiscordHeartbeatPayload).heartbeat_interval)
return
sendConstantHeartbeats(
socket,
(data.d as DiscordHeartbeatPayload).heartbeat_interval,
);
return;
case GatewayOpcode.HeartbeatACK:
// Incase the user wants to listen to heartbeat responses
return eventHandlers.heartbeat?.()
return eventHandlers.heartbeat?.();
case GatewayOpcode.Reconnect:
case GatewayOpcode.InvalidSession:
// Reconnect to the gateway https://discordapp.com/developers/docs/topics/gateway#reconnect
// I think this should be handled automatically when the websocket closes
return
return;
case GatewayOpcode.Resume:
return clearInterval(resumeInterval)
return clearInterval(resumeInterval);
case GatewayOpcode.Dispatch:
if (data.t === "READY") {
// Important for RESUME
sessionID = (data.d as ReadyPayload).session_id
return eventHandlers.ready?.()
sessionID = (data.d as ReadyPayload).session_id;
return eventHandlers.ready?.();
}
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 === "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)
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
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)
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
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())
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)
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 options = data.d as GuildBanPayload;
const guild = cache.guilds.get(options.guild_id);
if (!guild) return;
const user = createUser(options.user)
const user = createUser(options.user);
return data.t === "GUILD_BAN_ADD"
? eventHandlers.guildBanAdd?.(guild, user)
: eventHandlers.guildBanRemove?.(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 options = data.d as GuildEmojisUpdatePayload;
const guild = cache.guilds.get(options.guild_id);
if (!guild) return;
const cachedEmojis = guild.emojis
guild.emojis = options.emojis
const cachedEmojis = guild.emojis;
guild.emojis = options.emojis;
return eventHandlers.guildEmojisUpdate?.(guild, options.emojis, cachedEmojis)
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 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 memberCount = guild.memberCount + 1;
guild.memberCount = memberCount;
const member = createMember(
options,
options.guild_id,
[...guild.roles.values()].map((role) => role.raw),
guild.owner_id
)
guild.members.set(options.user.id, member)
guild.owner_id,
);
guild.members.set(options.user.id, member);
return eventHandlers.guildMemberAdd?.(guild, member)
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 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 memberCount = guild.memberCount - 1;
guild.memberCount = memberCount;
const member = guild.members.get(options.user.id)
return eventHandlers.guildMemberRemove?.(guild, member || createUser(options.user))
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 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 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(),
joined_at: new Date(cachedMember?.joined_at || Date.now())
.toISOString(),
deaf: cachedMember?.deaf || false,
mute: cachedMember?.mute || false,
}
};
const member = createMember(
newMemberData,
options.guild_id,
[...guild.roles.values()].map((r) => r.raw),
guild.owner_id
)
guild.members.set(options.user.id, member)
guild.owner_id,
);
guild.members.set(options.user.id, member);
if (cachedMember?.nick !== options.nick)
eventHandlers.nicknameUpdate?.(guild, member, options.nick, cachedMember?.nick)
const roleIDs = cachedMember?.roles || []
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.role_lost?.(guild, member, id)
})
if (!options.roles.includes(id)) {
eventHandlers.role_lost?.(guild, member, id);
}
});
options.roles.forEach((id) => {
if (!roleIDs.includes(id)) eventHandlers.role_gained?.(guild, member, id)
})
if (!roleIDs.includes(id)) {
eventHandlers.role_gained?.(guild, member, id);
}
});
return eventHandlers.guild_member_update?.(guild, member, cachedMember)
return eventHandlers.guild_member_update?.(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
const options = data.d as GuildMemberChunkPayload;
const guild = cache.guilds.get(options.guild_id);
if (!guild) return;
options.members.forEach((member) =>
guild.members.set(
@@ -212,44 +276,48 @@ export function handleDiscordPayload(data: DiscordPayload, socket: WebSocket, re
member,
options.guild_id,
[...guild.roles.values()].map((r) => r.raw),
guild.owner_id
)
guild.owner_id,
),
)
)
);
}
if (data.t && ["GUILD_ROLE_CREATE", "GUILD_ROLE_DELETE", "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", "GUILD_ROLE_DELETE", "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 role = createRole(options.role);
const roles = guild.roles.set(options.role.id, role);
guild.roles = roles;
return eventHandlers.roleCreate?.(guild, role);
}
const cached_role = guild.roles.get(options.role.id)
if (!cached_role) return
const cached_role = guild.roles.get(options.role.id);
if (!cached_role) return;
if (data.t === "GUILD_ROLE_DELETE") {
const roles = guild.roles
roles.delete(options.role.id)
guild.roles = roles
return eventHandlers.roleDelete?.(guild, cached_role)
const roles = guild.roles;
roles.delete(options.role.id);
guild.roles = roles;
return eventHandlers.roleDelete?.(guild, cached_role);
}
if (data.t === "GUILD_ROLE_UPDATE") {
const role = createRole(options.role)
return eventHandlers.roleUpdate?.(guild, role, cached_role)
const role = createRole(options.role);
return eventHandlers.roleUpdate?.(guild, role, cached_role);
}
}
if (data.t === "MESSAGE_CREATE") {
const options = data.d as MessageCreateOptions
const channel = cache.channels.get(options.channel_id)
const message = createMessage(options)
const options = data.d as MessageCreateOptions;
const channel = cache.channels.get(options.channel_id);
const message = createMessage(options);
if (channel) {
// channel.last_message_id = () => options.id
@@ -257,103 +325,131 @@ export function handleDiscordPayload(data: DiscordPayload, socket: WebSocket, re
// TODO: LIMIT THIS TO 100 messages
// }
}
return eventHandlers.messageCreate?.(message)
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
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
const channel = cache.channels.get(options.channel_id);
if (!channel) return;
deletedMessages.forEach((id) => {
console.log(id)
console.log(id);
// const message = channel.messages().get(id)
// if (message) {
// // TODO: update the messages cache
// }
// return eventHandlers.message_delete?.(message || { id, channel })
})
});
}
if (data.t === "MESSAGE_UPDATE") {
const options = data.d as MessageUpdatePayload
const channel = cache.channels.get(options.channel_id)
if (!channel) return
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 (
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 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 {
(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]
};
message.reactions = message.reactions
? [...message.reactions, newReaction]
: [newReaction];
}
cache.messages.set(options.message_id, message)
cache.messages.set(options.message_id, message);
}
return isAdd
? eventHandlers.reactionAdd?.(message || options, options.emoji, options.user_id)
: eventHandlers.reactionRemove?.(message || options, options.emoji, options.user_id)
? 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)
return eventHandlers.reactionRemoveAll?.(
data.d as BaseMessageReactionPayload,
);
}
if (data.t === "MESSAGE_REACTION_REMOVE_EMOJI") {
return eventHandlers.reactionRemoveEmoji?.(data.d as MessageReactionRemoveEmojiPayload)
return eventHandlers.reactionRemoveEmoji?.(
data.d as MessageReactionRemoveEmojiPayload,
);
}
if (data.t === "PRESENCE_UPDATE") {
return eventHandlers.presenceUpdate?.(data.d as PresenceUpdatePayload)
return eventHandlers.presenceUpdate?.(data.d as PresenceUpdatePayload);
}
if (data.t === "TYPING_START") {
return eventHandlers.typingStart?.(data.d as TypingStartPayload)
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)
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 payload = data.d as VoiceStateUpdatePayload;
if (!payload.guild_id) return;
const guild = cache.guilds.get(payload.guild_id)
if (!guild) return
const guild = cache.guilds.get(payload.guild_id);
if (!guild) return;
const member = guild.members.get(payload.user_id)
if (!member) return
const member = guild.members.get(payload.user_id);
if (!member) return;
const cached_state = guild.voice_states.find((state) => state.user_id === payload.user_id)
const cached_state = guild.voice_states.find((state) =>
state.user_id === payload.user_id
);
// No cached state before so lets make one for em
if (!cached_state) {
guild.voice_states = [...guild.voice_states, payload]
return
guild.voice_states = [...guild.voice_states, payload];
return;
}
if (cached_state.channel_id !== payload.channel_id) {
@@ -361,33 +457,39 @@ export function handleDiscordPayload(data: DiscordPayload, socket: WebSocket, re
if (payload.channel_id) {
cached_state.channel_id
? // Was in a channel before
eventHandlers.voiceChannelSwitch?.(member, payload.channel_id, cached_state.channel_id)
eventHandlers.voiceChannelSwitch?.(
member,
payload.channel_id,
cached_state.channel_id,
)
: // Was not in a channel before so user just joined
eventHandlers.voiceChannelJoin?.(member, payload.channel_id)
}
// Left the channel
eventHandlers.voiceChannelJoin?.(member, payload.channel_id);
} // Left the channel
else if (cached_state.channel_id) {
eventHandlers.voiceChannelLeave?.(member, cached_state.channel_id)
eventHandlers.voiceChannelLeave?.(member, cached_state.channel_id);
}
}
return eventHandlers.voiceStateUpdate?.(member, payload)
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)
const options = data.d as WebhookUpdatePayload;
return eventHandlers.webhooksUpdate?.(
options.channel_id,
options.guild_id,
);
}
return
return;
default:
return
return;
}
}
export async function resumeConnection(payload: object, socket: WebSocket) {
return setInterval(async () => {
socket = await connectWebSocket(botGatewayData.url)
socket = await connectWebSocket(botGatewayData.url);
await socket.send(
JSON.stringify({
op: GatewayOpcode.Resume,
@@ -396,7 +498,7 @@ export async function resumeConnection(payload: object, socket: WebSocket) {
session_id: sessionID,
seq: previousSequenceNumber,
},
})
)
}, 1000 * 15)
}),
);
}, 1000 * 15);
}
+25 -18
View File
@@ -1,32 +1,39 @@
import { blue, green, red, yellow } from "https://deno.land/std@v1.0.0-rc1/fmt/colors.ts"
import {
blue,
green,
red,
yellow,
} from "https://deno.land/std@0.50.0/fmt/colors.ts";
export const getTime = () => {
const now = new Date()
const hours = now.getHours()
const minute = now.getMinutes()
const now = new Date();
const hours = now.getHours();
const minute = now.getMinutes();
let hour = hours
let amOrPm = `AM`
let hour = hours;
let amOrPm = `AM`;
if (hour > 12) {
amOrPm = `PM`
hour = hour - 12
amOrPm = `PM`;
hour = hour - 12;
}
return `${hour >= 10 ? hour : `0${hour}`}:${minute >= 10 ? minute : `0${minute}`} ${amOrPm}`
}
return `${hour >= 10 ? hour : `0${hour}`}:${
minute >= 10 ? minute : `0${minute}`
} ${amOrPm}`;
};
export const logGreen = (text: unknown) => {
console.log(green(`[${getTime()}] => ${JSON.stringify(text)}`))
}
console.log(green(`[${getTime()}] => ${JSON.stringify(text)}`));
};
export const logBlue = (text: unknown) => {
console.log(blue(`[${getTime()}] => ${JSON.stringify(text)}`))
}
console.log(blue(`[${getTime()}] => ${JSON.stringify(text)}`));
};
export const logRed = (text: unknown) => {
console.log(red(`[${getTime()}] => ${JSON.stringify(text)}`))
}
console.log(red(`[${getTime()}] => ${JSON.stringify(text)}`));
};
export const logYellow = (text: unknown) => {
console.log(yellow(`[${getTime()}] => ${JSON.stringify(text)}`))
}
console.log(yellow(`[${getTime()}] => ${JSON.stringify(text)}`));
};