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";
|