summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
authorTheArcaneBrony <myrainbowdash949@gmail.com>2022-08-27 07:07:05 +0200
committerTheArcaneBrony <myrainbowdash949@gmail.com>2022-08-30 17:10:46 +0200
commitfeca7a5d620362d028c8f31353b0c996693697e3 (patch)
treeefda8bf7552085e247bc0082d151bcb8d7f31cee /src
parentprettier (diff)
downloadserver-feca7a5d620362d028c8f31353b0c996693697e3.tar.xz
Merge 'webrtc' into 'dev/staging_webrtc'
Diffstat (limited to 'src')
-rw-r--r--src/Server.ts7
-rw-r--r--src/api/util/handlers/Voice.ts2
-rw-r--r--src/gateway/Server.ts1
-rw-r--r--src/gateway/events/Close.ts2
-rw-r--r--src/gateway/events/Connection.ts22
-rw-r--r--src/gateway/events/Message.ts12
-rw-r--r--src/gateway/listener/listener.ts6
-rw-r--r--src/gateway/opcodes/Identify.ts18
-rw-r--r--src/gateway/opcodes/LazyRequest.ts4
-rw-r--r--src/gateway/opcodes/RequestGuildMembers.ts2
-rw-r--r--src/gateway/opcodes/Resume.ts2
-rw-r--r--src/gateway/opcodes/VoiceStateUpdate.ts24
-rw-r--r--src/gateway/opcodes/instanceOf.ts4
-rw-r--r--src/gateway/util/Constants.ts69
-rw-r--r--src/gateway/util/Heartbeat.ts4
-rw-r--r--src/gateway/util/Send.ts2
-rw-r--r--src/gateway/util/WebSocket.ts2
-rw-r--r--src/util/config/types/RegionConfiguration.ts2
-rw-r--r--src/util/entities/Attachment.ts6
-rw-r--r--src/util/entities/AuditLog.ts7
-rw-r--r--src/util/entities/BackupCodes.ts2
-rw-r--r--src/util/entities/Ban.ts9
-rw-r--r--src/util/entities/Channel.ts21
-rw-r--r--src/util/entities/ConnectedAccount.ts5
-rw-r--r--src/util/entities/Emoji.ts7
-rw-r--r--src/util/entities/Guild.ts52
-rw-r--r--src/util/entities/Invite.ts6
-rw-r--r--src/util/entities/Member.ts10
-rw-r--r--src/util/entities/Message.ts24
-rw-r--r--src/util/entities/Migration.ts1
-rw-r--r--src/util/entities/Note.ts7
-rw-r--r--src/util/entities/RateLimit.ts1
-rw-r--r--src/util/entities/ReadState.ts7
-rw-r--r--src/util/entities/Recipient.ts9
-rw-r--r--src/util/entities/Relationship.ts7
-rw-r--r--src/util/entities/Role.ts5
-rw-r--r--src/util/entities/Session.ts2
-rw-r--r--src/util/entities/Sticker.ts8
-rw-r--r--src/util/entities/StickerPack.ts4
-rw-r--r--src/util/entities/Team.ts4
-rw-r--r--src/util/entities/TeamMember.ts8
-rw-r--r--src/util/entities/Template.ts7
-rw-r--r--src/util/entities/User.ts13
-rw-r--r--src/util/entities/VoiceState.ts13
-rw-r--r--src/util/entities/Webhook.ts13
-rw-r--r--src/util/migrations/mariadb/1660258393551-CodeCleanup3.ts231
-rw-r--r--src/util/migrations/mariadb/1660260587556-CodeCleanup4.ts38
-rw-r--r--src/util/migrations/mariadb/1660265930624-CodeCleanup5.ts52
-rw-r--r--src/util/plugin/Plugin.ts13
-rw-r--r--src/util/plugin/PluginLoader.ts39
-rw-r--r--src/util/plugin/PluginManifest.ts9
-rw-r--r--src/util/plugin/index.ts3
-rw-r--r--src/util/schemas/SelectProtocolSchema.ts19
-rw-r--r--src/util/schemas/Validator.ts54
-rw-r--r--src/util/schemas/VoiceIdentifySchema.ts12
-rw-r--r--src/util/schemas/VoiceVideoSchema.ts17
-rw-r--r--src/util/schemas/index.ts4
-rw-r--r--src/util/util/Database.ts1
-rw-r--r--src/util/util/NamingStrategy.ts12
-rw-r--r--src/util/util/imports/TypedEmitter.ts41
-rw-r--r--src/webrtc/Server.ts56
-rw-r--r--src/webrtc/events/Close.ts9
-rw-r--r--src/webrtc/events/Connection.ts60
-rw-r--r--src/webrtc/events/Message.ts38
-rw-r--r--src/webrtc/index.ts2
-rw-r--r--src/webrtc/opcodes/BackendVersion.ts6
-rw-r--r--src/webrtc/opcodes/Heartbeat.ts9
-rw-r--r--src/webrtc/opcodes/Identify.ts60
-rw-r--r--src/webrtc/opcodes/SelectProtocol.ts44
-rw-r--r--src/webrtc/opcodes/Speaking.ts22
-rw-r--r--src/webrtc/opcodes/Video.ts117
-rw-r--r--src/webrtc/opcodes/index.ts19
-rw-r--r--src/webrtc/start.ts13
-rw-r--r--src/webrtc/util/Constants.ts26
-rw-r--r--src/webrtc/util/MediaServer.ts51
-rw-r--r--src/webrtc/util/index.ts2
76 files changed, 1327 insertions, 195 deletions
diff --git a/src/Server.ts b/src/Server.ts

index 83e16fa6..f68870d8 100644 --- a/src/Server.ts +++ b/src/Server.ts
@@ -5,6 +5,7 @@ import * as Api from "@fosscord/api"; import { CDNServer } from "@fosscord/cdn"; import * as Gateway from "@fosscord/gateway"; import { Config, getOrInitialiseDatabase } from "@fosscord/util"; +import * as WebRTC from "@fosscord/webrtc"; import * as Sentry from "@sentry/node"; import * as Tracing from "@sentry/tracing"; import express from "express"; @@ -18,12 +19,10 @@ const port = Number(process.env.PORT) || 3001; const production = process.env.NODE_ENV == "development" ? false : true; server.on("request", app); -// @ts-ignore const api = new Api.FosscordServer({ server, port, production, app }); -// @ts-ignore const cdn = new CDNServer({ server, port, production, app }); -// @ts-ignore const gateway = new Gateway.Server({ server, port, production }); +const webrtc = new WebRTC.Server({ server, port, production }); //this is what has been added for the /stop API route process.on("SIGTERM", () => { @@ -52,7 +51,7 @@ async function main() { app.use(Sentry.Handlers.requestHandler()); app.use(Sentry.Handlers.tracingHandler()); } - await Promise.all([api.start(), cdn.start(), gateway.start()]); + await Promise.all([api.start(), cdn.start(), gateway.start(), webrtc.start()]); if (Config.get().sentry.enabled) { app.use(Sentry.Handlers.errorHandler()); app.use(function onError(err: any, req: any, res: any, next: any) { diff --git a/src/api/util/handlers/Voice.ts b/src/api/util/handlers/Voice.ts
index 4d60eb91..138bd880 100644 --- a/src/api/util/handlers/Voice.ts +++ b/src/api/util/handlers/Voice.ts
@@ -13,7 +13,7 @@ export async function getVoiceRegions(ipAddress: string, vip: boolean) { for (let ar of availableRegions) { //TODO the endpoint location should be saved in the database if not already present to prevent IPAnalysis call - const dist = distanceBetweenLocations(clientIpAnalysis, ar.location || (await IPAnalysis(ar.endpoint))); + const dist = distanceBetweenLocations(clientIpAnalysis, ar.location || (await IPAnalysis(ar.endpoint!))); if (dist < min) { min = dist; diff --git a/src/gateway/Server.ts b/src/gateway/Server.ts
index 97da3fa0..95fc667a 100644 --- a/src/gateway/Server.ts +++ b/src/gateway/Server.ts
@@ -23,6 +23,7 @@ export class Server { } this.server.on("upgrade", (request, socket, head) => { + if (request.url?.includes("voice")) return; // @ts-ignore this.ws.handleUpgrade(request, socket, head, (socket) => { this.ws.emit("connection", socket, request); diff --git a/src/gateway/events/Close.ts b/src/gateway/events/Close.ts
index 34831eab..6019ba68 100644 --- a/src/gateway/events/Close.ts +++ b/src/gateway/events/Close.ts
@@ -2,7 +2,7 @@ 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); + console.log("[Gateway] closed", code, reason); if (this.heartbeatTimeout) clearTimeout(this.heartbeatTimeout); if (this.readyTimeout) clearTimeout(this.readyTimeout); this.deflate?.close(); diff --git a/src/gateway/events/Connection.ts b/src/gateway/events/Connection.ts
index 5a5ce48f..60b479a8 100644 --- a/src/gateway/events/Connection.ts +++ b/src/gateway/events/Connection.ts
@@ -3,7 +3,7 @@ import { IncomingMessage } from "http"; import { URL } from "url"; import WS from "ws"; import { createDeflate } from "zlib"; -import { CLOSECODES, OPCODES } from "../util/Constants"; +import { CloseCodes, GatewayOPCodes } from "../util/Constants"; import { setHeartbeat } from "../util/Heartbeat"; import { Send } from "../util/Send"; import { Close } from "./Close"; @@ -35,7 +35,7 @@ export async function Connection(this: WS.Server, socket: WebSocket, request: In "pong", "unexpected-response" ].forEach((x) => { - socket.on(x, (y) => console.log(x, y)); + socket.on(x, (y) => console.log("[Gateway]", x, y)); }); console.log(`[Gateway] Connections: ${this.clients.size}`); @@ -47,17 +47,17 @@ export async function Connection(this: WS.Server, socket: WebSocket, request: In if (socket.encoding === "etf" && erlpack) { throw new Error("Erlpack is not installed: 'npm i @yukikaze-bot/erlpack'"); } - return socket.close(CLOSECODES.Decode_error); + 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); + 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); + if (socket.compress !== "zlib-stream") return socket.close(CloseCodes.Decode_error); socket.deflate = createDeflate({ chunkSize: 65535 }); socket.deflate.on("data", (chunk) => socket.send(chunk)); } @@ -69,18 +69,18 @@ export async function Connection(this: WS.Server, socket: WebSocket, request: In setHeartbeat(socket); + socket.readyTimeout = setTimeout(() => { + return socket.close(CloseCodes.Session_timed_out); + }, 1000 * 30); + await Send(socket, { - op: OPCODES.Hello, + op: GatewayOPCodes.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); + return socket.close(CloseCodes.Unknown_error); } } diff --git a/src/gateway/events/Message.ts b/src/gateway/events/Message.ts
index 96950a42..e5ee5828 100644 --- a/src/gateway/events/Message.ts +++ b/src/gateway/events/Message.ts
@@ -1,7 +1,7 @@ import { Payload, WebSocket } from "@fosscord/gateway"; import OPCodeHandlers from "../opcodes"; import { check } from "../opcodes/instanceOf"; -import { CLOSECODES } from "../util/Constants"; +import { CloseCodes } from "../util/Constants"; let erlpack: any; try { erlpack = require("@yukikaze-bot/erlpack"); @@ -29,13 +29,13 @@ export async function Message(this: WebSocket, buffer: Buffer) { return; } - if (process.env.WS_VERBOSE) console.log(`[Websocket] Incomming message: ${JSON.stringify(data)}`); + if (process.env.WS_VERBOSE) console.log(`[Gateway] 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); + this.close(CloseCodes.Decode_error); } } @@ -44,14 +44,14 @@ export async function Message(this: WebSocket, buffer: Buffer) { if (!OPCodeHandler) { console.error("[Gateway] Unkown opcode " + data.op); // TODO: if all opcodes are implemented comment this out: - // this.close(CLOSECODES.Unknown_opcode); + // 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); + console.error("[Gateway]", error); + if (!this.CLOSED && this.CLOSING) return this.close(CloseCodes.Unknown_error); } } diff --git a/src/gateway/listener/listener.ts b/src/gateway/listener/listener.ts
index 811318af..6e88c446 100644 --- a/src/gateway/listener/listener.ts +++ b/src/gateway/listener/listener.ts
@@ -13,7 +13,7 @@ import { RelationshipType } from "@fosscord/util"; import { Channel as AMQChannel } from "amqplib"; -import { OPCODES } from "../util/Constants"; +import { GatewayOPCodes } from "../util/Constants"; import { Send } from "../util/Send"; // TODO: close connection on Invalidated Token @@ -27,7 +27,7 @@ export function handlePresenceUpdate(this: WebSocket, { event, acknowledge, data acknowledge?.(); if (event === EVENTEnum.PresenceUpdate) { return Send(this, { - op: OPCODES.Dispatch, + op: GatewayOPCodes.Dispatch, t: event, d: data, s: this.sequence++ @@ -212,7 +212,7 @@ async function consume(this: WebSocket, opts: EventOpts) { } Send(this, { - op: OPCODES.Dispatch, + op: GatewayOPCodes.Dispatch, t: event, d: data, s: this.sequence++ diff --git a/src/gateway/opcodes/Identify.ts b/src/gateway/opcodes/Identify.ts
index ac6955fd..033f9247 100644 --- a/src/gateway/opcodes/Identify.ts +++ b/src/gateway/opcodes/Identify.ts
@@ -24,7 +24,7 @@ import { UserSettings } from "@fosscord/util"; import { setupListener } from "../listener/listener"; -import { CLOSECODES, OPCODES } from "../util/Constants"; +import { CloseCodes, GatewayOPCodes } from "../util/Constants"; import { Send } from "../util/Send"; import { genSessionId } from "../util/SessionUtils"; import { check } from "./instanceOf"; @@ -46,7 +46,7 @@ export async function onIdentify(this: WebSocket, data: Payload) { var { decoded } = await checkToken(identify.token, jwtSecret); // will throw an error if invalid } catch (error) { console.error("invalid token", error); - return this.close(CLOSECODES.Authentication_failed); + return this.close(CloseCodes.Authentication_failed); } this.user_id = decoded.id; @@ -87,15 +87,15 @@ export async function onIdentify(this: WebSocket, data: Payload) { Application.findOne({ where: { id: this.user_id } }) ]); - if (!user) return this.close(CLOSECODES.Authentication_failed); + if (!user) return this.close(CloseCodes.Authentication_failed); if (!user.settings) { //settings may not exist after updating... user.settings = new UserSettings(); user.settings.id = user.id; - //await (user.settings as UserSettings).save(); + await user.settings.save(); } - if (!identify.intents) identify.intents = "30064771071"; + if (!identify.intents) identify.intents = "0x6ffffffff"; this.intents = new Intents(identify.intents); if (identify.shard) { this.shard_id = identify.shard[0]; @@ -108,7 +108,7 @@ export async function onIdentify(this: WebSocket, data: Payload) { this.shard_count <= 0 ) { console.log(identify.shard); - return this.close(CLOSECODES.Invalid_shard); + return this.close(CloseCodes.Invalid_shard); } } let users: PublicUser[] = []; @@ -130,7 +130,7 @@ export async function onIdentify(this: WebSocket, data: Payload) { if (user.bot) { setTimeout(() => { Send(this, { - op: OPCODES.Dispatch, + op: GatewayOPCodes.Dispatch, t: EVENTEnum.GuildCreate, s: this.sequence++, d: guild @@ -223,7 +223,7 @@ export async function onIdentify(this: WebSocket, data: Payload) { const d: ReadyEventData = { v: 8, - application: { id: application?.id ?? "", flags: application?.flags ?? 0 }, //TODO: check this code! + application: { id: application?.id ?? "", flags: application?.flags ?? "" }, //TODO: check this code! user: privateUser, user_settings: user.settings, // @ts-ignore @@ -268,7 +268,7 @@ export async function onIdentify(this: WebSocket, data: Payload) { // TODO: send real proper data structure await Send(this, { - op: OPCODES.Dispatch, + op: GatewayOPCodes.Dispatch, t: EVENTEnum.Ready, s: this.sequence++, d diff --git a/src/gateway/opcodes/LazyRequest.ts b/src/gateway/opcodes/LazyRequest.ts
index ea69779e..c37eb058 100644 --- a/src/gateway/opcodes/LazyRequest.ts +++ b/src/gateway/opcodes/LazyRequest.ts
@@ -1,6 +1,6 @@ import { handlePresenceUpdate, Payload, WebSocket } from "@fosscord/gateway"; import { getOrInitialiseDatabase, getPermission, LazyRequest, listenEvent, Member, Role } from "@fosscord/util"; -import { OPCODES } from "../util/Constants"; +import { GatewayOPCodes } from "../util/Constants"; import { Send } from "../util/Send"; import { check } from "./instanceOf"; @@ -129,7 +129,7 @@ export async function onLazyRequest(this: WebSocket, { d }: Payload) { }); return Send(this, { - op: OPCODES.Dispatch, + op: GatewayOPCodes.Dispatch, s: this.sequence++, t: "GUILD_MEMBER_LIST_UPDATE", d: { diff --git a/src/gateway/opcodes/RequestGuildMembers.ts b/src/gateway/opcodes/RequestGuildMembers.ts
index b80721dc..d70bb1a8 100644 --- a/src/gateway/opcodes/RequestGuildMembers.ts +++ b/src/gateway/opcodes/RequestGuildMembers.ts
@@ -1,5 +1,5 @@ import { Payload, WebSocket } from "@fosscord/gateway"; export function onRequestGuildMembers(this: WebSocket, data: Payload) { - // return this.close(CLOSECODES.Unknown_error); + // return this.close(CloseCodes.Unknown_error); } diff --git a/src/gateway/opcodes/Resume.ts b/src/gateway/opcodes/Resume.ts
index f320864b..b30ea8f8 100644 --- a/src/gateway/opcodes/Resume.ts +++ b/src/gateway/opcodes/Resume.ts
@@ -8,5 +8,5 @@ export async function onResume(this: WebSocket, data: Payload) { d: false }); - // return this.close(CLOSECODES.Invalid_session); + // return this.close(CloseCodes.Invalid_session); } diff --git a/src/gateway/opcodes/VoiceStateUpdate.ts b/src/gateway/opcodes/VoiceStateUpdate.ts
index 20502584..be7bddd9 100644 --- a/src/gateway/opcodes/VoiceStateUpdate.ts +++ b/src/gateway/opcodes/VoiceStateUpdate.ts
@@ -20,20 +20,16 @@ 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 onlySettingsChanged = false; 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 (voiceState.session_id !== this.session_id) { + // new session + } else { + if (voiceState.channel_id === body.channel_id) onlySettingsChanged = true; } //If a user change voice channel between guild we should send a left event first @@ -70,7 +66,7 @@ export async function onVoiceStateUpdate(this: WebSocket, data: Payload) { if (voiceState.session_id !== this.session_id) voiceState.token = genVoiceToken(); voiceState.session_id = this.session_id; - const { id, ...newObj } = voiceState; + const { id, token, ...newObj } = voiceState; await Promise.all([ voiceState.save(), @@ -82,7 +78,7 @@ export async function onVoiceStateUpdate(this: WebSocket, data: Payload) { ]); //If it's null it means that we are leaving the channel and this event is not needed - if (voiceState.channel_id !== null) { + if (voiceState.channel_id !== null && !onlySettingsChanged) { const guild = await Guild.findOne({ where: { id: voiceState.guild_id } }); const regions = Config.get().regions; let guildRegion: Region; @@ -95,11 +91,11 @@ export async function onVoiceStateUpdate(this: WebSocket, data: Payload) { await emitEvent({ event: "VOICE_SERVER_UPDATE", data: { - token: voiceState.token, + token: token, guild_id: voiceState.guild_id, - endpoint: guildRegion.endpoint + endpoint: guildRegion.endpoint ? guildRegion.endpoint + "/voice" : `localhost:${process.env.PORT || 3001}/voice` }, - guild_id: voiceState.guild_id + user_id: this.user_id } as VoiceServerUpdateEvent); } } diff --git a/src/gateway/opcodes/instanceOf.ts b/src/gateway/opcodes/instanceOf.ts
index 95d74963..530f639a 100644 --- a/src/gateway/opcodes/instanceOf.ts +++ b/src/gateway/opcodes/instanceOf.ts
@@ -1,6 +1,6 @@ import { WebSocket } from "@fosscord/gateway"; import { instanceOf } from "@fosscord/util"; -import { CLOSECODES } from "../util/Constants"; +import { CloseCodes } from "../util/Constants"; export function check(this: WebSocket, schema: any, data: any) { try { @@ -12,7 +12,7 @@ export function check(this: WebSocket, schema: any, data: any) { } catch (error) { console.error(error); // invalid payload - this.close(CLOSECODES.Decode_error); + this.close(CloseCodes.Decode_error); throw error; } } diff --git a/src/gateway/util/Constants.ts b/src/gateway/util/Constants.ts
index 78455ff8..b3e3c0d9 100644 --- a/src/gateway/util/Constants.ts +++ b/src/gateway/util/Constants.ts
@@ -1,4 +1,6 @@ -export enum OPCODES { +import { VoiceOPCodes } from "@fosscord/webrtc"; + +export enum GatewayOPCodes { Dispatch = 0, Heartbeat = 1, Identify = 2, @@ -22,28 +24,59 @@ export enum OPCODES { Stream_Watch = 20, Stream_Ping = 21, Stream_Set_Paused = 22, - Request_Application_Commands = 24 + Request_Application_Commands = 24, + Embedded_Activity_Launch = 25, + Embedded_Activity_Close = 26, + Embedded_Activity_Update = 27, + Request_Forum_Unreads = 28, + Remote_Command = 29 +} + +export enum GatewayOPCodes { + DISPATCH = 0, + HEARTBEAT = 1, + IDENTIFY = 2, + PRESENCE_UPDATE = 3, + VOICE_STATE_UPDATE = 4, + VOICE_SERVER_PING = 5, + RESUME = 6, + RECONNECT = 7, + REQUEST_GUILD_MEMBERS = 8, + INVALID_SESSION = 9, + HELLO = 10, + HEARTBEAT_ACK = 11, + CALL_CONNECT = 13, + GUILD_SUBSCRIPTIONS = 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 } -export enum CLOSECODES { + +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 + Unknown_opcode = 4001, + Decode_error = 4002, + Not_authenticated = 4003, + Authentication_failed = 4004, + Already_authenticated = 4005, + Invalid_session = 4006, + Invalid_seq = 4007, + Rate_limited = 4008, + Session_timed_out = 4009, + Invalid_shard = 4010, + Sharding_required = 4011, + Invalid_API_version = 4012, + Invalid_intent = 4013, + Disallowed_intent = 4014 } export interface Payload { - op: OPCODES; + op: GatewayOPCodes | VoiceOPCodes; d?: any; s?: number; t?: string; diff --git a/src/gateway/util/Heartbeat.ts b/src/gateway/util/Heartbeat.ts
index f6871cfe..cf2b1999 100644 --- a/src/gateway/util/Heartbeat.ts +++ b/src/gateway/util/Heartbeat.ts
@@ -1,4 +1,4 @@ -import { CLOSECODES } from "./Constants"; +import { CloseCodes } from "./Constants"; import { WebSocket } from "./WebSocket"; // TODO: make heartbeat timeout configurable @@ -6,6 +6,6 @@ export function setHeartbeat(socket: WebSocket) { if (socket.heartbeatTimeout) clearTimeout(socket.heartbeatTimeout); socket.heartbeatTimeout = setTimeout(() => { - return socket.close(CLOSECODES.Session_timed_out); + return socket.close(CloseCodes.Session_timed_out); }, 1000 * 45); } diff --git a/src/gateway/util/Send.ts b/src/gateway/util/Send.ts
index 7826dd40..3d10216d 100644 --- a/src/gateway/util/Send.ts +++ b/src/gateway/util/Send.ts
@@ -7,7 +7,7 @@ try { 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)}`); + if (process.env.WS_VERBOSE) console.log(`[Gateway] Outgoing message: ${JSON.stringify(data)}`); let buffer: Buffer | string; if (socket.encoding === "etf") buffer = erlpack.pack(data); // TODO: encode circular object diff --git a/src/gateway/util/WebSocket.ts b/src/gateway/util/WebSocket.ts
index 9496da85..b0be11db 100644 --- a/src/gateway/util/WebSocket.ts +++ b/src/gateway/util/WebSocket.ts
@@ -1,4 +1,5 @@ import { Intents, Permissions } from "@fosscord/util"; +import { Client } from "@fosscord/webrtc"; import WS from "ws"; import { Deflate } from "zlib"; @@ -19,4 +20,5 @@ export interface WebSocket extends WS { events: Record<string, Function>; member_events: Record<string, Function>; listen_options: any; + client: Client; } diff --git a/src/util/config/types/RegionConfiguration.ts b/src/util/config/types/RegionConfiguration.ts
index b4b8c4a3..418f46f1 100644 --- a/src/util/config/types/RegionConfiguration.ts +++ b/src/util/config/types/RegionConfiguration.ts
@@ -7,7 +7,7 @@ export class RegionConfiguration { { id: "fosscord", name: "Fosscord", - endpoint: "127.0.0.1:3004", + endpoint: undefined, vip: false, custom: false, deprecated: false diff --git a/src/util/entities/Attachment.ts b/src/util/entities/Attachment.ts
index 8392f415..395e695a 100644 --- a/src/util/entities/Attachment.ts +++ b/src/util/entities/Attachment.ts
@@ -1,7 +1,9 @@ -import { BeforeRemove, Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm"; +import "reflect-metadata"; +import { BeforeRemove, Column, Entity, JoinColumn, ManyToOne, Relation, RelationId } from "typeorm"; import { URL } from "url"; import { deleteFile } from "../util/cdn"; import { BaseClass } from "./BaseClass"; +import { Message } from "./Message"; @Entity("attachments") export class Attachment extends BaseClass { @@ -34,7 +36,7 @@ export class Attachment extends BaseClass { @ManyToOne(() => require("./Message").Message, (message: import("./Message").Message) => message.attachments, { onDelete: "CASCADE" }) - message: import("./Message").Message; + message: Relation<Message>; @BeforeRemove() onDelete() { diff --git a/src/util/entities/AuditLog.ts b/src/util/entities/AuditLog.ts
index 6f394f42..f8c65145 100644 --- a/src/util/entities/AuditLog.ts +++ b/src/util/entities/AuditLog.ts
@@ -1,4 +1,5 @@ -import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm"; +import "reflect-metadata"; +import { Column, Entity, JoinColumn, ManyToOne, Relation, RelationId } from "typeorm"; import { BaseClass } from "./BaseClass"; import { ChannelPermissionOverwrite } from "./Channel"; import { User } from "./User"; @@ -97,7 +98,7 @@ export enum AuditLogEvents { export class AuditLog extends BaseClass { @JoinColumn({ name: "target_id" }) @ManyToOne(() => User) - target?: User; + target?: Relation<User>; @Column({ nullable: true }) @RelationId((auditlog: AuditLog) => auditlog.user) @@ -105,7 +106,7 @@ export class AuditLog extends BaseClass { @JoinColumn({ name: "user_id" }) @ManyToOne(() => User, (user: User) => user.id) - user: User; + user: Relation<User>; @Column({ type: "int" }) action_type: AuditLogEvents; diff --git a/src/util/entities/BackupCodes.ts b/src/util/entities/BackupCodes.ts
index 503b1dbd..79b60a5e 100644 --- a/src/util/entities/BackupCodes.ts +++ b/src/util/entities/BackupCodes.ts
@@ -6,7 +6,7 @@ import { User } from "./User"; export class BackupCode extends BaseClass { @JoinColumn({ name: "user_id" }) @ManyToOne(() => User, { onDelete: "CASCADE" }) - user: User; + user: Relation<User>; @Column() code: string; diff --git a/src/util/entities/Ban.ts b/src/util/entities/Ban.ts
index 27c75278..e7daaf2a 100644 --- a/src/util/entities/Ban.ts +++ b/src/util/entities/Ban.ts
@@ -1,4 +1,5 @@ -import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm"; +import "reflect-metadata"; +import { Column, Entity, JoinColumn, ManyToOne, Relation, RelationId } from "typeorm"; import { BaseClass } from "./BaseClass"; import { Guild } from "./Guild"; import { User } from "./User"; @@ -13,7 +14,7 @@ export class Ban extends BaseClass { @ManyToOne(() => User, { onDelete: "CASCADE" }) - user: User; + user: Relation<User>; @Column({ nullable: true }) @RelationId((ban: Ban) => ban.guild) @@ -23,7 +24,7 @@ export class Ban extends BaseClass { @ManyToOne(() => Guild, { onDelete: "CASCADE" }) - guild: Guild; + guild: Relation<Guild>; @Column({ nullable: true }) @RelationId((ban: Ban) => ban.executor) @@ -31,7 +32,7 @@ export class Ban extends BaseClass { @JoinColumn({ name: "executor_id" }) @ManyToOne(() => User) - executor: User; + executor: Relation<User>; @Column() ip: string; diff --git a/src/util/entities/Channel.ts b/src/util/entities/Channel.ts
index b17fdba0..17a077ba 100644 --- a/src/util/entities/Channel.ts +++ b/src/util/entities/Channel.ts
@@ -56,7 +56,7 @@ export class Channel extends BaseClass { cascade: true, orphanedRowAction: "delete" }) - recipients?: Recipient[]; + recipients?: Relation<Recipient[]>; @Column({ nullable: true }) last_message_id: string; @@ -69,7 +69,7 @@ export class Channel extends BaseClass { @ManyToOne(() => Guild, { onDelete: "CASCADE" }) - guild: Guild; + guild: Relation<Guild>; @Column({ nullable: true }) @RelationId((channel: Channel) => channel.parent) @@ -77,7 +77,7 @@ export class Channel extends BaseClass { @JoinColumn({ name: "parent_id" }) @ManyToOne(() => Channel) - parent?: Channel; + parent?: Relation<Channel>; // for group DMs and owned custom channel types @Column({ nullable: true }) @@ -86,7 +86,7 @@ export class Channel extends BaseClass { @JoinColumn({ name: "owner_id" }) @ManyToOne(() => User) - owner: User; + owner: Relation<User>; @Column({ nullable: true }) last_pin_timestamp?: number; @@ -113,16 +113,13 @@ export class Channel extends BaseClass { nsfw?: boolean; @Column({ nullable: true }) - rate_limit_per_user?: number; - - @Column({ nullable: true }) topic?: string; @OneToMany(() => Invite, (invite: Invite) => invite.channel, { cascade: true, orphanedRowAction: "delete" }) - invites?: Invite[]; + invites?: Relation<Invite[]>; @Column({ nullable: true }) retention_policy_id?: string; @@ -131,25 +128,25 @@ export class Channel extends BaseClass { cascade: true, orphanedRowAction: "delete" }) - messages?: Message[]; + messages?: Relation<Message[]>; @OneToMany(() => VoiceState, (voice_state: VoiceState) => voice_state.channel, { cascade: true, orphanedRowAction: "delete" }) - voice_states?: VoiceState[]; + voice_states?: Relation<VoiceState[]>; @OneToMany(() => ReadState, (read_state: ReadState) => read_state.channel, { cascade: true, orphanedRowAction: "delete" }) - read_states?: ReadState[]; + read_states?: Relation<ReadState[]>; @OneToMany(() => Webhook, (webhook: Webhook) => webhook.channel, { cascade: true, orphanedRowAction: "delete" }) - webhooks?: Webhook[]; + webhooks?: Relation<Webhook[]>; @Column({ nullable: true }) flags?: number = 0; diff --git a/src/util/entities/ConnectedAccount.ts b/src/util/entities/ConnectedAccount.ts
index 018b3995..a74b7a43 100644 --- a/src/util/entities/ConnectedAccount.ts +++ b/src/util/entities/ConnectedAccount.ts
@@ -1,4 +1,5 @@ -import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm"; +import "reflect-metadata"; +import { Column, Entity, JoinColumn, ManyToOne, Relation, RelationId } from "typeorm"; import { BaseClass } from "./BaseClass"; import { User } from "./User"; @@ -14,7 +15,7 @@ export class ConnectedAccount extends BaseClass { @ManyToOne(() => User, { onDelete: "CASCADE" }) - user: User; + user: Relation<User>; @Column({ select: false }) access_token: string; diff --git a/src/util/entities/Emoji.ts b/src/util/entities/Emoji.ts
index a2552995..4a453afe 100644 --- a/src/util/entities/Emoji.ts +++ b/src/util/entities/Emoji.ts
@@ -1,4 +1,5 @@ -import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm"; +import "reflect-metadata"; +import { Column, Entity, JoinColumn, ManyToOne, Relation, RelationId } from "typeorm"; import { User } from "."; import { BaseClass } from "./BaseClass"; import { Guild } from "./Guild"; @@ -18,7 +19,7 @@ export class Emoji extends BaseClass { @ManyToOne(() => Guild, { onDelete: "CASCADE" }) - guild: Guild; + guild: Relation<Guild>; @Column({ nullable: true }) @RelationId((emoji: Emoji) => emoji.user) @@ -26,7 +27,7 @@ export class Emoji extends BaseClass { @JoinColumn({ name: "user_id" }) @ManyToOne(() => User) - user: User; + user: Relation<User>; @Column() managed: boolean; diff --git a/src/util/entities/Guild.ts b/src/util/entities/Guild.ts
index 015c6d04..9efb7e74 100644 --- a/src/util/entities/Guild.ts +++ b/src/util/entities/Guild.ts
@@ -41,8 +41,8 @@ export class Guild extends BaseClass { afk_channel_id?: string; @JoinColumn({ name: "afk_channel_id" }) - @ManyToOne(() => Channel) - afk_channel?: Channel; + @ManyToOne(() => Channel, { nullable: true }) + afk_channel?: Relation<Channel>; @Column({ nullable: true }) afk_timeout?: number = Config.get().defaults.guild.afkTimeout; @@ -57,7 +57,7 @@ export class Guild extends BaseClass { cascade: true, orphanedRowAction: "delete" }) - bans: Ban[]; + bans: Relation<Ban[]>; @Column({ nullable: true }) banner?: string; @@ -107,7 +107,7 @@ export class Guild extends BaseClass { orphanedRowAction: "delete", onDelete: "CASCADE" }) - members: Member[]; + members: Relation<Member[]>; @JoinColumn({ name: "role_ids" }) @OneToMany(() => Role, (role: Role) => role.guild, { @@ -115,14 +115,14 @@ export class Guild extends BaseClass { orphanedRowAction: "delete", onDelete: "CASCADE" }) - roles: Role[]; + roles: Relation<Role[]>; @JoinColumn({ name: "channel_ids" }) @OneToMany(() => Channel, (channel: Channel) => channel.guild, { cascade: true, orphanedRowAction: "delete" }) - channels: Channel[]; + channels: Relation<Channel[]>; @Column({ nullable: true }) @RelationId((guild: Guild) => guild.template) @@ -130,7 +130,7 @@ export class Guild extends BaseClass { @JoinColumn({ name: "template_id", referencedColumnName: "id" }) @ManyToOne(() => Template) - template: Template; + template: Relation<Template>; @JoinColumn({ name: "emoji_ids" }) @OneToMany(() => Emoji, (emoji: Emoji) => emoji.guild, { @@ -138,7 +138,7 @@ export class Guild extends BaseClass { orphanedRowAction: "delete", onDelete: "CASCADE" }) - emojis: Emoji[]; + emojis: Relation<Emoji[]>; @JoinColumn({ name: "sticker_ids" }) @OneToMany(() => Sticker, (sticker: Sticker) => sticker.guild, { @@ -146,7 +146,7 @@ export class Guild extends BaseClass { orphanedRowAction: "delete", onDelete: "CASCADE" }) - stickers: Sticker[]; + stickers: Relation<Sticker[]>; @JoinColumn({ name: "invite_ids" }) @OneToMany(() => Invite, (invite: Invite) => invite.guild, { @@ -154,7 +154,7 @@ export class Guild extends BaseClass { orphanedRowAction: "delete", onDelete: "CASCADE" }) - invites: Invite[]; + invites: Relation<Invite[]>; @JoinColumn({ name: "voice_state_ids" }) @OneToMany(() => VoiceState, (voicestate: VoiceState) => voicestate.guild, { @@ -162,7 +162,7 @@ export class Guild extends BaseClass { orphanedRowAction: "delete", onDelete: "CASCADE" }) - voice_states: VoiceState[]; + voice_states: Relation<VoiceState[]>; @JoinColumn({ name: "webhook_ids" }) @OneToMany(() => Webhook, (webhook: Webhook) => webhook.guild, { @@ -170,7 +170,7 @@ export class Guild extends BaseClass { orphanedRowAction: "delete", onDelete: "CASCADE" }) - webhooks: Webhook[]; + webhooks: Relation<Webhook[]>; @Column({ nullable: true }) mfa_level?: number; @@ -183,8 +183,8 @@ export class Guild extends BaseClass { owner_id?: string; // optional to allow for ownerless guilds @JoinColumn({ name: "owner_id", referencedColumnName: "id" }) - @ManyToOne(() => User) - owner?: User; // optional to allow for ownerless guilds + @ManyToOne(() => User, { nullable: true }) + owner?: Relation<User>; // optional to allow for ownerless guilds @Column({ nullable: true }) preferred_locale?: string; @@ -200,16 +200,16 @@ export class Guild extends BaseClass { public_updates_channel_id: string; @JoinColumn({ name: "public_updates_channel_id" }) - @ManyToOne(() => Channel) - public_updates_channel?: Channel; + @ManyToOne(() => Channel, { nullable: true }) + public_updates_channel?: Relation<Channel>; @Column({ nullable: true }) @RelationId((guild: Guild) => guild.rules_channel) rules_channel_id?: string; @JoinColumn({ name: "rules_channel_id" }) - @ManyToOne(() => Channel) - rules_channel?: string; + @ManyToOne(() => Channel, { nullable: true }) + rules_channel?: Relation<Channel>; @Column({ nullable: true }) region?: string; @@ -222,8 +222,8 @@ export class Guild extends BaseClass { system_channel_id?: string; @JoinColumn({ name: "system_channel_id" }) - @ManyToOne(() => Channel) - system_channel?: Channel; + @ManyToOne(() => Channel, { nullable: true }) + system_channel?: Relation<Channel>; @Column({ nullable: true }) system_channel_flags?: number; @@ -251,8 +251,8 @@ export class Guild extends BaseClass { widget_channel_id?: string; @JoinColumn({ name: "widget_channel_id" }) - @ManyToOne(() => Channel) - widget_channel?: Channel; + @ManyToOne(() => Channel, { nullable: true }) + widget_channel?: Relation<Channel>; @Column({ nullable: true }) widget_enabled?: boolean; @@ -333,13 +333,13 @@ export class Guild extends BaseClass { const ids = new Map(); - body.channels.forEach((x) => { - if (x.id) { - ids.set(x.id, Snowflake.generate()); + body.channels!.forEach((x) => { + if (x!.id) { + ids.set(x!.id, Snowflake.generate()); } }); - for (const channel of body.channels?.sort((a, b) => (a.parent_id ? 1 : -1))) { + for (const channel of body.channels!.sort((a, b) => (a.parent_id ? 1 : -1))) { let id = ids.get(channel.id) || Snowflake.generate(); let parent_id = ids.get(channel.parent_id); diff --git a/src/util/entities/Invite.ts b/src/util/entities/Invite.ts
index f6ba85d7..383c932b 100644 --- a/src/util/entities/Invite.ts +++ b/src/util/entities/Invite.ts
@@ -39,7 +39,7 @@ export class Invite extends BaseClassWithoutId { @ManyToOne(() => Guild, { onDelete: "CASCADE" }) - guild: Guild; + guild: Relation<Guild>; @Column({ nullable: true }) @RelationId((invite: Invite) => invite.channel) @@ -49,7 +49,7 @@ export class Invite extends BaseClassWithoutId { @ManyToOne(() => Channel, { onDelete: "CASCADE" }) - channel: Channel; + channel: Relation<Channel>; @Column({ nullable: true }) @RelationId((invite: Invite) => invite.inviter) @@ -69,7 +69,7 @@ export class Invite extends BaseClassWithoutId { @ManyToOne(() => User, { onDelete: "CASCADE" }) - target_user?: string; // could be used for "User specific invites" https://github.com/fosscord/fosscord/issues/62 + target_user?: Relation<User>; // could be used for "User specific invites" https://github.com/fosscord/fosscord/issues/62 @Column({ nullable: true }) target_user_type?: number; diff --git a/src/util/entities/Member.ts b/src/util/entities/Member.ts
index 42a014d4..f5329481 100644 --- a/src/util/entities/Member.ts +++ b/src/util/entities/Member.ts
@@ -40,7 +40,7 @@ export class Member extends BaseClassWithoutId { @ManyToOne(() => User, { onDelete: "CASCADE" }) - user: User; + user: Relation<User>; @Column() @RelationId((member: Member) => member.guild) @@ -50,7 +50,7 @@ export class Member extends BaseClassWithoutId { @ManyToOne(() => Guild, { onDelete: "CASCADE" }) - guild: Guild; + guild: Relation<Guild>; @Column({ nullable: true }) nick?: string; @@ -64,7 +64,7 @@ export class Member extends BaseClassWithoutId { } }) @ManyToMany(() => Role, { cascade: true }) - roles: Role[]; + roles: Relation<Role[]>; @Column() joined_at: Date; @@ -106,7 +106,7 @@ export class Member extends BaseClassWithoutId { } static async removeFromGuild(user_id: string, guild_id: string) { - const guild = await Guild.findOneOrFail({ select: ["owner_id", "member_count"], where: { id: guild_id } }); + const guild = await require("./Guild").Guild.findOneOrFail({ select: ["owner_id", "member_count"], where: { id: guild_id } }); if (guild.owner_id === user_id) throw new Error("The owner cannot be removed of the guild"); const member = await Member.findOneOrFail({ where: { id: user_id, guild_id }, relations: ["user"] }); @@ -224,7 +224,7 @@ export class Member extends BaseClassWithoutId { throw new HTTPError(`You are at the ${maxGuilds} server limit.`, 403); } - const guild = await Guild.findOneOrFail({ + const guild = await require("./Guild").Guild.findOneOrFail({ where: { id: guild_id }, diff --git a/src/util/entities/Message.ts b/src/util/entities/Message.ts
index 8122b532..2b5e6f86 100644 --- a/src/util/entities/Message.ts +++ b/src/util/entities/Message.ts
@@ -51,7 +51,7 @@ export class Message extends BaseClass { @ManyToOne(() => Channel, { onDelete: "CASCADE" }) - channel: Channel; + channel: Relation<Channel>; @Column({ nullable: true }) @RelationId((message: Message) => message.guild) @@ -61,7 +61,7 @@ export class Message extends BaseClass { @ManyToOne(() => Guild, { onDelete: "CASCADE" }) - guild?: Guild; + guild?: Relation<Guild>; @Column({ nullable: true }) @RelationId((message: Message) => message.author) @@ -72,7 +72,7 @@ export class Message extends BaseClass { @ManyToOne(() => User, { onDelete: "CASCADE" }) - author?: User; + author?: Relation<User>; @Column({ nullable: true }) @RelationId((message: Message) => message.member) @@ -82,7 +82,7 @@ export class Message extends BaseClass { @ManyToOne(() => User, { onDelete: "CASCADE" }) - member?: Member; + member?: Relation<Member>; @Column({ nullable: true }) @RelationId((message: Message) => message.webhook) @@ -90,7 +90,7 @@ export class Message extends BaseClass { @JoinColumn({ name: "webhook_id" }) @ManyToOne(() => Webhook) - webhook?: Webhook; + webhook?: Relation<Webhook>; @Column({ nullable: true }) @RelationId((message: Message) => message.application) @@ -98,7 +98,7 @@ export class Message extends BaseClass { @JoinColumn({ name: "application_id" }) @ManyToOne(() => Application) - application?: Application; + application?: Relation<Application>; @Column({ nullable: true }) content?: string; @@ -118,25 +118,25 @@ export class Message extends BaseClass { @JoinTable({ name: "message_user_mentions" }) @ManyToMany(() => User) - mentions: User[]; + mentions: Relation<User[]>; @JoinTable({ name: "message_role_mentions" }) @ManyToMany(() => Role) - mention_roles: Role[]; + mention_roles: Relation<Role[]>; @JoinTable({ name: "message_channel_mentions" }) @ManyToMany(() => Channel) - mention_channels: Channel[]; + mention_channels: Relation<Channel[]>; @JoinTable({ name: "message_stickers" }) @ManyToMany(() => Sticker, { cascade: true, onDelete: "CASCADE" }) - sticker_items?: Sticker[]; + sticker_items?: Relation<Sticker[]>; @OneToMany(() => Attachment, (attachment: Attachment) => attachment.message, { cascade: true, orphanedRowAction: "delete" }) - attachments?: Attachment[]; + attachments?: Relation<Attachment[]>; @Column({ type: "simple-json" }) embeds: Embed[]; @@ -170,7 +170,7 @@ export class Message extends BaseClass { @JoinColumn({ name: "message_reference_id" }) @ManyToOne(() => Message) - referenced_message?: Message; + referenced_message?: Relation<Message>; @Column({ type: "simple-json", nullable: true }) interaction?: { diff --git a/src/util/entities/Migration.ts b/src/util/entities/Migration.ts
index 626ec429..07fd25a6 100644 --- a/src/util/entities/Migration.ts +++ b/src/util/entities/Migration.ts
@@ -1,3 +1,4 @@ +import "reflect-metadata"; import { Column, Entity, ObjectIdColumn, PrimaryGeneratedColumn } from "typeorm"; import { BaseClassWithoutId } from "."; diff --git a/src/util/entities/Note.ts b/src/util/entities/Note.ts
index b3ac45ee..d6eefd63 100644 --- a/src/util/entities/Note.ts +++ b/src/util/entities/Note.ts
@@ -1,4 +1,5 @@ -import { Column, Entity, JoinColumn, ManyToOne, Unique } from "typeorm"; +import "reflect-metadata"; +import { Column, Entity, JoinColumn, ManyToOne, Relation, Unique } from "typeorm"; import { BaseClass } from "./BaseClass"; import { User } from "./User"; @@ -7,11 +8,11 @@ import { User } from "./User"; export class Note extends BaseClass { @JoinColumn({ name: "owner_id" }) @ManyToOne(() => User, { onDelete: "CASCADE" }) - owner: User; + owner: Relation<User>; @JoinColumn({ name: "target_id" }) @ManyToOne(() => User, { onDelete: "CASCADE" }) - target: User; + target: Relation<User>; @Column() content: string; diff --git a/src/util/entities/RateLimit.ts b/src/util/entities/RateLimit.ts
index f5916f6b..1f6c359c 100644 --- a/src/util/entities/RateLimit.ts +++ b/src/util/entities/RateLimit.ts
@@ -1,3 +1,4 @@ +import "reflect-metadata"; import { Column, Entity } from "typeorm"; import { BaseClass } from "./BaseClass"; diff --git a/src/util/entities/ReadState.ts b/src/util/entities/ReadState.ts
index 77d2c08a..462a06f9 100644 --- a/src/util/entities/ReadState.ts +++ b/src/util/entities/ReadState.ts
@@ -1,4 +1,5 @@ -import { Column, Entity, Index, JoinColumn, ManyToOne, RelationId } from "typeorm"; +import "reflect-metadata"; +import { Column, Entity, Index, JoinColumn, ManyToOne, Relation, RelationId } from "typeorm"; import { BaseClass } from "./BaseClass"; import { Channel } from "./Channel"; import { User } from "./User"; @@ -18,7 +19,7 @@ export class ReadState extends BaseClass { @ManyToOne(() => Channel, { onDelete: "CASCADE" }) - channel: Channel; + channel: Relation<Channel>; @Column() @RelationId((read_state: ReadState) => read_state.user) @@ -28,7 +29,7 @@ export class ReadState extends BaseClass { @ManyToOne(() => User, { onDelete: "CASCADE" }) - user: User; + user: Relation<User>; // fully read marker @Column({ nullable: true }) diff --git a/src/util/entities/Recipient.ts b/src/util/entities/Recipient.ts
index fc9e629b..a1ae1e17 100644 --- a/src/util/entities/Recipient.ts +++ b/src/util/entities/Recipient.ts
@@ -1,5 +1,8 @@ -import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm"; +import "reflect-metadata"; +import { Column, Entity, JoinColumn, ManyToOne, Relation, RelationId } from "typeorm"; import { BaseClass } from "./BaseClass"; +import { Channel } from "./Channel"; +import { User } from "./User"; @Entity("recipients") export class Recipient extends BaseClass { @@ -11,7 +14,7 @@ export class Recipient extends BaseClass { @ManyToOne(() => require("./Channel").Channel, { onDelete: "CASCADE" }) - channel: import("./Channel").Channel; + channel: Relation<Channel>; @Column() @RelationId((recipient: Recipient) => recipient.user) @@ -21,7 +24,7 @@ export class Recipient extends BaseClass { @ManyToOne(() => require("./User").User, { onDelete: "CASCADE" }) - user: import("./User").User; + user: Relation<User>; @Column({ default: false }) closed: boolean; diff --git a/src/util/entities/Relationship.ts b/src/util/entities/Relationship.ts
index b55d9e64..c4525125 100644 --- a/src/util/entities/Relationship.ts +++ b/src/util/entities/Relationship.ts
@@ -1,4 +1,5 @@ -import { Column, Entity, Index, JoinColumn, ManyToOne, RelationId } from "typeorm"; +import "reflect-metadata"; +import { Column, Entity, Index, JoinColumn, ManyToOne, Relation, RelationId } from "typeorm"; import { BaseClass } from "./BaseClass"; import { User } from "./User"; @@ -20,7 +21,7 @@ export class Relationship extends BaseClass { @ManyToOne(() => User, { onDelete: "CASCADE" }) - from: User; + from: Relation<User>; @Column({}) @RelationId((relationship: Relationship) => relationship.to) @@ -30,7 +31,7 @@ export class Relationship extends BaseClass { @ManyToOne(() => User, { onDelete: "CASCADE" }) - to: User; + to: Relation<User>; @Column({ nullable: true }) nickname?: string; diff --git a/src/util/entities/Role.ts b/src/util/entities/Role.ts
index b1fd9bb1..1c5e19cf 100644 --- a/src/util/entities/Role.ts +++ b/src/util/entities/Role.ts
@@ -1,4 +1,5 @@ -import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm"; +import "reflect-metadata"; +import { Column, Entity, JoinColumn, ManyToOne, Relation, RelationId } from "typeorm"; import { BaseClass } from "./BaseClass"; import { Guild } from "./Guild"; @@ -13,7 +14,7 @@ export class Role extends BaseClass { @ManyToOne(() => Guild, { onDelete: "CASCADE" }) - guild: Guild; + guild: Relation<Guild>; @Column() color: number; diff --git a/src/util/entities/Session.ts b/src/util/entities/Session.ts
index 0cb4c309..bc9e6906 100644 --- a/src/util/entities/Session.ts +++ b/src/util/entities/Session.ts
@@ -16,7 +16,7 @@ export class Session extends BaseClass { @ManyToOne(() => User, { onDelete: "CASCADE" }) - user: User; + user: Relation<User>; //TODO check, should be 32 char long hex string @Column({ nullable: false, select: false }) diff --git a/src/util/entities/Sticker.ts b/src/util/entities/Sticker.ts
index 69836e62..0879e064 100644 --- a/src/util/entities/Sticker.ts +++ b/src/util/entities/Sticker.ts
@@ -34,11 +34,11 @@ export class Sticker extends BaseClass { pack_id?: string; @JoinColumn({ name: "pack_id" }) - @ManyToOne(() => require("./StickerPack").StickerPack, { + @ManyToOne(() => StickerPack, { onDelete: "CASCADE", nullable: true }) - pack: import("./StickerPack").StickerPack; + pack: Relation<StickerPack>; @Column({ nullable: true }) guild_id?: string; @@ -47,7 +47,7 @@ export class Sticker extends BaseClass { @ManyToOne(() => Guild, { onDelete: "CASCADE" }) - guild?: Guild; + guild?: Relation<Guild>; @Column({ nullable: true }) user_id?: string; @@ -56,7 +56,7 @@ export class Sticker extends BaseClass { @ManyToOne(() => User, { onDelete: "CASCADE" }) - user?: User; + user?: Relation<User>; @Column({ type: "int" }) type: StickerType; diff --git a/src/util/entities/StickerPack.ts b/src/util/entities/StickerPack.ts
index 4619af34..ca8b4cad 100644 --- a/src/util/entities/StickerPack.ts +++ b/src/util/entities/StickerPack.ts
@@ -17,7 +17,7 @@ export class StickerPack extends BaseClass { cascade: true, orphanedRowAction: "delete" }) - stickers: Sticker[]; + stickers: Relation<Sticker[]>; // sku_id: string @@ -27,5 +27,5 @@ export class StickerPack extends BaseClass { @ManyToOne(() => Sticker, { nullable: true }) @JoinColumn() - cover_sticker?: Sticker; + cover_sticker?: Relation<Sticker>; } diff --git a/src/util/entities/Team.ts b/src/util/entities/Team.ts
index 1d2d7002..48657930 100644 --- a/src/util/entities/Team.ts +++ b/src/util/entities/Team.ts
@@ -12,7 +12,7 @@ export class Team extends BaseClass { @OneToMany(() => TeamMember, (member: TeamMember) => member.team, { orphanedRowAction: "delete" }) - members: TeamMember[]; + members: Relation<TeamMember[]>; @Column() name: string; @@ -23,5 +23,5 @@ export class Team extends BaseClass { @JoinColumn({ name: "owner_user_id" }) @ManyToOne(() => User) - owner_user: User; + owner_user: Relation<User>; } diff --git a/src/util/entities/TeamMember.ts b/src/util/entities/TeamMember.ts
index d11ebf95..e2950c65 100644 --- a/src/util/entities/TeamMember.ts +++ b/src/util/entities/TeamMember.ts
@@ -1,5 +1,7 @@ -import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm"; +import "reflect-metadata"; +import { Column, Entity, JoinColumn, ManyToOne, Relation, RelationId } from "typeorm"; import { BaseClass } from "./BaseClass"; +import { Team } from "./Team"; import { User } from "./User"; export enum TeamMemberState { @@ -23,7 +25,7 @@ export class TeamMember extends BaseClass { @ManyToOne(() => require("./Team").Team, (team: import("./Team").Team) => team.members, { onDelete: "CASCADE" }) - team: import("./Team").Team; + team: Relation<Team>; @Column({ nullable: true }) @RelationId((member: TeamMember) => member.user) @@ -33,5 +35,5 @@ export class TeamMember extends BaseClass { @ManyToOne(() => User, { onDelete: "CASCADE" }) - user: User; + user: Relation<User>; } diff --git a/src/util/entities/Template.ts b/src/util/entities/Template.ts
index 1d952283..bcd2b259 100644 --- a/src/util/entities/Template.ts +++ b/src/util/entities/Template.ts
@@ -1,4 +1,5 @@ -import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm"; +import "reflect-metadata"; +import { Column, Entity, JoinColumn, ManyToOne, Relation, RelationId } from "typeorm"; import { BaseClass } from "./BaseClass"; import { Guild } from "./Guild"; import { User } from "./User"; @@ -23,7 +24,7 @@ export class Template extends BaseClass { @JoinColumn({ name: "creator_id" }) @ManyToOne(() => User) - creator: User; + creator: Relation<User>; @Column() created_at: Date; @@ -37,7 +38,7 @@ export class Template extends BaseClass { @JoinColumn({ name: "source_guild_id" }) @ManyToOne(() => Guild) - source_guild: Guild; + source_guild: Relation<Guild>; @Column({ type: "simple-json" }) serialized_source_guild: Guild; diff --git a/src/util/entities/User.ts b/src/util/entities/User.ts
index 1237b676..760ef9c7 100644 --- a/src/util/entities/User.ts +++ b/src/util/entities/User.ts
@@ -141,21 +141,21 @@ export class User extends BaseClass { rights: string = Config.get().register.defaultRights; // Rights @OneToMany(() => Session, (session: Session) => session.user) - sessions: Session[]; + sessions: Relation<Session[]>; @JoinColumn({ name: "relationship_ids" }) @OneToMany(() => Relationship, (relationship: Relationship) => relationship.from, { cascade: true, orphanedRowAction: "delete" }) - relationships: Relationship[]; + relationships: Relation<Relationship[]>; @JoinColumn({ name: "connected_account_ids" }) @OneToMany(() => ConnectedAccount, (account: ConnectedAccount) => account.user, { cascade: true, orphanedRowAction: "delete" }) - connected_accounts: ConnectedAccount[]; + connected_accounts: Relation<ConnectedAccount[]>; @Column({ type: "simple-json", select: false }) data: { @@ -264,6 +264,8 @@ export class User extends BaseClass { // appearently discord doesn't save the date of birth and just calculate if nsfw is allowed // if nsfw_allowed is null/undefined it'll require date_of_birth to set it to true/false const language = req?.language === "en" ? "en-US" : req?.language || "en-US"; + const settings = new UserSettings(); + settings.locale = language; const user = OrmUtils.mergeDeep(new User(), { //required: @@ -280,11 +282,14 @@ export class User extends BaseClass { //await (user.settings as UserSettings).save(); await user.save(); + await user.settings.save(); setImmediate(async () => { if (Config.get().guild.autoJoin.enabled) { for (const guild of Config.get().guild.autoJoin.guilds || []) { - await Member.addToGuild(user.id, guild).catch((e) => {}); + await require("./Member") + .Member.addToGuild(user.id, guild) + .catch((e: any) => {}); } } }); diff --git a/src/util/entities/VoiceState.ts b/src/util/entities/VoiceState.ts
index baf2c687..55fac313 100644 --- a/src/util/entities/VoiceState.ts +++ b/src/util/entities/VoiceState.ts
@@ -1,4 +1,5 @@ -import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm"; +import "reflect-metadata"; +import { Column, Entity, JoinColumn, ManyToOne, Relation, RelationId } from "typeorm"; import { BaseClass } from "./BaseClass"; import { Channel } from "./Channel"; import { Guild } from "./Guild"; @@ -16,7 +17,7 @@ export class VoiceState extends BaseClass { @ManyToOne(() => Guild, { onDelete: "CASCADE" }) - guild?: Guild; + guild?: Relation<Guild>; @Column({ nullable: true }) @RelationId((voice_state: VoiceState) => voice_state.channel) @@ -26,7 +27,7 @@ export class VoiceState extends BaseClass { @ManyToOne(() => Channel, { onDelete: "CASCADE" }) - channel: Channel; + channel: Relation<Channel>; @Column({ nullable: true }) @RelationId((voice_state: VoiceState) => voice_state.user) @@ -36,20 +37,20 @@ export class VoiceState extends BaseClass { @ManyToOne(() => User, { onDelete: "CASCADE" }) - user: User; + user: Relation<User>; // @JoinColumn([{ name: "user_id", referencedColumnName: "id" },{ name: "guild_id", referencedColumnName: "guild_id" }]) // @ManyToOne(() => Member, { // onDelete: "CASCADE", // }) //TODO find a way to make it work without breaking Guild.voice_states - member: Member; + member: Relation<Member>; @Column() session_id: string; @Column({ nullable: true }) - token: string; + token?: string; @Column() deaf: boolean; diff --git a/src/util/entities/Webhook.ts b/src/util/entities/Webhook.ts
index 3d94ddb6..42f74fae 100644 --- a/src/util/entities/Webhook.ts +++ b/src/util/entities/Webhook.ts
@@ -1,4 +1,5 @@ -import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm"; +import "reflect-metadata"; +import { Column, Entity, JoinColumn, ManyToOne, Relation, RelationId } from "typeorm"; import { Application } from "./Application"; import { BaseClass } from "./BaseClass"; import { Channel } from "./Channel"; @@ -32,7 +33,7 @@ export class Webhook extends BaseClass { @ManyToOne(() => Guild, { onDelete: "CASCADE" }) - guild: Guild; + guild: Relation<Guild>; @Column({ nullable: true }) @RelationId((webhook: Webhook) => webhook.channel) @@ -42,7 +43,7 @@ export class Webhook extends BaseClass { @ManyToOne(() => Channel, { onDelete: "CASCADE" }) - channel: Channel; + channel: Relation<Channel>; @Column({ nullable: true }) @RelationId((webhook: Webhook) => webhook.application) @@ -52,7 +53,7 @@ export class Webhook extends BaseClass { @ManyToOne(() => Application, { onDelete: "CASCADE" }) - application: Application; + application: Relation<Application>; @Column({ nullable: true }) @RelationId((webhook: Webhook) => webhook.user) @@ -62,7 +63,7 @@ export class Webhook extends BaseClass { @ManyToOne(() => User, { onDelete: "CASCADE" }) - user: User; + user: Relation<User>; @Column({ nullable: true }) @RelationId((webhook: Webhook) => webhook.guild) @@ -72,5 +73,5 @@ export class Webhook extends BaseClass { @ManyToOne(() => Guild, { onDelete: "CASCADE" }) - source_guild: Guild; + source_guild: Relation<Guild>; } diff --git a/src/util/migrations/mariadb/1660258393551-CodeCleanup3.ts b/src/util/migrations/mariadb/1660258393551-CodeCleanup3.ts new file mode 100644
index 00000000..8a6126c7 --- /dev/null +++ b/src/util/migrations/mariadb/1660258393551-CodeCleanup3.ts
@@ -0,0 +1,231 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class CodeCleanup31660258393551 implements MigrationInterface { + name = "CodeCleanup31660258393551"; + + public async up(queryRunner: QueryRunner): Promise<void> { + await queryRunner.query(` + ALTER TABLE \`applications\` DROP FOREIGN KEY \`FK_2ce5a55796fe4c2f77ece57a647\` + `); + await queryRunner.query(` + DROP INDEX \`REL_2ce5a55796fe4c2f77ece57a64\` ON \`applications\` + `); + await queryRunner.query(` + CREATE TABLE \`user_settings\` ( + \`id\` varchar(255) NOT NULL, + \`afk_timeout\` int NULL, + \`allow_accessibility_detection\` tinyint NULL, + \`animate_emoji\` tinyint NULL, + \`animate_stickers\` int NULL, + \`contact_sync_enabled\` tinyint NULL, + \`convert_emoticons\` tinyint NULL, + \`custom_status\` text NULL, + \`default_guilds_restricted\` tinyint NULL, + \`detect_platform_accounts\` tinyint NULL, + \`developer_mode\` tinyint NULL, + \`disable_games_tab\` tinyint NULL, + \`enable_tts_command\` tinyint NULL, + \`explicit_content_filter\` int NULL, + \`friend_source_flags\` text NULL, + \`gateway_connected\` tinyint NULL, + \`gif_auto_play\` tinyint NULL, + \`guild_folders\` text NULL, + \`guild_positions\` text NULL, + \`inline_attachment_media\` tinyint NULL, + \`inline_embed_media\` tinyint NULL, + \`locale\` varchar(255) NULL, + \`message_display_compact\` tinyint NULL, + \`native_phone_integration_enabled\` tinyint NULL, + \`render_embeds\` tinyint NULL, + \`render_reactions\` tinyint NULL, + \`restricted_guilds\` text NULL, + \`show_current_game\` tinyint NULL, + \`status\` varchar(255) NULL, + \`stream_notifications_enabled\` tinyint NULL, + \`theme\` varchar(255) NULL, + \`timezone_offset\` int NULL, + PRIMARY KEY (\`id\`) + ) ENGINE = InnoDB + `); + await queryRunner.query(` + ALTER TABLE \`users\` DROP COLUMN \`settings\` + `); + await queryRunner.query(` + ALTER TABLE \`applications\` DROP COLUMN \`type\` + `); + await queryRunner.query(` + ALTER TABLE \`applications\` DROP COLUMN \`hook\` + `); + await queryRunner.query(` + ALTER TABLE \`applications\` DROP COLUMN \`redirect_uris\` + `); + await queryRunner.query(` + ALTER TABLE \`applications\` DROP COLUMN \`rpc_application_state\` + `); + await queryRunner.query(` + ALTER TABLE \`applications\` DROP COLUMN \`store_application_state\` + `); + await queryRunner.query(` + ALTER TABLE \`applications\` DROP COLUMN \`verification_state\` + `); + await queryRunner.query(` + ALTER TABLE \`applications\` DROP COLUMN \`interactions_endpoint_url\` + `); + await queryRunner.query(` + ALTER TABLE \`applications\` DROP COLUMN \`integration_public\` + `); + await queryRunner.query(` + ALTER TABLE \`applications\` DROP COLUMN \`integration_require_code_grant\` + `); + await queryRunner.query(` + ALTER TABLE \`applications\` DROP COLUMN \`discoverability_state\` + `); + await queryRunner.query(` + ALTER TABLE \`applications\` DROP COLUMN \`discovery_eligibility_flags\` + `); + await queryRunner.query(` + ALTER TABLE \`applications\` DROP COLUMN \`tags\` + `); + await queryRunner.query(` + ALTER TABLE \`applications\` DROP COLUMN \`install_params\` + `); + await queryRunner.query(` + ALTER TABLE \`applications\` DROP COLUMN \`bot_user_id\` + `); + await queryRunner.query(` + ALTER TABLE \`guilds\` + ADD \`premium_progress_bar_enabled\` tinyint NULL + `); + await queryRunner.query(` + ALTER TABLE \`applications\` + ADD \`rpc_origins\` text NULL + `); + await queryRunner.query(` + ALTER TABLE \`applications\` + ADD \`primary_sku_id\` varchar(255) NULL + `); + await queryRunner.query(` + ALTER TABLE \`applications\` + ADD \`slug\` varchar(255) NULL + `); + await queryRunner.query(` + ALTER TABLE \`applications\` + ADD \`guild_id\` varchar(255) NULL + `); + await queryRunner.query(` + ALTER TABLE \`applications\` CHANGE \`description\` \`description\` varchar(255) NOT NULL + `); + await queryRunner.query(` + ALTER TABLE \`applications\` DROP COLUMN \`flags\` + `); + await queryRunner.query(` + ALTER TABLE \`applications\` + ADD \`flags\` varchar(255) NOT NULL + `); + await queryRunner.query(` + ALTER TABLE \`applications\` + ADD CONSTRAINT \`FK_e5bf78cdbbe9ba91062d74c5aba\` FOREIGN KEY (\`guild_id\`) REFERENCES \`guilds\`(\`id\`) ON DELETE NO ACTION ON UPDATE NO ACTION + `); + } + + public async down(queryRunner: QueryRunner): Promise<void> { + await queryRunner.query(` + ALTER TABLE \`applications\` DROP FOREIGN KEY \`FK_e5bf78cdbbe9ba91062d74c5aba\` + `); + await queryRunner.query(` + ALTER TABLE \`applications\` DROP COLUMN \`flags\` + `); + await queryRunner.query(` + ALTER TABLE \`applications\` + ADD \`flags\` int NOT NULL + `); + await queryRunner.query(` + ALTER TABLE \`applications\` CHANGE \`description\` \`description\` varchar(255) NULL + `); + await queryRunner.query(` + ALTER TABLE \`applications\` DROP COLUMN \`guild_id\` + `); + await queryRunner.query(` + ALTER TABLE \`applications\` DROP COLUMN \`slug\` + `); + await queryRunner.query(` + ALTER TABLE \`applications\` DROP COLUMN \`primary_sku_id\` + `); + await queryRunner.query(` + ALTER TABLE \`applications\` DROP COLUMN \`rpc_origins\` + `); + await queryRunner.query(` + ALTER TABLE \`guilds\` DROP COLUMN \`premium_progress_bar_enabled\` + `); + await queryRunner.query(` + ALTER TABLE \`applications\` + ADD \`bot_user_id\` varchar(255) NULL + `); + await queryRunner.query(` + ALTER TABLE \`applications\` + ADD \`install_params\` text NULL + `); + await queryRunner.query(` + ALTER TABLE \`applications\` + ADD \`tags\` text NULL + `); + await queryRunner.query(` + ALTER TABLE \`applications\` + ADD \`discovery_eligibility_flags\` int NULL + `); + await queryRunner.query(` + ALTER TABLE \`applications\` + ADD \`discoverability_state\` int NULL + `); + await queryRunner.query(` + ALTER TABLE \`applications\` + ADD \`integration_require_code_grant\` tinyint NULL + `); + await queryRunner.query(` + ALTER TABLE \`applications\` + ADD \`integration_public\` tinyint NULL + `); + await queryRunner.query(` + ALTER TABLE \`applications\` + ADD \`interactions_endpoint_url\` varchar(255) NULL + `); + await queryRunner.query(` + ALTER TABLE \`applications\` + ADD \`verification_state\` int NULL + `); + await queryRunner.query(` + ALTER TABLE \`applications\` + ADD \`store_application_state\` int NULL + `); + await queryRunner.query(` + ALTER TABLE \`applications\` + ADD \`rpc_application_state\` int NULL + `); + await queryRunner.query(` + ALTER TABLE \`applications\` + ADD \`redirect_uris\` text NULL + `); + await queryRunner.query(` + ALTER TABLE \`applications\` + ADD \`hook\` tinyint NOT NULL + `); + await queryRunner.query(` + ALTER TABLE \`applications\` + ADD \`type\` text NULL + `); + await queryRunner.query(` + ALTER TABLE \`users\` + ADD \`settings\` text NOT NULL + `); + await queryRunner.query(` + DROP TABLE \`user_settings\` + `); + await queryRunner.query(` + CREATE UNIQUE INDEX \`REL_2ce5a55796fe4c2f77ece57a64\` ON \`applications\` (\`bot_user_id\`) + `); + await queryRunner.query(` + ALTER TABLE \`applications\` + ADD CONSTRAINT \`FK_2ce5a55796fe4c2f77ece57a647\` FOREIGN KEY (\`bot_user_id\`) REFERENCES \`users\`(\`id\`) ON DELETE NO ACTION ON UPDATE NO ACTION + `); + } +} diff --git a/src/util/migrations/mariadb/1660260587556-CodeCleanup4.ts b/src/util/migrations/mariadb/1660260587556-CodeCleanup4.ts new file mode 100644
index 00000000..aa750f17 --- /dev/null +++ b/src/util/migrations/mariadb/1660260587556-CodeCleanup4.ts
@@ -0,0 +1,38 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class CodeCleanup41660260587556 implements MigrationInterface { + name = "CodeCleanup41660260587556"; + + public async up(queryRunner: QueryRunner): Promise<void> { + await queryRunner.query(` + ALTER TABLE \`users\` + ADD \`settingsId\` varchar(255) NULL + `); + await queryRunner.query(` + ALTER TABLE \`users\` + ADD UNIQUE INDEX \`IDX_76ba283779c8441fd5ff819c8c\` (\`settingsId\`) + `); + await queryRunner.query(` + CREATE UNIQUE INDEX \`REL_76ba283779c8441fd5ff819c8c\` ON \`users\` (\`settingsId\`) + `); + await queryRunner.query(` + ALTER TABLE \`users\` + ADD CONSTRAINT \`FK_76ba283779c8441fd5ff819c8cf\` FOREIGN KEY (\`settingsId\`) REFERENCES \`user_settings\`(\`id\`) ON DELETE NO ACTION ON UPDATE NO ACTION + `); + } + + public async down(queryRunner: QueryRunner): Promise<void> { + await queryRunner.query(` + ALTER TABLE \`users\` DROP FOREIGN KEY \`FK_76ba283779c8441fd5ff819c8cf\` + `); + await queryRunner.query(` + DROP INDEX \`REL_76ba283779c8441fd5ff819c8c\` ON \`users\` + `); + await queryRunner.query(` + ALTER TABLE \`users\` DROP INDEX \`IDX_76ba283779c8441fd5ff819c8c\` + `); + await queryRunner.query(` + ALTER TABLE \`users\` DROP COLUMN \`settingsId\` + `); + } +} diff --git a/src/util/migrations/mariadb/1660265930624-CodeCleanup5.ts b/src/util/migrations/mariadb/1660265930624-CodeCleanup5.ts new file mode 100644
index 00000000..6629d4d4 --- /dev/null +++ b/src/util/migrations/mariadb/1660265930624-CodeCleanup5.ts
@@ -0,0 +1,52 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class CodeCleanup51660265930624 implements MigrationInterface { + name = "CodeCleanup51660265930624"; + + public async up(queryRunner: QueryRunner): Promise<void> { + await queryRunner.query(` + ALTER TABLE \`users\` + ADD \`settingsId\` varchar(255) NULL + `); + await queryRunner.query(` + ALTER TABLE \`users\` + ADD UNIQUE INDEX \`IDX_76ba283779c8441fd5ff819c8c\` (\`settingsId\`) + `); + await queryRunner.query(` + ALTER TABLE \`channels\` + ADD \`flags\` int NULL + `); + await queryRunner.query(` + ALTER TABLE \`channels\` + ADD \`default_thread_rate_limit_per_user\` int NULL + `); + await queryRunner.query(` + CREATE UNIQUE INDEX \`REL_76ba283779c8441fd5ff819c8c\` ON \`users\` (\`settingsId\`) + `); + await queryRunner.query(` + ALTER TABLE \`users\` + ADD CONSTRAINT \`FK_76ba283779c8441fd5ff819c8cf\` FOREIGN KEY (\`settingsId\`) REFERENCES \`user_settings\`(\`id\`) ON DELETE NO ACTION ON UPDATE NO ACTION + `); + } + + public async down(queryRunner: QueryRunner): Promise<void> { + await queryRunner.query(` + ALTER TABLE \`users\` DROP FOREIGN KEY \`FK_76ba283779c8441fd5ff819c8cf\` + `); + await queryRunner.query(` + DROP INDEX \`REL_76ba283779c8441fd5ff819c8c\` ON \`users\` + `); + await queryRunner.query(` + ALTER TABLE \`channels\` DROP COLUMN \`default_thread_rate_limit_per_user\` + `); + await queryRunner.query(` + ALTER TABLE \`channels\` DROP COLUMN \`flags\` + `); + await queryRunner.query(` + ALTER TABLE \`users\` DROP INDEX \`IDX_76ba283779c8441fd5ff819c8c\` + `); + await queryRunner.query(` + ALTER TABLE \`users\` DROP COLUMN \`settingsId\` + `); + } +} diff --git a/src/util/plugin/Plugin.ts b/src/util/plugin/Plugin.ts new file mode 100644
index 00000000..ee4fd95c --- /dev/null +++ b/src/util/plugin/Plugin.ts
@@ -0,0 +1,13 @@ +import { TypedEventEmitter } from "@fosscord/util"; +import EventEmitter from "events"; + +type PluginEvents = { + error: (error: Error | unknown) => void; + loaded: () => void; +}; + +export class Plugin extends (EventEmitter as new () => TypedEventEmitter<PluginEvents>) { + async init() { + // insert default config into database? + } +} diff --git a/src/util/plugin/PluginLoader.ts b/src/util/plugin/PluginLoader.ts new file mode 100644
index 00000000..000f3345 --- /dev/null +++ b/src/util/plugin/PluginLoader.ts
@@ -0,0 +1,39 @@ +import fs from "fs"; +import path from "path"; +import { Plugin, PluginManifest } from "./"; + +const root = process.env.PLUGIN_LOCATION || "../plugins"; + +let pluginsLoaded = false; +export class PluginLoader { + public static loadPlugins() { + console.log(`Plugin root directory: ${path.resolve(root)}`); + const dirs = fs.readdirSync(root).filter((x) => { + try { + fs.readdirSync(path.join(root, x)); + return true; + } catch (e) { + return false; + } + }); + console.log(dirs); + dirs.forEach(async (x) => { + let modPath = path.resolve(path.join(root, x)); + console.log(`Trying to load plugin: ${modPath}`); + const manifest = require(path.join(modPath, "plugin.json")) as PluginManifest; + console.log( + `Plugin info: ${manifest.name} (${manifest.id}), written by ${manifest.authors}, available at ${manifest.repository}` + ); + const module_ = require(path.join(modPath, "dist", "index.js")) as Plugin; + try { + await module_.init(); + module_.emit("loaded"); + } catch (error) { + module_.emit("error", error); + } + }); + + // + //module_.pluginPath = + } +} diff --git a/src/util/plugin/PluginManifest.ts b/src/util/plugin/PluginManifest.ts new file mode 100644
index 00000000..d940f2c8 --- /dev/null +++ b/src/util/plugin/PluginManifest.ts
@@ -0,0 +1,9 @@ +export class PluginManifest { + id: string; + name: string; + authors: string[]; + repository: string; + license: string; + version: string; // semver + versionCode: number; // integer +} diff --git a/src/util/plugin/index.ts b/src/util/plugin/index.ts new file mode 100644
index 00000000..c4c0c2ac --- /dev/null +++ b/src/util/plugin/index.ts
@@ -0,0 +1,3 @@ +export * from "./Plugin"; +export * from "./PluginLoader"; +export * from "./PluginManifest"; diff --git a/src/util/schemas/SelectProtocolSchema.ts b/src/util/schemas/SelectProtocolSchema.ts new file mode 100644
index 00000000..0ba0c23b --- /dev/null +++ b/src/util/schemas/SelectProtocolSchema.ts
@@ -0,0 +1,19 @@ +export interface SelectProtocolSchema { + protocol: "webrtc" | "udp"; + data: + | string + | { + address: string; + port: number; + mode: string; + }; + sdp?: string; + codecs?: { + name: "opus" | "VP8" | "VP9" | "H264"; + type: "audio" | "video"; + priority: number; + payload_type: number; + rtx_payload_type?: number | null; + }[]; + rtc_connection_id?: string; // uuid +} diff --git a/src/util/schemas/Validator.ts b/src/util/schemas/Validator.ts new file mode 100644
index 00000000..d506e14c --- /dev/null +++ b/src/util/schemas/Validator.ts
@@ -0,0 +1,54 @@ +import Ajv from "ajv"; +import addFormats from "ajv-formats"; +import fs from "fs"; +import path from "path"; + +const SchemaPath = path.join(__dirname, "..", "..", "..", "assets", "schemas.json"); +const schemas = JSON.parse(fs.readFileSync(SchemaPath, { encoding: "utf8" })); + +export const ajv = new Ajv({ + allErrors: true, + parseDate: true, + allowDate: true, + schemas, + coerceTypes: true, + messages: true, + strict: true, + strictRequired: true +}); + +addFormats(ajv); + +export function validateSchema<G>(schema: string, data: G): G { + const valid = ajv.validate(schema, normalizeBody(data)); + if (!valid) throw ajv.errors; + return data; +} + +// Normalizer is introduced to workaround https://github.com/ajv-validator/ajv/issues/1287 +// this removes null values as ajv doesn't treat them as undefined +// normalizeBody allows to handle circular structures without issues +// taken from https://github.com/serverless/serverless/blob/master/lib/classes/ConfigSchemaHandler/index.js#L30 (MIT license) +export const normalizeBody = (body: any = {}) => { + const normalizedObjectsSet = new WeakSet(); + const normalizeObject = (object: any) => { + if (normalizedObjectsSet.has(object)) return; + normalizedObjectsSet.add(object); + if (Array.isArray(object)) { + for (const [index, value] of object.entries()) { + if (typeof value === "object") normalizeObject(value); + } + } else { + for (const [key, value] of Object.entries(object)) { + if (value == null) { + if (key === "icon" || key === "avatar" || key === "banner" || key === "splash" || key === "discovery_splash") continue; + delete object[key]; + } else if (typeof value === "object") { + normalizeObject(value); + } + } + } + }; + normalizeObject(body); + return body; +}; diff --git a/src/util/schemas/VoiceIdentifySchema.ts b/src/util/schemas/VoiceIdentifySchema.ts new file mode 100644
index 00000000..df023713 --- /dev/null +++ b/src/util/schemas/VoiceIdentifySchema.ts
@@ -0,0 +1,12 @@ +export interface VoiceIdentifySchema { + server_id: string; + user_id: string; + session_id: string; + token: string; + video?: boolean; + streams?: { + type: string; + rid: string; + quality: number; + }[]; +} diff --git a/src/util/schemas/VoiceVideoSchema.ts b/src/util/schemas/VoiceVideoSchema.ts new file mode 100644
index 00000000..0ba519e1 --- /dev/null +++ b/src/util/schemas/VoiceVideoSchema.ts
@@ -0,0 +1,17 @@ +export interface VoiceVideoSchema { + audio_ssrc: number; + video_ssrc: number; + rtx_ssrc?: number; + user_id?: string; + streams?: { + type: "video" | "audio"; + rid: string; + ssrc: number; + active: boolean; + quality: number; + rtx_ssrc: number; + max_bitrate: number; + max_framerate: number; + max_resolution: { type: string; width: number; height: number }; + }[]; +} diff --git a/src/util/schemas/index.ts b/src/util/schemas/index.ts
index a15ab4b0..9f796cc3 100644 --- a/src/util/schemas/index.ts +++ b/src/util/schemas/index.ts
@@ -30,6 +30,7 @@ export * from "./RelationshipPostSchema"; export * from "./RelationshipPutSchema"; export * from "./RoleModifySchema"; export * from "./RolePositionUpdateSchema"; +export * from "./SelectProtocolSchema"; export * from "./TemplateCreateSchema"; export * from "./TemplateModifySchema"; export * from "./TotpDisableSchema"; @@ -37,7 +38,10 @@ export * from "./TotpEnableSchema"; export * from "./TotpSchema"; export * from "./UserModifySchema"; export * from "./UserSettingsSchema"; +export * from "./Validator"; export * from "./VanityUrlSchema"; +export * from "./VoiceIdentifySchema"; export * from "./VoiceStateUpdateSchema"; +export * from "./VoiceVideoSchema"; export * from "./WebhookCreateSchema"; export * from "./WidgetModifySchema"; diff --git a/src/util/util/Database.ts b/src/util/util/Database.ts
index 647de26a..247c5715 100644 --- a/src/util/util/Database.ts +++ b/src/util/util/Database.ts
@@ -91,6 +91,7 @@ function getDataSourceOptions(): DataSourceOptions { cache: { duration: 1000 * 3 // cache all find queries for 3 seconds }, + // relationLoadStrategy: "query", bigNumberStrings: false, supportBigNumbers: true, name: "default", diff --git a/src/util/util/NamingStrategy.ts b/src/util/util/NamingStrategy.ts new file mode 100644
index 00000000..1ff256d6 --- /dev/null +++ b/src/util/util/NamingStrategy.ts
@@ -0,0 +1,12 @@ +import { DefaultNamingStrategy } from "typeorm"; + +export class NamingStrategy extends DefaultNamingStrategy { + eagerJoinRelationAlias(alias: string, propertyPath: string) { + const result = super.eagerJoinRelationAlias(alias, propertyPath); + + console.log({ alias, propertyPath, result }); + return result; + } +} + +export const namingStrategy = new NamingStrategy(); diff --git a/src/util/util/imports/TypedEmitter.ts b/src/util/util/imports/TypedEmitter.ts new file mode 100644
index 00000000..7a0fffe2 --- /dev/null +++ b/src/util/util/imports/TypedEmitter.ts
@@ -0,0 +1,41 @@ +export type EventMap = { + [key: string]: (...args: any[]) => void; +}; + +/** + * Type-safe event emitter. + * + * Use it like this: + * + * ```typescript + * type MyEvents = { + * error: (error: Error) => void; + * message: (from: string, content: string) => void; + * } + * + * const myEmitter = new EventEmitter() as TypedEmitter<MyEvents>; + * + * myEmitter.emit("error", "x") // <- Will catch this type error; + * ``` + */ +export interface TypedEventEmitter<Events extends EventMap> { + addListener<E extends keyof Events>(event: E, listener: Events[E]): this; + on<E extends keyof Events>(event: E, listener: Events[E]): this; + once<E extends keyof Events>(event: E, listener: Events[E]): this; + prependListener<E extends keyof Events>(event: E, listener: Events[E]): this; + prependOnceListener<E extends keyof Events>(event: E, listener: Events[E]): this; + + off<E extends keyof Events>(event: E, listener: Events[E]): this; + removeAllListeners<E extends keyof Events>(event?: E): this; + removeListener<E extends keyof Events>(event: E, listener: Events[E]): this; + + emit<E extends keyof Events>(event: E, ...args: Parameters<Events[E]>): boolean; + // The sloppy `eventNames()` return type is to mitigate type incompatibilities - see #5 + eventNames(): (keyof Events | string | symbol)[]; + rawListeners<E extends keyof Events>(event: E): Events[E][]; + listeners<E extends keyof Events>(event: E): Events[E][]; + listenerCount<E extends keyof Events>(event: E): number; + + getMaxListeners(): number; + setMaxListeners(maxListeners: number): this; +} diff --git a/src/webrtc/Server.ts b/src/webrtc/Server.ts new file mode 100644
index 00000000..071df144 --- /dev/null +++ b/src/webrtc/Server.ts
@@ -0,0 +1,56 @@ +import { closeDatabase, Config, getOrInitialiseDatabase, initEvent } from "@fosscord/util"; +import dotenv from "dotenv"; +import http from "http"; +import ws from "ws"; +import { Connection } from "./events/Connection"; +dotenv.config(); + +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) => { + if (!request.url?.includes("voice")) return; + this.ws.handleUpgrade(request, socket, head, (socket) => { + // @ts-ignore + socket.server = this; + this.ws.emit("connection", socket, request); + }); + }); + + this.ws = new ws.Server({ + maxPayload: 1024 * 1024 * 100, + noServer: true + }); + this.ws.on("connection", Connection); + this.ws.on("error", console.error); + } + + async start(): Promise<void> { + await getOrInitialiseDatabase(); + await Config.init(); + await initEvent(); + if (!this.server.listening) { + this.server.listen(this.port); + console.log(`[WebRTC] online on 0.0.0.0:${this.port}`); + } + } + + async stop() { + closeDatabase(); + this.server.close(); + } +} diff --git a/src/webrtc/events/Close.ts b/src/webrtc/events/Close.ts new file mode 100644
index 00000000..4cf80bb2 --- /dev/null +++ b/src/webrtc/events/Close.ts
@@ -0,0 +1,9 @@ +import { WebSocket } from "@fosscord/gateway"; +import { Session } from "@fosscord/util"; + +export async function onClose(this: WebSocket, code: number, reason: string) { + console.log("[WebRTC] closed", code, reason.toString()); + + if (this.session_id) await Session.delete({ session_id: this.session_id }); + this.removeAllListeners(); +} diff --git a/src/webrtc/events/Connection.ts b/src/webrtc/events/Connection.ts new file mode 100644
index 00000000..741fa320 --- /dev/null +++ b/src/webrtc/events/Connection.ts
@@ -0,0 +1,60 @@ +import { CloseCodes, Send, setHeartbeat, WebSocket } from "@fosscord/gateway"; +import { IncomingMessage } from "http"; +import { URL } from "url"; +import WS from "ws"; +import { VoiceOPCodes } from "../util"; +import { onClose } from "./Close"; +import { onMessage } from "./Message"; +var 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 { + socket.on("close", onClose.bind(socket)); + socket.on("message", onMessage.bind(socket)); + console.log("[WebRTC] new connection", request.url); + + if (process.env.WS_LOGEVENTS) { + [ + "close", + "error", + "upgrade", + //"message", + "open", + "ping", + "pong", + "unexpected-response" + ].forEach((x) => { + socket.on(x, (y) => console.log("[WebRTC]", x, y)); + }); + } + + const { searchParams } = new URL(`http://localhost${request.url}`); + + socket.encoding = "json"; + socket.version = Number(searchParams.get("v")) || 5; + if (socket.version < 3) return socket.close(CloseCodes.Unknown_error, "invalid version"); + + setHeartbeat(socket); + + socket.readyTimeout = setTimeout(() => { + return socket.close(CloseCodes.Session_timed_out); + }, 1000 * 30); + + await Send(socket, { + op: VoiceOPCodes.HELLO, + d: { + heartbeat_interval: 1000 * 30 + } + }); + } catch (error) { + console.error("[WebRTC]", error); + return socket.close(CloseCodes.Unknown_error); + } +} diff --git a/src/webrtc/events/Message.ts b/src/webrtc/events/Message.ts new file mode 100644
index 00000000..85697710 --- /dev/null +++ b/src/webrtc/events/Message.ts
@@ -0,0 +1,38 @@ +import { CloseCodes, Payload, WebSocket } from "@fosscord/gateway"; +import { Tuple } from "lambert-server"; +import OPCodeHandlers from "../opcodes"; +import { VoiceOPCodes } from "../util"; + +const PayloadSchema = { + op: Number, + $d: new Tuple(Object, Number), // or number for heartbeat sequence + $s: Number, + $t: String +}; + +export async function onMessage(this: WebSocket, buffer: Buffer) { + try { + var data: Payload = JSON.parse(buffer.toString()); + if (data.op !== VoiceOPCodes.IDENTIFY && !this.user_id) return this.close(CloseCodes.Not_authenticated); + + // @ts-ignore + const OPCodeHandler = OPCodeHandlers[data.op]; + if (!OPCodeHandler) { + // @ts-ignore + console.error("[WebRTC] Unkown opcode " + VoiceOPCodes[data.op]); + // TODO: if all opcodes are implemented comment this out: + // this.close(CloseCodes.Unknown_opcode); + return; + } + + if (![VoiceOPCodes.HEARTBEAT, VoiceOPCodes.SPEAKING].includes(data.op as VoiceOPCodes)) { + // @ts-ignore + console.log("[WebRTC] Opcode " + VoiceOPCodes[data.op]); + } + + return await OPCodeHandler.call(this, data); + } catch (error) { + console.error("[WebRTC] error", error); + // if (!this.CLOSED && this.CLOSING) return this.close(CloseCodes.Unknown_error); + } +} diff --git a/src/webrtc/index.ts b/src/webrtc/index.ts new file mode 100644
index 00000000..ccb088ac --- /dev/null +++ b/src/webrtc/index.ts
@@ -0,0 +1,2 @@ +export * from "./Server"; +export * from "./util/index"; diff --git a/src/webrtc/opcodes/BackendVersion.ts b/src/webrtc/opcodes/BackendVersion.ts new file mode 100644
index 00000000..d9e7c598 --- /dev/null +++ b/src/webrtc/opcodes/BackendVersion.ts
@@ -0,0 +1,6 @@ +import { Payload, Send, WebSocket } from "@fosscord/gateway"; +import { VoiceOPCodes } from "../util"; + +export async function onBackendVersion(this: WebSocket, data: Payload) { + await Send(this, { op: VoiceOPCodes.VOICE_BACKEND_VERSION, d: { voice: "0.8.43", rtc_worker: "0.3.26" } }); +} diff --git a/src/webrtc/opcodes/Heartbeat.ts b/src/webrtc/opcodes/Heartbeat.ts new file mode 100644
index 00000000..ea8390d7 --- /dev/null +++ b/src/webrtc/opcodes/Heartbeat.ts
@@ -0,0 +1,9 @@ +import { CloseCodes, Payload, Send, setHeartbeat, WebSocket } from "@fosscord/gateway"; +import { VoiceOPCodes } from "../util"; + +export async function onHeartbeat(this: WebSocket, data: Payload) { + setHeartbeat(this); + if (isNaN(data.d)) return this.close(CloseCodes.Decode_error); + + await Send(this, { op: VoiceOPCodes.HEARTBEAT_ACK, d: data.d }); +} diff --git a/src/webrtc/opcodes/Identify.ts b/src/webrtc/opcodes/Identify.ts new file mode 100644
index 00000000..5f1253c2 --- /dev/null +++ b/src/webrtc/opcodes/Identify.ts
@@ -0,0 +1,60 @@ +import { CloseCodes, Payload, Send, WebSocket } from "@fosscord/gateway"; +import { validateSchema, VoiceIdentifySchema, VoiceState } from "@fosscord/util"; +import { endpoint, getClients, VoiceOPCodes } from "@fosscord/webrtc"; +import SemanticSDP from "semantic-sdp"; +const defaultSDP = require("../../../assets/sdp.json"); + +export async function onIdentify(this: WebSocket, data: Payload) { + clearTimeout(this.readyTimeout); + const { server_id, user_id, session_id, token, streams, video } = validateSchema("VoiceIdentifySchema", data.d) as VoiceIdentifySchema; + + const voiceState = await VoiceState.findOneBy({ guild_id: server_id, user_id, token, session_id }); + if (!voiceState) return this.close(CloseCodes.Authentication_failed); + + this.user_id = user_id; + this.session_id = session_id; + const sdp = SemanticSDP.SDPInfo.expand(defaultSDP); + sdp.setDTLS(SemanticSDP.DTLSInfo.expand({ setup: "actpass", hash: "sha-256", fingerprint: endpoint.getDTLSFingerprint() })); + + this.client = { + websocket: this, + out: { + tracks: new Map() + }, + in: { + audio_ssrc: 0, + video_ssrc: 0, + rtx_ssrc: 0 + }, + sdp, + channel_id: voiceState.channel_id + }; + + const clients = getClients(voiceState.channel_id)!; + clients.add(this.client); + + this.on("close", () => { + clients.delete(this.client); + }); + + await Send(this, { + op: VoiceOPCodes.READY, + d: { + streams: [ + // { type: "video", ssrc: this.ssrc + 1, rtx_ssrc: this.ssrc + 2, rid: "100", quality: 100, active: false } + ], + ssrc: -1, + port: endpoint.getLocalPort(), + modes: [ + "aead_aes256_gcm_rtpsize", + "aead_aes256_gcm", + "xsalsa20_poly1305_lite_rtpsize", + "xsalsa20_poly1305_lite", + "xsalsa20_poly1305_suffix", + "xsalsa20_poly1305" + ], + ip: "127.0.0.1", + experiments: [] + } + }); +} diff --git a/src/webrtc/opcodes/SelectProtocol.ts b/src/webrtc/opcodes/SelectProtocol.ts new file mode 100644
index 00000000..913ddfff --- /dev/null +++ b/src/webrtc/opcodes/SelectProtocol.ts
@@ -0,0 +1,44 @@ +import { Payload, Send, WebSocket } from "@fosscord/gateway"; +import { SelectProtocolSchema, validateSchema } from "@fosscord/util"; +import { endpoint, PublicIP, VoiceOPCodes } from "@fosscord/webrtc"; +import SemanticSDP from "semantic-sdp"; + +export async function onSelectProtocol(this: WebSocket, payload: Payload) { + const data = validateSchema("SelectProtocolSchema", payload.d) as SelectProtocolSchema; + + const offer = SemanticSDP.SDPInfo.parse("m=audio\n" + data.sdp!); + this.client.sdp!.setICE(offer.getICE()); + this.client.sdp!.setDTLS(offer.getDTLS()); + + const transport = endpoint.createTransport(this.client.sdp!); + this.client.transport = transport; + transport.setRemoteProperties(this.client.sdp!); + transport.setLocalProperties(this.client.sdp!); + + const dtls = transport.getLocalDTLSInfo(); + const ice = transport.getLocalICEInfo(); + const port = endpoint.getLocalPort(); + const fingerprint = dtls.getHash() + " " + dtls.getFingerprint(); + const candidates = transport.getLocalCandidates(); + const candidate = candidates[0]; + + const answer = `m=audio ${port} ICE/SDP +a=fingerprint:${fingerprint} +c=IN IP4 ${PublicIP} +a=rtcp:${port} +a=ice-ufrag:${ice.getUfrag()} +a=ice-pwd:${ice.getPwd()} +a=fingerprint:${fingerprint} +a=candidate:1 1 ${candidate.getTransport()} ${candidate.getFoundation()} ${candidate.getAddress()} ${candidate.getPort()} typ host +`; + + await Send(this, { + op: VoiceOPCodes.SELECT_PROTOCOL_ACK, + d: { + video_codec: "H264", + sdp: answer, + media_session_id: this.session_id, + audio_codec: "opus" + } + }); +} diff --git a/src/webrtc/opcodes/Speaking.ts b/src/webrtc/opcodes/Speaking.ts new file mode 100644
index 00000000..ceeace5b --- /dev/null +++ b/src/webrtc/opcodes/Speaking.ts
@@ -0,0 +1,22 @@ +import { Payload, Send, WebSocket } from "@fosscord/gateway"; +import { getClients, VoiceOPCodes } from "../util"; + +// {"speaking":1,"delay":5,"ssrc":2805246727} + +export async function onSpeaking(this: WebSocket, data: Payload) { + if (!this.client) return; + + getClients(this.client.channel_id).forEach((client) => { + if (client === this.client) return; + const ssrc = this.client.out.tracks.get(client.websocket.user_id); + + Send(client.websocket, { + op: VoiceOPCodes.SPEAKING, + d: { + user_id: client.websocket.user_id, + speaking: data.d.speaking, + ssrc: ssrc?.audio_ssrc || 0 + } + }); + }); +} diff --git a/src/webrtc/opcodes/Video.ts b/src/webrtc/opcodes/Video.ts new file mode 100644
index 00000000..bb376e28 --- /dev/null +++ b/src/webrtc/opcodes/Video.ts
@@ -0,0 +1,117 @@ +import { Payload, Send, WebSocket } from "@fosscord/gateway"; +import { validateSchema, VoiceVideoSchema } from "@fosscord/util"; +import { channels, getClients, VoiceOPCodes } from "@fosscord/webrtc"; +import { IncomingStreamTrack, SSRCs } from "medooze-media-server"; +import SemanticSDP from "semantic-sdp"; + +export async function onVideo(this: WebSocket, payload: Payload) { + if (!this.client) return; + const { transport, channel_id } = this.client; + if (!transport) return; + const d = validateSchema("VoiceVideoSchema", payload.d) as VoiceVideoSchema; + + await Send(this, { op: VoiceOPCodes.MEDIA_SINK_WANTS, d: { any: 100 } }); + + const id = "stream" + this.user_id; + + var stream = this.client.in.stream!; + if (!stream) { + stream = this.client.transport!.createIncomingStream( + // @ts-ignore + SemanticSDP.StreamInfo.expand({ + id, + // @ts-ignore + tracks: [] + }) + ); + this.client.in.stream = stream; + + const interval = setInterval(() => { + for (const track of stream.getTracks()) { + for (const layer of Object.values(track.getStats())) { + console.log(track.getId(), layer.total); + } + } + }, 5000); + + stream.on("stopped", () => { + console.log("stream stopped"); + clearInterval(interval); + }); + this.on("close", () => { + transport!.stop(); + }); + const out = transport.createOutgoingStream( + // @ts-ignore + SemanticSDP.StreamInfo.expand({ + id: "out" + this.user_id, + // @ts-ignore + tracks: [] + }) + ); + this.client.out.stream = out; + + const clients = channels.get(channel_id)!; + + clients.forEach((client) => { + if (client.websocket.user_id === this.user_id) return; + if (!client.in.stream) return; + + client.in.stream?.getTracks().forEach((track) => { + attachTrack.call(this, track, client.websocket.user_id); + }); + }); + } + + if (d.audio_ssrc) { + handleSSRC.call(this, "audio", { media: d.audio_ssrc, rtx: d.audio_ssrc + 1 }); + } + if (d.video_ssrc && d.rtx_ssrc) { + handleSSRC.call(this, "video", { media: d.video_ssrc, rtx: d.rtx_ssrc }); + } +} + +function attachTrack(this: WebSocket, track: IncomingStreamTrack, user_id: string) { + if (!this.client) return; + const outTrack = this.client.transport!.createOutgoingStreamTrack(track.getMedia()); + outTrack.attachTo(track); + this.client.out.stream!.addTrack(outTrack); + var ssrcs = this.client.out.tracks.get(user_id)!; + if (!ssrcs) ssrcs = this.client.out.tracks.set(user_id, { audio_ssrc: 0, rtx_ssrc: 0, video_ssrc: 0 }).get(user_id)!; + + if (track.getMedia() === "audio") { + ssrcs.audio_ssrc = outTrack.getSSRCs().media!; + } else if (track.getMedia() === "video") { + ssrcs.video_ssrc = outTrack.getSSRCs().media!; + ssrcs.rtx_ssrc = outTrack.getSSRCs().rtx!; + } + + Send(this, { + op: VoiceOPCodes.VIDEO, + d: { + user_id: user_id, + ...ssrcs + } as VoiceVideoSchema + }); +} + +function handleSSRC(this: WebSocket, type: "audio" | "video", ssrcs: SSRCs) { + const stream = this.client.in.stream!; + const transport = this.client.transport!; + + const id = type + ssrcs.media; + var track = stream.getTrack(id); + if (!track) { + console.log("createIncomingStreamTrack", id); + track = transport.createIncomingStreamTrack(type, { id, ssrcs }); + stream.addTrack(track); + + const clients = getClients(this.client.channel_id)!; + clients.forEach((client) => { + if (client.websocket.user_id === this.user_id) return; + if (!client.out.stream) return; + + attachTrack.call(this, track, client.websocket.user_id); + }); + } +} diff --git a/src/webrtc/opcodes/index.ts b/src/webrtc/opcodes/index.ts new file mode 100644
index 00000000..8e36f2ae --- /dev/null +++ b/src/webrtc/opcodes/index.ts
@@ -0,0 +1,19 @@ +import { Payload, WebSocket } from "@fosscord/gateway"; +import { VoiceOPCodes } from "../util"; +import { onBackendVersion } from "./BackendVersion"; +import { onHeartbeat } from "./Heartbeat"; +import { onIdentify } from "./Identify"; +import { onSelectProtocol } from "./SelectProtocol"; +import { onSpeaking } from "./Speaking"; +import { onVideo } from "./Video"; + +export type OPCodeHandler = (this: WebSocket, data: Payload) => any; + +export default { + [VoiceOPCodes.HEARTBEAT]: onHeartbeat, + [VoiceOPCodes.IDENTIFY]: onIdentify, + [VoiceOPCodes.VOICE_BACKEND_VERSION]: onBackendVersion, + [VoiceOPCodes.VIDEO]: onVideo, + [VoiceOPCodes.SPEAKING]: onSpeaking, + [VoiceOPCodes.SELECT_PROTOCOL]: onSelectProtocol +}; diff --git a/src/webrtc/start.ts b/src/webrtc/start.ts new file mode 100644
index 00000000..899a98c1 --- /dev/null +++ b/src/webrtc/start.ts
@@ -0,0 +1,13 @@ +process.on("uncaughtException", console.error); +process.on("unhandledRejection", console.error); + +import { config } from "dotenv"; +import { Server } from "./Server"; +config(); + +const port = Number(process.env.PORT) || 3004; + +const server = new Server({ + port +}); +server.start(); diff --git a/src/webrtc/util/Constants.ts b/src/webrtc/util/Constants.ts new file mode 100644
index 00000000..ee662cac --- /dev/null +++ b/src/webrtc/util/Constants.ts
@@ -0,0 +1,26 @@ +export enum VoiceStatus { + CONNECTED = 0, + CONNECTING = 1, + AUTHENTICATING = 2, + RECONNECTING = 3, + DISCONNECTED = 4 +} + +export enum VoiceOPCodes { + IDENTIFY = 0, + SELECT_PROTOCOL = 1, + READY = 2, + HEARTBEAT = 3, + SELECT_PROTOCOL_ACK = 4, + SPEAKING = 5, + HEARTBEAT_ACK = 6, + RESUME = 7, + HELLO = 8, + RESUMED = 9, + VIDEO = 12, + CLIENT_DISCONNECT = 13, + SESSION_UPDATE = 14, + MEDIA_SINK_WANTS = 15, + VOICE_BACKEND_VERSION = 16, + CHANNEL_OPTIONS_UPDATE = 17 +} diff --git a/src/webrtc/util/MediaServer.ts b/src/webrtc/util/MediaServer.ts new file mode 100644
index 00000000..8e246313 --- /dev/null +++ b/src/webrtc/util/MediaServer.ts
@@ -0,0 +1,51 @@ +import { WebSocket } from "@fosscord/gateway"; +import MediaServer, { IncomingStream, OutgoingStream, Transport } from "medooze-media-server"; +import SemanticSDP from "semantic-sdp"; +MediaServer.enableLog(true); + +export const PublicIP = process.env.PUBLIC_IP || "127.0.0.1"; + +try { + const range = process.env.WEBRTC_PORT_RANGE || "4000"; + var ports = range.split("-"); + const min = Number(ports[0]); + const max = Number(ports[1]); + + MediaServer.setPortRange(min, max); +} catch (error) { + console.error("Invalid env var: WEBRTC_PORT_RANGE", process.env.WEBRTC_PORT_RANGE, error); + process.exit(1); +} + +export const endpoint = MediaServer.createEndpoint(PublicIP); + +export const channels = new Map<string, Set<Client>>(); + +export interface Client { + transport?: Transport; + websocket: WebSocket; + out: { + stream?: OutgoingStream; + tracks: Map< + string, + { + audio_ssrc: number; + video_ssrc: number; + rtx_ssrc: number; + } + >; + }; + in: { + stream?: IncomingStream; + audio_ssrc: number; + video_ssrc: number; + rtx_ssrc: number; + }; + sdp: SemanticSDP.SDPInfo; + channel_id: string; +} + +export function getClients(channel_id: string) { + if (!channels.has(channel_id)) channels.set(channel_id, new Set()); + return channels.get(channel_id)!; +} diff --git a/src/webrtc/util/index.ts b/src/webrtc/util/index.ts new file mode 100644
index 00000000..f0d49049 --- /dev/null +++ b/src/webrtc/util/index.ts
@@ -0,0 +1,2 @@ +export * from "./Constants"; +export * from "./MediaServer";