mirror of
https://github.com/jaronpate/unicord.git
synced 2026-10-18 21:47:40 +00:00
338 lines
11 KiB
TypeScript
338 lines
11 KiB
TypeScript
import { WebSocket, Data } from 'ws';
|
|
import axios, { AxiosInstance } from 'axios';
|
|
import { Client } from '..';
|
|
import {
|
|
Session,
|
|
User,
|
|
Payload,
|
|
Intent,
|
|
Handler,
|
|
HandlerFunction,
|
|
Command,
|
|
Status,
|
|
Presence,
|
|
ActivityType,
|
|
CommandFuntion,
|
|
Interaction,
|
|
InteractionFunction,
|
|
InteractionContext
|
|
} from '../types';
|
|
import { Context } from './context';
|
|
|
|
export class Gateway {
|
|
socket: WebSocket;
|
|
axios: AxiosInstance;
|
|
// Config
|
|
readonly token: string;
|
|
readonly client: Client;
|
|
private intents: number = 513;
|
|
private commandNotFound: CommandFuntion = null;
|
|
// Discord gateway data
|
|
private readonly url: string = 'wss://gateway.discord.gg/?v=10&encoding=json';
|
|
private heartbeat_interval: number | null = null;
|
|
private session: Session = null;
|
|
public user: User = null;
|
|
// Utils
|
|
public event_handlers: Map<string, Array<Handler | HandlerFunction>> = new Map();
|
|
public interaction_handlers: Map<string, Array<Interaction | InteractionFunction>> = new Map();
|
|
|
|
constructor({
|
|
token,
|
|
intents,
|
|
client,
|
|
commandNotFound
|
|
}: {
|
|
token: string;
|
|
intents?: Intent[];
|
|
client: Client;
|
|
commandNotFound?: CommandFuntion;
|
|
}) {
|
|
// Copy config
|
|
this.token = token;
|
|
this.client = client;
|
|
this.commandNotFound = commandNotFound;
|
|
// Initialize socket
|
|
this.socket = new WebSocket(this.url);
|
|
// Initialize axios
|
|
this.axios = axios.create({
|
|
headers: {
|
|
'User-Agent': 'DiscordBot (discord.ts, 0.0.1)',
|
|
Authorization: `Bot ${this.token}`
|
|
},
|
|
baseURL: 'https://discord.com/api/v10'
|
|
});
|
|
this.axios.interceptors.response.use((response) => response.data);
|
|
// Calculate intents
|
|
if (intents) {
|
|
this.intents = intents.reduce((acc, intent) => acc | intent, 0);
|
|
}
|
|
}
|
|
|
|
public connect = () => {
|
|
// Initialize socket
|
|
this.socket = new WebSocket(this.url);
|
|
// Register the listeners
|
|
this.socket.on('open', this.onOpen);
|
|
this.socket.on('message', this.onMessage);
|
|
// this.socket.on('close', this.onClose);
|
|
// this.socket.on('error', this.onError);
|
|
return this;
|
|
};
|
|
|
|
public reconnect = () => {
|
|
return this.connect();
|
|
};
|
|
|
|
public setPresence = (presence: Presence) => {
|
|
this.send({
|
|
op: 3,
|
|
d: presence
|
|
});
|
|
};
|
|
|
|
public handlers = {
|
|
register: (events: string | string[], handler: Handler | HandlerFunction) => {
|
|
this.registerHandler(events, handler, 'command');
|
|
},
|
|
get: (event: string) => {
|
|
return this.event_handlers.get(event);
|
|
}
|
|
};
|
|
|
|
public interactions = {
|
|
register: (custom_ids: string | string[], handler: Interaction | InteractionFunction) => {
|
|
this.registerHandler(custom_ids, handler, 'interaction');
|
|
},
|
|
get: (event: string) => {
|
|
return this.interaction_handlers.get(event);
|
|
}
|
|
};
|
|
|
|
public api = {
|
|
get: (url: string, params?: any) => {
|
|
return this.axios.get<unknown, Record<string, any>>(url, { params });
|
|
},
|
|
post: (url: string, data?: any) => {
|
|
return this.axios.post<unknown, Record<string, any>>(url, data);
|
|
},
|
|
patch: (url: string, data?: any) => {
|
|
return this.axios.patch<unknown, Record<string, any>>(url, data);
|
|
},
|
|
delete: (url: string, data?: any) => {
|
|
return this.axios.delete<unknown, Record<string, any>>(url, data);
|
|
}
|
|
};
|
|
|
|
// prettier-ignore
|
|
private registerHandler(events: string | string[], handler: Handler | HandlerFunction, type: 'command'): void;
|
|
// prettier-ignore
|
|
private registerHandler(events: string | string[], handler: Interaction | InteractionFunction, type: 'interaction'): void;
|
|
|
|
private registerHandler(events: string | string[], handler: any, type: 'command' | 'interaction') {
|
|
const handler_map = type === 'command' ? this.event_handlers : this.interaction_handlers;
|
|
if (Array.isArray(events)) {
|
|
// If Array register handler for each event
|
|
for (const event of events) {
|
|
const current_handlers = handler_map.get(event) ?? [];
|
|
handler_map.set(event, [...current_handlers, handler]);
|
|
}
|
|
} else {
|
|
// Otherwise register for the single event
|
|
const current_handlers = handler_map.get(events) ?? [];
|
|
handler_map.set(events, [...current_handlers, handler]);
|
|
}
|
|
}
|
|
private send = (payload: Partial<Payload>) => {
|
|
payload.s = this.session?.s ?? null;
|
|
console.log('sending: %s', payload);
|
|
this.socket.send(JSON.stringify(payload));
|
|
};
|
|
|
|
private indentify = () => {
|
|
this.send({
|
|
op: 2,
|
|
d: {
|
|
token: this.token,
|
|
intents: this.intents,
|
|
properties: {
|
|
os: process.platform,
|
|
browser: 'discord.ts',
|
|
device: 'discord.ts'
|
|
}
|
|
}
|
|
});
|
|
};
|
|
|
|
private heartbeat = () => {
|
|
if (!this.heartbeat_interval) {
|
|
throw new Error('No heartbeat interval set');
|
|
}
|
|
setInterval(() => {
|
|
this.send({
|
|
op: 1,
|
|
d: {}
|
|
});
|
|
}, this.heartbeat_interval);
|
|
};
|
|
|
|
private initialize = (payload: Payload) => {
|
|
this.session = payload.d.session;
|
|
this.user = payload.d.user;
|
|
this.user.tag = `${this.user.username}#${this.user.discriminator}`;
|
|
// Construct presence
|
|
const status: Presence = {
|
|
status: Status.Online,
|
|
since: null,
|
|
activities: [
|
|
{
|
|
name: payload.d.guilds.length + ' servers',
|
|
type: ActivityType.Watching,
|
|
created_at: Date.now()
|
|
}
|
|
],
|
|
afk: false
|
|
};
|
|
// Set presence
|
|
this.setPresence(status);
|
|
// Cache guilds? (these are not whole guilds, just guild ids)
|
|
// for (const guild of payload.d.guilds) {
|
|
// this.guilds.set(guild.id, guild);
|
|
// }
|
|
console.log('session: %s', this.session);
|
|
console.log('user: %s', this.user);
|
|
};
|
|
|
|
private parseEvent = (payload: Payload) => {
|
|
// If not a dispatch event, return
|
|
if (!payload.t) {
|
|
return;
|
|
}
|
|
// Run default handlers
|
|
switch (payload.t) {
|
|
case 'READY':
|
|
this.initialize(payload);
|
|
break;
|
|
case 'INTERACTION_CREATE':
|
|
this.handleInteractionCreate(payload);
|
|
break;
|
|
case 'MESSAGE_CREATE':
|
|
this.handleMessageCreate(payload);
|
|
break;
|
|
case 'MESSAGE_UPDATE':
|
|
this.handleMessageCreate(payload);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
// Call event handlers
|
|
const handlers = this.handlers.get(payload.t);
|
|
const context = new Context({ client: this.client, message: payload.d });
|
|
if (handlers) {
|
|
for (const handler of handlers) {
|
|
handler instanceof Handler
|
|
? handler.execute(this.client, context, payload.d)
|
|
: handler(this.client, context, payload.d);
|
|
}
|
|
}
|
|
};
|
|
|
|
private handleInteractionCreate = (payload: Payload) => {
|
|
const context = new InteractionContext({ client: this.client, interaction: payload.d });
|
|
const handlers = this.client.interactions.get(payload.d.data.custom_id);
|
|
if (handlers) {
|
|
for (const handler of handlers) {
|
|
if (handler instanceof Interaction) {
|
|
handler.execute(this.client, context, payload.d);
|
|
} else {
|
|
handler(this.client, context, payload.d);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
private handleMessageCreate = async (payload: Payload) => {
|
|
// Ignore bots
|
|
if (payload.d.author.bot) {
|
|
return;
|
|
}
|
|
console.log(payload.d);
|
|
// Is this a command?
|
|
if (payload.d.content.startsWith(this.client.prefix)) {
|
|
// Parse command
|
|
const args = payload.d.content
|
|
.slice(this.client.prefix.length)
|
|
.trim()
|
|
.split(/\s+(?=(?:[^\'"]*[\'"][^\'"]*[\'"])*[^\'"]*$)/g);
|
|
// Check if command exists
|
|
const handler = this.client.commands.get(args[0]);
|
|
// Generate context
|
|
const context = new Context({ client: this.client, message: payload.d });
|
|
if (handler) {
|
|
// Execute command
|
|
handler instanceof Command
|
|
? handler.execute(this.client, context, args.slice(1))
|
|
: handler(this.client, context, args.slice(1));
|
|
} else {
|
|
let reply = null;
|
|
// Send error message if command does not exist
|
|
if (this.commandNotFound) {
|
|
// If custom command not found handler is set, use that
|
|
reply = this.commandNotFound(this.client, context);
|
|
} else {
|
|
// Otherwise send default message
|
|
reply = await context.reply('Command not found :(', true);
|
|
}
|
|
if (reply) {
|
|
// If reply is set, delete it after 3 seconds
|
|
setTimeout(() => {
|
|
context.delete(reply.id);
|
|
}, 3000);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
private parseOpCode = (payload: Payload) => {
|
|
switch (payload.op) {
|
|
case 0:
|
|
this.parseEvent(payload);
|
|
break;
|
|
case 9:
|
|
setTimeout(this.reconnect, 500);
|
|
break;
|
|
case 10:
|
|
this.heartbeat_interval = payload.d.heartbeat_interval;
|
|
this.heartbeat();
|
|
this.indentify();
|
|
break;
|
|
case 11:
|
|
console.log('heartbeat acknowledged');
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
};
|
|
|
|
private onMessage = (packet: Data): void => {
|
|
let data: Payload | null = null;
|
|
try {
|
|
data = JSON.parse(packet.toString());
|
|
} catch (e) {
|
|
console.error(e);
|
|
}
|
|
if (data) {
|
|
console.log('received: %s', data);
|
|
console.log('received: %s', data.d);
|
|
if (this.session && data.s && data.s > this.session.s) {
|
|
this.session.s = data.s;
|
|
} else {
|
|
this.parseOpCode(data);
|
|
}
|
|
}
|
|
};
|
|
|
|
private onOpen = () => {
|
|
console.log('Socket open');
|
|
};
|
|
}
|