RedisCacheAdapter (!), fixed resuming, error code handling, resume event added. Some caching missing yet though
This commit is contained in:
parent
be3bec017c
commit
1c02edb015
35 changed files with 340 additions and 158 deletions
|
@ -1,14 +1,13 @@
|
|||
import { Gateway, GatewayEventHandler } from '../index.ts'
|
||||
import getChannelByType from '../../utils/getChannelByType.ts'
|
||||
|
||||
export const channelCreate: GatewayEventHandler = (
|
||||
export const channelCreate: GatewayEventHandler = async (
|
||||
gateway: Gateway,
|
||||
d: any
|
||||
) => {
|
||||
const channel = getChannelByType(gateway.client, d)
|
||||
|
||||
if (channel !== undefined) {
|
||||
gateway.client.channels.set(d.id, d)
|
||||
await gateway.client.channels.set(d.id, d)
|
||||
gateway.client.emit('channelCreate', channel)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import { Gateway, GatewayEventHandler } from '../index.ts'
|
||||
import { Channel } from '../../structures/channel.ts'
|
||||
|
||||
export const channelDelete: GatewayEventHandler = (
|
||||
export const channelDelete: GatewayEventHandler = async(
|
||||
gateway: Gateway,
|
||||
d: any
|
||||
) => {
|
||||
const channel: Channel = gateway.client.channels.get(d.id)
|
||||
const channel: Channel = await gateway.client.channels.get(d.id)
|
||||
if (channel !== undefined) {
|
||||
gateway.client.channels.delete(d.id)
|
||||
await gateway.client.channels.delete(d.id)
|
||||
gateway.client.emit('channelDelete', channel)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,17 +3,17 @@ import cache from '../../models/cache.ts'
|
|||
import { TextChannel } from '../../structures/textChannel.ts'
|
||||
import { ChannelPayload } from "../../types/channelTypes.ts"
|
||||
|
||||
export const channelPinsUpdate: GatewayEventHandler = (
|
||||
export const channelPinsUpdate: GatewayEventHandler = async(
|
||||
gateway: Gateway,
|
||||
d: any
|
||||
) => {
|
||||
const after: TextChannel = gateway.client.channels.get(d.channel_id)
|
||||
const after: TextChannel = await gateway.client.channels.get(d.channel_id)
|
||||
if (after !== undefined) {
|
||||
const before = after.refreshFromData({
|
||||
last_pin_timestamp: d.last_pin_timestamp
|
||||
})
|
||||
const raw = gateway.client.channels._get(d.channel_id) ;
|
||||
gateway.client.channels.set(after.id, Object.assign(raw, { 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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,14 +2,14 @@ import { Channel } from '../../structures/channel.ts'
|
|||
import getChannelByType from '../../utils/getChannelByType.ts'
|
||||
import { Gateway, GatewayEventHandler } from '../index.ts'
|
||||
|
||||
export const channelUpdate: GatewayEventHandler = (
|
||||
export const channelUpdate: GatewayEventHandler = async (
|
||||
gateway: Gateway,
|
||||
d: any
|
||||
) => {
|
||||
const oldChannel: Channel = gateway.client.channels.get(d.id)
|
||||
const oldChannel: Channel = await gateway.client.channels.get(d.id)
|
||||
|
||||
if (oldChannel !== undefined) {
|
||||
gateway.client.channels.set(d.id, d)
|
||||
await gateway.client.channels.set(d.id, d)
|
||||
if (oldChannel.type !== d.type) {
|
||||
const channel: Channel = getChannelByType(gateway.client, d) ?? oldChannel
|
||||
gateway.client.emit('channelUpdate', oldChannel, channel)
|
||||
|
|
|
@ -1,15 +1,16 @@
|
|||
import { Gateway, GatewayEventHandler } from '../index.ts'
|
||||
import { Guild } from '../../structures/guild.ts'
|
||||
import { GuildPayload } from "../../types/guildTypes.ts"
|
||||
|
||||
export const guildCreate: GatewayEventHandler = (gateway: Gateway, d: any) => {
|
||||
let guild: Guild | void = gateway.client.guilds.get(d.id)
|
||||
export const guildCreate: GatewayEventHandler = async(gateway: Gateway, d: any) => {
|
||||
let guild: Guild | void = await gateway.client.guilds.get(d.id)
|
||||
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
|
||||
gateway.client.guilds.set(d.id, d)
|
||||
await gateway.client.guilds.set(d.id, d)
|
||||
guild.refreshFromData(d)
|
||||
} else {
|
||||
gateway.client.guilds.set(d.id, d)
|
||||
guild = gateway.client.guilds.get(d.id)
|
||||
await gateway.client.guilds.set(d.id, d)
|
||||
guild = new Guild(gateway.client, d as GuildPayload)
|
||||
gateway.client.emit('guildCreate', guild)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import { Guild } from '../../structures/guild.ts'
|
||||
import { Gateway, GatewayEventHandler } from '../index.ts'
|
||||
|
||||
export const guildDelte: GatewayEventHandler = (gateway: Gateway, d: any) => {
|
||||
const guild: Guild | void = gateway.client.guilds.get(d.id)
|
||||
export const guildDelte: GatewayEventHandler = async (gateway: Gateway, d: any) => {
|
||||
const guild: Guild | void = await gateway.client.guilds.get(d.id)
|
||||
|
||||
if (guild !== undefined) {
|
||||
guild.refreshFromData(d)
|
||||
gateway.client.guilds.delete(d.id)
|
||||
await gateway.client.guilds.delete(d.id)
|
||||
gateway.client.emit('guildDelete', guild)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
import { Gateway, GatewayEventHandler } from '../index.ts'
|
||||
import cache from '../../models/cache.ts'
|
||||
import { Guild } from '../../structures/guild.ts'
|
||||
|
||||
export const guildUpdate: GatewayEventHandler = (gateway: Gateway, d: any) => {
|
||||
const before: Guild | void = gateway.client.guilds.get(d.id)
|
||||
export const guildUpdate: GatewayEventHandler = async(gateway: Gateway, d: any) => {
|
||||
const before: Guild | void = await gateway.client.guilds.get(d.id)
|
||||
if(!before) return
|
||||
gateway.client.guilds.set(d.id, d)
|
||||
const after: Guild | void = gateway.client.guilds.get(d.id)
|
||||
await gateway.client.guilds.set(d.id, d)
|
||||
const after: Guild | void = await gateway.client.guilds.get(d.id)
|
||||
gateway.client.emit('guildUpdate', before, after)
|
||||
}
|
||||
|
|
|
@ -11,13 +11,14 @@ import { guildBanAdd } from './guildBanAdd.ts'
|
|||
import { ready } from './ready.ts'
|
||||
import { guildBanRemove } from './guildBanRemove.ts'
|
||||
import { messageCreate } from "./messageCreate.ts"
|
||||
import { resume } from "./resume.ts"
|
||||
|
||||
export const gatewayHandlers: {
|
||||
[eventCode in GatewayEvents]: GatewayEventHandler | undefined
|
||||
} = {
|
||||
READY: ready,
|
||||
RECONNECT: undefined,
|
||||
RESUMED: undefined,
|
||||
RESUMED: resume,
|
||||
CHANNEL_CREATE: channelCreate,
|
||||
CHANNEL_DELETE: channelDelete,
|
||||
CHANNEL_UPDATE: channelUpdate,
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import { Channel } from "../../structures/channel.ts"
|
||||
import { Message } from "../../structures/message.ts"
|
||||
import { MessageMentions } from "../../structures/MessageMentions.ts"
|
||||
import { User } from "../../structures/user.ts"
|
||||
import { MessagePayload } from "../../types/channelTypes.ts"
|
||||
import { Gateway, GatewayEventHandler } from '../index.ts'
|
||||
|
||||
|
@ -7,9 +9,12 @@ export const messageCreate: GatewayEventHandler = async(
|
|||
gateway: Gateway,
|
||||
d: MessagePayload
|
||||
) => {
|
||||
let channel = gateway.client.channels.get(d.channel_id)
|
||||
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 Channel
|
||||
let message = new Message(gateway.client, d, channel)
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -2,10 +2,11 @@ import { User } from '../../structures/user.ts'
|
|||
import { GuildPayload } from '../../types/guildTypes.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.sessionID = d.session_id
|
||||
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)
|
||||
})
|
||||
|
|
6
src/gateway/handlers/resume.ts
Normal file
6
src/gateway/handlers/resume.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
import { Gateway, GatewayEventHandler } from '../index.ts'
|
||||
|
||||
export const resume: GatewayEventHandler = (gateway: Gateway, d: any) => {
|
||||
gateway.debug(`Session Resumed!`)
|
||||
gateway.client.emit('resume')
|
||||
}
|
|
@ -5,10 +5,11 @@ import {
|
|||
DISCORD_API_VERSION
|
||||
} from '../consts/urlsAndVersions.ts'
|
||||
import { GatewayResponse } from '../types/gatewayResponse.ts'
|
||||
import { GatewayOpcodes, GatewayIntents } from '../types/gatewayTypes.ts'
|
||||
import { GatewayOpcodes, GatewayIntents, GatewayCloseCodes } from '../types/gatewayTypes.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.
|
||||
|
@ -25,15 +26,17 @@ class Gateway {
|
|||
heartbeatInterval = 0
|
||||
heartbeatIntervalID?: number
|
||||
sequenceID?: number
|
||||
sessionID?: string
|
||||
lastPingTimestamp = 0
|
||||
sessionID?: string
|
||||
private heartbeatServerResponded = false
|
||||
client: Client
|
||||
cache: GatewayCache
|
||||
|
||||
constructor (client: Client, token: string, intents: GatewayIntents[]) {
|
||||
this.token = token
|
||||
this.intents = intents
|
||||
this.client = client
|
||||
this.cache = new GatewayCache(client)
|
||||
this.websocket = new WebSocket(
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||
`${DISCORD_GATEWAY_URL}/?v=${DISCORD_API_VERSION}&encoding=json`,
|
||||
|
@ -51,7 +54,7 @@ class Gateway {
|
|||
this.debug("Connected to Gateway!")
|
||||
}
|
||||
|
||||
private onmessage (event: MessageEvent): void {
|
||||
private async onmessage (event: MessageEvent): Promise<void> {
|
||||
let data = event.data
|
||||
if (data instanceof ArrayBuffer) {
|
||||
data = new Uint8Array(data)
|
||||
|
@ -72,8 +75,7 @@ class Gateway {
|
|||
this.heartbeatServerResponded = false
|
||||
} else {
|
||||
clearInterval(this.heartbeatIntervalID)
|
||||
this.websocket.close()
|
||||
this.initWebsocket()
|
||||
this.reconnect()
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -90,6 +92,7 @@ class Gateway {
|
|||
this.sendIdentify()
|
||||
this.initialized = true
|
||||
} else {
|
||||
console.log("Calling Resume")
|
||||
this.sendResume()
|
||||
}
|
||||
break
|
||||
|
@ -102,18 +105,16 @@ class Gateway {
|
|||
|
||||
case GatewayOpcodes.INVALID_SESSION:
|
||||
// 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
|
||||
if (!d) {
|
||||
setTimeout(this.sendResume, 3000)
|
||||
} else {
|
||||
setTimeout(this.sendIdentify, 3000)
|
||||
}
|
||||
setTimeout(() => this.sendIdentify(true), 3000)
|
||||
break
|
||||
|
||||
case GatewayOpcodes.DISPATCH: {
|
||||
this.heartbeatServerResponded = true
|
||||
if (s !== null) {
|
||||
this.sequenceID = s
|
||||
await this.cache.set("seq", s)
|
||||
}
|
||||
if (t !== null && t !== undefined) {
|
||||
const handler = gatewayHandlers[t]
|
||||
|
@ -124,23 +125,68 @@ class Gateway {
|
|||
}
|
||||
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:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
private onclose (event: CloseEvent): void {
|
||||
console.log(event.code)
|
||||
// TODO: Handle close event codes.
|
||||
this.debug("Connection Closed with code: " + event.code)
|
||||
|
||||
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 {
|
||||
const eventError = event as ErrorEvent
|
||||
|
||||
console.log(eventError)
|
||||
}
|
||||
|
||||
private async sendIdentify () {
|
||||
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")
|
||||
|
@ -148,6 +194,14 @@ class Gateway {
|
|||
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(
|
||||
JSON.stringify({
|
||||
op: GatewayOpcodes.IDENTIFY,
|
||||
|
@ -175,17 +229,22 @@ 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(
|
||||
JSON.stringify({
|
||||
op: GatewayOpcodes.RESUME,
|
||||
d: {
|
||||
token: this.token,
|
||||
session_id: this.sessionID,
|
||||
seq: this.sequenceID
|
||||
}
|
||||
})
|
||||
JSON.stringify(resumePayload)
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -193,6 +252,13 @@ class Gateway {
|
|||
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 {
|
||||
this.websocket = new WebSocket(
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||
|
|
|
@ -4,29 +4,29 @@ import { Base } from "../structures/base.ts";
|
|||
export class BaseManager<T, T2> {
|
||||
client: Client
|
||||
cacheName: string
|
||||
dataType: typeof Base
|
||||
dataType: any
|
||||
|
||||
constructor(client: Client, cacheName: string, dataType: typeof Base) {
|
||||
constructor(client: Client, cacheName: string, dataType: any) {
|
||||
this.client = client
|
||||
this.cacheName = cacheName
|
||||
this.dataType = dataType
|
||||
}
|
||||
|
||||
_get(key: string): T {
|
||||
return this.client.cache.get(this.cacheName, key) as T
|
||||
_get(key: string): Promise<T> {
|
||||
return this.client.cache.get(this.cacheName, key) as Promise<T>
|
||||
}
|
||||
|
||||
get(key: string): T2 | void {
|
||||
const raw = this._get(key)
|
||||
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
|
||||
}
|
||||
|
||||
set(key: string, value: T) {
|
||||
async set(key: string, value: T) {
|
||||
return this.client.cache.set(this.cacheName, key, value)
|
||||
}
|
||||
|
||||
delete(key: string) {
|
||||
async delete(key: string) {
|
||||
return this.client.cache.delete(this.cacheName, key)
|
||||
}
|
||||
}
|
|
@ -11,11 +11,11 @@ export class ChannelsManager extends BaseManager<ChannelPayload, Channel> {
|
|||
}
|
||||
|
||||
// Override get method as Generic
|
||||
get<T = Channel>(key: string): T {
|
||||
async get<T = Channel>(key: string): Promise<T> {
|
||||
return new this.dataType(this.client, this._get(key)) as any
|
||||
}
|
||||
|
||||
fetch(id: string) {
|
||||
fetch(id: string): Promise<Channel> {
|
||||
return new Promise((res, rej) => {
|
||||
this.client.rest.get(CHANNEL(id)).then(data => {
|
||||
this.set(id, data as ChannelPayload)
|
||||
|
|
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)
|
||||
}
|
||||
}
|
|
@ -1,7 +1,10 @@
|
|||
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/channelTypes.ts";
|
||||
import { CHANNEL_MESSAGE } from "../types/endpoint.ts";
|
||||
import { UserPayload } from "../types/userTypes.ts";
|
||||
import { BaseManager } from "./BaseManager.ts";
|
||||
|
||||
export class MessagesManager extends BaseManager<MessagePayload, Message> {
|
||||
|
@ -11,9 +14,15 @@ export class MessagesManager extends BaseManager<MessagePayload, Message> {
|
|||
|
||||
fetch(channelID: string, id: string) {
|
||||
return new Promise((res, rej) => {
|
||||
this.client.rest.get(CHANNEL_MESSAGE(channelID, id)).then(data => {
|
||||
this.client.rest.get(CHANNEL_MESSAGE(channelID, id)).then(async data => {
|
||||
this.set(id, data as MessagePayload)
|
||||
res(new Message(this.client, 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))
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
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) => any
|
||||
set: (cacheName: string, key: string, value: any) => any
|
||||
delete: (cacheName: string, key: string) => boolean
|
||||
array: (cacheName: string) => void | any[]
|
||||
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 {
|
||||
|
@ -19,13 +20,13 @@ export class DefaultCacheAdapter implements ICacheAdapter {
|
|||
this.client = client
|
||||
}
|
||||
|
||||
get(cacheName: string, key: string) {
|
||||
async get(cacheName: string, key: string) {
|
||||
const cache = this.data[cacheName]
|
||||
if (!cache) return;
|
||||
return cache.get(key)
|
||||
}
|
||||
|
||||
set(cacheName: string, key: string, value: any) {
|
||||
async set(cacheName: string, key: string, value: any) {
|
||||
let cache = this.data[cacheName]
|
||||
if (!cache) {
|
||||
this.data[cacheName] = new Collection()
|
||||
|
@ -34,15 +35,64 @@ export class DefaultCacheAdapter implements ICacheAdapter {
|
|||
cache.set(key, value)
|
||||
}
|
||||
|
||||
delete(cacheName: string, key: string) {
|
||||
async delete(cacheName: string, key: string) {
|
||||
const cache = this.data[cacheName]
|
||||
if (!cache) return false
|
||||
return cache.delete(key)
|
||||
}
|
||||
|
||||
array(cacheName: string) {
|
||||
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))
|
||||
}
|
||||
}
|
|
@ -42,6 +42,11 @@ export class Client extends EventEmitter {
|
|||
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}`)
|
||||
}
|
||||
|
|
|
@ -13,8 +13,7 @@ export enum HttpResponseCode {
|
|||
NotFound = 404,
|
||||
MethodNotAllowed = 405,
|
||||
TooManyRequests = 429,
|
||||
GatewayUnavailable = 502,
|
||||
// ServerError left untyped because it's 5xx.
|
||||
GatewayUnavailable = 502
|
||||
}
|
||||
|
||||
export type RequestMethods =
|
||||
|
|
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)
|
||||
this.type = data.type
|
||||
this.id = data.id
|
||||
this.client.channels.set(this.id, data)
|
||||
// TODO: Cache in Gateway Event Code
|
||||
// this.client.channels.set(this.id, data)
|
||||
}
|
||||
|
||||
protected readFromData (data: ChannelPayload): void {
|
||||
|
|
|
@ -10,7 +10,7 @@ export class DMChannel extends TextChannel {
|
|||
constructor (client: Client, data: DMChannelPayload) {
|
||||
super(client, data)
|
||||
this.recipients = data.recipients
|
||||
cache.set('dmchannel', this.id, this)
|
||||
// cache.set('dmchannel', this.id, this)
|
||||
}
|
||||
|
||||
protected readFromData (data: DMChannelPayload): void {
|
||||
|
|
|
@ -14,7 +14,8 @@ export class GroupDMChannel extends Channel {
|
|||
this.name = data.name
|
||||
this.icon = data.icon
|
||||
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 {
|
||||
|
|
|
@ -83,12 +83,12 @@ export class Guild extends Base {
|
|||
// this.roles = data.roles.map(
|
||||
// v => cache.get('role', v.id) ?? new Role(client, v)
|
||||
// )
|
||||
data.roles.forEach(role => {
|
||||
this.roles.set(role.id, new Role(client, role))
|
||||
})
|
||||
this.emojis = data.emojis.map(
|
||||
v => cache.get('emoji', v.id) ?? new Emoji(client, v)
|
||||
)
|
||||
// data.roles.forEach(role => {
|
||||
// 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.mfaLevel = data.mfa_level
|
||||
this.systemChannelID = data.system_channel_id
|
||||
|
@ -97,19 +97,20 @@ export class Guild extends Base {
|
|||
this.joinedAt = data.joined_at
|
||||
this.large = data.large
|
||||
this.memberCount = data.member_count
|
||||
this.voiceStates = data.voice_states?.map(
|
||||
v =>
|
||||
cache.get('voiceState', `${v.guild_id}:${v.user_id}`) ??
|
||||
new VoiceState(client, v)
|
||||
)
|
||||
this.members = data.members?.map(
|
||||
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)
|
||||
)
|
||||
// TODO: Cache in Gateway Event code
|
||||
// this.voiceStates = data.voice_states?.map(
|
||||
// v =>
|
||||
// cache.get('voiceState', `${v.guild_id}:${v.user_id}`) ??
|
||||
// new VoiceState(client, v)
|
||||
// )
|
||||
// this.members = data.members?.map(
|
||||
// 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.presences = data.presences
|
||||
this.maxPresences = data.max_presences
|
||||
this.maxMembers = data.max_members
|
||||
|
|
|
@ -22,7 +22,8 @@ export class CategoryChannel extends Channel {
|
|||
this.permissionOverwrites = data.permission_overwrites
|
||||
this.nsfw = data.nsfw
|
||||
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 {
|
||||
|
|
|
@ -27,7 +27,8 @@ export class GuildTextChannel extends TextChannel {
|
|||
this.parentID = data.parent_id
|
||||
this.topic = data.topic
|
||||
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 {
|
||||
|
|
|
@ -23,7 +23,8 @@ export class VoiceChannel extends Channel {
|
|||
this.permissionOverwrites = data.permission_overwrites
|
||||
this.nsfw = data.nsfw
|
||||
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 {
|
||||
|
|
|
@ -25,7 +25,8 @@ export class Member extends Base {
|
|||
this.premiumSince = data.premium_since
|
||||
this.deaf = data.deaf
|
||||
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 {
|
||||
|
|
|
@ -16,6 +16,8 @@ import { Embed } from './embed.ts'
|
|||
import { CHANNEL_MESSAGE } from '../types/endpoint.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 {
|
||||
// eslint-disable-next-line @typescript-eslint/prefer-readonly
|
||||
|
@ -31,7 +33,7 @@ export class Message extends Base {
|
|||
editedTimestamp?: string
|
||||
tts: boolean
|
||||
mentionEveryone: boolean
|
||||
mentions: User[]
|
||||
mentions: MessageMentions
|
||||
mentionRoles: string[]
|
||||
mentionChannels?: ChannelMention[]
|
||||
attachments: Attachment[]
|
||||
|
@ -46,22 +48,24 @@ export class Message extends Base {
|
|||
messageReference?: MessageReference
|
||||
flags?: number
|
||||
|
||||
constructor (client: Client, data: MessagePayload, channel?: Channel, noSave?: boolean) {
|
||||
constructor (client: Client, data: MessagePayload, channel: Channel, author: User, mentions: MessageMentions) {
|
||||
super(client)
|
||||
this.data = data
|
||||
this.id = data.id
|
||||
this.channelID = data.channel_id
|
||||
this.guildID = data.guild_id
|
||||
this.author =
|
||||
this.client.users.get(data.author.id) || new User(this.client, data.author)
|
||||
this.author = author
|
||||
// this.author =
|
||||
// this.client.users.get(data.author.id) || new User(this.client, data.author)
|
||||
this.content = data.content
|
||||
this.timestamp = data.timestamp
|
||||
this.editedTimestamp = data.edited_timestamp
|
||||
this.tts = data.tts
|
||||
this.mentionEveryone = data.mention_everyone
|
||||
this.mentions = data.mentions.map(
|
||||
v => this.client.users.get(v.id) || new User(client, v)
|
||||
)
|
||||
this.mentions = mentions
|
||||
// this.mentions = data.mentions.map(
|
||||
// v => this.client.users.get(v.id) || new User(client, v)
|
||||
// )
|
||||
this.mentionRoles = data.mention_roles
|
||||
this.mentionChannels = data.mention_channels
|
||||
this.attachments = data.attachments
|
||||
|
@ -75,28 +79,28 @@ export class Message extends Base {
|
|||
this.application = data.application
|
||||
this.messageReference = data.message_reference
|
||||
this.flags = data.flags
|
||||
if(channel) this.channel = channel || this.client.channels.get(this.channelID)
|
||||
else throw new Error("Message received without Channel (neither in cache)") // unlikely to happen
|
||||
if(!noSave) this.client.messages.set(this.id, data)
|
||||
this.channel = channel
|
||||
// TODO: Cache in Gateway Event Code
|
||||
// if(!noSave) this.client.messages.set(this.id, data)
|
||||
}
|
||||
|
||||
protected readFromData (data: MessagePayload): void {
|
||||
super.readFromData(data)
|
||||
this.channelID = data.channel_id ?? this.channelID
|
||||
this.guildID = data.guild_id ?? this.guildID
|
||||
this.author =
|
||||
this.client.users.get(data.author.id) ||
|
||||
this.author ||
|
||||
new User(this.client, data.author)
|
||||
// this.author =
|
||||
// this.client.users.get(data.author.id) ||
|
||||
// this.author ||
|
||||
// new User(this.client, data.author)
|
||||
this.content = data.content ?? this.content
|
||||
this.timestamp = data.timestamp ?? this.timestamp
|
||||
this.editedTimestamp = data.edited_timestamp ?? this.editedTimestamp
|
||||
this.tts = data.tts ?? this.tts
|
||||
this.mentionEveryone = data.mention_everyone ?? this.mentionEveryone
|
||||
this.mentions =
|
||||
data.mentions.map(
|
||||
v => this.client.users.get(v.id) || new User(this.client, v)
|
||||
) ?? this.mentions
|
||||
// this.mentions =
|
||||
// data.mentions.map(
|
||||
// v => this.client.users.get(v.id) || new User(this.client, v)
|
||||
// ) ?? this.mentions
|
||||
this.mentionRoles = data.mention_roles ?? this.mentionRoles
|
||||
this.mentionChannels = data.mention_channels ?? this.mentionChannels
|
||||
this.attachments = data.attachments ?? this.attachments
|
||||
|
@ -112,24 +116,10 @@ export class Message extends Base {
|
|||
this.flags = data.flags ?? this.flags
|
||||
}
|
||||
|
||||
// TODO: We have to seperate fetch()
|
||||
async edit (text?: string, option?: MessageOption): Promise<Message> {
|
||||
if (text !== undefined && option !== undefined) {
|
||||
throw new Error('Either text or option is necessary.')
|
||||
}
|
||||
|
||||
let newMsg = await this.client.rest.patch(CHANNEL_MESSAGE(this.channelID, this.id), {
|
||||
content: text,
|
||||
embed: option?.embed.toJSON(),
|
||||
file: option?.file,
|
||||
tts: option?.tts,
|
||||
allowed_mentions: option?.allowedMention
|
||||
}) as MessagePayload
|
||||
|
||||
return new Message(this.client, newMsg)
|
||||
edit (text?: string, option?: MessageOption): Promise<Message> {
|
||||
return (this.channel as TextChannel).editMessage(this.id, text, option)
|
||||
}
|
||||
|
||||
// TODO: We have to seperate fetch()
|
||||
delete (): Promise<void> {
|
||||
return this.client.rest.delete(CHANNEL_MESSAGE(this.channelID, this.id)) as any
|
||||
}
|
||||
|
|
|
@ -27,7 +27,8 @@ export class Role extends Base {
|
|||
this.permissions = data.permissions
|
||||
this.managed = data.managed
|
||||
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 {
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
import cache from '../models/cache.ts'
|
||||
import { Client } from '../models/client.ts'
|
||||
import { MessageOption, TextChannelPayload } from '../types/channelTypes.ts'
|
||||
import { MessageOption, MessagePayload, TextChannelPayload } from '../types/channelTypes.ts'
|
||||
import { CHANNEL_MESSAGE, CHANNEL_MESSAGES } from '../types/endpoint.ts'
|
||||
import { Channel } from './channel.ts'
|
||||
import { Message } from './message.ts'
|
||||
import { MessageMentions } from "./MessageMentions.ts"
|
||||
import { User } from "./user.ts"
|
||||
|
||||
export class TextChannel extends Channel {
|
||||
lastMessageID?: string
|
||||
|
@ -13,7 +15,8 @@ export class TextChannel extends Channel {
|
|||
super(client, data)
|
||||
this.lastMessageID = data.last_message_id
|
||||
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 {
|
||||
|
@ -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 (
|
||||
messageID: string,
|
||||
message: Message | string,
|
||||
text?: string,
|
||||
option?: MessageOption
|
||||
): Promise<Message> {
|
||||
if (text !== undefined && option !== undefined) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -45,7 +45,8 @@ export class User extends Base {
|
|||
this.flags = data.flags
|
||||
this.premiumType = data.premium_type
|
||||
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 {
|
||||
|
|
|
@ -30,7 +30,8 @@ export class VoiceState extends Base {
|
|||
this.selfStream = data.self_stream
|
||||
this.selfVideo = data.self_video
|
||||
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 {
|
||||
|
|
|
@ -7,9 +7,15 @@ import { TextChannel } from '../structures/textChannel.ts'
|
|||
import { Guild } from '../structures/guild.ts'
|
||||
import { User } from '../structures/user.ts'
|
||||
import { Message } from "../structures/message.ts"
|
||||
import { RedisCacheAdapter } from "../models/CacheAdapter.ts"
|
||||
|
||||
const bot = new Client()
|
||||
|
||||
bot.setAdapter(new RedisCacheAdapter(bot, {
|
||||
hostname: "127.0.0.1",
|
||||
port: 6379
|
||||
}))
|
||||
|
||||
bot.on('ready', () => {
|
||||
console.log(`[Login] Logged in as ${bot.user?.tag}!`)
|
||||
})
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
export class Collection<K, V> extends Map<K, V> {
|
||||
export class Collection<K = string, V = any> extends Map<K, V> {
|
||||
maxSize?: number;
|
||||
|
||||
set(key: K, value: V) {
|
||||
|
@ -84,4 +84,12 @@ export class Collection<K, V> extends Map<K, V> {
|
|||
|
||||
return accumulator
|
||||
}
|
||||
|
||||
static fromObject<V>(object: { [key: string]: V }) {
|
||||
return new Collection<string, V>(Object.entries(object))
|
||||
}
|
||||
|
||||
toObject() {
|
||||
return Object.entries(this)
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue