summary refs log tree commit diff
path: root/gateway/src/opcodes
diff options
context:
space:
mode:
authorFlam3rboy <34555296+Flam3rboy@users.noreply.github.com>2021-08-12 20:18:05 +0200
committerFlam3rboy <34555296+Flam3rboy@users.noreply.github.com>2021-08-12 20:18:05 +0200
commitfa31e7f8db61efe085f7d8a317e6a8640ebb3f46 (patch)
tree5d12bfa42b9ed09e67935c1e6a5063babe33eeb8 /gateway/src/opcodes
parentMerge branch 'master' into gateway (diff)
downloadserver-fa31e7f8db61efe085f7d8a317e6a8640ebb3f46.tar.xz
:sparkles: gateway
Diffstat (limited to 'gateway/src/opcodes')
-rw-r--r--gateway/src/opcodes/Heartbeat.ts12
-rw-r--r--gateway/src/opcodes/Identify.ts174
-rw-r--r--gateway/src/opcodes/LazyRequest.ts105
-rw-r--r--gateway/src/opcodes/PresenceUpdate.ts6
-rw-r--r--gateway/src/opcodes/RequestGuildMembers.ts7
-rw-r--r--gateway/src/opcodes/Resume.ts14
-rw-r--r--gateway/src/opcodes/VoiceStateUpdate.ts26
-rw-r--r--gateway/src/opcodes/experiments.json76
-rw-r--r--gateway/src/opcodes/index.ts25
-rw-r--r--gateway/src/opcodes/instanceOf.ts18
10 files changed, 463 insertions, 0 deletions
diff --git a/gateway/src/opcodes/Heartbeat.ts b/gateway/src/opcodes/Heartbeat.ts
new file mode 100644
index 00000000..015257b9
--- /dev/null
+++ b/gateway/src/opcodes/Heartbeat.ts
@@ -0,0 +1,12 @@
+import { CLOSECODES, Payload } from "../util/Constants";
+import { Send } from "../util/Send";
+import { setHeartbeat } from "../util/setHeartbeat";
+import WebSocket from "../util/WebSocket";
+
+export async function onHeartbeat(this: WebSocket, data: Payload) {
+	// TODO: validate payload
+
+	setHeartbeat(this);
+
+	await Send(this, { op: 11 });
+}
diff --git a/gateway/src/opcodes/Identify.ts b/gateway/src/opcodes/Identify.ts
new file mode 100644
index 00000000..43368367
--- /dev/null
+++ b/gateway/src/opcodes/Identify.ts
@@ -0,0 +1,174 @@
+import { CLOSECODES, Payload, OPCODES } from "../util/Constants";
+import WebSocket from "../util/WebSocket";
+import {
+	ChannelModel,
+	checkToken,
+	GuildModel,
+	Intents,
+	MemberDocument,
+	MemberModel,
+	ReadyEventData,
+	UserModel,
+	toObject,
+	EVENTEnum,
+	Config,
+} from "@fosscord/server-util";
+import { setupListener } from "../listener/listener";
+import { IdentifySchema } from "../schema/Identify";
+import { Send } from "../util/Send";
+// import experiments from "./experiments.json";
+const experiments: any = [];
+import { check } from "./instanceOf";
+
+// TODO: bot sharding
+// TODO: check priviliged intents
+// TODO: check if already identified
+
+export async function onIdentify(this: WebSocket, data: Payload) {
+	clearTimeout(this.readyTimeout);
+	check.call(this, IdentifySchema, data.d);
+
+	const identify: IdentifySchema = data.d;
+
+	try {
+		const { jwtSecret } = Config.get().security;
+		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);
+	}
+	this.user_id = decoded.id;
+	if (!identify.intents) identify.intents = 0b11111111111111n;
+	this.intents = new Intents(identify.intents);
+	if (identify.shard) {
+		this.shard_id = identify.shard[0];
+		this.shard_count = identify.shard[1];
+		if (
+			!this.shard_count ||
+			!this.shard_id ||
+			this.shard_id >= this.shard_count ||
+			this.shard_id < 0 ||
+			this.shard_count <= 0
+		) {
+			return this.close(CLOSECODES.Invalid_shard);
+		}
+	}
+
+	const members = toObject(await MemberModel.find({ id: this.user_id }).exec());
+	const merged_members = members.map((x: any) => {
+		const y = { ...x, user_id: x.id };
+		delete y.settings;
+		delete y.id;
+		return [y];
+	}) as MemberDocument[][];
+	const user_guild_settings_entries = members.map((x) => x.settings);
+
+	const channels = await ChannelModel.find({ recipient_ids: this.user_id }).exec();
+	const user = await UserModel.findOne({ id: this.user_id }).exec();
+	if (!user) return this.close(CLOSECODES.Authentication_failed);
+
+	const public_user = {
+		username: user.username,
+		discriminator: user.discriminator,
+		id: user.id,
+		public_flags: user.public_flags,
+		avatar: user.avatar,
+		bot: user.bot,
+	};
+
+	const guilds = await GuildModel.find({ id: { $in: user.guilds } })
+		.populate({ path: "joined_at", match: { id: this.user_id } })
+		.exec();
+
+	const privateUser = {
+		avatar: user.avatar,
+		mobile: user.mobile,
+		desktop: user.desktop,
+		discriminator: user.discriminator,
+		email: user.email,
+		flags: user.flags,
+		id: user.id,
+		mfa_enabled: user.mfa_enabled,
+		nsfw_allowed: user.nsfw_allowed,
+		phone: user.phone,
+		premium: user.premium,
+		premium_type: user.premium_type,
+		public_flags: user.public_flags,
+		username: user.username,
+		verified: user.verified,
+		bot: user.bot,
+		accent_color: user.accent_color || 0,
+		banner: user.banner,
+	};
+
+	const d: ReadyEventData = {
+		v: 8,
+		user: privateUser,
+		user_settings: user.user_settings,
+		// @ts-ignore
+		guilds: toObject(guilds).map((x) => {
+			// @ts-ignore
+			x.guild_hashes = {
+				channels: { omitted: false, hash: "y4PV2fZ0gmo" },
+				metadata: { omitted: false, hash: "bs1/ckvud3Y" },
+				roles: { omitted: false, hash: "SxA+c5CaYpo" },
+				version: 1,
+			};
+			return x;
+		}),
+		guild_experiments: [], // TODO
+		geo_ordered_rtc_regions: [], // TODO
+		relationships: user.user_data.relationships,
+		read_state: {
+			// TODO
+			entries: [],
+			partial: false,
+			version: 304128,
+		},
+		user_guild_settings: {
+			entries: user_guild_settings_entries,
+			partial: false, // TODO partial
+			version: 642,
+		},
+		// @ts-ignore
+		private_channels: toObject(channels).map((x: ChannelDocument) => {
+			x.recipient_ids = x.recipients.map((y: any) => y.id);
+			delete x.recipients;
+			return x;
+		}),
+		session_id: "", // TODO
+		analytics_token: "", // TODO
+		connected_accounts: [], // TODO
+		consents: {
+			personalization: {
+				consented: false, // TODO
+			},
+		},
+		country_code: user.user_settings.locale,
+		friend_suggestion_count: 0, // TODO
+		// @ts-ignore
+		experiments: experiments, // TODO
+		guild_join_requests: [], // TODO what is this?
+		users: [
+			public_user,
+			...toObject(channels)
+				.map((x: any) => x.recipients)
+				.flat(),
+		].unique(), // TODO
+		merged_members: merged_members,
+		// shard // TODO: only for bots sharding
+		// application // TODO for applications
+	};
+
+	console.log("Send ready");
+
+	// TODO: send real proper data structure
+	await Send(this, {
+		op: OPCODES.Dispatch,
+		t: EVENTEnum.Ready,
+		s: this.sequence++,
+		d,
+	});
+
+	await setupListener.call(this);
+}
diff --git a/gateway/src/opcodes/LazyRequest.ts b/gateway/src/opcodes/LazyRequest.ts
new file mode 100644
index 00000000..b1d553b9
--- /dev/null
+++ b/gateway/src/opcodes/LazyRequest.ts
@@ -0,0 +1,105 @@
+// @ts-nocheck WIP
+import {
+	db,
+	getPermission,
+	MemberModel,
+	MongooseCache,
+	PublicUserProjection,
+	RoleModel,
+	toObject,
+} from "@fosscord/server-util";
+import { LazyRequest } from "../schema/LazyRequest";
+import { OPCODES, Payload } from "../util/Constants";
+import { Send } from "../util/Send";
+import WebSocket from "../util/WebSocket";
+import { check } from "./instanceOf";
+
+// TODO: check permission and only show roles/members that have access to this channel
+// TODO: config: if want to list all members (even those who are offline) sorted by role, or just those who are online
+
+export async function onLazyRequest(this: WebSocket, { d }: Payload) {
+	// TODO: check data
+	check.call(this, LazyRequest, d);
+	const { guild_id, typing, channels, activities } = d as LazyRequest;
+
+	const permissions = await getPermission(this.user_id, guild_id);
+	permissions.hasThrow("VIEW_CHANNEL");
+
+	// MongoDB query to retrieve all hoisted roles and join them with the members and users collection
+	const roles = toObject(
+		await db
+			.collection("roles")
+			.aggregate([
+				{
+					$match: {
+						guild_id,
+						// hoist: true // TODO: also match @everyone role
+					},
+				},
+				{ $sort: { position: 1 } },
+				{
+					$lookup: {
+						from: "members",
+						let: { id: "$id" },
+						pipeline: [
+							{ $match: { $expr: { $in: ["$$id", "$roles"] } } },
+							{ $limit: 100 },
+							{
+								$lookup: {
+									from: "users",
+									let: { user_id: "$id" },
+									pipeline: [
+										{ $match: { $expr: { $eq: ["$id", "$$user_id"] } } },
+										{ $project: PublicUserProjection },
+									],
+									as: "user",
+								},
+							},
+							{
+								$unwind: "$user",
+							},
+						],
+						as: "members",
+					},
+				},
+			])
+			.toArray()
+	);
+
+	const groups = roles.map((x) => ({ id: x.id === guild_id ? "online" : x.id, count: x.members.length }));
+	const member_count = roles.reduce((a, b) => b.members.length + a, 0);
+	const items = [];
+
+	for (const role of roles) {
+		items.push({
+			group: {
+				count: role.members.length,
+				id: role.id === guild_id ? "online" : role.name,
+			},
+		});
+		for (const member of role.members) {
+			member.roles.remove(guild_id);
+			items.push({ member });
+		}
+	}
+
+	return Send(this, {
+		op: OPCODES.Dispatch,
+		s: this.sequence++,
+		t: "GUILD_MEMBER_LIST_UPDATE",
+		d: {
+			ops: [
+				{
+					range: [0, 99],
+					op: "SYNC",
+					items,
+				},
+			],
+			online_count: member_count, // TODO count online count
+			member_count,
+			id: "everyone",
+			guild_id,
+			groups,
+		},
+	});
+}
diff --git a/gateway/src/opcodes/PresenceUpdate.ts b/gateway/src/opcodes/PresenceUpdate.ts
new file mode 100644
index 00000000..3760f8a3
--- /dev/null
+++ b/gateway/src/opcodes/PresenceUpdate.ts
@@ -0,0 +1,6 @@
+import { CLOSECODES, Payload } from "../util/Constants";
+import WebSocket from "../util/WebSocket";
+
+export function onPresenceUpdate(this: WebSocket, data: Payload) {
+	// return this.close(CLOSECODES.Unknown_error);
+}
diff --git a/gateway/src/opcodes/RequestGuildMembers.ts b/gateway/src/opcodes/RequestGuildMembers.ts
new file mode 100644
index 00000000..2701d978
--- /dev/null
+++ b/gateway/src/opcodes/RequestGuildMembers.ts
@@ -0,0 +1,7 @@
+import { CLOSECODES, Payload } from "../util/Constants";
+
+import WebSocket from "../util/WebSocket";
+
+export function onRequestGuildMembers(this: WebSocket, data: Payload) {
+	// return this.close(CLOSECODES.Unknown_error);
+}
diff --git a/gateway/src/opcodes/Resume.ts b/gateway/src/opcodes/Resume.ts
new file mode 100644
index 00000000..4efde9b0
--- /dev/null
+++ b/gateway/src/opcodes/Resume.ts
@@ -0,0 +1,14 @@
+import { CLOSECODES, Payload } from "../util/Constants";
+import { Send } from "../util/Send";
+
+import WebSocket from "../util/WebSocket";
+
+export async function onResume(this: WebSocket, data: Payload) {
+	console.log("Got Resume -> cancel not implemented");
+	await Send(this, {
+		op: 9,
+		d: false,
+	});
+
+	// return this.close(CLOSECODES.Invalid_session);
+}
diff --git a/gateway/src/opcodes/VoiceStateUpdate.ts b/gateway/src/opcodes/VoiceStateUpdate.ts
new file mode 100644
index 00000000..0d51513d
--- /dev/null
+++ b/gateway/src/opcodes/VoiceStateUpdate.ts
@@ -0,0 +1,26 @@
+import { VoiceStateUpdateSchema } from "../schema/VoiceStateUpdate.ts";
+import { CLOSECODES, Payload } from "../util/Constants";
+import { Send } from "../util/Send";
+
+import WebSocket from "../util/WebSocket";
+import { check } from "./instanceOf";
+// TODO: implementation
+// TODO: check if a voice server is setup
+// TODO: save voice servers in database and retrieve them
+// Notice: Bot users respect the voice channel's user limit, if set. When the voice channel is full, you will not receive the Voice State Update or Voice Server Update events in response to your own Voice State Update. Having MANAGE_CHANNELS permission bypasses this limit and allows you to join regardless of the channel being full or not.
+
+export async function onVoiceStateUpdate(this: WebSocket, data: Payload) {
+	check.call(this, VoiceStateUpdateSchema, data.d);
+	const body = data.d as VoiceStateUpdateSchema;
+
+	await Send(this, {
+		op: 0,
+		s: this.sequence++,
+		t: "VOICE_SERVER_UPDATE",
+		d: {
+			token: ``,
+			guild_id: body.guild_id,
+			endpoint: `localhost:3004`,
+		},
+	});
+}
diff --git a/gateway/src/opcodes/experiments.json b/gateway/src/opcodes/experiments.json
new file mode 100644
index 00000000..0370b5da
--- /dev/null
+++ b/gateway/src/opcodes/experiments.json
@@ -0,0 +1,76 @@
+[
+	[4047587481, 0, 0, -1, 0],
+	[1509401575, 0, 1, -1, 0],
+	[1865079242, 0, 1, -1, 0],
+	[1962538549, 1, 0, -1, 0],
+	[3816091942, 3, 2, -1, 0],
+	[4130837190, 0, 10, -1, 0],
+	[1861568052, 0, 1, -1, 0],
+	[2290910058, 6, 2, -1, 0],
+	[1578940118, 1, 1, -1, 0],
+	[1571676964, 0, 1, -1, 2],
+	[3640172371, 0, 2, -1, 2],
+	[1658164312, 2, 1, -1, 0],
+	[98883956, 1, 1, -1, 0],
+	[3114091169, 0, 1, -1, 0],
+	[2570684145, 4, 1, -1, 2],
+	[4007615411, 0, 1, -1, 0],
+	[3665310159, 2, 1, -1, 1],
+	[852550504, 3, 1, -1, 0],
+	[2333572067, 0, 1, -1, 0],
+	[935994771, 1, 1, -1, 0],
+	[1127795596, 1, 1, -1, 0],
+	[4168223991, 0, 1, -1, 0],
+	[18585280, 0, 1, -1, 1],
+	[327482016, 0, 1, -1, 2],
+	[3458098201, 7, 1, -1, 0],
+	[478613943, 2, 1, -1, 1],
+	[2792197902, 0, 1, -1, 2],
+	[284670956, 0, 1, -1, 0],
+	[2099185390, 0, 1, -1, 0],
+	[1202202685, 0, 1, -1, 0],
+	[2122174751, 0, 1, -1, 0],
+	[3633864632, 0, 1, -1, 0],
+	[3103053065, 0, 1, -1, 0],
+	[820624960, 0, 1, -1, 0],
+	[1134479292, 0, 1, -1, 0],
+	[2511257455, 3, 1, -1, 3],
+	[2599708267, 0, 1, -1, 0],
+	[613180822, 1, 1, -1, 0],
+	[2885186814, 0, 1, -1, 0],
+	[221503477, 0, 1, -1, 0],
+	[1054317075, 0, 1, -1, 3],
+	[683872522, 0, 1, -1, 1],
+	[1739278764, 0, 2, -1, 0],
+	[2855249023, 0, 1, -1, 0],
+	[3721841948, 0, 1, -1, 0],
+	[1285203515, 0, 1, -1, 0],
+	[1365487849, 6, 1, -1, 0],
+	[955229746, 0, 1, -1, 0],
+	[3128009767, 0, 10, -1, 0],
+	[441885003, 0, 1, -1, 0],
+	[3433971238, 0, 1, -1, 2],
+	[1038765354, 3, 1, -1, 0],
+	[1174347196, 0, 1, -1, 0],
+	[3649806352, 1, 1, -1, 0],
+	[2973729510, 2, 1, -1, 0],
+	[2571931329, 1, 6, -1, 0],
+	[3884442008, 0, 1, -1, 0],
+	[978673395, 1, 1, -1, 0],
+	[4050927174, 0, 1, -1, 0],
+	[1260103069, 0, 1, -1, 0],
+	[4168894280, 0, 1, -1, 0],
+	[4045587091, 0, 1, -1, 0],
+	[2003494159, 1, 1, -1, 0],
+	[51193042, 0, 1, -1, 0],
+	[2634540382, 3, 1, -1, 0],
+	[886364171, 0, 1, -1, 0],
+	[3898604944, 0, 1, -1, 0],
+	[3388129398, 0, 1, -1, 0],
+	[3964382884, 2, 1, -1, 1],
+	[3305874255, 0, 1, -1, 0],
+	[156590431, 0, 1, -1, 0],
+	[3106485751, 0, 0, -1, 0],
+	[3035674767, 0, 1, -1, 0],
+	[851697110, 0, 1, -1, 0]
+]
diff --git a/gateway/src/opcodes/index.ts b/gateway/src/opcodes/index.ts
new file mode 100644
index 00000000..fa57f568
--- /dev/null
+++ b/gateway/src/opcodes/index.ts
@@ -0,0 +1,25 @@
+import { Payload } from "../util/Constants";
+import WebSocket from "../util/WebSocket";
+import { onHeartbeat } from "./Heartbeat";
+import { onIdentify } from "./Identify";
+import { onLazyRequest } from "./LazyRequest";
+import { onPresenceUpdate } from "./PresenceUpdate";
+import { onRequestGuildMembers } from "./RequestGuildMembers";
+import { onResume } from "./Resume";
+import { onVoiceStateUpdate } from "./VoiceStateUpdate";
+
+export type OPCodeHandler = (this: WebSocket, data: Payload) => any;
+
+export default {
+	1: onHeartbeat,
+	2: onIdentify,
+	3: onPresenceUpdate,
+	4: onVoiceStateUpdate,
+	// 5: Voice Server Ping
+	6: onResume,
+	// 7: Reconnect: You should attempt to reconnect and resume immediately.
+	8: onRequestGuildMembers,
+	// 9: Invalid Session
+	// 10: Hello
+	14: onLazyRequest,
+};
diff --git a/gateway/src/opcodes/instanceOf.ts b/gateway/src/opcodes/instanceOf.ts
new file mode 100644
index 00000000..c4ee5ee6
--- /dev/null
+++ b/gateway/src/opcodes/instanceOf.ts
@@ -0,0 +1,18 @@
+import { instanceOf } from "lambert-server";
+import { CLOSECODES } from "../util/Constants";
+import WebSocket from "../util/WebSocket";
+
+export function check(this: WebSocket, schema: any, data: any) {
+	try {
+		const error = instanceOf(schema, data, { path: "body" });
+		if (error !== true) {
+			throw error;
+		}
+		return true;
+	} catch (error) {
+		console.error(error);
+		// invalid payload
+		this.close(CLOSECODES.Decode_error);
+		throw error;
+	}
+}