Merge pull request #18 from DjDeveloperr/main
feature: Cache Adapters and REST Manager
This commit is contained in:
commit
98b202fb30
47 changed files with 1518 additions and 192 deletions
|
@ -1,13 +1,13 @@
|
||||||
import { Gateway, GatewayEventHandler } from '../index.ts'
|
import { Gateway, GatewayEventHandler } from '../index.ts'
|
||||||
import getChannelByType from '../../utils/getChannelByType.ts'
|
import getChannelByType from '../../utils/getChannelByType.ts'
|
||||||
|
|
||||||
export const channelCreate: GatewayEventHandler = (
|
export const channelCreate: GatewayEventHandler = async (
|
||||||
gateway: Gateway,
|
gateway: Gateway,
|
||||||
d: any
|
d: any
|
||||||
) => {
|
) => {
|
||||||
const channel = getChannelByType(gateway.client, d)
|
const channel = getChannelByType(gateway.client, d)
|
||||||
|
|
||||||
if (channel !== undefined) {
|
if (channel !== undefined) {
|
||||||
|
await gateway.client.channels.set(d.id, d)
|
||||||
gateway.client.emit('channelCreate', channel)
|
gateway.client.emit('channelCreate', channel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,13 @@
|
||||||
import { Gateway, GatewayEventHandler } from '../index.ts'
|
import { Gateway, GatewayEventHandler } from '../index.ts'
|
||||||
import cache from '../../models/cache.ts'
|
|
||||||
import { Channel } from '../../structures/channel.ts'
|
import { Channel } from '../../structures/channel.ts'
|
||||||
|
|
||||||
export const channelDelete: GatewayEventHandler = (
|
export const channelDelete: GatewayEventHandler = async(
|
||||||
gateway: Gateway,
|
gateway: Gateway,
|
||||||
d: any
|
d: any
|
||||||
) => {
|
) => {
|
||||||
const channel: Channel = cache.get('channel', d.id)
|
const channel: Channel = await gateway.client.channels.get(d.id)
|
||||||
if (channel !== undefined) {
|
if (channel !== undefined) {
|
||||||
cache.del('channel', d.id)
|
await gateway.client.channels.delete(d.id)
|
||||||
gateway.client.emit('channelDelete', channel)
|
gateway.client.emit('channelDelete', channel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +1,19 @@
|
||||||
import { Gateway, GatewayEventHandler } from '../index.ts'
|
import { Gateway, GatewayEventHandler } from '../index.ts'
|
||||||
import cache from '../../models/cache.ts'
|
import cache from '../../models/cache.ts'
|
||||||
import { TextChannel } from '../../structures/textChannel.ts'
|
import { TextChannel } from '../../structures/textChannel.ts'
|
||||||
|
import { ChannelPayload } from "../../types/channel.ts"
|
||||||
|
|
||||||
export const channelPinsUpdate: GatewayEventHandler = (
|
export const channelPinsUpdate: GatewayEventHandler = async(
|
||||||
gateway: Gateway,
|
gateway: Gateway,
|
||||||
d: any
|
d: any
|
||||||
) => {
|
) => {
|
||||||
const after: TextChannel = cache.get('textchannel', d.channel_id)
|
const after: TextChannel = await gateway.client.channels.get(d.channel_id)
|
||||||
if (after !== undefined) {
|
if (after !== undefined) {
|
||||||
const before = after.refreshFromData({
|
const before = after.refreshFromData({
|
||||||
last_pin_timestamp: d.last_pin_timestamp
|
last_pin_timestamp: d.last_pin_timestamp
|
||||||
})
|
})
|
||||||
|
const raw = await gateway.client.channels._get(d.channel_id) ;
|
||||||
|
await gateway.client.channels.set(after.id, Object.assign(raw, { last_pin_timestamp: d.last_pin_timestamp }))
|
||||||
gateway.client.emit('channelPinsUpdate', before, after)
|
gateway.client.emit('channelPinsUpdate', before, after)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
import cache from '../../models/cache.ts'
|
|
||||||
import { Channel } from '../../structures/channel.ts'
|
import { Channel } from '../../structures/channel.ts'
|
||||||
import getChannelByType from '../../utils/getChannelByType.ts'
|
import getChannelByType from '../../utils/getChannelByType.ts'
|
||||||
import { Gateway, GatewayEventHandler } from '../index.ts'
|
import { Gateway, GatewayEventHandler } from '../index.ts'
|
||||||
|
|
||||||
export const channelUpdate: GatewayEventHandler = (
|
export const channelUpdate: GatewayEventHandler = async (
|
||||||
gateway: Gateway,
|
gateway: Gateway,
|
||||||
d: any
|
d: any
|
||||||
) => {
|
) => {
|
||||||
const oldChannel: Channel = cache.get('channel', d.id)
|
const oldChannel: Channel = await gateway.client.channels.get(d.id)
|
||||||
|
|
||||||
if (oldChannel !== undefined) {
|
if (oldChannel !== undefined) {
|
||||||
|
await gateway.client.channels.set(d.id, d)
|
||||||
if (oldChannel.type !== d.type) {
|
if (oldChannel.type !== d.type) {
|
||||||
const channel: Channel = getChannelByType(gateway.client, d) ?? oldChannel
|
const channel: Channel = getChannelByType(gateway.client, d) ?? oldChannel
|
||||||
gateway.client.emit('channelUpdate', oldChannel, channel)
|
gateway.client.emit('channelUpdate', oldChannel, channel)
|
||||||
|
|
|
@ -1,14 +1,16 @@
|
||||||
import { Gateway, GatewayEventHandler } from '../index.ts'
|
import { Gateway, GatewayEventHandler } from '../index.ts'
|
||||||
import cache from '../../models/cache.ts'
|
|
||||||
import { Guild } from '../../structures/guild.ts'
|
import { Guild } from '../../structures/guild.ts'
|
||||||
|
import { GuildPayload } from "../../types/guild.ts"
|
||||||
|
|
||||||
export const guildCreate: GatewayEventHandler = (gateway: Gateway, d: any) => {
|
export const guildCreate: GatewayEventHandler = async(gateway: Gateway, d: any) => {
|
||||||
let guild: Guild = cache.get('guild', d.id)
|
let guild: Guild | void = await gateway.client.guilds.get(d.id)
|
||||||
if (guild !== undefined) {
|
if (guild !== undefined) {
|
||||||
|
// It was just lazy load, so we don't fire the event as its gonna fire for every guild bot is in
|
||||||
|
await gateway.client.guilds.set(d.id, d)
|
||||||
guild.refreshFromData(d)
|
guild.refreshFromData(d)
|
||||||
} else {
|
} else {
|
||||||
guild = new Guild(gateway.client, d)
|
await gateway.client.guilds.set(d.id, d)
|
||||||
|
guild = new Guild(gateway.client, d as GuildPayload)
|
||||||
|
gateway.client.emit('guildCreate', guild)
|
||||||
}
|
}
|
||||||
|
|
||||||
gateway.client.emit('guildCreate', guild)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,12 @@
|
||||||
import cache from '../../models/cache.ts'
|
|
||||||
import { Guild } from '../../structures/guild.ts'
|
import { Guild } from '../../structures/guild.ts'
|
||||||
import { Gateway, GatewayEventHandler } from '../index.ts'
|
import { Gateway, GatewayEventHandler } from '../index.ts'
|
||||||
|
|
||||||
export const guildDelte: GatewayEventHandler = (gateway: Gateway, d: any) => {
|
export const guildDelte: GatewayEventHandler = async (gateway: Gateway, d: any) => {
|
||||||
const guild: Guild = cache.get('guild', d.id)
|
const guild: Guild | void = await gateway.client.guilds.get(d.id)
|
||||||
|
|
||||||
if (guild !== undefined) {
|
if (guild !== undefined) {
|
||||||
guild.refreshFromData(d)
|
guild.refreshFromData(d)
|
||||||
cache.del('guild', d.id)
|
await gateway.client.guilds.delete(d.id)
|
||||||
gateway.client.emit('guildDelete', guild)
|
gateway.client.emit('guildDelete', guild)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
import { Gateway, GatewayEventHandler } from '../index.ts'
|
import { Gateway, GatewayEventHandler } from '../index.ts'
|
||||||
import cache from '../../models/cache.ts'
|
|
||||||
import { Guild } from '../../structures/guild.ts'
|
import { Guild } from '../../structures/guild.ts'
|
||||||
|
|
||||||
export const guildUpdate: GatewayEventHandler = (gateway: Gateway, d: any) => {
|
export const guildUpdate: GatewayEventHandler = async(gateway: Gateway, d: any) => {
|
||||||
const after: Guild = cache.get('guild', d.id)
|
const before: Guild | void = await gateway.client.guilds.get(d.id)
|
||||||
if (after !== undefined) {
|
if(!before) return
|
||||||
const before: Guild = after.refreshFromData(d)
|
await gateway.client.guilds.set(d.id, d)
|
||||||
gateway.client.emit('guildUpdate', before, after)
|
const after: Guild | void = await gateway.client.guilds.get(d.id)
|
||||||
}
|
gateway.client.emit('guildUpdate', before, after)
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,13 +10,15 @@ import { guildUpdate } from './guildUpdate.ts'
|
||||||
import { guildBanAdd } from './guildBanAdd.ts'
|
import { guildBanAdd } from './guildBanAdd.ts'
|
||||||
import { ready } from './ready.ts'
|
import { ready } from './ready.ts'
|
||||||
import { guildBanRemove } from './guildBanRemove.ts'
|
import { guildBanRemove } from './guildBanRemove.ts'
|
||||||
|
import { messageCreate } from "./messageCreate.ts"
|
||||||
|
import { resume } from "./resume.ts"
|
||||||
|
|
||||||
export const gatewayHandlers: {
|
export const gatewayHandlers: {
|
||||||
[eventCode in GatewayEvents]: GatewayEventHandler | undefined
|
[eventCode in GatewayEvents]: GatewayEventHandler | undefined
|
||||||
} = {
|
} = {
|
||||||
READY: ready,
|
READY: ready,
|
||||||
RECONNECT: undefined,
|
RECONNECT: undefined,
|
||||||
RESUMED: undefined,
|
RESUMED: resume,
|
||||||
CHANNEL_CREATE: channelCreate,
|
CHANNEL_CREATE: channelCreate,
|
||||||
CHANNEL_DELETE: channelDelete,
|
CHANNEL_DELETE: channelDelete,
|
||||||
CHANNEL_UPDATE: channelUpdate,
|
CHANNEL_UPDATE: channelUpdate,
|
||||||
|
@ -37,7 +39,7 @@ export const gatewayHandlers: {
|
||||||
GUILD_ROLE_DELETE: undefined,
|
GUILD_ROLE_DELETE: undefined,
|
||||||
INVITE_CREATE: undefined,
|
INVITE_CREATE: undefined,
|
||||||
INVITE_DELETE: undefined,
|
INVITE_DELETE: undefined,
|
||||||
MESSAGE_CREATE: undefined,
|
MESSAGE_CREATE: messageCreate,
|
||||||
MESSAGE_UPDATE: undefined,
|
MESSAGE_UPDATE: undefined,
|
||||||
MESSAGE_DELETE: undefined,
|
MESSAGE_DELETE: undefined,
|
||||||
MESSAGE_DELETE_BULK: undefined,
|
MESSAGE_DELETE_BULK: undefined,
|
||||||
|
|
21
src/gateway/handlers/messageCreate.ts
Normal file
21
src/gateway/handlers/messageCreate.ts
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import { Channel } from "../../structures/channel.ts"
|
||||||
|
import { Message } from "../../structures/message.ts"
|
||||||
|
import { MessageMentions } from "../../structures/MessageMentions.ts"
|
||||||
|
import { TextChannel } from "../../structures/textChannel.ts"
|
||||||
|
import { User } from "../../structures/user.ts"
|
||||||
|
import { MessagePayload } from "../../types/channel.ts"
|
||||||
|
import { Gateway, GatewayEventHandler } from '../index.ts'
|
||||||
|
|
||||||
|
export const messageCreate: GatewayEventHandler = async(
|
||||||
|
gateway: Gateway,
|
||||||
|
d: MessagePayload
|
||||||
|
) => {
|
||||||
|
let channel = await gateway.client.channels.get(d.channel_id)
|
||||||
|
// Fetch the channel if not cached
|
||||||
|
if(!channel) channel = (await gateway.client.channels.fetch(d.channel_id) as any) as TextChannel
|
||||||
|
let user = new User(gateway.client, d.author)
|
||||||
|
await gateway.client.users.set(d.author.id, d.author)
|
||||||
|
let mentions = new MessageMentions()
|
||||||
|
let message = new Message(gateway.client, d, channel, user, mentions)
|
||||||
|
gateway.client.emit('messageCreate', message)
|
||||||
|
}
|
|
@ -1,11 +1,14 @@
|
||||||
import { Guild } from '../../structures/guild.ts'
|
|
||||||
import { User } from '../../structures/user.ts'
|
import { User } from '../../structures/user.ts'
|
||||||
import { GuildPayload } from '../../types/guild.ts'
|
import { GuildPayload } from '../../types/guild.ts'
|
||||||
import { Gateway, GatewayEventHandler } from '../index.ts'
|
import { Gateway, GatewayEventHandler } from '../index.ts'
|
||||||
|
|
||||||
export const ready: GatewayEventHandler = (gateway: Gateway, d: any) => {
|
export const ready: GatewayEventHandler = async (gateway: Gateway, d: any) => {
|
||||||
gateway.client.user = new User(gateway.client, d.user)
|
gateway.client.user = new User(gateway.client, d.user)
|
||||||
gateway.sessionID = d.session_id
|
gateway.sessionID = d.session_id
|
||||||
d.guilds.forEach((guild: GuildPayload) => new Guild(gateway.client, guild))
|
gateway.debug(`Received READY. Session: ${gateway.sessionID}`)
|
||||||
|
await gateway.cache.set("session_id", gateway.sessionID)
|
||||||
|
d.guilds.forEach((guild: GuildPayload) => {
|
||||||
|
gateway.client.guilds.set(guild.id, guild)
|
||||||
|
})
|
||||||
gateway.client.emit('ready')
|
gateway.client.emit('ready')
|
||||||
}
|
}
|
7
src/gateway/handlers/resume.ts
Normal file
7
src/gateway/handlers/resume.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import { Gateway, GatewayEventHandler } from '../index.ts'
|
||||||
|
|
||||||
|
export const resume: GatewayEventHandler = (gateway: Gateway, d: any) => {
|
||||||
|
gateway.debug(`Session Resumed!`)
|
||||||
|
gateway.client.emit('resume')
|
||||||
|
gateway.client.emit('ready')
|
||||||
|
}
|
|
@ -5,8 +5,11 @@ import {
|
||||||
DISCORD_API_VERSION
|
DISCORD_API_VERSION
|
||||||
} from '../consts/urlsAndVersions.ts'
|
} from '../consts/urlsAndVersions.ts'
|
||||||
import { GatewayResponse } from '../types/gatewayResponse.ts'
|
import { GatewayResponse } from '../types/gatewayResponse.ts'
|
||||||
import { GatewayOpcodes, GatewayIntents } from '../types/gateway.ts'
|
import { GatewayOpcodes, GatewayIntents, GatewayCloseCodes } from '../types/gateway.ts'
|
||||||
import { gatewayHandlers } from './handlers/index.ts'
|
import { gatewayHandlers } from './handlers/index.ts'
|
||||||
|
import { GATEWAY_BOT } from '../types/endpoint.ts'
|
||||||
|
import { GatewayBotPayload } from "../types/gatewayBot.ts"
|
||||||
|
import { GatewayCache } from "../managers/GatewayCache.ts"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles Discord gateway connection.
|
* Handles Discord gateway connection.
|
||||||
|
@ -23,15 +26,17 @@ class Gateway {
|
||||||
heartbeatInterval = 0
|
heartbeatInterval = 0
|
||||||
heartbeatIntervalID?: number
|
heartbeatIntervalID?: number
|
||||||
sequenceID?: number
|
sequenceID?: number
|
||||||
|
lastPingTimestamp = 0
|
||||||
sessionID?: string
|
sessionID?: string
|
||||||
lastPingTimestemp = 0
|
|
||||||
private heartbeatServerResponded = false
|
private heartbeatServerResponded = false
|
||||||
client: Client
|
client: Client
|
||||||
|
cache: GatewayCache
|
||||||
|
|
||||||
constructor (client: Client, token: string, intents: GatewayIntents[]) {
|
constructor (client: Client, token: string, intents: GatewayIntents[]) {
|
||||||
this.token = token
|
this.token = token
|
||||||
this.intents = intents
|
this.intents = intents
|
||||||
this.client = client
|
this.client = client
|
||||||
|
this.cache = new GatewayCache(client)
|
||||||
this.websocket = new WebSocket(
|
this.websocket = new WebSocket(
|
||||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||||
`${DISCORD_GATEWAY_URL}/?v=${DISCORD_API_VERSION}&encoding=json`,
|
`${DISCORD_GATEWAY_URL}/?v=${DISCORD_API_VERSION}&encoding=json`,
|
||||||
|
@ -46,9 +51,10 @@ class Gateway {
|
||||||
|
|
||||||
private onopen (): void {
|
private onopen (): void {
|
||||||
this.connected = true
|
this.connected = true
|
||||||
|
this.debug("Connected to Gateway!")
|
||||||
}
|
}
|
||||||
|
|
||||||
private onmessage (event: MessageEvent): void {
|
private async onmessage (event: MessageEvent): Promise<void> {
|
||||||
let data = event.data
|
let data = event.data
|
||||||
if (data instanceof ArrayBuffer) {
|
if (data instanceof ArrayBuffer) {
|
||||||
data = new Uint8Array(data)
|
data = new Uint8Array(data)
|
||||||
|
@ -63,13 +69,13 @@ class Gateway {
|
||||||
switch (op) {
|
switch (op) {
|
||||||
case GatewayOpcodes.HELLO:
|
case GatewayOpcodes.HELLO:
|
||||||
this.heartbeatInterval = d.heartbeat_interval
|
this.heartbeatInterval = d.heartbeat_interval
|
||||||
|
this.debug(`Received HELLO. Heartbeat Interval: ${this.heartbeatInterval}`)
|
||||||
this.heartbeatIntervalID = setInterval(() => {
|
this.heartbeatIntervalID = setInterval(() => {
|
||||||
if (this.heartbeatServerResponded) {
|
if (this.heartbeatServerResponded) {
|
||||||
this.heartbeatServerResponded = false
|
this.heartbeatServerResponded = false
|
||||||
} else {
|
} else {
|
||||||
clearInterval(this.heartbeatIntervalID)
|
clearInterval(this.heartbeatIntervalID)
|
||||||
this.websocket.close()
|
this.reconnect()
|
||||||
this.initWebsocket()
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -79,36 +85,36 @@ class Gateway {
|
||||||
d: this.sequenceID ?? null
|
d: this.sequenceID ?? null
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
this.lastPingTimestemp = Date.now()
|
this.lastPingTimestamp = Date.now()
|
||||||
}, this.heartbeatInterval)
|
}, this.heartbeatInterval)
|
||||||
|
|
||||||
if (!this.initialized) {
|
if (!this.initialized) {
|
||||||
this.sendIdentify()
|
this.sendIdentify()
|
||||||
this.initialized = true
|
this.initialized = true
|
||||||
} else {
|
} else {
|
||||||
|
console.log("Calling Resume")
|
||||||
this.sendResume()
|
this.sendResume()
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
|
|
||||||
case GatewayOpcodes.HEARTBEAT_ACK:
|
case GatewayOpcodes.HEARTBEAT_ACK:
|
||||||
this.heartbeatServerResponded = true
|
this.heartbeatServerResponded = true
|
||||||
this.client.ping = Date.now() - this.lastPingTimestemp
|
this.client.ping = Date.now() - this.lastPingTimestamp
|
||||||
|
this.debug(`Received Heartbeat Ack. Ping Recognized: ${this.client.ping}ms`)
|
||||||
break
|
break
|
||||||
|
|
||||||
case GatewayOpcodes.INVALID_SESSION:
|
case GatewayOpcodes.INVALID_SESSION:
|
||||||
// Because we know this gonna be bool
|
// Because we know this gonna be bool
|
||||||
|
this.debug(`Invalid Session! Identifying with forced new session`)
|
||||||
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
|
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
|
||||||
if (!d) {
|
setTimeout(() => this.sendIdentify(true), 3000)
|
||||||
setTimeout(this.sendResume, 3000)
|
|
||||||
} else {
|
|
||||||
setTimeout(this.sendIdentify, 3000)
|
|
||||||
}
|
|
||||||
break
|
break
|
||||||
|
|
||||||
case GatewayOpcodes.DISPATCH: {
|
case GatewayOpcodes.DISPATCH: {
|
||||||
this.heartbeatServerResponded = true
|
this.heartbeatServerResponded = true
|
||||||
if (s !== null) {
|
if (s !== null) {
|
||||||
this.sequenceID = s
|
this.sequenceID = s
|
||||||
|
await this.cache.set("seq", s)
|
||||||
}
|
}
|
||||||
if (t !== null && t !== undefined) {
|
if (t !== null && t !== undefined) {
|
||||||
const handler = gatewayHandlers[t]
|
const handler = gatewayHandlers[t]
|
||||||
|
@ -119,23 +125,83 @@ class Gateway {
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
case GatewayOpcodes.RESUME: {
|
||||||
|
// this.token = d.token
|
||||||
|
this.sessionID = d.session_id
|
||||||
|
this.sequenceID = d.seq
|
||||||
|
await this.cache.set("seq", d.seq)
|
||||||
|
await this.cache.set("session_id", this.sessionID)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case GatewayOpcodes.RECONNECT: {
|
||||||
|
this.reconnect()
|
||||||
|
break
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private onclose (event: CloseEvent): void {
|
private onclose (event: CloseEvent): void {
|
||||||
console.log(event.code)
|
this.debug("Connection Closed with code: " + event.code)
|
||||||
// TODO: Handle close event codes.
|
|
||||||
|
if(event.code == GatewayCloseCodes.UNKNOWN_ERROR) {
|
||||||
|
this.debug("API has encountered Unknown Error. Reconnecting...")
|
||||||
|
this.reconnect()
|
||||||
|
} else if(event.code == GatewayCloseCodes.UNKNOWN_OPCODE) {
|
||||||
|
throw new Error("Unknown OP Code was sent. This shouldn't happen!")
|
||||||
|
} else if(event.code == GatewayCloseCodes.DECODE_ERROR) {
|
||||||
|
throw new Error("Invalid Payload was sent. This shouldn't happen!")
|
||||||
|
} else if(event.code == GatewayCloseCodes.NOT_AUTHENTICATED) {
|
||||||
|
throw new Error("Not Authorized: Payload was sent before Identifying.")
|
||||||
|
} else if(event.code == GatewayCloseCodes.AUTHENTICATION_FAILED) {
|
||||||
|
throw new Error("Invalid Token provided!")
|
||||||
|
} else if(event.code == GatewayCloseCodes.INVALID_SEQ) {
|
||||||
|
this.debug("Invalid Seq was sent. Reconnecting.")
|
||||||
|
this.reconnect()
|
||||||
|
} else if(event.code == GatewayCloseCodes.RATE_LIMITED) {
|
||||||
|
throw new Error("You're ratelimited. Calm down.")
|
||||||
|
} else if(event.code == GatewayCloseCodes.SESSION_TIMED_OUT) {
|
||||||
|
this.debug("Session Timeout. Reconnecting.")
|
||||||
|
this.reconnect(true)
|
||||||
|
} else if(event.code == GatewayCloseCodes.INVALID_SHARD) {
|
||||||
|
this.debug("Invalid Shard was sent. Reconnecting.")
|
||||||
|
this.reconnect()
|
||||||
|
} else if(event.code == GatewayCloseCodes.SHARDING_REQUIRED) {
|
||||||
|
throw new Error("Couldn't connect. Sharding is requried!")
|
||||||
|
} else if(event.code == GatewayCloseCodes.INVALID_API_VERSION) {
|
||||||
|
throw new Error("Invalid API Version was used. This shouldn't happen!")
|
||||||
|
} else if(event.code == GatewayCloseCodes.INVALID_INTENTS) {
|
||||||
|
throw new Error("Invalid Intents")
|
||||||
|
} else if(event.code == GatewayCloseCodes.DISALLOWED_INTENTS) {
|
||||||
|
throw new Error("Given Intents aren't allowed")
|
||||||
|
} else {
|
||||||
|
this.debug("Unknown Close code, probably connection error. Reconnecting.")
|
||||||
|
this.reconnect()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private onerror (event: Event | ErrorEvent): void {
|
private onerror (event: Event | ErrorEvent): void {
|
||||||
const eventError = event as ErrorEvent
|
const eventError = event as ErrorEvent
|
||||||
|
|
||||||
console.log(eventError)
|
console.log(eventError)
|
||||||
}
|
}
|
||||||
|
|
||||||
private sendIdentify (): void {
|
private async sendIdentify (forceNewSession?: boolean) {
|
||||||
|
this.debug("Fetching /gateway/bot...")
|
||||||
|
const info = await this.client.rest.get(GATEWAY_BOT()) as GatewayBotPayload
|
||||||
|
if(info.session_start_limit.remaining == 0) throw new Error("Session Limit Reached. Retry After " + info.session_start_limit.reset_after + "ms")
|
||||||
|
this.debug("Recommended Shards: " + info.shards)
|
||||||
|
this.debug("=== Session Limit Info ===")
|
||||||
|
this.debug(`Remaining: ${info.session_start_limit.remaining}/${info.session_start_limit.total}`)
|
||||||
|
this.debug(`Reset After: ${info.session_start_limit.reset_after}ms`)
|
||||||
|
if(!forceNewSession) {
|
||||||
|
let sessionIDCached = await this.cache.get("session_id")
|
||||||
|
if(sessionIDCached) {
|
||||||
|
this.debug("Found Cached SessionID: " + sessionIDCached)
|
||||||
|
this.sessionID = sessionIDCached
|
||||||
|
return this.sendResume()
|
||||||
|
}
|
||||||
|
}
|
||||||
this.websocket.send(
|
this.websocket.send(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
op: GatewayOpcodes.IDENTIFY,
|
op: GatewayOpcodes.IDENTIFY,
|
||||||
|
@ -163,19 +229,36 @@ class Gateway {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private sendResume (): void {
|
private async sendResume (): Promise<void> {
|
||||||
|
this.debug(`Preparing to resume with Session: ${this.sessionID}`)
|
||||||
|
if(this.sequenceID === undefined) {
|
||||||
|
let cached = await this.cache.get("seq")
|
||||||
|
if(cached) this.sequenceID = typeof cached == "string" ? parseInt(cached) : cached
|
||||||
|
}
|
||||||
|
const resumePayload = {
|
||||||
|
op: GatewayOpcodes.RESUME,
|
||||||
|
d: {
|
||||||
|
token: this.token,
|
||||||
|
session_id: this.sessionID,
|
||||||
|
seq: this.sequenceID || null
|
||||||
|
}
|
||||||
|
}
|
||||||
this.websocket.send(
|
this.websocket.send(
|
||||||
JSON.stringify({
|
JSON.stringify(resumePayload)
|
||||||
op: GatewayOpcodes.RESUME,
|
|
||||||
d: {
|
|
||||||
token: this.token,
|
|
||||||
session_id: this.sessionID,
|
|
||||||
seq: this.sequenceID
|
|
||||||
}
|
|
||||||
})
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
debug(msg: string) {
|
||||||
|
this.client.debug("Gateway", msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
async reconnect(forceNew?: boolean) {
|
||||||
|
clearInterval(this.heartbeatIntervalID)
|
||||||
|
if(forceNew) await this.cache.delete("session_id")
|
||||||
|
this.close()
|
||||||
|
this.initWebsocket()
|
||||||
|
}
|
||||||
|
|
||||||
initWebsocket (): void {
|
initWebsocket (): void {
|
||||||
this.websocket = new WebSocket(
|
this.websocket = new WebSocket(
|
||||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||||
|
|
32
src/managers/BaseManager.ts
Normal file
32
src/managers/BaseManager.ts
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
import { Client } from "../models/client.ts";
|
||||||
|
import { Base } from "../structures/base.ts";
|
||||||
|
|
||||||
|
export class BaseManager<T, T2> {
|
||||||
|
client: Client
|
||||||
|
cacheName: string
|
||||||
|
dataType: any
|
||||||
|
|
||||||
|
constructor(client: Client, cacheName: string, dataType: any) {
|
||||||
|
this.client = client
|
||||||
|
this.cacheName = cacheName
|
||||||
|
this.dataType = dataType
|
||||||
|
}
|
||||||
|
|
||||||
|
_get(key: string): Promise<T> {
|
||||||
|
return this.client.cache.get(this.cacheName, key) as Promise<T>
|
||||||
|
}
|
||||||
|
|
||||||
|
async get(key: string): Promise<T2 | void> {
|
||||||
|
const raw = await this._get(key)
|
||||||
|
if(!raw) return
|
||||||
|
return new this.dataType(this.client, raw) as any
|
||||||
|
}
|
||||||
|
|
||||||
|
async set(key: string, value: T) {
|
||||||
|
return this.client.cache.set(this.cacheName, key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(key: string) {
|
||||||
|
return this.client.cache.delete(this.cacheName, key)
|
||||||
|
}
|
||||||
|
}
|
26
src/managers/ChannelsManager.ts
Normal file
26
src/managers/ChannelsManager.ts
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import { Client } from "../models/client.ts";
|
||||||
|
import { Channel } from "../structures/channel.ts";
|
||||||
|
import { User } from "../structures/user.ts";
|
||||||
|
import { ChannelPayload } from "../types/channel.ts";
|
||||||
|
import { CHANNEL } from "../types/endpoint.ts";
|
||||||
|
import { BaseManager } from "./BaseManager.ts";
|
||||||
|
|
||||||
|
export class ChannelsManager extends BaseManager<ChannelPayload, Channel> {
|
||||||
|
constructor(client: Client) {
|
||||||
|
super(client, "channels", User)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Override get method as Generic
|
||||||
|
async get<T = Channel>(key: string): Promise<T> {
|
||||||
|
return new this.dataType(this.client, this._get(key)) as any
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch(id: string): Promise<Channel> {
|
||||||
|
return new Promise((res, rej) => {
|
||||||
|
this.client.rest.get(CHANNEL(id)).then(data => {
|
||||||
|
this.set(id, data as ChannelPayload)
|
||||||
|
res(new Channel(this.client, data as ChannelPayload))
|
||||||
|
}).catch(e => rej(e))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
20
src/managers/EmojisManager.ts
Normal file
20
src/managers/EmojisManager.ts
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import { Client } from "../models/client.ts";
|
||||||
|
import { Emoji } from "../structures/emoji.ts";
|
||||||
|
import { EmojiPayload } from "../types/emoji.ts";
|
||||||
|
import { CHANNEL } from "../types/endpoint.ts";
|
||||||
|
import { BaseManager } from "./BaseManager.ts";
|
||||||
|
|
||||||
|
export class EmojisManager extends BaseManager<EmojiPayload, Emoji> {
|
||||||
|
constructor(client: Client) {
|
||||||
|
super(client, "emojis", Emoji)
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch(id: string) {
|
||||||
|
return new Promise((res, rej) => {
|
||||||
|
this.client.rest.get(CHANNEL(id)).then(data => {
|
||||||
|
this.set(id, data as EmojiPayload)
|
||||||
|
res(new Emoji(this.client, data as EmojiPayload))
|
||||||
|
}).catch(e => rej(e))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
24
src/managers/GatewayCache.ts
Normal file
24
src/managers/GatewayCache.ts
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import { Client } from "../models/client.ts";
|
||||||
|
|
||||||
|
export class GatewayCache {
|
||||||
|
client: Client
|
||||||
|
cacheName: string = "discord_gateway_cache"
|
||||||
|
|
||||||
|
constructor(client: Client, cacheName?: string) {
|
||||||
|
this.client = client
|
||||||
|
if(cacheName) this.cacheName = cacheName
|
||||||
|
}
|
||||||
|
|
||||||
|
get(key: string) {
|
||||||
|
return this.client.cache.get(this.cacheName, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
set(key: string, value: any) {
|
||||||
|
return this.client.cache.set(this.cacheName, key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(key: string) {
|
||||||
|
console.log(`[GatewayCache] DEL ${key}`)
|
||||||
|
return this.client.cache.delete(this.cacheName, key)
|
||||||
|
}
|
||||||
|
}
|
20
src/managers/GuildsManager.ts
Normal file
20
src/managers/GuildsManager.ts
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import { Client } from "../models/client.ts";
|
||||||
|
import { Guild } from "../structures/guild.ts";
|
||||||
|
import { GUILD } from "../types/endpoint.ts";
|
||||||
|
import { GuildPayload } from "../types/guild.ts";
|
||||||
|
import { BaseManager } from "./BaseManager.ts";
|
||||||
|
|
||||||
|
export class GuildManager extends BaseManager<GuildPayload, Guild> {
|
||||||
|
constructor(client: Client) {
|
||||||
|
super(client, "guilds", Guild)
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch(id: string) {
|
||||||
|
return new Promise((res, rej) => {
|
||||||
|
this.client.rest.get(GUILD(id)).then(data => {
|
||||||
|
this.set(id, data as GuildPayload)
|
||||||
|
res(new Guild(this.client, data as GuildPayload))
|
||||||
|
}).catch(e => rej(e))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
29
src/managers/MessagesManager.ts
Normal file
29
src/managers/MessagesManager.ts
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
import { Client } from "../models/client.ts";
|
||||||
|
import { Message } from "../structures/message.ts";
|
||||||
|
import { MessageMentions } from "../structures/MessageMentions.ts";
|
||||||
|
import { User } from "../structures/user.ts";
|
||||||
|
import { MessagePayload } from "../types/channel.ts";
|
||||||
|
import { CHANNEL_MESSAGE } from "../types/endpoint.ts";
|
||||||
|
import { UserPayload } from "../types/user.ts";
|
||||||
|
import { BaseManager } from "./BaseManager.ts";
|
||||||
|
|
||||||
|
export class MessagesManager extends BaseManager<MessagePayload, Message> {
|
||||||
|
constructor(client: Client) {
|
||||||
|
super(client, "messages", Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch(channelID: string, id: string) {
|
||||||
|
return new Promise((res, rej) => {
|
||||||
|
this.client.rest.get(CHANNEL_MESSAGE(channelID, id)).then(async data => {
|
||||||
|
this.set(id, data as MessagePayload)
|
||||||
|
let channel = await this.client.channels.get(channelID)
|
||||||
|
if(!channel) channel = await this.client.channels.fetch(channelID)
|
||||||
|
let author = new User(this.client, (data as MessagePayload).author as UserPayload)
|
||||||
|
await this.client.users.set(author.id, (data as MessagePayload).author)
|
||||||
|
// TODO: Make this thing work (MessageMentions)
|
||||||
|
let mentions = new MessageMentions()
|
||||||
|
res(new Message(this.client, data as MessagePayload, channel, author, mentions))
|
||||||
|
}).catch(e => rej(e))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
25
src/managers/RolesManager.ts
Normal file
25
src/managers/RolesManager.ts
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import { Client } from "../models/client.ts";
|
||||||
|
import { Guild } from "../structures/guild.ts";
|
||||||
|
import { Role } from "../structures/role.ts";
|
||||||
|
import { User } from "../structures/user.ts";
|
||||||
|
import { GUILD_ROLE } from "../types/endpoint.ts";
|
||||||
|
import { RolePayload } from "../types/role.ts";
|
||||||
|
import { BaseManager } from "./BaseManager.ts";
|
||||||
|
|
||||||
|
export class RolesManager extends BaseManager<RolePayload, Role> {
|
||||||
|
guild: Guild
|
||||||
|
|
||||||
|
constructor(client: Client, guild: Guild) {
|
||||||
|
super(client, "roles:" + guild.id, Role)
|
||||||
|
this.guild = guild
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch(id: string) {
|
||||||
|
return new Promise((res, rej) => {
|
||||||
|
this.client.rest.get(GUILD_ROLE(this.guild.id, id)).then(data => {
|
||||||
|
this.set(id, data as RolePayload)
|
||||||
|
res(new Role(this.client, data as RolePayload))
|
||||||
|
}).catch(e => rej(e))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
20
src/managers/UsersManager.ts
Normal file
20
src/managers/UsersManager.ts
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import { Client } from "../models/client.ts";
|
||||||
|
import { User } from "../structures/user.ts";
|
||||||
|
import { USER } from "../types/endpoint.ts";
|
||||||
|
import { UserPayload } from "../types/user.ts";
|
||||||
|
import { BaseManager } from "./BaseManager.ts";
|
||||||
|
|
||||||
|
export class UserManager extends BaseManager<UserPayload, User> {
|
||||||
|
constructor(client: Client) {
|
||||||
|
super(client, "users", User)
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch(id: string) {
|
||||||
|
return new Promise((res, rej) => {
|
||||||
|
this.client.rest.get(USER(id)).then(data => {
|
||||||
|
this.set(id, data as UserPayload)
|
||||||
|
res(new User(this.client, data as UserPayload))
|
||||||
|
}).catch(e => rej(e))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
98
src/models/CacheAdapter.ts
Normal file
98
src/models/CacheAdapter.ts
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
import { Collection } from "../utils/collection.ts";
|
||||||
|
import { Client } from "./client.ts";
|
||||||
|
import { connect, Redis, RedisConnectOptions } from "https://denopkg.com/keroxp/deno-redis/mod.ts";
|
||||||
|
|
||||||
|
export interface ICacheAdapter {
|
||||||
|
client: Client
|
||||||
|
get: (cacheName: string, key: string) => Promise<any> | any
|
||||||
|
set: (cacheName: string, key: string, value: any) => Promise<any> | any
|
||||||
|
delete: (cacheName: string, key: string) => Promise<boolean> | boolean
|
||||||
|
array: (cacheName: string) => void | any[] | Promise<any[] | void>
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DefaultCacheAdapter implements ICacheAdapter {
|
||||||
|
client: Client
|
||||||
|
data: {
|
||||||
|
[name: string]: Collection<string, any>
|
||||||
|
} = {}
|
||||||
|
|
||||||
|
constructor(client: Client) {
|
||||||
|
this.client = client
|
||||||
|
}
|
||||||
|
|
||||||
|
async get(cacheName: string, key: string) {
|
||||||
|
const cache = this.data[cacheName]
|
||||||
|
if (!cache) return;
|
||||||
|
return cache.get(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
async set(cacheName: string, key: string, value: any) {
|
||||||
|
let cache = this.data[cacheName]
|
||||||
|
if (!cache) {
|
||||||
|
this.data[cacheName] = new Collection()
|
||||||
|
cache = this.data[cacheName]
|
||||||
|
}
|
||||||
|
cache.set(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(cacheName: string, key: string) {
|
||||||
|
const cache = this.data[cacheName]
|
||||||
|
if (!cache) return false
|
||||||
|
return cache.delete(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
async array(cacheName: string) {
|
||||||
|
const cache = this.data[cacheName]
|
||||||
|
if (!cache) return
|
||||||
|
return cache.array()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class RedisCacheAdapter implements ICacheAdapter {
|
||||||
|
client: Client
|
||||||
|
_redis: Promise<Redis>
|
||||||
|
redis?: Redis
|
||||||
|
ready: boolean = false
|
||||||
|
|
||||||
|
constructor(client: Client, options: RedisConnectOptions) {
|
||||||
|
this.client = client
|
||||||
|
this._redis = connect(options)
|
||||||
|
this._redis.then(redis => {
|
||||||
|
this.redis = redis
|
||||||
|
this.ready = true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async _checkReady() {
|
||||||
|
if(!this.ready) return await this._redis;
|
||||||
|
else return;
|
||||||
|
}
|
||||||
|
|
||||||
|
async get(cacheName: string, key: string) {
|
||||||
|
await this._checkReady()
|
||||||
|
let cache = await this.redis?.hget(cacheName, key)
|
||||||
|
if(!cache) return
|
||||||
|
try {
|
||||||
|
return JSON.parse(cache as string)
|
||||||
|
} catch(e) { return cache }
|
||||||
|
}
|
||||||
|
|
||||||
|
async set(cacheName: string, key: string, value: any) {
|
||||||
|
await this._checkReady()
|
||||||
|
return await this.redis?.hset(cacheName, key, typeof value === "object" ? JSON.stringify(value) : value)
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(cacheName: string, key: string) {
|
||||||
|
await this._checkReady()
|
||||||
|
let exists = await this.redis?.hexists(cacheName, key)
|
||||||
|
if(!exists) return false
|
||||||
|
await this.redis?.hdel(cacheName, key)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
async array(cacheName: string) {
|
||||||
|
await this._checkReady()
|
||||||
|
let data = await this.redis?.hvals(cacheName)
|
||||||
|
return data?.map((e: string) => JSON.parse(e))
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,30 +1,72 @@
|
||||||
import { User } from '../structures/user.ts'
|
import { User } from '../structures/user.ts'
|
||||||
import { GatewayIntents } from '../types/gateway.ts'
|
import { GatewayIntents } from '../types/gateway.ts'
|
||||||
import { Gateway } from '../gateway/index.ts'
|
import { Gateway } from '../gateway/index.ts'
|
||||||
import { Rest } from './rest.ts'
|
import { RESTManager } from './rest.ts'
|
||||||
import EventEmitter from 'https://deno.land/std@0.74.0/node/events.ts'
|
import EventEmitter from 'https://deno.land/std@0.74.0/node/events.ts'
|
||||||
|
import { DefaultCacheAdapter, ICacheAdapter } from "./CacheAdapter.ts"
|
||||||
|
import { UserManager } from "../managers/UsersManager.ts"
|
||||||
|
import { GuildManager } from "../managers/GuildsManager.ts"
|
||||||
|
import { EmojisManager } from "../managers/EmojisManager.ts"
|
||||||
|
import { ChannelsManager } from "../managers/ChannelsManager.ts"
|
||||||
|
import { MessagesManager } from "../managers/MessagesManager.ts"
|
||||||
|
|
||||||
|
/** Some Client Options to modify behaviour */
|
||||||
|
export interface ClientOptions {
|
||||||
|
token?: string
|
||||||
|
intents?: GatewayIntents[]
|
||||||
|
cache?: ICacheAdapter
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Discord Client.
|
* Discord Client.
|
||||||
*/
|
*/
|
||||||
export class Client extends EventEmitter {
|
export class Client extends EventEmitter {
|
||||||
gateway?: Gateway
|
gateway?: Gateway
|
||||||
rest?: Rest
|
rest: RESTManager = new RESTManager(this)
|
||||||
user?: User
|
user?: User
|
||||||
ping = 0
|
ping = 0
|
||||||
token?: string
|
token?: string
|
||||||
|
cache: ICacheAdapter = new DefaultCacheAdapter(this)
|
||||||
|
intents?: GatewayIntents[]
|
||||||
|
|
||||||
|
users: UserManager = new UserManager(this)
|
||||||
|
guilds: GuildManager = new GuildManager(this)
|
||||||
|
channels: ChannelsManager = new ChannelsManager(this)
|
||||||
|
messages: MessagesManager = new MessagesManager(this)
|
||||||
|
emojis: EmojisManager = new EmojisManager(this)
|
||||||
|
|
||||||
// constructor () {
|
constructor (options: ClientOptions = {}) {
|
||||||
// super()
|
super()
|
||||||
// }
|
this.token = options.token
|
||||||
|
this.intents = options.intents
|
||||||
|
if(options.cache) this.cache = options.cache
|
||||||
|
}
|
||||||
|
|
||||||
|
setAdapter(adapter: ICacheAdapter) {
|
||||||
|
this.cache = adapter
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
debug(tag: string, msg: string) {
|
||||||
|
this.emit("debug", `[${tag}] ${msg}`)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This function is used for connect to discord.
|
* This function is used for connect to discord.
|
||||||
* @param token Your token. This is required.
|
* @param token Your token. This is required.
|
||||||
* @param intents Gateway intents in array. This is required.
|
* @param intents Gateway intents in array. This is required.
|
||||||
*/
|
*/
|
||||||
connect (token: string, intents: GatewayIntents[]): void {
|
connect (token?: string, intents?: GatewayIntents[]): void {
|
||||||
this.token = token
|
if(!token && this.token) token = this.token
|
||||||
|
else if(!this.token && token) {
|
||||||
|
this.token = token
|
||||||
|
}
|
||||||
|
else throw new Error("No Token Provided")
|
||||||
|
if(!intents && this.intents) intents = this.intents
|
||||||
|
else if(intents && !this.intents) {
|
||||||
|
this.intents = intents
|
||||||
|
}
|
||||||
|
else throw new Error("No Gateway Intents were provided")
|
||||||
this.gateway = new Gateway(this, token, intents)
|
this.gateway = new Gateway(this, token, intents)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,367 @@
|
||||||
import { Client } from './client.ts'
|
import { delay } from "../utils/index.ts";
|
||||||
|
import * as baseEndpoints from "../consts/urlsAndVersions.ts";
|
||||||
|
import { Client } from "./client.ts";
|
||||||
|
|
||||||
class Rest {
|
export enum HttpResponseCode {
|
||||||
client: Client
|
Ok = 200,
|
||||||
constructor (client: Client) {
|
Created = 201,
|
||||||
this.client = client
|
NoContent = 204,
|
||||||
}
|
NotModified = 304,
|
||||||
// TODO: make endpoints function
|
BadRequest = 400,
|
||||||
|
Unauthorized = 401,
|
||||||
|
Forbidden = 403,
|
||||||
|
NotFound = 404,
|
||||||
|
MethodNotAllowed = 405,
|
||||||
|
TooManyRequests = 429,
|
||||||
|
GatewayUnavailable = 502
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Rest }
|
export type RequestMethods =
|
||||||
|
| "get"
|
||||||
|
| "post"
|
||||||
|
| "put"
|
||||||
|
| "patch"
|
||||||
|
| "head"
|
||||||
|
| "delete";
|
||||||
|
|
||||||
|
export interface QueuedRequest {
|
||||||
|
callback: () => Promise<
|
||||||
|
void | {
|
||||||
|
rateLimited: any;
|
||||||
|
beforeFetch: boolean;
|
||||||
|
bucketID?: string | null;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
bucketID?: string | null;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RateLimitedPath {
|
||||||
|
url: string;
|
||||||
|
resetTimestamp: number;
|
||||||
|
bucketID: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class RESTManager {
|
||||||
|
client: Client;
|
||||||
|
globallyRateLimited: boolean = false;
|
||||||
|
queueInProcess: boolean = false;
|
||||||
|
pathQueues: { [key: string]: QueuedRequest[] } = {};
|
||||||
|
ratelimitedPaths = new Map<string, RateLimitedPath>();
|
||||||
|
|
||||||
|
constructor(client: Client) {
|
||||||
|
this.client = client;
|
||||||
|
}
|
||||||
|
|
||||||
|
async processRateLimitedPaths() {
|
||||||
|
const now = Date.now();
|
||||||
|
this.ratelimitedPaths.forEach((value, key) => {
|
||||||
|
if (value.resetTimestamp > now) return;
|
||||||
|
this.ratelimitedPaths.delete(key);
|
||||||
|
if (key === "global") this.globallyRateLimited = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
await delay(1000);
|
||||||
|
this.processRateLimitedPaths();
|
||||||
|
}
|
||||||
|
|
||||||
|
addToQueue(request: QueuedRequest) {
|
||||||
|
const route = request.url.substring(baseEndpoints.DISCORD_API_URL.length + 1);
|
||||||
|
const parts = route.split("/");
|
||||||
|
// Remove the major param
|
||||||
|
parts.shift();
|
||||||
|
const [id] = parts;
|
||||||
|
|
||||||
|
if (this.pathQueues[id]) {
|
||||||
|
this.pathQueues[id].push(request);
|
||||||
|
} else {
|
||||||
|
this.pathQueues[id] = [request];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async cleanupQueues() {
|
||||||
|
Object.entries(this.pathQueues).map(([key, value]) => {
|
||||||
|
if (!value.length) {
|
||||||
|
// Remove it entirely
|
||||||
|
delete this.pathQueues[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async processQueue() {
|
||||||
|
if (
|
||||||
|
(Object.keys(this.pathQueues).length) && !this.globallyRateLimited
|
||||||
|
) {
|
||||||
|
await Promise.allSettled(
|
||||||
|
Object.values(this.pathQueues).map(async (pathQueue) => {
|
||||||
|
const request = pathQueue.shift();
|
||||||
|
if (!request) return;
|
||||||
|
|
||||||
|
const rateLimitedURLResetIn = await this.checkRatelimits(request.url);
|
||||||
|
|
||||||
|
if (request.bucketID) {
|
||||||
|
const rateLimitResetIn = await this.checkRatelimits(request.bucketID);
|
||||||
|
if (rateLimitResetIn) {
|
||||||
|
// This request is still rate limited readd to queue
|
||||||
|
this.addToQueue(request);
|
||||||
|
} else if (rateLimitedURLResetIn) {
|
||||||
|
// This URL is rate limited readd to queue
|
||||||
|
this.addToQueue(request);
|
||||||
|
} else {
|
||||||
|
// This request is not rate limited so it should be run
|
||||||
|
const result = await request.callback();
|
||||||
|
if (result && result.rateLimited) {
|
||||||
|
this.addToQueue(
|
||||||
|
{ ...request, bucketID: result.bucketID || request.bucketID },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (rateLimitedURLResetIn) {
|
||||||
|
// This URL is rate limited readd to queue
|
||||||
|
this.addToQueue(request);
|
||||||
|
} else {
|
||||||
|
// This request has no bucket id so it should be processed
|
||||||
|
const result = await request.callback();
|
||||||
|
if (request && result && result.rateLimited) {
|
||||||
|
this.addToQueue(
|
||||||
|
{ ...request, bucketID: result.bucketID || request.bucketID },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(this.pathQueues).length) {
|
||||||
|
await delay(1000);
|
||||||
|
this.processQueue();
|
||||||
|
this.cleanupQueues();
|
||||||
|
} else this.queueInProcess = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
createRequestBody(body: any, method: RequestMethods) {
|
||||||
|
const headers: { [key: string]: string } = {
|
||||||
|
Authorization: `Bot ${this.client.token}`,
|
||||||
|
"User-Agent":
|
||||||
|
`DiscordBot (discord.deno)`,
|
||||||
|
};
|
||||||
|
|
||||||
|
if(!this.client.token) delete headers.Authorization;
|
||||||
|
|
||||||
|
if (method === "get") body = undefined;
|
||||||
|
|
||||||
|
if (body?.reason) {
|
||||||
|
headers["X-Audit-Log-Reason"] = encodeURIComponent(body.reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (body?.file) {
|
||||||
|
const form = new FormData();
|
||||||
|
form.append("file", body.file.blob, body.file.name);
|
||||||
|
form.append("payload_json", JSON.stringify({ ...body, file: undefined }));
|
||||||
|
body.file = form;
|
||||||
|
} else if (
|
||||||
|
body && !["get", "delete"].includes(method)
|
||||||
|
) {
|
||||||
|
headers["Content-Type"] = "application/json";
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
headers,
|
||||||
|
body: body?.file || JSON.stringify(body),
|
||||||
|
method: method.toUpperCase(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkRatelimits(url: string) {
|
||||||
|
const ratelimited = this.ratelimitedPaths.get(url);
|
||||||
|
const global = this.ratelimitedPaths.get("global");
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
if (ratelimited && now < ratelimited.resetTimestamp) {
|
||||||
|
return ratelimited.resetTimestamp - now;
|
||||||
|
}
|
||||||
|
if (global && now < global.resetTimestamp) {
|
||||||
|
return global.resetTimestamp - now;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async runMethod(
|
||||||
|
method: RequestMethods,
|
||||||
|
url: string,
|
||||||
|
body?: unknown,
|
||||||
|
retryCount = 0,
|
||||||
|
bucketID?: string | null,
|
||||||
|
) {
|
||||||
|
|
||||||
|
const errorStack = new Error("Location In Your Files:");
|
||||||
|
Error.captureStackTrace(errorStack);
|
||||||
|
|
||||||
|
return await new Promise((resolve, reject) => {
|
||||||
|
const callback = async () => {
|
||||||
|
try {
|
||||||
|
const rateLimitResetIn = await this.checkRatelimits(url);
|
||||||
|
if (rateLimitResetIn) {
|
||||||
|
return { rateLimited: rateLimitResetIn, beforeFetch: true, bucketID };
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = method === "get" && body
|
||||||
|
? Object.entries(body as any).map(([key, value]) =>
|
||||||
|
`${encodeURIComponent(key)}=${encodeURIComponent(value as any)}`
|
||||||
|
)
|
||||||
|
.join("&")
|
||||||
|
: "";
|
||||||
|
const urlToUse = method === "get" && query ? `${url}?${query}` : url;
|
||||||
|
|
||||||
|
const response = await fetch(urlToUse, this.createRequestBody(body, method));
|
||||||
|
const bucketIDFromHeaders = this.processHeaders(url, response.headers);
|
||||||
|
this.handleStatusCode(response, errorStack);
|
||||||
|
|
||||||
|
// Sometimes Discord returns an empty 204 response that can't be made to JSON.
|
||||||
|
if (response.status === 204) return resolve();
|
||||||
|
|
||||||
|
const json = await response.json();
|
||||||
|
if (
|
||||||
|
json.retry_after ||
|
||||||
|
json.message === "You are being rate limited."
|
||||||
|
) {
|
||||||
|
if (retryCount > 10) {
|
||||||
|
throw new Error("Max RateLimit Retries hit");
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
rateLimited: json.retry_after,
|
||||||
|
beforeFetch: false,
|
||||||
|
bucketID: bucketIDFromHeaders,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return resolve(json);
|
||||||
|
} catch (error) {
|
||||||
|
return reject(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.addToQueue({
|
||||||
|
callback,
|
||||||
|
bucketID,
|
||||||
|
url,
|
||||||
|
});
|
||||||
|
if (!this.queueInProcess) {
|
||||||
|
this.queueInProcess = true;
|
||||||
|
this.processQueue();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async logErrors(response: Response, errorStack?: unknown) {
|
||||||
|
try {
|
||||||
|
const error = await response.json();
|
||||||
|
console.error(error);
|
||||||
|
} catch {
|
||||||
|
console.error(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleStatusCode(response: Response, errorStack?: unknown) {
|
||||||
|
const status = response.status;
|
||||||
|
|
||||||
|
if (
|
||||||
|
(status >= 200 && status < 400) ||
|
||||||
|
status === HttpResponseCode.TooManyRequests
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logErrors(response, errorStack);
|
||||||
|
|
||||||
|
switch (status) {
|
||||||
|
case HttpResponseCode.BadRequest:
|
||||||
|
case HttpResponseCode.Unauthorized:
|
||||||
|
case HttpResponseCode.Forbidden:
|
||||||
|
case HttpResponseCode.NotFound:
|
||||||
|
case HttpResponseCode.MethodNotAllowed:
|
||||||
|
throw new Error("Request Client Error");
|
||||||
|
case HttpResponseCode.GatewayUnavailable:
|
||||||
|
throw new Error("Request Server Error");
|
||||||
|
}
|
||||||
|
|
||||||
|
// left are all unknown
|
||||||
|
throw new Error("Request Unknown Error");
|
||||||
|
}
|
||||||
|
|
||||||
|
processHeaders(url: string, headers: Headers) {
|
||||||
|
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 bucketID = headers.get("x-ratelimit-bucket");
|
||||||
|
|
||||||
|
// If there is no remaining rate limit for this endpoint, we save it in cache
|
||||||
|
if (remaining && remaining === "0") {
|
||||||
|
ratelimited = true;
|
||||||
|
|
||||||
|
this.ratelimitedPaths.set(url, {
|
||||||
|
url,
|
||||||
|
resetTimestamp: Number(resetTimestamp) * 1000,
|
||||||
|
bucketID,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (bucketID) {
|
||||||
|
this.ratelimitedPaths.set(bucketID, {
|
||||||
|
url,
|
||||||
|
resetTimestamp: Number(resetTimestamp) * 1000,
|
||||||
|
bucketID,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there is no remaining global limit, we save it in cache
|
||||||
|
if (global) {
|
||||||
|
const reset = Date.now() + Number(retryAfter);
|
||||||
|
this.globallyRateLimited = true;
|
||||||
|
ratelimited = true;
|
||||||
|
|
||||||
|
this.ratelimitedPaths.set("global", {
|
||||||
|
url: "global",
|
||||||
|
resetTimestamp: reset,
|
||||||
|
bucketID,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (bucketID) {
|
||||||
|
this.ratelimitedPaths.set(bucketID, {
|
||||||
|
url: "global",
|
||||||
|
resetTimestamp: reset,
|
||||||
|
bucketID,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ratelimited ? bucketID : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
get(url: string, body?: unknown) {
|
||||||
|
return this.runMethod("get", url, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
post(url: string, body?: unknown) {
|
||||||
|
return this.runMethod("post", url, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(url: string, body?: unknown) {
|
||||||
|
return this.runMethod("delete", url, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
patch(url: string, body?: unknown) {
|
||||||
|
return this.runMethod("patch", url, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
put(url: string, body?: unknown) {
|
||||||
|
return this.runMethod("put", url, body);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1 +1 @@
|
||||||
//TODO: write code
|
// TODO: write code
|
3
src/structures/MessageMentions.ts
Normal file
3
src/structures/MessageMentions.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
export class MessageMentions {
|
||||||
|
str: string = "str"
|
||||||
|
}
|
|
@ -15,7 +15,8 @@ export class Channel extends Base {
|
||||||
super(client, data)
|
super(client, data)
|
||||||
this.type = data.type
|
this.type = data.type
|
||||||
this.id = data.id
|
this.id = data.id
|
||||||
cache.set('channel', this.id, this)
|
// TODO: Cache in Gateway Event Code
|
||||||
|
// this.client.channels.set(this.id, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected readFromData (data: ChannelPayload): void {
|
protected readFromData (data: ChannelPayload): void {
|
||||||
|
|
|
@ -10,7 +10,7 @@ export class DMChannel extends TextChannel {
|
||||||
constructor (client: Client, data: DMChannelPayload) {
|
constructor (client: Client, data: DMChannelPayload) {
|
||||||
super(client, data)
|
super(client, data)
|
||||||
this.recipients = data.recipients
|
this.recipients = data.recipients
|
||||||
cache.set('dmchannel', this.id, this)
|
// cache.set('dmchannel', this.id, this)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected readFromData (data: DMChannelPayload): void {
|
protected readFromData (data: DMChannelPayload): void {
|
||||||
|
|
|
@ -14,7 +14,8 @@ export class GroupDMChannel extends Channel {
|
||||||
this.name = data.name
|
this.name = data.name
|
||||||
this.icon = data.icon
|
this.icon = data.icon
|
||||||
this.ownerID = data.owner_id
|
this.ownerID = data.owner_id
|
||||||
cache.set('groupchannel', this.id, this)
|
// TODO: Cache in Gateway Event Code
|
||||||
|
// cache.set('groupchannel', this.id, this)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected readFromData (data: GroupDMChannelPayload): void {
|
protected readFromData (data: GroupDMChannelPayload): void {
|
||||||
|
|
|
@ -5,10 +5,11 @@ import { Base } from './base.ts'
|
||||||
import { Channel } from './channel.ts'
|
import { Channel } from './channel.ts'
|
||||||
import { Emoji } from './emoji.ts'
|
import { Emoji } from './emoji.ts'
|
||||||
import { Member } from './member.ts'
|
import { Member } from './member.ts'
|
||||||
import { Role } from './role.ts'
|
|
||||||
import { VoiceState } from './voiceState.ts'
|
import { VoiceState } from './voiceState.ts'
|
||||||
import cache from '../models/cache.ts'
|
import cache from '../models/cache.ts'
|
||||||
import getChannelByType from '../utils/getChannelByType.ts'
|
import getChannelByType from '../utils/getChannelByType.ts'
|
||||||
|
import { RolesManager } from "../managers/RolesManager.ts"
|
||||||
|
import { Role } from "./role.ts"
|
||||||
|
|
||||||
export class Guild extends Base {
|
export class Guild extends Base {
|
||||||
id: string
|
id: string
|
||||||
|
@ -28,7 +29,7 @@ export class Guild extends Base {
|
||||||
verificationLevel?: string
|
verificationLevel?: string
|
||||||
defaultMessageNotifications?: string
|
defaultMessageNotifications?: string
|
||||||
explicitContentFilter?: string
|
explicitContentFilter?: string
|
||||||
roles?: Role[]
|
roles: RolesManager = new RolesManager(this.client, this)
|
||||||
emojis?: Emoji[]
|
emojis?: Emoji[]
|
||||||
features?: GuildFeatures[]
|
features?: GuildFeatures[]
|
||||||
mfaLevel?: string
|
mfaLevel?: string
|
||||||
|
@ -79,12 +80,15 @@ export class Guild extends Base {
|
||||||
this.verificationLevel = data.verification_level
|
this.verificationLevel = data.verification_level
|
||||||
this.defaultMessageNotifications = data.default_message_notifications
|
this.defaultMessageNotifications = data.default_message_notifications
|
||||||
this.explicitContentFilter = data.explicit_content_filter
|
this.explicitContentFilter = data.explicit_content_filter
|
||||||
this.roles = data.roles.map(
|
// this.roles = data.roles.map(
|
||||||
v => cache.get('role', v.id) ?? new Role(client, v)
|
// v => cache.get('role', v.id) ?? new Role(client, v)
|
||||||
)
|
// )
|
||||||
this.emojis = data.emojis.map(
|
// data.roles.forEach(role => {
|
||||||
v => cache.get('emoji', v.id) ?? new Emoji(client, v)
|
// this.roles.set(role.id, new Role(client, role))
|
||||||
)
|
// })
|
||||||
|
// this.emojis = data.emojis.map(
|
||||||
|
// v => cache.get('emoji', v.id) ?? new Emoji(client, v)
|
||||||
|
// )
|
||||||
this.features = data.features
|
this.features = data.features
|
||||||
this.mfaLevel = data.mfa_level
|
this.mfaLevel = data.mfa_level
|
||||||
this.systemChannelID = data.system_channel_id
|
this.systemChannelID = data.system_channel_id
|
||||||
|
@ -93,19 +97,20 @@ export class Guild extends Base {
|
||||||
this.joinedAt = data.joined_at
|
this.joinedAt = data.joined_at
|
||||||
this.large = data.large
|
this.large = data.large
|
||||||
this.memberCount = data.member_count
|
this.memberCount = data.member_count
|
||||||
this.voiceStates = data.voice_states?.map(
|
// TODO: Cache in Gateway Event code
|
||||||
v =>
|
// this.voiceStates = data.voice_states?.map(
|
||||||
cache.get('voiceState', `${v.guild_id}:${v.user_id}`) ??
|
// v =>
|
||||||
new VoiceState(client, v)
|
// cache.get('voiceState', `${v.guild_id}:${v.user_id}`) ??
|
||||||
)
|
// new VoiceState(client, v)
|
||||||
this.members = data.members?.map(
|
// )
|
||||||
v =>
|
// this.members = data.members?.map(
|
||||||
cache.get('member', `${this.id}:${v.user.id}`) ??
|
// v =>
|
||||||
new Member(client, v)
|
// cache.get('member', `${this.id}:${v.user.id}`) ??
|
||||||
)
|
// new Member(client, v)
|
||||||
this.channels = data.channels?.map(
|
// )
|
||||||
v => cache.get('channel', v.id) ?? getChannelByType(this.client, v)
|
// this.channels = data.channels?.map(
|
||||||
)
|
// v => cache.get('channel', v.id) ?? getChannelByType(this.client, v)
|
||||||
|
// )
|
||||||
this.presences = data.presences
|
this.presences = data.presences
|
||||||
this.maxPresences = data.max_presences
|
this.maxPresences = data.max_presences
|
||||||
this.maxMembers = data.max_members
|
this.maxMembers = data.max_members
|
||||||
|
@ -120,7 +125,6 @@ export class Guild extends Base {
|
||||||
this.approximateNumberCount = data.approximate_number_count
|
this.approximateNumberCount = data.approximate_number_count
|
||||||
this.approximatePresenceCount = data.approximate_presence_count
|
this.approximatePresenceCount = data.approximate_presence_count
|
||||||
}
|
}
|
||||||
cache.set('guild', this.id, this)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected readFromData (data: GuildPayload): void {
|
protected readFromData (data: GuildPayload): void {
|
||||||
|
@ -147,10 +151,10 @@ export class Guild extends Base {
|
||||||
data.default_message_notifications ?? this.defaultMessageNotifications
|
data.default_message_notifications ?? this.defaultMessageNotifications
|
||||||
this.explicitContentFilter =
|
this.explicitContentFilter =
|
||||||
data.explicit_content_filter ?? this.explicitContentFilter
|
data.explicit_content_filter ?? this.explicitContentFilter
|
||||||
this.roles =
|
// this.roles =
|
||||||
data.roles.map(
|
// data.roles.map(
|
||||||
v => cache.get('role', v.id) ?? new Role(this.client, v)
|
// v => cache.get('role', v.id) ?? new Role(this.client, v)
|
||||||
) ?? this.roles
|
// ) ?? this.roles
|
||||||
this.emojis =
|
this.emojis =
|
||||||
data.emojis.map(
|
data.emojis.map(
|
||||||
v => cache.get('emoji', v.id) ?? new Emoji(this.client, v)
|
v => cache.get('emoji', v.id) ?? new Emoji(this.client, v)
|
||||||
|
|
|
@ -22,7 +22,8 @@ export class CategoryChannel extends Channel {
|
||||||
this.permissionOverwrites = data.permission_overwrites
|
this.permissionOverwrites = data.permission_overwrites
|
||||||
this.nsfw = data.nsfw
|
this.nsfw = data.nsfw
|
||||||
this.parentID = data.parent_id
|
this.parentID = data.parent_id
|
||||||
cache.set('guildcategorychannel', this.id, this)
|
// TODO: Cache in Gateway Event Code
|
||||||
|
// cache.set('guildcategorychannel', this.id, this)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected readFromData (data: GuildChannelCategoryPayload): void {
|
protected readFromData (data: GuildChannelCategoryPayload): void {
|
||||||
|
|
|
@ -27,7 +27,8 @@ export class GuildTextChannel extends TextChannel {
|
||||||
this.parentID = data.parent_id
|
this.parentID = data.parent_id
|
||||||
this.topic = data.topic
|
this.topic = data.topic
|
||||||
this.rateLimit = data.rate_limit_per_user
|
this.rateLimit = data.rate_limit_per_user
|
||||||
cache.set('guildtextchannel', this.id, this)
|
// TODO: Cache in Gateway Event Code
|
||||||
|
// cache.set('guildtextchannel', this.id, this)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected readFromData (data: GuildTextChannelPayload): void {
|
protected readFromData (data: GuildTextChannelPayload): void {
|
||||||
|
|
|
@ -23,7 +23,8 @@ export class VoiceChannel extends Channel {
|
||||||
this.permissionOverwrites = data.permission_overwrites
|
this.permissionOverwrites = data.permission_overwrites
|
||||||
this.nsfw = data.nsfw
|
this.nsfw = data.nsfw
|
||||||
this.parentID = data.parent_id
|
this.parentID = data.parent_id
|
||||||
cache.set('guildvoicechannel', this.id, this)
|
// TODO: Cache in Gateway Event Code
|
||||||
|
// cache.set('guildvoicechannel', this.id, this)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected readFromData (data: GuildVoiceChannelPayload): void {
|
protected readFromData (data: GuildVoiceChannelPayload): void {
|
||||||
|
|
|
@ -25,7 +25,8 @@ export class Member extends Base {
|
||||||
this.premiumSince = data.premium_since
|
this.premiumSince = data.premium_since
|
||||||
this.deaf = data.deaf
|
this.deaf = data.deaf
|
||||||
this.mute = data.mute
|
this.mute = data.mute
|
||||||
cache.set('member', this.id, this)
|
// TODO: Cache in Gateway Event Code
|
||||||
|
// cache.set('member', this.id, this)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected readFromData (data: MemberPayload): void {
|
protected readFromData (data: MemberPayload): void {
|
||||||
|
|
|
@ -15,12 +15,16 @@ import { Member } from './member.ts'
|
||||||
import { Embed } from './embed.ts'
|
import { Embed } from './embed.ts'
|
||||||
import { CHANNEL_MESSAGE } from '../types/endpoint.ts'
|
import { CHANNEL_MESSAGE } from '../types/endpoint.ts'
|
||||||
import cache from '../models/cache.ts'
|
import cache from '../models/cache.ts'
|
||||||
|
import { Channel } from "./channel.ts"
|
||||||
|
import { MessageMentions } from "./MessageMentions.ts"
|
||||||
|
import { TextChannel } from "./textChannel.ts"
|
||||||
|
|
||||||
export class Message extends Base {
|
export class Message extends Base {
|
||||||
// eslint-disable-next-line @typescript-eslint/prefer-readonly
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly
|
||||||
private data: MessagePayload
|
private data: MessagePayload
|
||||||
id: string
|
id: string
|
||||||
channelID: string
|
channelID: string
|
||||||
|
channel: Channel
|
||||||
guildID?: string
|
guildID?: string
|
||||||
author: User
|
author: User
|
||||||
member?: Member
|
member?: Member
|
||||||
|
@ -29,7 +33,7 @@ export class Message extends Base {
|
||||||
editedTimestamp?: string
|
editedTimestamp?: string
|
||||||
tts: boolean
|
tts: boolean
|
||||||
mentionEveryone: boolean
|
mentionEveryone: boolean
|
||||||
mentions: User[]
|
mentions: MessageMentions
|
||||||
mentionRoles: string[]
|
mentionRoles: string[]
|
||||||
mentionChannels?: ChannelMention[]
|
mentionChannels?: ChannelMention[]
|
||||||
attachments: Attachment[]
|
attachments: Attachment[]
|
||||||
|
@ -44,22 +48,24 @@ export class Message extends Base {
|
||||||
messageReference?: MessageReference
|
messageReference?: MessageReference
|
||||||
flags?: number
|
flags?: number
|
||||||
|
|
||||||
constructor (client: Client, data: MessagePayload) {
|
constructor (client: Client, data: MessagePayload, channel: Channel, author: User, mentions: MessageMentions) {
|
||||||
super(client)
|
super(client)
|
||||||
this.data = data
|
this.data = data
|
||||||
this.id = data.id
|
this.id = data.id
|
||||||
this.channelID = data.channel_id
|
this.channelID = data.channel_id
|
||||||
this.guildID = data.guild_id
|
this.guildID = data.guild_id
|
||||||
this.author =
|
this.author = author
|
||||||
cache.get('user', data.author.id) ?? new User(this.client, data.author)
|
// this.author =
|
||||||
|
// this.client.users.get(data.author.id) || new User(this.client, data.author)
|
||||||
this.content = data.content
|
this.content = data.content
|
||||||
this.timestamp = data.timestamp
|
this.timestamp = data.timestamp
|
||||||
this.editedTimestamp = data.edited_timestamp
|
this.editedTimestamp = data.edited_timestamp
|
||||||
this.tts = data.tts
|
this.tts = data.tts
|
||||||
this.mentionEveryone = data.mention_everyone
|
this.mentionEveryone = data.mention_everyone
|
||||||
this.mentions = data.mentions.map(
|
this.mentions = mentions
|
||||||
v => cache.get('user', v.id) ?? new User(client, v)
|
// this.mentions = data.mentions.map(
|
||||||
)
|
// v => this.client.users.get(v.id) || new User(client, v)
|
||||||
|
// )
|
||||||
this.mentionRoles = data.mention_roles
|
this.mentionRoles = data.mention_roles
|
||||||
this.mentionChannels = data.mention_channels
|
this.mentionChannels = data.mention_channels
|
||||||
this.attachments = data.attachments
|
this.attachments = data.attachments
|
||||||
|
@ -73,26 +79,28 @@ export class Message extends Base {
|
||||||
this.application = data.application
|
this.application = data.application
|
||||||
this.messageReference = data.message_reference
|
this.messageReference = data.message_reference
|
||||||
this.flags = data.flags
|
this.flags = data.flags
|
||||||
cache.set('message', this.id, this)
|
this.channel = channel
|
||||||
|
// TODO: Cache in Gateway Event Code
|
||||||
|
// if(!noSave) this.client.messages.set(this.id, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected readFromData (data: MessagePayload): void {
|
protected readFromData (data: MessagePayload): void {
|
||||||
super.readFromData(data)
|
super.readFromData(data)
|
||||||
this.channelID = data.channel_id ?? this.channelID
|
this.channelID = data.channel_id ?? this.channelID
|
||||||
this.guildID = data.guild_id ?? this.guildID
|
this.guildID = data.guild_id ?? this.guildID
|
||||||
this.author =
|
// this.author =
|
||||||
cache.get('user', data.author.id) ??
|
// this.client.users.get(data.author.id) ||
|
||||||
this.author ??
|
// this.author ||
|
||||||
new User(this.client, data.author)
|
// new User(this.client, data.author)
|
||||||
this.content = data.content ?? this.content
|
this.content = data.content ?? this.content
|
||||||
this.timestamp = data.timestamp ?? this.timestamp
|
this.timestamp = data.timestamp ?? this.timestamp
|
||||||
this.editedTimestamp = data.edited_timestamp ?? this.editedTimestamp
|
this.editedTimestamp = data.edited_timestamp ?? this.editedTimestamp
|
||||||
this.tts = data.tts ?? this.tts
|
this.tts = data.tts ?? this.tts
|
||||||
this.mentionEveryone = data.mention_everyone ?? this.mentionEveryone
|
this.mentionEveryone = data.mention_everyone ?? this.mentionEveryone
|
||||||
this.mentions =
|
// this.mentions =
|
||||||
data.mentions.map(
|
// data.mentions.map(
|
||||||
v => cache.get('user', v.id) ?? new User(this.client, v)
|
// v => this.client.users.get(v.id) || new User(this.client, v)
|
||||||
) ?? this.mentions
|
// ) ?? this.mentions
|
||||||
this.mentionRoles = data.mention_roles ?? this.mentionRoles
|
this.mentionRoles = data.mention_roles ?? this.mentionRoles
|
||||||
this.mentionChannels = data.mention_channels ?? this.mentionChannels
|
this.mentionChannels = data.mention_channels ?? this.mentionChannels
|
||||||
this.attachments = data.attachments ?? this.attachments
|
this.attachments = data.attachments ?? this.attachments
|
||||||
|
@ -108,44 +116,11 @@ export class Message extends Base {
|
||||||
this.flags = data.flags ?? this.flags
|
this.flags = data.flags ?? this.flags
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: We have to seperate fetch()
|
edit (text?: string, option?: MessageOption): Promise<Message> {
|
||||||
async editMessage (text?: string, option?: MessageOption): Promise<Message> {
|
return (this.channel as TextChannel).editMessage(this.id, text, option)
|
||||||
if (text !== undefined && option !== undefined) {
|
|
||||||
throw new Error('Either text or option is necessary.')
|
|
||||||
}
|
|
||||||
const resp = await fetch(CHANNEL_MESSAGE(this.channelID, this.id), {
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bot ${this.client.token}`,
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
method: 'PATCH',
|
|
||||||
body: JSON.stringify({
|
|
||||||
content: text,
|
|
||||||
embed: option?.embed.toJSON(),
|
|
||||||
file: option?.file,
|
|
||||||
tts: option?.tts,
|
|
||||||
allowed_mentions: option?.allowedMention
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
return new Message(this.client, await resp.json())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: We have to seperate fetch()
|
delete (): Promise<void> {
|
||||||
async delete (): Promise<void> {
|
return this.client.rest.delete(CHANNEL_MESSAGE(this.channelID, this.id)) as any
|
||||||
const resp = await fetch(CHANNEL_MESSAGE(this.channelID, this.id), {
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bot ${this.client.token}`
|
|
||||||
},
|
|
||||||
method: 'DELETE'
|
|
||||||
})
|
|
||||||
|
|
||||||
// TODO: improve Error and Promise
|
|
||||||
return await new Promise((resolve, reject) => {
|
|
||||||
if (resp.status !== 204) {
|
|
||||||
reject(new Error())
|
|
||||||
}
|
|
||||||
resolve()
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,7 +27,8 @@ export class Role extends Base {
|
||||||
this.permissions = data.permissions
|
this.permissions = data.permissions
|
||||||
this.managed = data.managed
|
this.managed = data.managed
|
||||||
this.mentionable = data.mentionable
|
this.mentionable = data.mentionable
|
||||||
cache.set('role', this.id, this)
|
// TODO: Cache in Gateway Event Code
|
||||||
|
// cache.set('role', this.id, this)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected readFromData (data: RolePayload): void {
|
protected readFromData (data: RolePayload): void {
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
import cache from '../models/cache.ts'
|
import cache from '../models/cache.ts'
|
||||||
import { Client } from '../models/client.ts'
|
import { Client } from '../models/client.ts'
|
||||||
import { MessageOption, TextChannelPayload } from '../types/channel.ts'
|
import { MessageOption, MessagePayload, TextChannelPayload } from '../types/channel.ts'
|
||||||
import { CHANNEL_MESSAGE, CHANNEL_MESSAGES } from '../types/endpoint.ts'
|
import { CHANNEL_MESSAGE, CHANNEL_MESSAGES } from '../types/endpoint.ts'
|
||||||
import { Channel } from './channel.ts'
|
import { Channel } from './channel.ts'
|
||||||
import { Message } from './message.ts'
|
import { Message } from './message.ts'
|
||||||
|
import { MessageMentions } from "./MessageMentions.ts"
|
||||||
|
import { User } from "./user.ts"
|
||||||
|
|
||||||
export class TextChannel extends Channel {
|
export class TextChannel extends Channel {
|
||||||
lastMessageID?: string
|
lastMessageID?: string
|
||||||
|
@ -13,7 +15,8 @@ export class TextChannel extends Channel {
|
||||||
super(client, data)
|
super(client, data)
|
||||||
this.lastMessageID = data.last_message_id
|
this.lastMessageID = data.last_message_id
|
||||||
this.lastPinTimestamp = data.last_pin_timestamp
|
this.lastPinTimestamp = data.last_pin_timestamp
|
||||||
cache.set('textchannel', this.id, this)
|
// TODO: Cache in Gateway Event Code
|
||||||
|
// cache.set('textchannel', this.id, this)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected readFromData (data: TextChannelPayload): void {
|
protected readFromData (data: TextChannelPayload): void {
|
||||||
|
@ -41,32 +44,29 @@ export class TextChannel extends Channel {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
return new Message(this.client, await resp.json())
|
return new Message(this.client, await resp.json(), this, this.client.user as User, new MessageMentions())
|
||||||
}
|
}
|
||||||
|
|
||||||
async editMessage (
|
async editMessage (
|
||||||
messageID: string,
|
message: Message | string,
|
||||||
text?: string,
|
text?: string,
|
||||||
option?: MessageOption
|
option?: MessageOption
|
||||||
): Promise<Message> {
|
): Promise<Message> {
|
||||||
if (text !== undefined && option !== undefined) {
|
if (text !== undefined && option !== undefined) {
|
||||||
throw new Error('Either text or option is necessary.')
|
throw new Error('Either text or option is necessary.')
|
||||||
}
|
}
|
||||||
const resp = await fetch(CHANNEL_MESSAGE(this.id, messageID), {
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bot ${this.client.token}`,
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
method: 'PATCH',
|
|
||||||
body: JSON.stringify({
|
|
||||||
content: text,
|
|
||||||
embed: option?.embed,
|
|
||||||
file: option?.file,
|
|
||||||
tts: option?.tts,
|
|
||||||
allowed_mentions: option?.allowedMention
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
return new Message(this.client, await resp.json())
|
let newMsg = await this.client.rest.patch(CHANNEL_MESSAGE(this.id, typeof message == "string" ? message : message.id), {
|
||||||
|
content: text,
|
||||||
|
embed: option?.embed.toJSON(),
|
||||||
|
file: option?.file,
|
||||||
|
tts: option?.tts,
|
||||||
|
allowed_mentions: option?.allowedMention
|
||||||
|
}) as MessagePayload
|
||||||
|
|
||||||
|
// TODO: Actually construct this object
|
||||||
|
let mentions = new MessageMentions()
|
||||||
|
|
||||||
|
return new Message(this.client, newMsg, this, this.client.user as User, mentions)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,10 @@ export class User extends Base {
|
||||||
premiumType?: 0 | 1 | 2
|
premiumType?: 0 | 1 | 2
|
||||||
publicFlags?: number
|
publicFlags?: number
|
||||||
|
|
||||||
|
get tag(): string {
|
||||||
|
return `${this.username}#${this.discriminator}`;
|
||||||
|
}
|
||||||
|
|
||||||
get nickMention (): string {
|
get nickMention (): string {
|
||||||
return `<@!${this.id}>`
|
return `<@!${this.id}>`
|
||||||
}
|
}
|
||||||
|
@ -41,7 +45,8 @@ export class User extends Base {
|
||||||
this.flags = data.flags
|
this.flags = data.flags
|
||||||
this.premiumType = data.premium_type
|
this.premiumType = data.premium_type
|
||||||
this.publicFlags = data.public_flags
|
this.publicFlags = data.public_flags
|
||||||
cache.set('user', this.id, this)
|
// TODO: Cache in Gateway Event Code
|
||||||
|
// cache.set('user', this.id, this)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected readFromData (data: UserPayload): void {
|
protected readFromData (data: UserPayload): void {
|
||||||
|
@ -59,4 +64,8 @@ export class User extends Base {
|
||||||
this.premiumType = data.premium_type ?? this.premiumType
|
this.premiumType = data.premium_type ?? this.premiumType
|
||||||
this.publicFlags = data.public_flags ?? this.publicFlags
|
this.publicFlags = data.public_flags ?? this.publicFlags
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toString() {
|
||||||
|
return this.mention;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,7 +30,8 @@ export class VoiceState extends Base {
|
||||||
this.selfStream = data.self_stream
|
this.selfStream = data.self_stream
|
||||||
this.selfVideo = data.self_video
|
this.selfVideo = data.self_video
|
||||||
this.suppress = data.suppress
|
this.suppress = data.suppress
|
||||||
cache.set('voiceState', `${this.guildID}:${this.userID}`, this)
|
// TODO: Cache in Gateway Event Code
|
||||||
|
// cache.set('voiceState', `${this.guildID}:${this.userID}`, this)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected readFromData (data: VoiceStatePayload): void {
|
protected readFromData (data: VoiceStatePayload): void {
|
||||||
|
|
|
@ -1,2 +1 @@
|
||||||
const TOKEN = ''
|
export const TOKEN = ''
|
||||||
export { TOKEN }
|
|
|
@ -1,18 +1,27 @@
|
||||||
import { Client } from '../models/client.ts'
|
import { Client } from '../models/client.ts'
|
||||||
import { GatewayIntents } from '../types/gatewayTypes.ts'
|
import { GatewayIntents } from '../types/gateway.ts'
|
||||||
import { TOKEN } from './config.ts'
|
import { TOKEN } from './config.ts'
|
||||||
import { Channel } from '../structures/channel.ts'
|
import { Channel } from '../structures/channel.ts'
|
||||||
import { GuildTextChannel } from '../structures/guildTextChannel.ts'
|
import { GuildTextChannel } from '../structures/guildTextChannel.ts'
|
||||||
import { TextChannel } from '../structures/textChannel.ts'
|
import { TextChannel } from '../structures/textChannel.ts'
|
||||||
import { Guild } from '../structures/guild.ts'
|
import { Guild } from '../structures/guild.ts'
|
||||||
import { User } from '../structures/user.ts'
|
import { User } from '../structures/user.ts'
|
||||||
|
import { Message } from "../structures/message.ts"
|
||||||
|
import { RedisCacheAdapter } from "../models/CacheAdapter.ts"
|
||||||
|
|
||||||
const bot = new Client()
|
const bot = new Client()
|
||||||
|
|
||||||
|
bot.setAdapter(new RedisCacheAdapter(bot, {
|
||||||
|
hostname: "127.0.0.1",
|
||||||
|
port: 6379
|
||||||
|
}))
|
||||||
|
|
||||||
bot.on('ready', () => {
|
bot.on('ready', () => {
|
||||||
console.log('READY!')
|
console.log(`[Login] Logged in as ${bot.user?.tag}!`)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
bot.on('debug', console.log)
|
||||||
|
|
||||||
bot.on('channelDelete', (channel: Channel) => {
|
bot.on('channelDelete', (channel: Channel) => {
|
||||||
console.log('channelDelete', channel.id)
|
console.log('channelDelete', channel.id)
|
||||||
})
|
})
|
||||||
|
@ -59,6 +68,10 @@ bot.on('guildUpdate', (before: Guild, after: Guild) => {
|
||||||
console.log('guildUpdate', before.name, after.name)
|
console.log('guildUpdate', before.name, after.name)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
bot.on('messageCreate', (msg: Message) => {
|
||||||
|
console.log(`${msg.author.tag}: ${msg.content}`)
|
||||||
|
})
|
||||||
|
|
||||||
bot.connect(TOKEN, [
|
bot.connect(TOKEN, [
|
||||||
GatewayIntents.GUILD_MEMBERS,
|
GatewayIntents.GUILD_MEMBERS,
|
||||||
GatewayIntents.GUILD_PRESENCES,
|
GatewayIntents.GUILD_PRESENCES,
|
||||||
|
|
11
src/types/gatewayBot.ts
Normal file
11
src/types/gatewayBot.ts
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
export interface ISessionStartLimit {
|
||||||
|
total: number
|
||||||
|
remaining: number
|
||||||
|
reset_after: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GatewayBotPayload {
|
||||||
|
url: string
|
||||||
|
shards: number
|
||||||
|
session_start_limit: ISessionStartLimit
|
||||||
|
}
|
396
src/types/gatewayTypes.ts
Normal file
396
src/types/gatewayTypes.ts
Normal file
|
@ -0,0 +1,396 @@
|
||||||
|
// https://discord.com/developers/docs/topics/opcodes-and-status-codes#gateway
|
||||||
|
// https://discord.com/developers/docs/topics/gateway#commands-and-events-gateway-events
|
||||||
|
import { EmojiPayload } from './emoji.ts'
|
||||||
|
import { MemberPayload } from './guild.ts'
|
||||||
|
import { ActivityPayload, PresenceUpdatePayload } from './presence.ts'
|
||||||
|
import { RolePayload } from './role.ts'
|
||||||
|
import { UserPayload } from './user.ts'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gateway OPcodes from Discord docs.
|
||||||
|
*/
|
||||||
|
enum GatewayOpcodes { // 문서를 확인해본 결과 Opcode 5번은 비어있다. - UnderC -
|
||||||
|
DISPATCH = 0,
|
||||||
|
HEARTBEAT = 1,
|
||||||
|
IDENTIFY = 2,
|
||||||
|
PRESENCE_UPDATE = 3,
|
||||||
|
VOICE_STATE_UPDATE = 4,
|
||||||
|
RESUME = 6,
|
||||||
|
RECONNECT = 7,
|
||||||
|
REQUEST_GUILD_MEMBERS = 8,
|
||||||
|
INVALID_SESSION = 9,
|
||||||
|
HELLO = 10,
|
||||||
|
HEARTBEAT_ACK = 11
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gateway Close Codes from Discord docs.
|
||||||
|
*/
|
||||||
|
enum GatewayCloseCodes {
|
||||||
|
UNKNOWN_ERROR = 4000,
|
||||||
|
UNKNOWN_OPCODE = 4001,
|
||||||
|
DECODE_ERROR = 4002,
|
||||||
|
NOT_AUTHENTICATED = 4003,
|
||||||
|
AUTHENTICATION_FAILED = 4004,
|
||||||
|
ALREADY_AUTHENTICATED = 4005,
|
||||||
|
INVALID_SEQ = 4007,
|
||||||
|
RATE_LIMITED = 4008,
|
||||||
|
SESSION_TIMED_OUT = 4009,
|
||||||
|
INVALID_SHARD = 4010,
|
||||||
|
SHARDING_REQUIRED = 4011,
|
||||||
|
INVALID_API_VERSION = 4012,
|
||||||
|
INVALID_INTENTS = 4013,
|
||||||
|
DISALLOWED_INTENTS = 4014
|
||||||
|
}
|
||||||
|
|
||||||
|
enum GatewayIntents {
|
||||||
|
GUILDS = 1 << 0,
|
||||||
|
GUILD_MEMBERS = 1 << 1,
|
||||||
|
GUILD_BANS = 1 << 2,
|
||||||
|
GUILD_EMOJIS = 1 << 3,
|
||||||
|
GUILD_INTEGRATIONS = 1 << 4,
|
||||||
|
GUILD_WEBHOOKS = 1 << 5,
|
||||||
|
GUILD_INVITES = 1 << 6,
|
||||||
|
GUILD_VOICE_STATES = 1 << 7,
|
||||||
|
GUILD_PRESENCES = 1 << 8,
|
||||||
|
GUILD_MESSAGES = 1 << 9,
|
||||||
|
GUILD_MESSAGE_REACTIONS = 1 << 10,
|
||||||
|
GUILD_MESSAGE_TYPING = 1 << 11,
|
||||||
|
DIRECT_MESSAGES = 1 << 12,
|
||||||
|
DIRECT_MESSAGE_REACTIONS = 1 << 13,
|
||||||
|
DIRECT_MESSAGE_TYPING = 1 << 13
|
||||||
|
}
|
||||||
|
|
||||||
|
enum GatewayEvents {
|
||||||
|
Ready = 'READY',
|
||||||
|
Resumed = 'RESUMED',
|
||||||
|
Reconnect = 'RECONNECT',
|
||||||
|
Channel_Create = 'CHANNEL_CREATE',
|
||||||
|
Channel_Update = 'CHANNEL_UPDATE',
|
||||||
|
Channel_Delete = 'CHANNEL_DELETE',
|
||||||
|
Channel_Pins_Update = 'CHANNEL_PINS_UPDATE',
|
||||||
|
Guild_Create = 'GUILD_CREATE',
|
||||||
|
Guild_Update = 'GUILD_UPDATE',
|
||||||
|
Guild_Delete = 'GUILD_DELETE',
|
||||||
|
Guild_Ban_Add = 'GUILD_BAN_ADD',
|
||||||
|
Guild_Ban_Remove = 'GUILD_BAN_REMOVE',
|
||||||
|
Guild_Emojis_Update = 'GUILD_EMOJIS_UPDATE',
|
||||||
|
Guild_Integrations_Update = 'GUILD_INTEGRATIONS_UPDATE',
|
||||||
|
Guild_Member_Add = 'GUILD_MEMBER_ADD',
|
||||||
|
Guild_Member_Remove = 'GUILD_MEMBER_REMOVE',
|
||||||
|
Guild_Member_Update = 'GUILD_MEMBER_UPDATE',
|
||||||
|
Guild_Members_Chunk = 'GUILD_MEMBERS_CHUNK',
|
||||||
|
Guild_Role_Create = 'GUILD_ROLE_CREATE',
|
||||||
|
Guild_Role_Update = 'GUILD_ROLE_UPDATE',
|
||||||
|
Guild_Role_Delete = 'GUILD_ROLE_DELETE',
|
||||||
|
Invite_Create = 'INVITE_CREATE',
|
||||||
|
Invite_Delete = 'INVITE_DELETE',
|
||||||
|
Message_Create = 'MESSAGE_CREATE',
|
||||||
|
Message_Update = 'MESSAGE_UPDATE',
|
||||||
|
Message_Delete = 'MESSAGE_DELETE',
|
||||||
|
Message_Delete_Bulk = 'MESSAGE_DELETE_BULK',
|
||||||
|
Message_Reaction_Add = 'MESSAGE_REACTION_ADD',
|
||||||
|
Message_Reaction_Remove = 'MESSAGE_REACTION_REMOVE',
|
||||||
|
Message_Reaction_Remove_All = 'MESSAGE_REACTION_REMOVE_ALL',
|
||||||
|
Message_Reaction_Remove_Emoji = 'MESSAGE_REACTION_REMOVE_EMOJI',
|
||||||
|
Presence_Update = 'PRESENCE_UPDATE',
|
||||||
|
Typing_Start = 'TYPING_START',
|
||||||
|
User_Update = 'USER_UPDATE',
|
||||||
|
Voice_Server_Update = 'VOICE_SERVER_UPDATE',
|
||||||
|
Webhooks_Update = 'WEBHOOKS_UPDATE'
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IdentityPayload {
|
||||||
|
token: string
|
||||||
|
properties: IdentityConnection
|
||||||
|
compress?: boolean
|
||||||
|
large_threshold?: number
|
||||||
|
shard?: number[]
|
||||||
|
presence?: UpdateStatus
|
||||||
|
guildSubscriptions?: boolean
|
||||||
|
intents: number
|
||||||
|
}
|
||||||
|
|
||||||
|
enum UpdateStatus {
|
||||||
|
online = 'online',
|
||||||
|
dnd = 'dnd',
|
||||||
|
afk = 'idle',
|
||||||
|
invisible = 'invisible',
|
||||||
|
offline = 'offline'
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IdentityConnection {
|
||||||
|
$os: 'darwin' | 'windows' | 'linux' | 'custom os'
|
||||||
|
$browser: 'discord.deno'
|
||||||
|
$device: 'discord.deno'
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Resume {
|
||||||
|
token: string
|
||||||
|
session_id: string
|
||||||
|
seq: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GuildRequestMembers {
|
||||||
|
guild_id: string | string[]
|
||||||
|
query?: string
|
||||||
|
limit: number
|
||||||
|
presences?: boolean
|
||||||
|
user_ids?: string | string[]
|
||||||
|
nonce?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GatewayVoiceStateUpdate {
|
||||||
|
guild_id: string
|
||||||
|
channel_id: string
|
||||||
|
self_mute: boolean
|
||||||
|
self_deaf: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GatewayStatusUpdate {
|
||||||
|
since: number | undefined
|
||||||
|
activities: ActivityPayload[]
|
||||||
|
status: string
|
||||||
|
afk: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Hello {
|
||||||
|
heartbeat_interval: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ReadyEvent {
|
||||||
|
v: number
|
||||||
|
user: UserPayload
|
||||||
|
privateChannels: []
|
||||||
|
guilds: []
|
||||||
|
session_id: string
|
||||||
|
shard?: number[]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ChannelPinsUpdate {
|
||||||
|
guild_id?: string
|
||||||
|
channel_id: string
|
||||||
|
last_pin_timestamp?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GuildBanAdd {
|
||||||
|
guild_id: string
|
||||||
|
user: UserPayload
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GuildBanRemove {
|
||||||
|
guild_id: string
|
||||||
|
user: UserPayload
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GuildEmojiUpdate {
|
||||||
|
guild_id: string
|
||||||
|
emojis: []
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GuildIntegrationsUpdate {
|
||||||
|
guild_id: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GuildMemberAddExtra {
|
||||||
|
guild_id: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GuildMemberRemove {
|
||||||
|
guild_id: string
|
||||||
|
user: UserPayload
|
||||||
|
}
|
||||||
|
interface GuildMemberUpdate {
|
||||||
|
guild_id: string
|
||||||
|
roles: string[]
|
||||||
|
user: UserPayload
|
||||||
|
nick?: string | undefined
|
||||||
|
joined_at: string
|
||||||
|
premium_since?: string | undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GuildMemberChunk {
|
||||||
|
guild_id: string
|
||||||
|
members: MemberPayload[]
|
||||||
|
chunk_index: number
|
||||||
|
chunk_count: number
|
||||||
|
not_found?: []
|
||||||
|
presences?: PresenceUpdatePayload[]
|
||||||
|
nonce?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GuildRoleCreate {
|
||||||
|
guild_id: string
|
||||||
|
role: RolePayload
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GuildRoleUpdate {
|
||||||
|
guild_id: string
|
||||||
|
role: RolePayload
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GuildRoleDelete {
|
||||||
|
guild_id: string
|
||||||
|
role_id: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface InviteCreate {
|
||||||
|
channel_id: string
|
||||||
|
code: string
|
||||||
|
created_at: string
|
||||||
|
guild_id?: string
|
||||||
|
inviter?: UserPayload
|
||||||
|
max_age: number
|
||||||
|
max_uses: number
|
||||||
|
target_user?: UserPayload
|
||||||
|
target_user_type?: number
|
||||||
|
temporary: boolean
|
||||||
|
uses: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface InviteDelete {
|
||||||
|
channel_id: string
|
||||||
|
guild_id?: string
|
||||||
|
code: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MessageDelete {
|
||||||
|
id: string
|
||||||
|
channel_id: string
|
||||||
|
guild_id?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MessageDeleteBulk {
|
||||||
|
ids: string[]
|
||||||
|
channel_id: string
|
||||||
|
guild_id: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MessageReactionAdd {
|
||||||
|
user_id: string
|
||||||
|
channel_id: string
|
||||||
|
message_id: string
|
||||||
|
guild_id?: string
|
||||||
|
emoji: EmojiPayload
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MessageReactionRemove {
|
||||||
|
user_id: string
|
||||||
|
channel_id: string
|
||||||
|
message_id: string
|
||||||
|
guild_id?: string
|
||||||
|
emoji: EmojiPayload
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MessageReactionRemoveAll {
|
||||||
|
channel_id: string
|
||||||
|
guild_id?: string
|
||||||
|
message_id: string
|
||||||
|
emoji: EmojiPayload
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MessageReactionRemove {
|
||||||
|
channel_id: string
|
||||||
|
guild_id?: string
|
||||||
|
message_id: string
|
||||||
|
emoji: EmojiPayload
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PresenceUpdate {
|
||||||
|
user: UserPayload
|
||||||
|
guild_id: string
|
||||||
|
status: string
|
||||||
|
activities: ActivityPayload[]
|
||||||
|
client_status: UpdateStatus[]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CilentStatus {
|
||||||
|
desktop?: string
|
||||||
|
moblie?: string
|
||||||
|
web?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Activity {
|
||||||
|
name: string
|
||||||
|
type: number
|
||||||
|
url?: string | undefined
|
||||||
|
created_at: number
|
||||||
|
timestamps?: string
|
||||||
|
application_id: string
|
||||||
|
details?: string | undefined
|
||||||
|
state?: string | undefined
|
||||||
|
emoji?: EmojiPayload | undefined
|
||||||
|
party?: ActivityParty
|
||||||
|
assets?: ActivityAssets
|
||||||
|
secrets?: ActivitySecrets
|
||||||
|
instance?: boolean
|
||||||
|
flags?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ActivityTypes {
|
||||||
|
GAME = 0,
|
||||||
|
STREAMING = 1,
|
||||||
|
LISTENING = 2,
|
||||||
|
CUSTOM = 4,
|
||||||
|
COMPETING = 5
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ActivityTimestamps {
|
||||||
|
start?: number
|
||||||
|
end?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ActivityEmoji {
|
||||||
|
name: string
|
||||||
|
id?: string
|
||||||
|
animated?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ActivityParty {
|
||||||
|
id?: string
|
||||||
|
size?: number[]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ActivityAssets {
|
||||||
|
large_image?: string
|
||||||
|
large_text?: string
|
||||||
|
small_image?: string
|
||||||
|
small_text?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ActivitySecrets {
|
||||||
|
join?: string
|
||||||
|
spectate?: string
|
||||||
|
match?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ActivityFlags {
|
||||||
|
INSTANCE = 1 << 0,
|
||||||
|
JOIN = 1 << 1,
|
||||||
|
SPECTATE = 1 << 2,
|
||||||
|
JOIN_REQUEST = 1 << 3,
|
||||||
|
SYNC = 1 << 4,
|
||||||
|
PLAY = 1 << 5
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TypeStart {
|
||||||
|
channel_id: string
|
||||||
|
guild_id?: string
|
||||||
|
user_id: string
|
||||||
|
timestamp: number
|
||||||
|
member?: MemberPayload
|
||||||
|
}
|
||||||
|
|
||||||
|
interface VoiceServerUpdate {
|
||||||
|
token: string
|
||||||
|
guild_id: string
|
||||||
|
endpoint: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface WebhooksUpdate {
|
||||||
|
guild_id: string
|
||||||
|
channel_id: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://discord.com/developers/docs/topics/gateway#typing-start-typing-start-event-fields
|
||||||
|
export { GatewayCloseCodes, GatewayOpcodes, GatewayIntents, GatewayEvents }
|
|
@ -1,5 +1,5 @@
|
||||||
// https://discord.com/developers/docs/topics/opcodes-and-status-codes#voice
|
// https://discord.com/developers/docs/topics/opcodes-and-status-codes#voice
|
||||||
import { MemberPayload } from './guildTypes.ts'
|
import { MemberPayload } from './guild.ts'
|
||||||
|
|
||||||
enum VoiceOpcodes { // VoiceOpcodes 추가 - UnderC -
|
enum VoiceOpcodes { // VoiceOpcodes 추가 - UnderC -
|
||||||
IDENTIFY = 0,
|
IDENTIFY = 0,
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { UserPayload } from './userTypes.ts'
|
import { UserPayload } from './user.ts'
|
||||||
|
|
||||||
export interface WebhookPayload {
|
export interface WebhookPayload {
|
||||||
id: string
|
id: string
|
||||||
|
|
95
src/utils/collection.ts
Normal file
95
src/utils/collection.ts
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
export class Collection<K = string, V = any> extends Map<K, V> {
|
||||||
|
maxSize?: number;
|
||||||
|
|
||||||
|
set(key: K, value: V) {
|
||||||
|
if (this.maxSize || this.maxSize === 0) {
|
||||||
|
if (this.size >= this.maxSize) return this
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.set(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
array() {
|
||||||
|
return [...this.values()]
|
||||||
|
}
|
||||||
|
|
||||||
|
first(): V {
|
||||||
|
return this.values().next().value
|
||||||
|
}
|
||||||
|
|
||||||
|
last(): V {
|
||||||
|
return [...this.values()][this.size - 1]
|
||||||
|
}
|
||||||
|
|
||||||
|
random() {
|
||||||
|
const arr = [...this.values()]
|
||||||
|
return arr[Math.floor(Math.random() * arr.length)]
|
||||||
|
}
|
||||||
|
|
||||||
|
find(callback: (value: V, key: K) => boolean) {
|
||||||
|
for (const key of this.keys()) {
|
||||||
|
const value = this.get(key)!
|
||||||
|
if (callback(value, key)) return value
|
||||||
|
}
|
||||||
|
// If nothing matched
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
filter(callback: (value: V, key: K) => boolean) {
|
||||||
|
const relevant = new Collection<K, V>()
|
||||||
|
this.forEach((value, key) => {
|
||||||
|
if (callback(value, key)) relevant.set(key, value)
|
||||||
|
});
|
||||||
|
|
||||||
|
return relevant;
|
||||||
|
}
|
||||||
|
|
||||||
|
map<T>(callback: (value: V, key: K) => T) {
|
||||||
|
const results = []
|
||||||
|
for (const key of this.keys()) {
|
||||||
|
const value = this.get(key)!
|
||||||
|
results.push(callback(value, key))
|
||||||
|
}
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
some(callback: (value: V, key: K) => boolean) {
|
||||||
|
for (const key of this.keys()) {
|
||||||
|
const value = this.get(key)!
|
||||||
|
if (callback(value, key)) return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
every(callback: (value: V, key: K) => boolean) {
|
||||||
|
for (const key of this.keys()) {
|
||||||
|
const value = this.get(key)!
|
||||||
|
if (!callback(value, key)) return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
reduce<T>(
|
||||||
|
callback: (accumulator: T, value: V, key: K) => T,
|
||||||
|
initialValue?: T,
|
||||||
|
): T {
|
||||||
|
let accumulator: T = initialValue!
|
||||||
|
|
||||||
|
for (const key of this.keys()) {
|
||||||
|
const value = this.get(key)!
|
||||||
|
accumulator = callback(accumulator, value, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
return accumulator
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromObject<V>(object: { [key: string]: V }) {
|
||||||
|
return new Collection<string, V>(Object.entries(object))
|
||||||
|
}
|
||||||
|
|
||||||
|
toObject() {
|
||||||
|
return Object.entries(this)
|
||||||
|
}
|
||||||
|
}
|
3
src/utils/delay.ts
Normal file
3
src/utils/delay.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
export const delay = (ms: number) => new Promise((resolve, reject) => {
|
||||||
|
setTimeout(() => resolve(true), ms);
|
||||||
|
});
|
|
@ -1,3 +1,3 @@
|
||||||
import getChannelByType from './getChannelByType.ts'
|
export { default as getChannelByType } from './getChannelByType.ts'
|
||||||
|
export type AnyFunction<ReturnType = any> = (...args:any[]) => ReturnType;
|
||||||
export default { getChannelByType }
|
export { delay } from './delay.ts'
|
Loading…
Reference in a new issue