From 5e86d7ab9c5200d794c3adb2b422d20a2aefd2ce Mon Sep 17 00:00:00 2001 From: Flam3rboy <34555296+Flam3rboy@users.noreply.github.com> Date: Sat, 13 Aug 2022 02:00:50 +0200 Subject: restructure to single project --- gateway/src/Server.ts | 62 ------- gateway/src/events/Close.ts | 46 ------ gateway/src/events/Connection.ts | 94 ----------- gateway/src/events/Message.ts | 62 ------- gateway/src/index.ts | 4 - gateway/src/listener/listener.ts | 249 ----------------------------- gateway/src/opcodes/Heartbeat.ts | 11 -- gateway/src/opcodes/LazyRequest.ts | 173 -------------------- gateway/src/opcodes/PresenceUpdate.ts | 24 --- gateway/src/opcodes/RequestGuildMembers.ts | 5 - gateway/src/opcodes/Resume.ts | 12 -- gateway/src/opcodes/VoiceStateUpdate.ts | 116 -------------- gateway/src/opcodes/experiments.json | 76 --------- gateway/src/opcodes/index.ts | 25 --- gateway/src/opcodes/instanceOf.ts | 18 --- gateway/src/start.ts | 14 -- gateway/src/util/Constants.ts | 50 ------ gateway/src/util/Heartbeat.ts | 11 -- gateway/src/util/Send.ts | 33 ---- gateway/src/util/SessionUtils.ts | 13 -- gateway/src/util/WebSocket.ts | 22 --- gateway/src/util/index.ts | 5 - 22 files changed, 1125 deletions(-) delete mode 100644 gateway/src/Server.ts delete mode 100644 gateway/src/events/Close.ts delete mode 100644 gateway/src/events/Connection.ts delete mode 100644 gateway/src/events/Message.ts delete mode 100644 gateway/src/index.ts delete mode 100644 gateway/src/listener/listener.ts delete mode 100644 gateway/src/opcodes/Heartbeat.ts delete mode 100644 gateway/src/opcodes/LazyRequest.ts delete mode 100644 gateway/src/opcodes/PresenceUpdate.ts delete mode 100644 gateway/src/opcodes/RequestGuildMembers.ts delete mode 100644 gateway/src/opcodes/Resume.ts delete mode 100644 gateway/src/opcodes/VoiceStateUpdate.ts delete mode 100644 gateway/src/opcodes/experiments.json delete mode 100644 gateway/src/opcodes/index.ts delete mode 100644 gateway/src/opcodes/instanceOf.ts delete mode 100644 gateway/src/start.ts delete mode 100644 gateway/src/util/Constants.ts delete mode 100644 gateway/src/util/Heartbeat.ts delete mode 100644 gateway/src/util/Send.ts delete mode 100644 gateway/src/util/SessionUtils.ts delete mode 100644 gateway/src/util/WebSocket.ts delete mode 100644 gateway/src/util/index.ts (limited to 'gateway/src') diff --git a/gateway/src/Server.ts b/gateway/src/Server.ts deleted file mode 100644 index 82fbeba2..00000000 --- a/gateway/src/Server.ts +++ /dev/null @@ -1,62 +0,0 @@ -import dotenv from "dotenv"; -dotenv.config(); -import { closeDatabase, Config, getOrInitialiseDatabase, initEvent } from "@fosscord/util"; -import ws from "ws"; -import { Connection } from "./events/Connection"; -import http from "http"; - -export class Server { - public ws: ws.Server; - public port: number; - public server: http.Server; - public production: boolean; - - constructor({ - port, - server, - production, - }: { - port: number; - server?: http.Server; - production?: boolean; - }) { - this.port = port; - this.production = production || false; - - if (server) this.server = server; - else { - this.server = http.createServer(function (req, res) { - res.writeHead(200).end("Online"); - }); - } - - this.server.on("upgrade", (request, socket, head) => { - // @ts-ignore - this.ws.handleUpgrade(request, socket, head, (socket) => { - this.ws.emit("connection", socket, request); - }); - }); - - this.ws = new ws.Server({ - maxPayload: 4096, - noServer: true, - }); - this.ws.on("connection", Connection); - this.ws.on("error", console.error); - } - - async start(): Promise { - await getOrInitialiseDatabase(); - await Config.init(); - await initEvent(); - if (!this.server.listening) { - this.server.listen(this.port); - console.log(`[Gateway] online on 0.0.0.0:${this.port}`); - } - } - - async stop() { - closeDatabase(); - this.server.close(); - } -} diff --git a/gateway/src/events/Close.ts b/gateway/src/events/Close.ts deleted file mode 100644 index 5b7c512c..00000000 --- a/gateway/src/events/Close.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { WebSocket } from "@fosscord/gateway"; -import { - emitEvent, - PresenceUpdateEvent, - PrivateSessionProjection, - Session, - SessionsReplace, - User, -} from "@fosscord/util"; - -export async function Close(this: WebSocket, code: number, reason: string) { - console.log("[WebSocket] closed", code, reason); - if (this.heartbeatTimeout) clearTimeout(this.heartbeatTimeout); - if (this.readyTimeout) clearTimeout(this.readyTimeout); - this.deflate?.close(); - this.removeAllListeners(); - - if (this.session_id) { - await Session.delete({ session_id: this.session_id }); - const sessions = await Session.find({ - where: { user_id: this.user_id }, - select: PrivateSessionProjection, - }); - await emitEvent({ - event: "SESSIONS_REPLACE", - user_id: this.user_id, - data: sessions, - } as SessionsReplace); - const session = sessions.first() || { - activities: [], - client_info: {}, - status: "offline", - }; - - await emitEvent({ - event: "PRESENCE_UPDATE", - user_id: this.user_id, - data: { - user: await User.getPublicUser(this.user_id), - activities: session.activities, - client_status: session?.client_info, - status: session.status, - }, - } as PresenceUpdateEvent); - } -} diff --git a/gateway/src/events/Connection.ts b/gateway/src/events/Connection.ts deleted file mode 100644 index 508b4741..00000000 --- a/gateway/src/events/Connection.ts +++ /dev/null @@ -1,94 +0,0 @@ -import WS from "ws"; -import { WebSocket } from "@fosscord/gateway"; -import { Send } from "../util/Send"; -import { CLOSECODES, OPCODES } from "../util/Constants"; -import { setHeartbeat } from "../util/Heartbeat"; -import { IncomingMessage } from "http"; -import { Close } from "./Close"; -import { Message } from "./Message"; -import { createDeflate } from "zlib"; -import { URL } from "url"; -let erlpack: any; -try { - erlpack = require("@yukikaze-bot/erlpack"); -} catch (error) {} - -// TODO: check rate limit -// TODO: specify rate limit in config -// TODO: check msg max size - -export async function Connection( - this: WS.Server, - socket: WebSocket, - request: IncomingMessage -) { - try { - // @ts-ignore - socket.on("close", Close); - // @ts-ignore - socket.on("message", Message); - - if(process.env.WS_LOGEVENTS) - [ - "close", - "error", - "upgrade", - //"message", - "open", - "ping", - "pong", - "unexpected-response" - ].forEach(x=>{ - socket.on(x, y => console.log(x, y)); - }); - - console.log(`[Gateway] Connections: ${this.clients.size}`); - - const { searchParams } = new URL(`http://localhost${request.url}`); - // @ts-ignore - socket.encoding = searchParams.get("encoding") || "json"; - if (!["json", "etf"].includes(socket.encoding)) { - if (socket.encoding === "etf" && erlpack) { - throw new Error( - "Erlpack is not installed: 'npm i @yukikaze-bot/erlpack'" - ); - } - return socket.close(CLOSECODES.Decode_error); - } - - // @ts-ignore - socket.version = Number(searchParams.get("version")) || 8; - if (socket.version != 8) - return socket.close(CLOSECODES.Invalid_API_version); - - // @ts-ignore - socket.compress = searchParams.get("compress") || ""; - if (socket.compress) { - if (socket.compress !== "zlib-stream") - return socket.close(CLOSECODES.Decode_error); - socket.deflate = createDeflate({ chunkSize: 65535 }); - socket.deflate.on("data", (chunk) => socket.send(chunk)); - } - - socket.events = {}; - socket.member_events = {}; - socket.permissions = {}; - socket.sequence = 0; - - setHeartbeat(socket); - - await Send(socket, { - op: OPCODES.Hello, - d: { - heartbeat_interval: 1000 * 30, - }, - }); - - socket.readyTimeout = setTimeout(() => { - return socket.close(CLOSECODES.Session_timed_out); - }, 1000 * 30); - } catch (error) { - console.error(error); - return socket.close(CLOSECODES.Unknown_error); - } -} diff --git a/gateway/src/events/Message.ts b/gateway/src/events/Message.ts deleted file mode 100644 index 7ed1dd06..00000000 --- a/gateway/src/events/Message.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { CLOSECODES } from "../util/Constants"; -import { WebSocket, Payload } from "@fosscord/gateway"; -let erlpack: any; -try { - erlpack = require("@yukikaze-bot/erlpack"); -} catch (error) {} -import OPCodeHandlers from "../opcodes"; -import { check } from "../opcodes/instanceOf"; -import WS from "ws"; - -const PayloadSchema = { - op: Number, - $d: Object || Number, // or number for heartbeat sequence - $s: Number, - $t: String, -}; - -export async function Message(this: WebSocket, buffer: WS.RawData) { - // TODO: compression - let data: Payload; - - if (this.encoding === "etf" && buffer instanceof Buffer) - data = erlpack.unpack(buffer); - else if (this.encoding === "json") - data = JSON.parse(buffer as unknown as string); //TODO: is this even correct?? seems to work for web clients... - else if(/--debug|--inspect/.test(process.execArgv.join(' '))) { - debugger; - return; - } - else { - console.log("Invalid gateway connection! Use a debugger to inspect!"); - return; - } - - if(process.env.WS_VERBOSE) - console.log(`[Websocket] Incomming message: ${JSON.stringify(data)}`); - if(data.op !== 1) - check.call(this, PayloadSchema, data); - else { //custom validation for numbers, because heartbeat - if(data.s || data.t || (typeof data.d !== "number" && data.d)) { - console.log("Invalid heartbeat..."); - this.close(CLOSECODES.Decode_error); - } - } - - // @ts-ignore - const OPCodeHandler = OPCodeHandlers[data.op]; - if (!OPCodeHandler) { - console.error("[Gateway] Unkown opcode " + data.op); - // TODO: if all opcodes are implemented comment this out: - // this.close(CLOSECODES.Unknown_opcode); - return; - } - - try { - return await OPCodeHandler.call(this, data); - } catch (error) { - console.error(error); - if (!this.CLOSED && this.CLOSING) - return this.close(CLOSECODES.Unknown_error); - } -} diff --git a/gateway/src/index.ts b/gateway/src/index.ts deleted file mode 100644 index d77ce931..00000000 --- a/gateway/src/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from "./Server"; -export * from "./util/"; -export * from "./opcodes/"; -export * from "./listener/listener"; diff --git a/gateway/src/listener/listener.ts b/gateway/src/listener/listener.ts deleted file mode 100644 index 8c69e193..00000000 --- a/gateway/src/listener/listener.ts +++ /dev/null @@ -1,249 +0,0 @@ -import { - getPermission, - Permissions, - RabbitMQ, - listenEvent, - EventOpts, - ListenEventOpts, - Member, - EVENTEnum, - Relationship, - RelationshipType, -} from "@fosscord/util"; -import { OPCODES } from "../util/Constants"; -import { Send } from "../util/Send"; -import { WebSocket } from "@fosscord/gateway"; -import { Channel as AMQChannel } from "amqplib"; -import { Recipient } from "@fosscord/util"; - -// TODO: close connection on Invalidated Token -// TODO: check intent -// TODO: Guild Member Update is sent for current-user updates regardless of whether the GUILD_MEMBERS intent is set. - -// Sharding: calculate if the current shard id matches the formula: shard_id = (guild_id >> 22) % num_shards -// https://discord.com/developers/docs/topics/gateway#sharding - -export function handlePresenceUpdate( - this: WebSocket, - { event, acknowledge, data }: EventOpts -) { - acknowledge?.(); - if (event === EVENTEnum.PresenceUpdate) { - return Send(this, { - op: OPCODES.Dispatch, - t: event, - d: data, - s: this.sequence++, - }); - } -} - -// TODO: use already queried guilds/channels of Identify and don't fetch them again -export async function setupListener(this: WebSocket) { - const [members, recipients, relationships] = await Promise.all([ - Member.find({ - where: { id: this.user_id }, - relations: ["guild", "guild.channels"], - }), - Recipient.find({ - where: { user_id: this.user_id, closed: false }, - relations: ["channel"], - }), - Relationship.find({ where: { - from_id: this.user_id, - type: RelationshipType.friends, - } }), - ]); - - const guilds = members.map((x) => x.guild); - const dm_channels = recipients.map((x) => x.channel); - - const opts: { acknowledge: boolean; channel?: AMQChannel } = { - acknowledge: true, - }; - this.listen_options = opts; - const consumer = consume.bind(this); - - if (RabbitMQ.connection) { - opts.channel = await RabbitMQ.connection.createChannel(); - // @ts-ignore - opts.channel.queues = {}; - } - - this.events[this.user_id] = await listenEvent(this.user_id, consumer, opts); - - relationships.forEach(async (relationship) => { - this.events[relationship.to_id] = await listenEvent( - relationship.to_id, - handlePresenceUpdate.bind(this), - opts - ); - }); - - dm_channels.forEach(async (channel) => { - this.events[channel.id] = await listenEvent(channel.id, consumer, opts); - }); - - guilds.forEach(async (guild) => { - const permission = await getPermission(this.user_id, guild.id); - this.permissions[guild.id] = permission; - this.events[guild.id] = await listenEvent(guild.id, consumer, opts); - - guild.channels.forEach(async (channel) => { - if ( - permission - .overwriteChannel(channel.permission_overwrites!) - .has("VIEW_CHANNEL") - ) { - this.events[channel.id] = await listenEvent( - channel.id, - consumer, - opts - ); - } - }); - }); - - this.once("close", () => { - if (opts.channel) opts.channel.close(); - else { - Object.values(this.events).forEach((x) => x()); - Object.values(this.member_events).forEach((x) => x()); - } - }); -} - -// TODO: only subscribe for events that are in the connection intents -async function consume(this: WebSocket, opts: EventOpts) { - const { data, event } = opts; - let id = data.id as string; - const permission = this.permissions[id] || new Permissions("ADMINISTRATOR"); // default permission for dm - - const consumer = consume.bind(this); - const listenOpts = opts as ListenEventOpts; - opts.acknowledge?.(); - // console.log("event", event); - - // subscription managment - switch (event) { - case "GUILD_MEMBER_REMOVE": - this.member_events[data.user.id]?.(); - delete this.member_events[data.user.id]; - case "GUILD_MEMBER_ADD": - if (this.member_events[data.user.id]) break; // already subscribed - this.member_events[data.user.id] = await listenEvent( - data.user.id, - handlePresenceUpdate.bind(this), - this.listen_options - ); - break; - case "GUILD_MEMBER_REMOVE": - if (!this.member_events[data.user.id]) break; - this.member_events[data.user.id](); - break; - case "RELATIONSHIP_REMOVE": - case "CHANNEL_DELETE": - case "GUILD_DELETE": - delete this.events[id]; - opts.cancel(); - break; - case "CHANNEL_CREATE": - if ( - !permission - .overwriteChannel(data.permission_overwrites) - .has("VIEW_CHANNEL") - ) { - return; - } - this.events[id] = await listenEvent(id, consumer, listenOpts); - break; - case "RELATIONSHIP_ADD": - this.events[data.user.id] = await listenEvent( - data.user.id, - handlePresenceUpdate.bind(this), - this.listen_options - ); - break; - case "GUILD_CREATE": - this.events[id] = await listenEvent(id, consumer, listenOpts); - break; - case "CHANNEL_UPDATE": - const exists = this.events[id]; - // @ts-ignore - if ( - permission - .overwriteChannel(data.permission_overwrites) - .has("VIEW_CHANNEL") - ) { - if (exists) break; - this.events[id] = await listenEvent(id, consumer, listenOpts); - } else { - if (!exists) return; // return -> do not send channel update events for hidden channels - opts.cancel(id); - delete this.events[id]; - } - break; - } - - // permission checking - switch (event) { - case "INVITE_CREATE": - case "INVITE_DELETE": - case "GUILD_INTEGRATIONS_UPDATE": - if (!permission.has("MANAGE_GUILD")) return; - break; - case "WEBHOOKS_UPDATE": - if (!permission.has("MANAGE_WEBHOOKS")) return; - break; - case "GUILD_MEMBER_ADD": - case "GUILD_MEMBER_REMOVE": - case "GUILD_MEMBER_UPDATE": - // only send them, if the user subscribed for this part of the member list, or is a bot - case "PRESENCE_UPDATE": // exception if user is friend - break; - case "GUILD_BAN_ADD": - case "GUILD_BAN_REMOVE": - if (!permission.has("BAN_MEMBERS")) break; - break; - case "VOICE_STATE_UPDATE": - case "MESSAGE_CREATE": - case "MESSAGE_DELETE": - case "MESSAGE_DELETE_BULK": - case "MESSAGE_UPDATE": - case "CHANNEL_PINS_UPDATE": - case "MESSAGE_REACTION_ADD": - case "MESSAGE_REACTION_REMOVE": - case "MESSAGE_REACTION_REMOVE_ALL": - case "MESSAGE_REACTION_REMOVE_EMOJI": - case "TYPING_START": - // only gets send if the user is alowed to view the current channel - if (!permission.has("VIEW_CHANNEL")) return; - break; - case "GUILD_CREATE": - case "GUILD_DELETE": - case "GUILD_UPDATE": - case "GUILD_ROLE_CREATE": - case "GUILD_ROLE_UPDATE": - case "GUILD_ROLE_DELETE": - case "CHANNEL_CREATE": - case "CHANNEL_DELETE": - case "CHANNEL_UPDATE": - case "GUILD_EMOJIS_UPDATE": - case "READY": // will be sent by the gateway - case "USER_UPDATE": - case "APPLICATION_COMMAND_CREATE": - case "APPLICATION_COMMAND_DELETE": - case "APPLICATION_COMMAND_UPDATE": - default: - // always gets sent - // Any events not defined in an intent are considered "passthrough" and will always be sent - break; - } - - Send(this, { - op: OPCODES.Dispatch, - t: event, - d: data, - s: this.sequence++, - }); -} diff --git a/gateway/src/opcodes/Heartbeat.ts b/gateway/src/opcodes/Heartbeat.ts deleted file mode 100644 index 42b72d4b..00000000 --- a/gateway/src/opcodes/Heartbeat.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Payload, WebSocket } from "@fosscord/gateway"; -import { setHeartbeat } from "../util/Heartbeat"; -import { Send } from "../util/Send"; - -export async function onHeartbeat(this: WebSocket, _data: Payload) { - // TODO: validate payload - - setHeartbeat(this); - - await Send(this, { op: 11 }); -} diff --git a/gateway/src/opcodes/LazyRequest.ts b/gateway/src/opcodes/LazyRequest.ts deleted file mode 100644 index cd0586de..00000000 --- a/gateway/src/opcodes/LazyRequest.ts +++ /dev/null @@ -1,173 +0,0 @@ -import { getPermission, listenEvent, Member, Role, getOrInitialiseDatabase, LazyRequest } from "@fosscord/util"; -import { Send } from "../util/Send"; -import { OPCODES } from "../util/Constants"; -import { WebSocket, Payload, handlePresenceUpdate } from "@fosscord/gateway"; -import { check } from "./instanceOf"; -import { getRepository } from "typeorm"; - -// TODO: only show roles/members that have access to this channel -// TODO: config: to list all members (even those who are offline) sorted by role, or just those who are online -// TODO: rewrite typeorm - -async function getMembers(guild_id: string, range: [number, number]) { - if (!Array.isArray(range) || range.length !== 2) { - throw new Error("range is not a valid array"); - } - // TODO: wait for typeorm to implement ordering for .find queries https://github.com/typeorm/typeorm/issues/2620 - // TODO: rewrite this, released in 0.3.0 - - let members = await (await getOrInitialiseDatabase()).getRepository(Member) - .createQueryBuilder("member") - .where("member.guild_id = :guild_id", { guild_id }) - .leftJoinAndSelect("member.roles", "role") - .leftJoinAndSelect("member.user", "user") - .leftJoinAndSelect("user.sessions", "session") - .addSelect( - "CASE WHEN session.status = 'offline' THEN 0 ELSE 1 END", - "_status" - ) - .orderBy("role.position", "DESC") - .addOrderBy("_status", "DESC") - .addOrderBy("user.username", "ASC") - .offset(Number(range[0]) || 0) - .limit(Number(range[1]) || 100) - .getMany(); - - const groups = [] as any[]; - const items = []; - const member_roles = members - .map((m) => m.roles) - .flat() - .unique((r: Role) => r.id); - - const offlineItems = []; - - for (const role of member_roles) { - // @ts-ignore - const [role_members, other_members] = partition(members, (m: Member) => - m.roles.find((r) => r.id === role.id) - ); - const group = { - count: role_members.length, - id: role.id === guild_id ? "online" : role.id, - }; - - items.push({ group }); - groups.push(group); - - for (const member of role_members) { - const roles = member.roles - .filter((x: Role) => x.id !== guild_id) - .map((x: Role) => x.id); - - const session = member.user.sessions.first(); - - // TODO: properly mock/hide offline/invisible status - const item = { - member: { - ...member, - roles, - user: { ...member.user, sessions: undefined }, - presence: { - ...session, - activities: session?.activities || [], - user: { id: member.user.id }, - }, - }, - } - - if (!member?.user?.sessions || !member.user.sessions.length) { - offlineItems.push(item); - group.count--; - continue; - } - - items.push(item); - } - members = other_members; - } - - if (offlineItems.length) { - const group = { - count: offlineItems.length, - id: "offline", - }; - items.push({ group }); - groups.push(group); - - items.push(...offlineItems); - } - - return { - items, - groups, - range, - members: items.map((x) => 'member' in x ? x.member : undefined).filter(x => !!x), - }; -} - -export async function onLazyRequest(this: WebSocket, { d }: Payload) { - // TODO: check data - check.call(this, LazyRequest, d); - const { guild_id, typing, channels, activities } = d as LazyRequest; - - const channel_id = Object.keys(channels || {}).first(); - if (!channel_id) return; - - const permissions = await getPermission(this.user_id, guild_id, channel_id); - permissions.hasThrow("VIEW_CHANNEL"); - - const ranges = channels![channel_id]; - if (!Array.isArray(ranges)) throw new Error("Not a valid Array"); - - const member_count = await Member.count({ where: { guild_id } }); - const ops = await Promise.all(ranges.map((x) => getMembers(guild_id, x))); - - // TODO: unsubscribe member_events that are not in op.members - - ops.forEach((op) => { - op.members.forEach(async (member) => { - if (this.events[member.user.id]) return; // already subscribed as friend - if (this.member_events[member.user.id]) return; // already subscribed in member list - this.member_events[member.user.id] = await listenEvent( - member.user.id, - handlePresenceUpdate.bind(this), - this.listen_options - ); - }); - }); - - return Send(this, { - op: OPCODES.Dispatch, - s: this.sequence++, - t: "GUILD_MEMBER_LIST_UPDATE", - d: { - ops: ops.map((x) => ({ - items: x.items, - op: "SYNC", - range: x.range, - })), - online_count: member_count, - member_count, - id: "everyone", - guild_id, - groups: ops - .map((x) => x.groups) - .flat() - .unique(), - }, - }); -} - -function partition(array: T[], isValid: Function) { - // @ts-ignore - return array.reduce( - // @ts-ignore - ([pass, fail], elem) => { - return isValid(elem) - ? [[...pass, elem], fail] - : [pass, [...fail, elem]]; - }, - [[], []] - ); -} diff --git a/gateway/src/opcodes/PresenceUpdate.ts b/gateway/src/opcodes/PresenceUpdate.ts deleted file mode 100644 index f31c9161..00000000 --- a/gateway/src/opcodes/PresenceUpdate.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { WebSocket, Payload } from "@fosscord/gateway"; -import { ActivitySchema, emitEvent, PresenceUpdateEvent, Session, User } from "@fosscord/util"; -import { check } from "./instanceOf"; - -export async function onPresenceUpdate(this: WebSocket, { d }: Payload) { - check.call(this, ActivitySchema, d); - const presence = d as ActivitySchema; - - await Session.update( - { session_id: this.session_id }, - { status: presence.status, activities: presence.activities } - ); - - await emitEvent({ - event: "PRESENCE_UPDATE", - user_id: this.user_id, - data: { - user: await User.getPublicUser(this.user_id), - activities: presence.activities, - client_status: {}, // TODO: - status: presence.status, - }, - } as PresenceUpdateEvent); -} diff --git a/gateway/src/opcodes/RequestGuildMembers.ts b/gateway/src/opcodes/RequestGuildMembers.ts deleted file mode 100644 index b80721dc..00000000 --- a/gateway/src/opcodes/RequestGuildMembers.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { Payload, WebSocket } from "@fosscord/gateway"; - -export function onRequestGuildMembers(this: WebSocket, data: Payload) { - // return this.close(CLOSECODES.Unknown_error); -} diff --git a/gateway/src/opcodes/Resume.ts b/gateway/src/opcodes/Resume.ts deleted file mode 100644 index 42dc586d..00000000 --- a/gateway/src/opcodes/Resume.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { WebSocket, Payload } from "@fosscord/gateway"; -import { Send } from "../util/Send"; - -export async function onResume(this: WebSocket, data: Payload) { - console.log("Got Resume -> cancel not implemented"); - await Send(this, { - op: 9, - d: false, - }); - - // return this.close(CLOSECODES.Invalid_session); -} diff --git a/gateway/src/opcodes/VoiceStateUpdate.ts b/gateway/src/opcodes/VoiceStateUpdate.ts deleted file mode 100644 index 73f73565..00000000 --- a/gateway/src/opcodes/VoiceStateUpdate.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { Payload, WebSocket } from "@fosscord/gateway"; -import { genVoiceToken } from "../util/SessionUtils"; -import { check } from "./instanceOf"; -import { - Config, - emitEvent, - Guild, - Member, - VoiceServerUpdateEvent, - VoiceState, - VoiceStateUpdateEvent, - VoiceStateUpdateSchema, -} from "@fosscord/util"; -import { OrmUtils } from "@fosscord/util"; -import { Region } from "@fosscord/util/src/config"; -// TODO: check if a voice server is setup -// Notice: Bot users respect the voice channel's user limit, if set. When the voice channel is full, you will not receive the Voice State Update or Voice Server Update events in response to your own Voice State Update. Having MANAGE_CHANNELS permission bypasses this limit and allows you to join regardless of the channel being full or not. - -export async function onVoiceStateUpdate(this: WebSocket, data: Payload) { - check.call(this, VoiceStateUpdateSchema, data.d); - const body = data.d as VoiceStateUpdateSchema; - - if(body.guild_id == null) { - console.log(`[Gateway] VoiceStateUpdate called with guild_id == null by user ${this.user_id}!`); - return; - } - - let voiceState: VoiceState; - try { - voiceState = await VoiceState.findOneOrFail({ - where: { user_id: this.user_id }, - }); - if ( - voiceState.session_id !== this.session_id && - body.channel_id === null - ) { - //Should we also check guild_id === null? - //changing deaf or mute on a client that's not the one with the same session of the voicestate in the database should be ignored - return; - } - - //If a user change voice channel between guild we should send a left event first - if ( - voiceState.guild_id !== body.guild_id && - voiceState.session_id === this.session_id - ) { - await emitEvent({ - event: "VOICE_STATE_UPDATE", - data: { ...voiceState, channel_id: null }, - guild_id: voiceState.guild_id, - }); - } - - //The event send by Discord's client on channel leave has both guild_id and channel_id as null - if (body.guild_id === null) body.guild_id = voiceState.guild_id; - voiceState = OrmUtils.mergeDeep(voiceState, body); - } catch (error) { - voiceState = OrmUtils.mergeDeep(new VoiceState(), { - ...body, - user_id: this.user_id, - deaf: false, - mute: false, - suppress: false, - }); - } - - //TODO the member should only have these properties: hoisted_role, deaf, joined_at, mute, roles, user - //TODO the member.user should only have these properties: avatar, discriminator, id, username - //TODO this may fail - voiceState.member = await Member.findOneOrFail({ - where: { id: voiceState.user_id, guild_id: voiceState.guild_id }, - relations: ["user", "roles"], - }); - - //If the session changed we generate a new token - if (voiceState.session_id !== this.session_id) - voiceState.token = genVoiceToken(); - voiceState.session_id = this.session_id; - - const { id, ...newObj } = voiceState; - - await Promise.all([ - voiceState.save(), - emitEvent({ - event: "VOICE_STATE_UPDATE", - data: newObj, - guild_id: voiceState.guild_id, - } as VoiceStateUpdateEvent), - ]); - - //If it's null it means that we are leaving the channel and this event is not needed - if (voiceState.channel_id !== null) { - const guild = await Guild.findOne({ where: { id: voiceState.guild_id } }); - const regions = Config.get().regions; - let guildRegion: Region; - if (guild && guild.region) { - guildRegion = regions.available.filter( - (r) => r.id === guild.region - )[0]; - } else { - guildRegion = regions.available.filter( - (r) => r.id === regions.default - )[0]; - } - - await emitEvent({ - event: "VOICE_SERVER_UPDATE", - data: { - token: voiceState.token, - guild_id: voiceState.guild_id, - endpoint: guildRegion.endpoint, - }, - guild_id: voiceState.guild_id, - } as VoiceServerUpdateEvent); - } -} diff --git a/gateway/src/opcodes/experiments.json b/gateway/src/opcodes/experiments.json deleted file mode 100644 index 0370b5da..00000000 --- a/gateway/src/opcodes/experiments.json +++ /dev/null @@ -1,76 +0,0 @@ -[ - [4047587481, 0, 0, -1, 0], - [1509401575, 0, 1, -1, 0], - [1865079242, 0, 1, -1, 0], - [1962538549, 1, 0, -1, 0], - [3816091942, 3, 2, -1, 0], - [4130837190, 0, 10, -1, 0], - [1861568052, 0, 1, -1, 0], - [2290910058, 6, 2, -1, 0], - [1578940118, 1, 1, -1, 0], - [1571676964, 0, 1, -1, 2], - [3640172371, 0, 2, -1, 2], - [1658164312, 2, 1, -1, 0], - [98883956, 1, 1, -1, 0], - [3114091169, 0, 1, -1, 0], - [2570684145, 4, 1, -1, 2], - [4007615411, 0, 1, -1, 0], - [3665310159, 2, 1, -1, 1], - [852550504, 3, 1, -1, 0], - [2333572067, 0, 1, -1, 0], - [935994771, 1, 1, -1, 0], - [1127795596, 1, 1, -1, 0], - [4168223991, 0, 1, -1, 0], - [18585280, 0, 1, -1, 1], - [327482016, 0, 1, -1, 2], - [3458098201, 7, 1, -1, 0], - [478613943, 2, 1, -1, 1], - [2792197902, 0, 1, -1, 2], - [284670956, 0, 1, -1, 0], - [2099185390, 0, 1, -1, 0], - [1202202685, 0, 1, -1, 0], - [2122174751, 0, 1, -1, 0], - [3633864632, 0, 1, -1, 0], - [3103053065, 0, 1, -1, 0], - [820624960, 0, 1, -1, 0], - [1134479292, 0, 1, -1, 0], - [2511257455, 3, 1, -1, 3], - [2599708267, 0, 1, -1, 0], - [613180822, 1, 1, -1, 0], - [2885186814, 0, 1, -1, 0], - [221503477, 0, 1, -1, 0], - [1054317075, 0, 1, -1, 3], - [683872522, 0, 1, -1, 1], - [1739278764, 0, 2, -1, 0], - [2855249023, 0, 1, -1, 0], - [3721841948, 0, 1, -1, 0], - [1285203515, 0, 1, -1, 0], - [1365487849, 6, 1, -1, 0], - [955229746, 0, 1, -1, 0], - [3128009767, 0, 10, -1, 0], - [441885003, 0, 1, -1, 0], - [3433971238, 0, 1, -1, 2], - [1038765354, 3, 1, -1, 0], - [1174347196, 0, 1, -1, 0], - [3649806352, 1, 1, -1, 0], - [2973729510, 2, 1, -1, 0], - [2571931329, 1, 6, -1, 0], - [3884442008, 0, 1, -1, 0], - [978673395, 1, 1, -1, 0], - [4050927174, 0, 1, -1, 0], - [1260103069, 0, 1, -1, 0], - [4168894280, 0, 1, -1, 0], - [4045587091, 0, 1, -1, 0], - [2003494159, 1, 1, -1, 0], - [51193042, 0, 1, -1, 0], - [2634540382, 3, 1, -1, 0], - [886364171, 0, 1, -1, 0], - [3898604944, 0, 1, -1, 0], - [3388129398, 0, 1, -1, 0], - [3964382884, 2, 1, -1, 1], - [3305874255, 0, 1, -1, 0], - [156590431, 0, 1, -1, 0], - [3106485751, 0, 0, -1, 0], - [3035674767, 0, 1, -1, 0], - [851697110, 0, 1, -1, 0] -] diff --git a/gateway/src/opcodes/index.ts b/gateway/src/opcodes/index.ts deleted file mode 100644 index 027739db..00000000 --- a/gateway/src/opcodes/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { WebSocket, Payload } from "@fosscord/gateway"; -import { onHeartbeat } from "./Heartbeat"; -import { onIdentify } from "./Identify"; -import { onLazyRequest } from "./LazyRequest"; -import { onPresenceUpdate } from "./PresenceUpdate"; -import { onRequestGuildMembers } from "./RequestGuildMembers"; -import { onResume } from "./Resume"; -import { onVoiceStateUpdate } from "./VoiceStateUpdate"; - -export type OPCodeHandler = (this: WebSocket, data: Payload) => any; - -export default { - 1: onHeartbeat, - 2: onIdentify, - 3: onPresenceUpdate, - 4: onVoiceStateUpdate, - // 5: Voice Server Ping - 6: onResume, - // 7: Reconnect: You should attempt to reconnect and resume immediately. - 8: onRequestGuildMembers, - // 9: Invalid Session - // 10: Hello - // 13: Dm_update - 14: onLazyRequest, -}; diff --git a/gateway/src/opcodes/instanceOf.ts b/gateway/src/opcodes/instanceOf.ts deleted file mode 100644 index eb6f6ea1..00000000 --- a/gateway/src/opcodes/instanceOf.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { instanceOf } from "@fosscord/util"; -import { WebSocket } from "@fosscord/gateway"; -import { CLOSECODES } from "../util/Constants"; - -export function check(this: WebSocket, schema: any, data: any) { - try { - const error = instanceOf(schema, data, { path: "body" }); - if (error !== true) { - throw error; - } - return true; - } catch (error) { - console.error(error); - // invalid payload - this.close(CLOSECODES.Decode_error); - throw error; - } -} diff --git a/gateway/src/start.ts b/gateway/src/start.ts deleted file mode 100644 index 2000522a..00000000 --- a/gateway/src/start.ts +++ /dev/null @@ -1,14 +0,0 @@ -process.on("uncaughtException", console.error); -process.on("unhandledRejection", console.error); - -import { Server } from "./Server"; -import { config } from "dotenv"; -config(); - -let port = Number(process.env.PORT); -if (isNaN(port)) port = 3002; - -const server = new Server({ - port, -}); -server.start(); diff --git a/gateway/src/util/Constants.ts b/gateway/src/util/Constants.ts deleted file mode 100644 index 692f9028..00000000 --- a/gateway/src/util/Constants.ts +++ /dev/null @@ -1,50 +0,0 @@ -export enum OPCODES { - Dispatch = 0, - Heartbeat = 1, - Identify = 2, - Presence_Update = 3, - Voice_State_Update = 4, - Voice_Server_Ping = 5, // ? What is opcode 5? - Resume = 6, - Reconnect = 7, - Request_Guild_Members = 8, - Invalid_Session = 9, - Hello = 10, - Heartbeat_ACK = 11, - Guild_Sync = 12, - DM_Update = 13, - Lazy_Request = 14, - Lobby_Connect = 15, - Lobby_Disconnect = 16, - Lobby_Voice_States_Update = 17, - Stream_Create = 18, - Stream_Delete = 19, - Stream_Watch = 20, - Stream_Ping = 21, - Stream_Set_Paused = 22, - Request_Application_Commands = 24, -} -export enum CLOSECODES { - Unknown_error = 4000, - Unknown_opcode, - Decode_error, - Not_authenticated, - Authentication_failed, - Already_authenticated, - Invalid_session, - Invalid_seq, - Rate_limited, - Session_timed_out, - Invalid_shard, - Sharding_required, - Invalid_API_version, - Invalid_intent, - Disallowed_intent, -} - -export interface Payload { - op: OPCODES; - d?: any; - s?: number; - t?: string; -} diff --git a/gateway/src/util/Heartbeat.ts b/gateway/src/util/Heartbeat.ts deleted file mode 100644 index f6871cfe..00000000 --- a/gateway/src/util/Heartbeat.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { CLOSECODES } from "./Constants"; -import { WebSocket } from "./WebSocket"; - -// TODO: make heartbeat timeout configurable -export function setHeartbeat(socket: WebSocket) { - if (socket.heartbeatTimeout) clearTimeout(socket.heartbeatTimeout); - - socket.heartbeatTimeout = setTimeout(() => { - return socket.close(CLOSECODES.Session_timed_out); - }, 1000 * 45); -} diff --git a/gateway/src/util/Send.ts b/gateway/src/util/Send.ts deleted file mode 100644 index 2a28d8e0..00000000 --- a/gateway/src/util/Send.ts +++ /dev/null @@ -1,33 +0,0 @@ -let erlpack: any; -try { - erlpack = require("@yukikaze-bot/erlpack"); -} catch (error) { - console.log("Missing @yukikaze-bot/erlpack, electron-based desktop clients designed for discord.com will not be able to connect!"); -} -import { Payload, WebSocket } from "@fosscord/gateway"; - -export async function Send(socket: WebSocket, data: Payload) { - if(process.env.WS_VERBOSE) - console.log(`[Websocket] Outgoing message: ${JSON.stringify(data)}`); - let buffer: Buffer | string; - if (socket.encoding === "etf") buffer = erlpack.pack(data); - // TODO: encode circular object - else if (socket.encoding === "json") buffer = JSON.stringify(data); - else return; - // TODO: compression - if (socket.deflate) { - socket.deflate.write(buffer); - socket.deflate.flush(); - return; - } - - return new Promise((res, rej) => { - if (socket.readyState !== 1) { - return rej("socket not open"); - } - socket.send(buffer, (err: any) => { - if (err) return rej(err); - return res(null); - }); - }); -} diff --git a/gateway/src/util/SessionUtils.ts b/gateway/src/util/SessionUtils.ts deleted file mode 100644 index bf854042..00000000 --- a/gateway/src/util/SessionUtils.ts +++ /dev/null @@ -1,13 +0,0 @@ -export function genSessionId() { - return genRanHex(32); -} - -export function genVoiceToken() { - return genRanHex(16); -} - -function genRanHex(size: number) { - return [...Array(size)] - .map(() => Math.floor(Math.random() * 16).toString(16)) - .join(""); -} diff --git a/gateway/src/util/WebSocket.ts b/gateway/src/util/WebSocket.ts deleted file mode 100644 index e3313f40..00000000 --- a/gateway/src/util/WebSocket.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Intents, Permissions } from "@fosscord/util"; -import WS from "ws"; -import { Deflate } from "zlib"; - -export interface WebSocket extends WS { - version: number; - user_id: string; - session_id: string; - encoding: "etf" | "json"; - compress?: "zlib-stream"; - shard_count?: bigint; - shard_id?: bigint; - deflate?: Deflate; - heartbeatTimeout: NodeJS.Timeout; - readyTimeout: NodeJS.Timeout; - intents: Intents; - sequence: number; - permissions: Record; - events: Record; - member_events: Record; - listen_options: any; -} diff --git a/gateway/src/util/index.ts b/gateway/src/util/index.ts deleted file mode 100644 index 0be5ecee..00000000 --- a/gateway/src/util/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export * from "./Constants"; -export * from "./Send"; -export * from "./SessionUtils"; -export * from "./Heartbeat"; -export * from "./WebSocket"; -- cgit 1.4.1