summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/api/middlewares/Authentication.ts7
-rw-r--r--src/api/routes/auth/register.ts14
-rw-r--r--src/api/routes/auth/reset.ts4
-rw-r--r--src/api/routes/auth/verify/index.ts3
-rw-r--r--src/api/routes/channels/#channel_id/messages/index.ts109
-rw-r--r--src/api/routes/guilds/#guild_id/index.ts2
-rw-r--r--src/api/routes/guilds/#guild_id/members/#member_id/index.ts25
-rw-r--r--src/api/routes/guilds/index.ts2
-rw-r--r--src/api/routes/users/#id/profile.ts38
-rw-r--r--src/api/util/utility/ipAddress.ts2
-rw-r--r--src/gateway/events/Connection.ts2
-rw-r--r--src/gateway/opcodes/Heartbeat.ts2
-rw-r--r--src/gateway/opcodes/Identify.ts567
-rw-r--r--src/gateway/opcodes/LazyRequest.ts29
-rw-r--r--src/gateway/util/Capabilities.ts26
-rw-r--r--src/gateway/util/WebSocket.ts2
-rw-r--r--src/gateway/util/index.ts1
-rw-r--r--src/util/config/types/SecurityConfiguration.ts2
-rw-r--r--src/util/dtos/ReadyGuildDTO.ts70
-rw-r--r--src/util/entities/Channel.ts18
-rw-r--r--src/util/entities/Guild.ts8
-rw-r--r--src/util/entities/Member.ts10
-rw-r--r--src/util/entities/Message.ts26
-rw-r--r--src/util/entities/Role.ts3
-rw-r--r--src/util/entities/User.ts11
-rw-r--r--src/util/interfaces/Event.ts37
-rw-r--r--src/util/schemas/MessageCreateSchema.ts2
-rw-r--r--src/util/schemas/RegisterSchema.ts4
-rw-r--r--src/util/schemas/UserProfileResponse.ts26
-rw-r--r--src/util/schemas/responses/TypedResponses.ts2
-rw-r--r--src/util/schemas/responses/UserProfileResponse.ts31
-rw-r--r--src/util/util/JSON.ts10
-rw-r--r--src/util/util/Token.ts130
33 files changed, 750 insertions, 475 deletions
diff --git a/src/api/middlewares/Authentication.ts b/src/api/middlewares/Authentication.ts
index d0e4d8a0..812888a3 100644
--- a/src/api/middlewares/Authentication.ts
+++ b/src/api/middlewares/Authentication.ts
@@ -92,12 +92,7 @@ export async function Authentication(
 	Sentry.setUser({ id: req.user_id });
 
 	try {
-		const { jwtSecret } = Config.get().security;
-
-		const { decoded, user } = await checkToken(
-			req.headers.authorization,
-			jwtSecret,
-		);
+		const { decoded, user } = await checkToken(req.headers.authorization);
 
 		req.token = decoded;
 		req.user_id = decoded.id;
diff --git a/src/api/routes/auth/register.ts b/src/api/routes/auth/register.ts
index 321b4a65..14dc319a 100644
--- a/src/api/routes/auth/register.ts
+++ b/src/api/routes/auth/register.ts
@@ -225,6 +225,20 @@ router.post(
 		}
 
 		if (body.password) {
+			const min = register.password.minLength
+				? register.password.minLength
+				: 8;
+			if (body.password.length < min) {
+				throw FieldErrors({
+					password: {
+						code: "PASSWORD_REQUIREMENTS_MIN_LENGTH",
+						message: req.t(
+							"auth:register.PASSWORD_REQUIREMENTS_MIN_LENGTH",
+							{ min: min },
+						),
+					},
+				});
+			}
 			// the salt is saved in the password refer to bcrypt docs
 			body.password = await bcrypt.hash(body.password, 12);
 		} else if (register.password.required) {
diff --git a/src/api/routes/auth/reset.ts b/src/api/routes/auth/reset.ts
index f97045a6..cb4f8180 100644
--- a/src/api/routes/auth/reset.ts
+++ b/src/api/routes/auth/reset.ts
@@ -48,11 +48,9 @@ router.post(
 	async (req: Request, res: Response) => {
 		const { password, token } = req.body as PasswordResetSchema;
 
-		const { jwtSecret } = Config.get().security;
-
 		let user;
 		try {
-			const userTokenData = await checkToken(token, jwtSecret, true);
+			const userTokenData = await checkToken(token);
 			user = userTokenData.user;
 		} catch {
 			throw FieldErrors({
diff --git a/src/api/routes/auth/verify/index.ts b/src/api/routes/auth/verify/index.ts
index a98c17fa..49f74277 100644
--- a/src/api/routes/auth/verify/index.ts
+++ b/src/api/routes/auth/verify/index.ts
@@ -78,11 +78,10 @@ router.post(
 			}
 		}
 
-		const { jwtSecret } = Config.get().security;
 		let user;
 
 		try {
-			const userTokenData = await checkToken(token, jwtSecret, true);
+			const userTokenData = await checkToken(token);
 			user = userTokenData.user;
 		} catch {
 			throw FieldErrors({
diff --git a/src/api/routes/channels/#channel_id/messages/index.ts b/src/api/routes/channels/#channel_id/messages/index.ts
index f031fa75..edc0321c 100644
--- a/src/api/routes/channels/#channel_id/messages/index.ts
+++ b/src/api/routes/channels/#channel_id/messages/index.ts
@@ -20,7 +20,6 @@ import { handleMessage, postHandleMessage, route } from "@spacebar/api";
 import {
 	Attachment,
 	Channel,
-	ChannelType,
 	Config,
 	DmChannelDTO,
 	FieldErrors,
@@ -93,8 +92,6 @@ router.get(
 		if (limit < 1 || limit > 100)
 			throw new HTTPError("limit must be between 1 and 100", 422);
 
-		const halfLimit = Math.floor(limit / 2);
-
 		const permissions = await getPermission(
 			req.user_id,
 			channel.guild_id,
@@ -121,64 +118,72 @@ router.get(
 			],
 		};
 
-		if (after) {
-			if (BigInt(after) > BigInt(Snowflake.generate()))
-				return res.status(422);
-			query.where.id = MoreThan(after);
-		} else if (before) {
-			if (BigInt(before) < BigInt(req.params.channel_id))
-				return res.status(422);
-			query.where.id = LessThan(before);
-		} else if (around) {
-			query.where.id = [
-				MoreThan((BigInt(around) - BigInt(halfLimit)).toString()),
-				LessThan((BigInt(around) + BigInt(halfLimit)).toString()),
-			];
-
-			return res.json([]); // TODO: fix around
+		let messages: Message[];
+
+		if (around) {
+			query.take = Math.floor(limit / 2);
+			const [right, left] = await Promise.all([
+				Message.find({ ...query, where: { id: LessThan(around) } }),
+				Message.find({ ...query, where: { id: MoreThan(around) } }),
+			]);
+			right.push(...left);
+			messages = right;
+		} else {
+			if (after) {
+				if (BigInt(after) > BigInt(Snowflake.generate()))
+					return res.status(422);
+				query.where.id = MoreThan(after);
+			} else if (before) {
+				if (BigInt(before) < BigInt(Snowflake.generate()))
+					return res.status(422);
+				query.where.id = LessThan(before);
+			}
+
+			messages = await Message.find(query);
 		}
 
-		const messages = await Message.find(query);
 		const endpoint = Config.get().cdn.endpointPublic;
 
-		return res.json(
-			messages.map((x: Partial<Message>) => {
-				(x.reactions || []).forEach((y: Partial<Reaction>) => {
-					// eslint-disable-next-line @typescript-eslint/ban-ts-comment
-					//@ts-ignore
-					if ((y.user_ids || []).includes(req.user_id)) y.me = true;
-					delete y.user_ids;
-				});
-				if (!x.author)
-					x.author = User.create({
-						id: "4",
-						discriminator: "0000",
-						username: "Spacebar Ghost",
-						public_flags: 0,
-					});
-				x.attachments?.forEach((y: Attachment) => {
-					// dynamically set attachment proxy_url in case the endpoint changed
-					const uri = y.proxy_url.startsWith("http")
-						? y.proxy_url
-						: `https://example.org${y.proxy_url}`;
-					y.proxy_url = `${endpoint == null ? "" : endpoint}${
-						new URL(uri).pathname
-					}`;
+		const ret = messages.map((x: Message) => {
+			x = x.toJSON();
+
+			(x.reactions || []).forEach((y: Partial<Reaction>) => {
+				// eslint-disable-next-line @typescript-eslint/ban-ts-comment
+				//@ts-ignore
+				if ((y.user_ids || []).includes(req.user_id)) y.me = true;
+				delete y.user_ids;
+			});
+			if (!x.author)
+				x.author = User.create({
+					id: "4",
+					discriminator: "0000",
+					username: "Spacebar Ghost",
+					public_flags: 0,
 				});
+			x.attachments?.forEach((y: Attachment) => {
+				// dynamically set attachment proxy_url in case the endpoint changed
+				const uri = y.proxy_url.startsWith("http")
+					? y.proxy_url
+					: `https://example.org${y.proxy_url}`;
+				y.proxy_url = `${endpoint == null ? "" : endpoint}${
+					new URL(uri).pathname
+				}`;
+			});
 
-				/**
+			/**
 			Some clients ( discord.js ) only check if a property exists within the response,
 			which causes errors when, say, the `application` property is `null`.
 			**/
 
-				// for (var curr in x) {
-				// 	if (x[curr] === null)
-				// 		delete x[curr];
-				// }
+			// for (var curr in x) {
+			// 	if (x[curr] === null)
+			// 		delete x[curr];
+			// }
 
-				return x;
-			}),
-		);
+			return x;
+		});
+
+		return res.json(ret);
 	},
 );
 
@@ -304,9 +309,11 @@ router.post(
 			embeds,
 			channel_id,
 			attachments,
-			edited_timestamp: undefined,
 			timestamp: new Date(),
 		});
+		// eslint-disable-next-line @typescript-eslint/ban-ts-comment
+		//@ts-ignore dont care2
+		message.edited_timestamp = null;
 
 		channel.last_message_id = message.id;
 
diff --git a/src/api/routes/guilds/#guild_id/index.ts b/src/api/routes/guilds/#guild_id/index.ts
index afe60614..86777b36 100644
--- a/src/api/routes/guilds/#guild_id/index.ts
+++ b/src/api/routes/guilds/#guild_id/index.ts
@@ -161,7 +161,7 @@ router.patch(
 		const data = guild.toJSON();
 		// TODO: guild hashes
 		// TODO: fix vanity_url_code, template_id
-		delete data.vanity_url_code;
+		// delete data.vanity_url_code;
 		delete data.template_id;
 
 		await Promise.all([
diff --git a/src/api/routes/guilds/#guild_id/members/#member_id/index.ts b/src/api/routes/guilds/#guild_id/members/#member_id/index.ts
index 5f1f6fa7..cafb922e 100644
--- a/src/api/routes/guilds/#guild_id/members/#member_id/index.ts
+++ b/src/api/routes/guilds/#guild_id/members/#member_id/index.ts
@@ -27,6 +27,8 @@ import {
 	handleFile,
 	Member,
 	MemberChangeSchema,
+	PublicMemberProjection,
+	PublicUserProjection,
 	Role,
 	Sticker,
 } from "@spacebar/util";
@@ -39,7 +41,7 @@ router.get(
 	route({
 		responses: {
 			200: {
-				body: "Member",
+				body: "APIPublicMember",
 			},
 			403: {
 				body: "APIErrorResponse",
@@ -55,9 +57,28 @@ router.get(
 
 		const member = await Member.findOneOrFail({
 			where: { id: member_id, guild_id },
+			relations: ["roles", "user"],
+			select: {
+				index: true,
+				// only grab public member props
+				...Object.fromEntries(
+					PublicMemberProjection.map((x) => [x, true]),
+				),
+				// and public user props
+				user: Object.fromEntries(
+					PublicUserProjection.map((x) => [x, true]),
+				),
+				roles: {
+					id: true,
+				},
+			},
 		});
 
-		return res.json(member);
+		return res.json({
+			...member.toPublicMember(),
+			user: member.user.toPublicUser(),
+			roles: member.roles.map((x) => x.id),
+		});
 	},
 );
 
diff --git a/src/api/routes/guilds/index.ts b/src/api/routes/guilds/index.ts
index 26173ed5..545beb18 100644
--- a/src/api/routes/guilds/index.ts
+++ b/src/api/routes/guilds/index.ts
@@ -72,7 +72,7 @@ router.post(
 
 		await Member.addToGuild(req.user_id, guild.id);
 
-		res.status(201).json({ id: guild.id });
+		res.status(201).json(guild);
 	},
 );
 
diff --git a/src/api/routes/users/#id/profile.ts b/src/api/routes/users/#id/profile.ts
index a94eb546..eecec0f3 100644
--- a/src/api/routes/users/#id/profile.ts
+++ b/src/api/routes/users/#id/profile.ts
@@ -84,18 +84,6 @@ router.get(
 
 		// TODO: make proper DTO's in util?
 
-		const userDto = {
-			username: user.username,
-			discriminator: user.discriminator,
-			id: user.id,
-			public_flags: user.public_flags,
-			avatar: user.avatar,
-			accent_color: user.accent_color,
-			banner: user.banner,
-			bio: req.user_bot ? null : user.bio,
-			bot: user.bot,
-		};
-
 		const userProfile = {
 			bio: req.user_bot ? null : user.bio,
 			accent_color: user.accent_color,
@@ -104,28 +92,6 @@ router.get(
 			theme_colors: user.theme_colors,
 		};
 
-		const guildMemberDto = guild_member
-			? {
-					avatar: guild_member.avatar,
-					banner: guild_member.banner,
-					bio: req.user_bot ? null : guild_member.bio,
-					communication_disabled_until:
-						guild_member.communication_disabled_until,
-					deaf: guild_member.deaf,
-					flags: user.flags,
-					is_pending: guild_member.pending,
-					pending: guild_member.pending, // why is this here twice, discord?
-					joined_at: guild_member.joined_at,
-					mute: guild_member.mute,
-					nick: guild_member.nick,
-					premium_since: guild_member.premium_since,
-					roles: guild_member.roles
-						.map((x) => x.id)
-						.filter((id) => id != guild_id),
-					user: userDto,
-			  }
-			: undefined;
-
 		const guildMemberProfile = {
 			accent_color: null,
 			banner: guild_member?.banner || null,
@@ -139,11 +105,11 @@ router.get(
 			premium_guild_since: premium_guild_since, // TODO
 			premium_since: user.premium_since, // TODO
 			mutual_guilds: mutual_guilds, // TODO {id: "", nick: null} when ?with_mutual_guilds=true
-			user: userDto,
+			user: user.toPublicUser(),
 			premium_type: user.premium_type,
 			profile_themes_experiment_bucket: 4, // TODO: This doesn't make it available, for some reason?
 			user_profile: userProfile,
-			guild_member: guild_id && guildMemberDto,
+			guild_member: guild_member?.toPublicMember(),
 			guild_member_profile: guild_id && guildMemberProfile,
 		});
 	},
diff --git a/src/api/util/utility/ipAddress.ts b/src/api/util/utility/ipAddress.ts
index 172e9604..c51daf6c 100644
--- a/src/api/util/utility/ipAddress.ts
+++ b/src/api/util/utility/ipAddress.ts
@@ -102,7 +102,7 @@ export function getIpAdress(req: Request): string {
 	return (
 		// eslint-disable-next-line @typescript-eslint/ban-ts-comment
 		// @ts-ignore
-		req.headers[Config.get().security.forwadedFor] ||
+		req.headers[Config.get().security.forwardedFor] ||
 		req.socket.remoteAddress
 	);
 }
diff --git a/src/gateway/events/Connection.ts b/src/gateway/events/Connection.ts
index 68273ace..1991ebbe 100644
--- a/src/gateway/events/Connection.ts
+++ b/src/gateway/events/Connection.ts
@@ -45,7 +45,7 @@ export async function Connection(
 	socket: WebSocket,
 	request: IncomingMessage,
 ) {
-	const forwardedFor = Config.get().security.forwadedFor;
+	const forwardedFor = Config.get().security.forwardedFor;
 	const ipAddress = forwardedFor
 		? (request.headers[forwardedFor] as string)
 		: request.socket.remoteAddress;
diff --git a/src/gateway/opcodes/Heartbeat.ts b/src/gateway/opcodes/Heartbeat.ts
index 7866c3e9..b9b62be3 100644
--- a/src/gateway/opcodes/Heartbeat.ts
+++ b/src/gateway/opcodes/Heartbeat.ts
@@ -25,5 +25,5 @@ export async function onHeartbeat(this: WebSocket) {
 
 	setHeartbeat(this);
 
-	await Send(this, { op: 11 });
+	await Send(this, { op: 11, d: {} });
 }
diff --git a/src/gateway/opcodes/Identify.ts b/src/gateway/opcodes/Identify.ts
index 98fae3ed..7610901a 100644
--- a/src/gateway/opcodes/Identify.ts
+++ b/src/gateway/opcodes/Identify.ts
@@ -16,17 +16,23 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { WebSocket, Payload } from "@spacebar/gateway";
+import {
+	WebSocket,
+	Payload,
+	setupListener,
+	Capabilities,
+	CLOSECODES,
+	OPCODES,
+	Send,
+} from "@spacebar/gateway";
 import {
 	checkToken,
 	Intents,
 	Member,
 	ReadyEventData,
-	User,
 	Session,
 	EVENTEnum,
 	Config,
-	PublicMember,
 	PublicUser,
 	PrivateUserProjection,
 	ReadState,
@@ -36,310 +42,385 @@ import {
 	PrivateSessionProjection,
 	MemberPrivateProjection,
 	PresenceUpdateEvent,
-	UserSettings,
 	IdentifySchema,
 	DefaultUserGuildSettings,
-	UserGuildSettings,
 	ReadyGuildDTO,
 	Guild,
-	UserTokenData,
-	ConnectedAccount,
+	PublicUserProjection,
+	ReadyUserGuildSettingsEntries,
+	UserSettings,
+	Permissions,
+	DMChannel,
+	GuildOrUnavailable,
+	Recipient,
+	OPCodes,
 } from "@spacebar/util";
-import { Send } from "../util/Send";
-import { CLOSECODES, OPCODES } from "../util/Constants";
-import { setupListener } from "../listener/listener";
-// import experiments from "./experiments.json";
-const experiments: unknown[] = [];
 import { check } from "./instanceOf";
-import { Recipient } from "@spacebar/util";
 
 // TODO: user sharding
 // TODO: check privileged intents, if defined in the config
-// TODO: check if already identified
-
-// TODO: Refactor identify ( and lazyrequest, tbh )
 
 export async function onIdentify(this: WebSocket, data: Payload) {
+	if (this.user_id) {
+		// we've already identified
+		return this.close(CLOSECODES.Already_authenticated);
+	}
+
 	clearTimeout(this.readyTimeout);
-	// TODO: is this needed now that we use `json-bigint`?
-	if (typeof data.d?.client_state?.highest_last_message_id === "number")
-		data.d.client_state.highest_last_message_id += "";
-	check.call(this, IdentifySchema, data.d);
 
+	// Check payload matches schema
+	check.call(this, IdentifySchema, data.d);
 	const identify: IdentifySchema = data.d;
 
-	let decoded: UserTokenData["decoded"];
-	try {
-		const { jwtSecret } = Config.get().security;
-		decoded = (await checkToken(identify.token, jwtSecret)).decoded; // will throw an error if invalid
-	} catch (error) {
-		console.error("invalid token", error);
-		return this.close(CLOSECODES.Authentication_failed);
-	}
-	this.user_id = decoded.id;
-	const session_id = this.session_id;
-
-	const [
-		user,
-		read_states,
-		members,
-		recipients,
-		session,
-		application,
-		connected_accounts,
-	] = await Promise.all([
-		User.findOneOrFail({
-			where: { id: this.user_id },
-			relations: ["relationships", "relationships.to", "settings"],
-			select: [...PrivateUserProjection, "relationships"],
-		}),
-		ReadState.find({ where: { user_id: this.user_id } }),
-		Member.find({
-			where: { id: this.user_id },
-			select: MemberPrivateProjection,
-			relations: [
-				"guild",
-				"guild.channels",
-				"guild.emojis",
-				"guild.roles",
-				"guild.stickers",
-				"user",
-				"roles",
-			],
-		}),
-		Recipient.find({
-			where: { user_id: this.user_id, closed: false },
-			relations: [
-				"channel",
-				"channel.recipients",
-				"channel.recipients.user",
-			],
-			// TODO: public user selection
-		}),
-		// save the session and delete it when the websocket is closed
-		Session.create({
-			user_id: this.user_id,
-			session_id: session_id,
-			// TODO: check if status is only one of: online, dnd, offline, idle
-			status: identify.presence?.status || "offline", //does the session always start as online?
-			client_info: {
-				//TODO read from identity
-				client: "desktop",
-				os: identify.properties?.os,
-				version: 0,
-			},
-			activities: [],
-		}).save(),
-		Application.findOne({ where: { id: this.user_id } }),
-		ConnectedAccount.find({ where: { user_id: this.user_id } }),
-	]);
+	this.capabilities = new Capabilities(identify.capabilities || 0);
 
+	const { user } = await checkToken(identify.token, {
+		relations: ["relationships", "relationships.to", "settings"],
+		select: [...PrivateUserProjection, "relationships"],
+	});
 	if (!user) return this.close(CLOSECODES.Authentication_failed);
-	if (!user.settings) {
-		user.settings = new UserSettings();
-		await user.settings.save();
-	}
+	this.user_id = user.id;
 
-	if (!identify.intents) identify.intents = BigInt("0x6ffffffff");
+	// Check intents
+	if (!identify.intents) identify.intents = 30064771071n; // TODO: what is this number?
 	this.intents = new Intents(identify.intents);
+
+	// TODO: actually do intent things.
+
+	// Validate sharding
 	if (identify.shard) {
 		this.shard_id = identify.shard[0];
 		this.shard_count = identify.shard[1];
+
 		if (
 			this.shard_count == null ||
 			this.shard_id == null ||
-			this.shard_id >= this.shard_count ||
+			this.shard_id > this.shard_count ||
 			this.shard_id < 0 ||
 			this.shard_count <= 0
 		) {
-			console.log(identify.shard);
+			// TODO: why do we even care about this right now?
+			console.log(
+				`[Gateway] Invalid sharding from ${user.id}: ${identify.shard}`,
+			);
 			return this.close(CLOSECODES.Invalid_shard);
 		}
 	}
-	let users: PublicUser[] = [];
 
-	const merged_members = members.map((x: Member) => {
+	// Generate a new gateway session ( id is already made, just save it in db )
+	const session = Session.create({
+		user_id: this.user_id,
+		session_id: this.session_id,
+		status: identify.presence?.status || "online",
+		client_info: {
+			client: identify.properties?.$device,
+			os: identify.properties?.os,
+			version: 0,
+		},
+		activities: identify.presence?.activities, // TODO: validation
+	});
+
+	// Get from database:
+	// * the users read states
+	// * guild members for this user
+	// * recipients ( dm channels )
+	// * the bot application, if it exists
+	const [, application, read_states, members, recipients] = await Promise.all(
+		[
+			session.save(),
+
+			Application.findOne({
+				where: { id: this.user_id },
+				select: ["id", "flags"],
+			}),
+
+			ReadState.find({
+				where: { user_id: this.user_id },
+				select: [
+					"id",
+					"channel_id",
+					"last_message_id",
+					"last_pin_timestamp",
+					"mention_count",
+				],
+			}),
+
+			Member.find({
+				where: { id: this.user_id },
+				select: {
+					// We only want some member props
+					...Object.fromEntries(
+						MemberPrivateProjection.map((x) => [x, true]),
+					),
+					settings: true, // guild settings
+					roles: { id: true }, // the full role is fetched from the `guild` relation
+
+					// TODO: we don't really need every property of
+					// guild channels, emoji, roles, stickers
+					// but we do want almost everything from guild.
+					// How do you do that without just enumerating the guild props?
+					guild: true,
+				},
+				relations: [
+					"guild",
+					"guild.channels",
+					"guild.emojis",
+					"guild.roles",
+					"guild.stickers",
+					"roles",
+
+					// For these entities, `user` is always just the logged in user we fetched above
+					// "user",
+				],
+			}),
+
+			Recipient.find({
+				where: { user_id: this.user_id, closed: false },
+				relations: [
+					"channel",
+					"channel.recipients",
+					"channel.recipients.user",
+				],
+				select: {
+					channel: {
+						id: true,
+						flags: true,
+						// is_spam: true,	// TODO
+						last_message_id: true,
+						last_pin_timestamp: true,
+						type: true,
+						icon: true,
+						name: true,
+						owner_id: true,
+						recipients: {
+							// we don't actually need this ID or any other information about the recipient info,
+							// but typeorm does not select anything from the users relation of recipients unless we select
+							// at least one column.
+							id: true,
+							// We only want public user data for each dm channel
+							user: Object.fromEntries(
+								PublicUserProjection.map((x) => [x, true]),
+							),
+						},
+					},
+				},
+			}),
+		],
+	);
+
+	// We forgot to migrate user settings from the JSON column of `users`
+	// to the `user_settings` table theyre in now,
+	// so for instances that migrated, users may not have a `user_settings` row.
+	if (!user.settings) {
+		user.settings = new UserSettings();
+		await user.settings.save();
+	}
+
+	// Generate merged_members
+	const merged_members = members.map((x) => {
 		return [
 			{
 				...x,
 				roles: x.roles.map((x) => x.id),
+
+				// add back user, which we don't fetch from db
+				// TODO: For guild profiles, this may need to be changed.
+				// TODO: The only field required in the user prop is `id`,
+				// but our types are annoying so I didn't bother.
+				user: user.toPublicUser(),
+
+				guild: {
+					id: x.guild.id,
+				},
 				settings: undefined,
-				guild: undefined,
 			},
 		];
-	}) as PublicMember[][];
-	// TODO: This type is bad.
-	let guilds: Partial<Guild>[] = members.map((x) => ({
-		...x.guild,
-		joined_at: x.joined_at,
-	}));
+	});
 
-	const pending_guilds: typeof guilds = [];
-	if (user.bot)
-		guilds = guilds.map((guild) => {
-			pending_guilds.push(guild);
-			return { id: guild.id, unavailable: true };
+	// Populated with guilds 'unavailable' currently
+	// Just for bots
+	const pending_guilds: Guild[] = [];
+
+	// Generate guilds list ( make them unavailable if user is bot )
+	const guilds: GuildOrUnavailable[] = members.map((member) => {
+		// filter guild channels we don't have permission to view
+		// TODO: check if this causes issues when the user is granted other roles?
+		member.guild.channels = member.guild.channels.filter((channel) => {
+			const perms = Permissions.finalPermission({
+				user: {
+					id: member.id,
+					roles: member.roles.map((x) => x.id),
+				},
+				guild: member.guild,
+				channel,
+			});
+
+			return perms.has("VIEW_CHANNEL");
 		});
 
-	// TODO: Rewrite this. Perhaps a DTO?
-	const user_guild_settings_entries = members.map((x) => ({
-		...DefaultUserGuildSettings,
-		...x.settings,
-		guild_id: x.guild.id,
-		channel_overrides: Object.entries(
-			x.settings.channel_overrides ?? {},
-		).map((y) => ({
-			...y[1],
-			channel_id: y[0],
-		})),
-	})) as unknown as UserGuildSettings[];
-
-	const channels = recipients.map((x) => {
-		// eslint-disable-next-line @typescript-eslint/ban-ts-comment
-		//@ts-ignore
-		x.channel.recipients = x.channel.recipients.map((x) =>
-			x.user.toPublicUser(),
-		);
-		//TODO is this needed? check if users in group dm that are not friends are sent in the READY event
-		users = users.concat(x.channel.recipients as unknown as User[]);
-		if (x.channel.isDm()) {
-			x.channel.recipients = x.channel.recipients?.filter(
-				(x) => x.id !== this.user_id,
-			);
+		if (user.bot) {
+			pending_guilds.push(member.guild);
+			return { id: member.guild.id, unavailable: true };
 		}
-		return x.channel;
-	});
 
-	for (const relation of user.relationships) {
-		const related_user = relation.to;
-		const public_related_user = {
-			username: related_user.username,
-			discriminator: related_user.discriminator,
-			id: related_user.id,
-			public_flags: related_user.public_flags,
-			avatar: related_user.avatar,
-			bot: related_user.bot,
-			bio: related_user.bio,
-			premium_since: user.premium_since,
-			premium_type: user.premium_type,
-			accent_color: related_user.accent_color,
+		return {
+			...member.guild.toJSON(),
+			joined_at: member.joined_at,
+
+			threads: [],
 		};
-		users.push(public_related_user);
-	}
+	});
+
+	// Generate user_guild_settings
+	const user_guild_settings_entries: ReadyUserGuildSettingsEntries[] =
+		members.map((x) => ({
+			...DefaultUserGuildSettings,
+			...x.settings,
+			guild_id: x.guild_id,
+			channel_overrides: Object.entries(
+				x.settings.channel_overrides ?? {},
+			).map((y) => ({
+				...y[1],
+				channel_id: y[0],
+			})),
+		}));
+
+	// Popultaed with users from private channels, relationships.
+	// Uses a set to dedupe for us.
+	const users: Set<PublicUser> = new Set();
+
+	// Generate dm channels from recipients list. Append recipients to `users` list
+	const channels = recipients
+		.filter(({ channel }) => channel.isDm())
+		.map((r) => {
+			// TODO: fix the types of Recipient
+			// Their channels are only ever private (I think) and thus are always DM channels
+			const channel = r.channel as DMChannel;
+
+			// Remove ourself from the list of other users in dm channel
+			channel.recipients = channel.recipients.filter(
+				(recipient) => recipient.user.id !== this.user_id,
+			);
+
+			const channelUsers = channel.recipients?.map((recipient) =>
+				recipient.user.toPublicUser(),
+			);
+
+			if (channelUsers && channelUsers.length > 0)
+				channelUsers.forEach((user) => users.add(user));
 
-	setImmediate(async () => {
-		// run in seperate "promise context" because ready payload is not dependent on those events
+			return {
+				id: channel.id,
+				flags: channel.flags,
+				last_message_id: channel.last_message_id,
+				type: channel.type,
+				recipients: channelUsers || [],
+				is_spam: false, // TODO
+			};
+		});
+
+	// From user relationships ( friends ), also append to `users` list
+	user.relationships.forEach((x) => users.add(x.to.toPublicUser()));
+
+	// Send SESSIONS_REPLACE and PRESENCE_UPDATE
+	const allSessions = (
+		await Session.find({
+			where: { user_id: this.user_id },
+			select: PrivateSessionProjection,
+		})
+	).map((x) => ({
+		// TODO how is active determined?
+		// in our lazy request impl, we just pick the 'most relevant' session
+		active: x.session_id == session.session_id,
+		activities: x.activities,
+		client_info: x.client_info,
+		// TODO: what does all mean?
+		session_id: x.session_id == session.session_id ? "all" : x.session_id,
+		status: x.status,
+	}));
+
+	Promise.all([
 		emitEvent({
 			event: "SESSIONS_REPLACE",
 			user_id: this.user_id,
-			data: await Session.find({
-				where: { user_id: this.user_id },
-				select: PrivateSessionProjection,
-			}),
-		} as SessionsReplace);
+			data: allSessions,
+		} as SessionsReplace),
 		emitEvent({
 			event: "PRESENCE_UPDATE",
 			user_id: this.user_id,
 			data: {
-				user: await User.getPublicUser(this.user_id),
+				user: user.toPublicUser(),
 				activities: session.activities,
-				client_status: session?.client_info,
+				client_status: session.client_info,
 				status: session.status,
 			},
-		} as PresenceUpdateEvent);
-	});
+		} as PresenceUpdateEvent),
+	]);
 
-	read_states.forEach((s: Partial<ReadState>) => {
-		s.id = s.channel_id;
-		delete s.user_id;
-		delete s.channel_id;
-	});
+	// Build READY
 
-	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,
-		premium_usage_flags: user.premium_usage_flags,
-		purchased_flags: user.purchased_flags,
-		username: user.username,
-		verified: user.verified,
-		bot: user.bot,
-		accent_color: user.accent_color,
-		banner: user.banner,
-		bio: user.bio,
-		premium_since: user.premium_since,
-	};
+	read_states.forEach((x) => {
+		x.id = x.channel_id;
+	});
 
 	const d: ReadyEventData = {
 		v: 9,
-		application: {
-			id: application?.id ?? "",
-			flags: application?.flags ?? 0,
-		}, //TODO: check this code!
-		user: privateUser,
+		application: application
+			? { id: application.id, flags: application.flags }
+			: undefined,
+		user: user.toPrivateUser(),
 		user_settings: user.settings,
-		// eslint-disable-next-line @typescript-eslint/ban-ts-comment
-		// @ts-ignore
-		guilds: guilds.map((x: Guild & { joined_at: Date }) => {
-			return {
-				...new ReadyGuildDTO(x).toJSON(),
-				guild_hashes: {},
-				joined_at: x.joined_at,
-				name: x.name,
-				icon: x.icon,
-			};
-		}),
-		guild_experiments: [], // TODO
-		geo_ordered_rtc_regions: [], // TODO
+		guilds: this.capabilities.has(Capabilities.FLAGS.CLIENT_STATE_V2)
+			? guilds.map((x) => new ReadyGuildDTO(x).toJSON())
+			: guilds,
 		relationships: user.relationships.map((x) => x.toPublicRelationship()),
 		read_state: {
 			entries: read_states,
 			partial: false,
-			version: 304128,
+			version: 0, // TODO
 		},
 		user_guild_settings: {
 			entries: user_guild_settings_entries,
-			partial: false, // TODO partial
-			version: 642,
+			partial: false,
+			version: 0, // TODO
 		},
 		private_channels: channels,
-		session_id: session_id,
-		analytics_token: "", // TODO
-		connected_accounts,
-		consents: {
-			personalization: {
-				consented: false, // TODO
-			},
-		},
-		country_code: user.settings.locale,
-		friend_suggestion_count: 0, // TODO
-		// eslint-disable-next-line @typescript-eslint/ban-ts-comment
-		// @ts-ignore
-		experiments: experiments, // TODO
-		guild_join_requests: [], // TODO what is this?
-		users: users.filter((x) => x).unique(),
+		session_id: this.session_id,
+		country_code: user.settings.locale, // TODO: do ip analysis instead
+		users: Array.from(users),
 		merged_members: merged_members,
-		// shard // TODO: only for user sharding
-		sessions: [], // TODO:
+		sessions: allSessions,
+
+		resume_gateway_url:
+			Config.get().gateway.endpointClient ||
+			Config.get().gateway.endpointPublic ||
+			"ws://127.0.0.1:3001",
 
 		// lol hack whatever
 		required_action:
 			Config.get().login.requireVerification && !user.verified
 				? "REQUIRE_VERIFIED_EMAIL"
 				: undefined,
+
+		consents: {
+			personalization: {
+				consented: false, // TODO
+			},
+		},
+		experiments: [],
+		guild_join_requests: [],
+		connected_accounts: [],
+		guild_experiments: [],
+		geo_ordered_rtc_regions: [],
+		api_code_version: 1,
+		friend_suggestion_count: 0,
+		analytics_token: "",
+		tutorial: null,
+		session_type: "normal", // TODO
+		auth_session_id_hash: "", // TODO
 	};
 
-	// TODO: send real proper data structure
+	// Send READY
 	await Send(this, {
 		op: OPCODES.Dispatch,
 		t: EVENTEnum.Ready,
@@ -347,23 +428,41 @@ export async function onIdentify(this: WebSocket, data: Payload) {
 		d,
 	});
 
+	// If we're a bot user, send GUILD_CREATE for each unavailable guild
 	await Promise.all(
-		pending_guilds.map((guild) =>
+		pending_guilds.map((x) =>
 			Send(this, {
 				op: OPCODES.Dispatch,
 				t: EVENTEnum.GuildCreate,
 				s: this.sequence++,
-				d: guild,
-			})?.catch(console.error),
+				d: x,
+			})?.catch((e) =>
+				console.error(`[Gateway] error when sending bot guilds`, e),
+			),
 		),
 	);
 
-	//TODO send READY_SUPPLEMENTAL
+	// TODO: ready supplemental
+	await Send(this, {
+		op: OPCodes.DISPATCH,
+		t: EVENTEnum.ReadySupplemental,
+		s: this.sequence++,
+		d: {
+			merged_presences: {
+				guilds: [],
+				friends: [],
+			},
+			// these merged members seem to be all users currently in vc in your guilds
+			merged_members: [],
+			lazy_private_channels: [],
+			guilds: [], // { voice_states: [], id: string, embedded_activities: [] }
+			// embedded_activities are users currently in an activity?
+			disclose: [], // Config.get().general.uniqueUsernames ? ["pomelo"] : []
+		},
+	});
+
 	//TODO send GUILD_MEMBER_LIST_UPDATE
-	//TODO send SESSIONS_REPLACE
 	//TODO send VOICE_STATE_UPDATE to let the client know if another device is already connected to a voice channel
 
 	await setupListener.call(this);
-
-	// console.log(`${this.ipAddress} identified as ${d.user.id}`);
 }
diff --git a/src/gateway/opcodes/LazyRequest.ts b/src/gateway/opcodes/LazyRequest.ts
index cde91a75..4ad1ae7b 100644
--- a/src/gateway/opcodes/LazyRequest.ts
+++ b/src/gateway/opcodes/LazyRequest.ts
@@ -27,6 +27,8 @@ import {
 	User,
 	Presence,
 	partition,
+	Channel,
+	Permissions,
 } from "@spacebar/util";
 import {
 	WebSocket,
@@ -35,6 +37,7 @@ import {
 	OPCODES,
 	Send,
 } from "@spacebar/gateway";
+import murmur from "murmurhash-js/murmurhash3_gc";
 import { check } from "./instanceOf";
 
 // TODO: only show roles/members that have access to this channel
@@ -92,7 +95,7 @@ async function getMembers(guild_id: string, range: [number, number]) {
 		console.error(`LazyRequest`, e);
 	}
 
-	if (!members) {
+	if (!members || !members.length) {
 		return {
 			items: [],
 			groups: [],
@@ -271,6 +274,28 @@ export async function onLazyRequest(this: WebSocket, { d }: Payload) {
 		ranges.map((x) => getMembers(guild_id, x as [number, number])),
 	);
 
+	let list_id = "everyone";
+
+	const channel = await Channel.findOneOrFail({
+		where: { id: channel_id },
+	});
+	if (channel.permission_overwrites) {
+		const perms: string[] = [];
+
+		channel.permission_overwrites.forEach((overwrite) => {
+			const { id, allow, deny } = overwrite;
+
+			if (allow.toBigInt() & Permissions.FLAGS.VIEW_CHANNEL)
+				perms.push(`allow:${id}`);
+			else if (deny.toBigInt() & Permissions.FLAGS.VIEW_CHANNEL)
+				perms.push(`deny:${id}`);
+		});
+
+		if (perms.length > 0) {
+			list_id = murmur(perms.sort().join(",")).toString();
+		}
+	}
+
 	// TODO: unsubscribe member_events that are not in op.members
 
 	ops.forEach((op) => {
@@ -299,7 +324,7 @@ export async function onLazyRequest(this: WebSocket, { d }: Payload) {
 				member_count -
 				(groups.find((x) => x.id == "offline")?.count ?? 0),
 			member_count,
-			id: "everyone",
+			id: list_id,
 			guild_id,
 			groups,
 		},
diff --git a/src/gateway/util/Capabilities.ts b/src/gateway/util/Capabilities.ts
new file mode 100644
index 00000000..6c94bb45
--- /dev/null
+++ b/src/gateway/util/Capabilities.ts
@@ -0,0 +1,26 @@
+import { BitField, BitFieldResolvable, BitFlag } from "@spacebar/util";
+
+export type CapabilityResolvable = BitFieldResolvable | CapabilityString;
+type CapabilityString = keyof typeof Capabilities.FLAGS;
+
+export class Capabilities extends BitField {
+	static FLAGS = {
+		// Thanks, Opencord!
+		// https://github.com/MateriiApps/OpenCord/blob/master/app/src/main/java/com/xinto/opencord/gateway/io/Capabilities.kt
+		LAZY_USER_NOTES: BitFlag(0),
+		NO_AFFINE_USER_IDS: BitFlag(1),
+		VERSIONED_READ_STATES: BitFlag(2),
+		VERSIONED_USER_GUILD_SETTINGS: BitFlag(3),
+		DEDUPLICATE_USER_OBJECTS: BitFlag(4),
+		PRIORITIZED_READY_PAYLOAD: BitFlag(5),
+		MULTIPLE_GUILD_EXPERIMENT_POPULATIONS: BitFlag(6),
+		NON_CHANNEL_READ_STATES: BitFlag(7),
+		AUTH_TOKEN_REFRESH: BitFlag(8),
+		USER_SETTINGS_PROTO: BitFlag(9),
+		CLIENT_STATE_V2: BitFlag(10),
+		PASSIVE_GUILD_UPDATE: BitFlag(11),
+	};
+
+	any = (capability: CapabilityResolvable) => super.any(capability);
+	has = (capability: CapabilityResolvable) => super.has(capability);
+}
diff --git a/src/gateway/util/WebSocket.ts b/src/gateway/util/WebSocket.ts
index 972129c7..833756ff 100644
--- a/src/gateway/util/WebSocket.ts
+++ b/src/gateway/util/WebSocket.ts
@@ -19,6 +19,7 @@
 import { Intents, ListenEventOpts, Permissions } from "@spacebar/util";
 import WS from "ws";
 import { Deflate, Inflate } from "fast-zlib";
+import { Capabilities } from "./Capabilities";
 // import { Client } from "@spacebar/webrtc";
 
 export interface WebSocket extends WS {
@@ -40,5 +41,6 @@ export interface WebSocket extends WS {
 	events: Record<string, undefined | (() => unknown)>;
 	member_events: Record<string, () => unknown>;
 	listen_options: ListenEventOpts;
+	capabilities?: Capabilities;
 	// client?: Client;
 }
diff --git a/src/gateway/util/index.ts b/src/gateway/util/index.ts
index 627f12b2..6ef694d9 100644
--- a/src/gateway/util/index.ts
+++ b/src/gateway/util/index.ts
@@ -21,3 +21,4 @@ export * from "./Send";
 export * from "./SessionUtils";
 export * from "./Heartbeat";
 export * from "./WebSocket";
+export * from "./Capabilities";
diff --git a/src/util/config/types/SecurityConfiguration.ts b/src/util/config/types/SecurityConfiguration.ts
index 5e971cfe..35776642 100644
--- a/src/util/config/types/SecurityConfiguration.ts
+++ b/src/util/config/types/SecurityConfiguration.ts
@@ -28,7 +28,7 @@ export class SecurityConfiguration {
 	// header to get the real user ip address
 	// X-Forwarded-For for nginx/reverse proxies
 	// CF-Connecting-IP for cloudflare
-	forwadedFor: string | null = null;
+	forwardedFor: string | null = null;
 	ipdataApiKey: string | null =
 		"eca677b284b3bac29eb72f5e496aa9047f26543605efe99ff2ce35c9";
 	mfaBackupCodeCount: number = 10;
diff --git a/src/util/dtos/ReadyGuildDTO.ts b/src/util/dtos/ReadyGuildDTO.ts
index b21afe74..905ede74 100644
--- a/src/util/dtos/ReadyGuildDTO.ts
+++ b/src/util/dtos/ReadyGuildDTO.ts
@@ -18,13 +18,45 @@
 
 import {
 	Channel,
+	ChannelOverride,
+	ChannelType,
 	Emoji,
 	Guild,
-	PublicMember,
+	PublicUser,
 	Role,
 	Sticker,
+	UserGuildSettings,
+	PublicMember,
 } from "../entities";
 
+// TODO: this is not the best place for this type
+export type ReadyUserGuildSettingsEntries = Omit<
+	UserGuildSettings,
+	"channel_overrides"
+> & {
+	channel_overrides: (ChannelOverride & { channel_id: string })[];
+};
+
+// TODO: probably should move somewhere else
+export interface ReadyPrivateChannel {
+	id: string;
+	flags: number;
+	is_spam: boolean;
+	last_message_id?: string;
+	recipients: PublicUser[];
+	type: ChannelType.DM | ChannelType.GROUP_DM;
+}
+
+export type GuildOrUnavailable =
+	| { id: string; unavailable: boolean }
+	| (Guild & { joined_at?: Date; unavailable: undefined });
+
+const guildIsAvailable = (
+	guild: GuildOrUnavailable,
+): guild is Guild & { joined_at: Date; unavailable: false } => {
+	return guild.unavailable != true;
+};
+
 export interface IReadyGuildDTO {
 	application_command_counts?: { 1: number; 2: number; 3: number }; // ????????????
 	channels: Channel[];
@@ -65,12 +97,21 @@ export interface IReadyGuildDTO {
 		max_members: number | undefined;
 		nsfw_level: number | undefined;
 		hub_type?: unknown | null; // ????
+
+		home_header: null; // TODO
+		latest_onboarding_question_id: null; // TODO
+		safety_alerts_channel_id: null; // TODO
+		max_stage_video_channel_users: 50; // TODO
+		nsfw: boolean;
+		id: string;
 	};
 	roles: Role[];
 	stage_instances: unknown[];
 	stickers: Sticker[];
 	threads: unknown[];
 	version: string;
+	guild_hashes: unknown;
+	unavailable: boolean;
 }
 
 export class ReadyGuildDTO implements IReadyGuildDTO {
@@ -113,14 +154,30 @@ export class ReadyGuildDTO implements IReadyGuildDTO {
 		max_members: number | undefined;
 		nsfw_level: number | undefined;
 		hub_type?: unknown | null; // ????
+
+		home_header: null; // TODO
+		latest_onboarding_question_id: null; // TODO
+		safety_alerts_channel_id: null; // TODO
+		max_stage_video_channel_users: 50; // TODO
+		nsfw: boolean;
+		id: string;
 	};
 	roles: Role[];
 	stage_instances: unknown[];
 	stickers: Sticker[];
 	threads: unknown[];
 	version: string;
+	guild_hashes: unknown;
+	unavailable: boolean;
+	joined_at: Date;
+
+	constructor(guild: GuildOrUnavailable) {
+		if (!guildIsAvailable(guild)) {
+			this.id = guild.id;
+			this.unavailable = true;
+			return;
+		}
 
-	constructor(guild: Guild) {
 		this.application_command_counts = {
 			1: 5,
 			2: 2,
@@ -164,12 +221,21 @@ export class ReadyGuildDTO implements IReadyGuildDTO {
 			max_members: guild.max_members,
 			nsfw_level: guild.nsfw_level,
 			hub_type: null,
+
+			home_header: null,
+			id: guild.id,
+			latest_onboarding_question_id: null,
+			max_stage_video_channel_users: 50, // TODO
+			nsfw: guild.nsfw,
+			safety_alerts_channel_id: null,
 		};
 		this.roles = guild.roles;
 		this.stage_instances = [];
 		this.stickers = guild.stickers;
 		this.threads = [];
 		this.version = "1"; // ??????
+		this.guild_hashes = {};
+		this.joined_at = guild.joined_at;
 	}
 
 	toJSON() {
diff --git a/src/util/entities/Channel.ts b/src/util/entities/Channel.ts
index e23d93db..38627c39 100644
--- a/src/util/entities/Channel.ts
+++ b/src/util/entities/Channel.ts
@@ -468,6 +468,18 @@ export class Channel extends BaseClass {
 		];
 		return disallowedChannelTypes.indexOf(this.type) == -1;
 	}
+
+	toJSON() {
+		return {
+			...this,
+
+			// these fields are not returned depending on the type of channel
+			bitrate: this.bitrate || undefined,
+			user_limit: this.user_limit || undefined,
+			rate_limit_per_user: this.rate_limit_per_user || undefined,
+			owner_id: this.owner_id || undefined,
+		};
+	}
 }
 
 export interface ChannelPermissionOverwrite {
@@ -483,6 +495,12 @@ export enum ChannelPermissionOverwriteType {
 	group = 2,
 }
 
+export interface DMChannel extends Omit<Channel, "type" | "recipients"> {
+	type: ChannelType.DM | ChannelType.GROUP_DM;
+	recipients: Recipient[];
+}
+
+// TODO: probably more props
 export function isTextChannel(type: ChannelType): boolean {
 	switch (type) {
 		case ChannelType.GUILD_STORE:
diff --git a/src/util/entities/Guild.ts b/src/util/entities/Guild.ts
index e2b3e1bd..e364ed98 100644
--- a/src/util/entities/Guild.ts
+++ b/src/util/entities/Guild.ts
@@ -353,6 +353,7 @@ export class Guild extends BaseClass {
 			position: 0,
 			icon: undefined,
 			unicode_emoji: undefined,
+			flags: 0, // TODO?
 		}).save();
 
 		if (!body.channels || !body.channels.length)
@@ -389,4 +390,11 @@ export class Guild extends BaseClass {
 
 		return guild;
 	}
+
+	toJSON() {
+		return {
+			...this,
+			unavailable: this.unavailable == false ? undefined : true,
+		};
+	}
 }
diff --git a/src/util/entities/Member.ts b/src/util/entities/Member.ts
index 8c208202..8be6eae1 100644
--- a/src/util/entities/Member.ts
+++ b/src/util/entities/Member.ts
@@ -344,11 +344,7 @@ export class Member extends BaseClassWithoutId {
 				relations: ["user", "roles"],
 				take: 10,
 			})
-		).map((member) => ({
-			...member.toPublicMember(),
-			user: member.user.toPublicUser(),
-			roles: member.roles.map((x) => x.id),
-		}));
+		).map((member) => member.toPublicMember());
 
 		if (
 			await Member.count({
@@ -455,6 +451,10 @@ export class Member extends BaseClassWithoutId {
 		PublicMemberProjection.forEach((x) => {
 			member[x] = this[x];
 		});
+
+		if (member.roles) member.roles = member.roles.map((x: Role) => x.id);
+		if (member.user) member.user = member.user.toPublicUser();
+
 		return member as PublicMember;
 	}
 }
diff --git a/src/util/entities/Message.ts b/src/util/entities/Message.ts
index 519c431e..e5390300 100644
--- a/src/util/entities/Message.ts
+++ b/src/util/entities/Message.ts
@@ -193,7 +193,7 @@ export class Message extends BaseClass {
 	};
 
 	@Column({ nullable: true })
-	flags?: string;
+	flags?: number;
 
 	@Column({ type: "simple-json", nullable: true })
 	message_reference?: {
@@ -217,6 +217,30 @@ export class Message extends BaseClass {
 
 	@Column({ type: "simple-json", nullable: true })
 	components?: MessageComponent[];
+
+	toJSON(): Message {
+		return {
+			...this,
+			author_id: undefined,
+			member_id: undefined,
+			guild_id: undefined,
+			webhook_id: undefined,
+			application_id: undefined,
+			nonce: undefined,
+
+			tts: this.tts ?? false,
+			guild: this.guild ?? undefined,
+			webhook: this.webhook ?? undefined,
+			interaction: this.interaction ?? undefined,
+			reactions: this.reactions ?? undefined,
+			sticker_items: this.sticker_items ?? undefined,
+			message_reference: this.message_reference ?? undefined,
+			author: this.author?.toPublicUser() ?? undefined,
+			activity: this.activity ?? undefined,
+			application: this.application ?? undefined,
+			components: this.components ?? undefined,
+		};
+	}
 }
 
 export interface MessageComponent {
diff --git a/src/util/entities/Role.ts b/src/util/entities/Role.ts
index 85877c12..3ae5efc1 100644
--- a/src/util/entities/Role.ts
+++ b/src/util/entities/Role.ts
@@ -66,4 +66,7 @@ export class Role extends BaseClass {
 		integration_id?: string;
 		premium_subscriber?: boolean;
 	};
+
+	@Column()
+	flags: number;
 }
diff --git a/src/util/entities/User.ts b/src/util/entities/User.ts
index 3e72c3c9..3f1bda05 100644
--- a/src/util/entities/User.ts
+++ b/src/util/entities/User.ts
@@ -175,7 +175,7 @@ export class User extends BaseClass {
 	email?: string; // email of the user
 
 	@Column()
-	flags: string = "0"; // UserFlags // TODO: generate
+	flags: number = 0; // UserFlags // TODO: generate
 
 	@Column()
 	public_flags: number = 0;
@@ -281,6 +281,15 @@ export class User extends BaseClass {
 		return user as PublicUser;
 	}
 
+	toPrivateUser() {
+		// eslint-disable-next-line @typescript-eslint/no-explicit-any
+		const user: any = {};
+		PrivateUserProjection.forEach((x) => {
+			user[x] = this[x];
+		});
+		return user as UserPrivate;
+	}
+
 	static async getPublicUser(user_id: string, opts?: FindOneOptions<User>) {
 		return await User.findOneOrFail({
 			where: { id: user_id },
diff --git a/src/util/interfaces/Event.ts b/src/util/interfaces/Event.ts
index 76a5f8d0..deb54428 100644
--- a/src/util/interfaces/Event.ts
+++ b/src/util/interfaces/Event.ts
@@ -28,7 +28,6 @@ import {
 	Role,
 	Emoji,
 	PublicMember,
-	UserGuildSettings,
 	Guild,
 	Channel,
 	PublicUser,
@@ -40,6 +39,10 @@ import {
 	UserSettings,
 	IReadyGuildDTO,
 	ReadState,
+	UserPrivate,
+	ReadyUserGuildSettingsEntries,
+	ReadyPrivateChannel,
+	GuildOrUnavailable,
 } from "@spacebar/util";
 
 export interface Event {
@@ -68,22 +71,10 @@ export interface PublicRelationship {
 
 export interface ReadyEventData {
 	v: number;
-	user: PublicUser & {
-		mobile: boolean;
-		desktop: boolean;
-		email: string | undefined;
-		flags: string;
-		mfa_enabled: boolean;
-		nsfw_allowed: boolean;
-		phone: string | undefined;
-		premium: boolean;
-		premium_type: number;
-		verified: boolean;
-		bot: boolean;
-	};
-	private_channels: Channel[]; // this will be empty for bots
+	user: UserPrivate;
+	private_channels: ReadyPrivateChannel[]; // this will be empty for bots
 	session_id: string; // resuming
-	guilds: IReadyGuildDTO[];
+	guilds: IReadyGuildDTO[] | GuildOrUnavailable[]; // depends on capability
 	analytics_token?: string;
 	connected_accounts?: ConnectedAccount[];
 	consents?: {
@@ -115,7 +106,7 @@ export interface ReadyEventData {
 		version: number;
 	};
 	user_guild_settings?: {
-		entries: UserGuildSettings[];
+		entries: ReadyUserGuildSettingsEntries[];
 		version: number;
 		partial: boolean;
 	};
@@ -127,6 +118,17 @@ export interface ReadyEventData {
 	// probably all users who the user is in contact with
 	users?: PublicUser[];
 	sessions: unknown[];
+	api_code_version: number;
+	tutorial: number | null;
+	resume_gateway_url: string;
+	session_type: string;
+	auth_session_id_hash: string;
+	required_action?:
+		| "REQUIRE_VERIFIED_EMAIL"
+		| "REQUIRE_VERIFIED_PHONE"
+		| "REQUIRE_CAPTCHA" // TODO: allow these to be triggered
+		| "TOS_UPDATE_ACKNOWLEDGMENT"
+		| "AGREEMENTS";
 }
 
 export interface ReadyEvent extends Event {
@@ -581,6 +583,7 @@ export type EventData =
 
 export enum EVENTEnum {
 	Ready = "READY",
+	ReadySupplemental = "READY_SUPPLEMENTAL",
 	ChannelCreate = "CHANNEL_CREATE",
 	ChannelUpdate = "CHANNEL_UPDATE",
 	ChannelDelete = "CHANNEL_DELETE",
diff --git a/src/util/schemas/MessageCreateSchema.ts b/src/util/schemas/MessageCreateSchema.ts
index 45cd735e..7e130751 100644
--- a/src/util/schemas/MessageCreateSchema.ts
+++ b/src/util/schemas/MessageCreateSchema.ts
@@ -29,7 +29,7 @@ export interface MessageCreateSchema {
 	nonce?: string;
 	channel_id?: string;
 	tts?: boolean;
-	flags?: string;
+	flags?: number;
 	embeds?: Embed[];
 	embed?: Embed;
 	// TODO: ^ embed is deprecated in favor of embeds (https://discord.com/developers/docs/resources/channel#message-object)
diff --git a/src/util/schemas/RegisterSchema.ts b/src/util/schemas/RegisterSchema.ts
index f6c99b18..7b7de9c7 100644
--- a/src/util/schemas/RegisterSchema.ts
+++ b/src/util/schemas/RegisterSchema.ts
@@ -42,4 +42,8 @@ export interface RegisterSchema {
 	captcha_key?: string;
 
 	promotional_email_opt_in?: boolean;
+
+	// part of pomelo
+	unique_username_registration?: boolean;
+	global_name?: string;
 }
diff --git a/src/util/schemas/UserProfileResponse.ts b/src/util/schemas/UserProfileResponse.ts
deleted file mode 100644
index 10bbcdbf..00000000
--- a/src/util/schemas/UserProfileResponse.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-/*
-	Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
-	Copyright (C) 2023 Spacebar and Spacebar Contributors
-	
-	This program is free software: you can redistribute it and/or modify
-	it under the terms of the GNU Affero General Public License as published
-	by the Free Software Foundation, either version 3 of the License, or
-	(at your option) any later version.
-	
-	This program is distributed in the hope that it will be useful,
-	but WITHOUT ANY WARRANTY; without even the implied warranty of
-	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-	GNU Affero General Public License for more details.
-	
-	You should have received a copy of the GNU Affero General Public License
-	along with this program.  If not, see <https://www.gnu.org/licenses/>.
-*/
-
-import { PublicConnectedAccount, PublicUser } from "..";
-
-export interface UserProfileResponse {
-	user: PublicUser;
-	connected_accounts: PublicConnectedAccount;
-	premium_guild_since?: Date;
-	premium_since?: Date;
-}
diff --git a/src/util/schemas/responses/TypedResponses.ts b/src/util/schemas/responses/TypedResponses.ts
index 099efba3..4349b93c 100644
--- a/src/util/schemas/responses/TypedResponses.ts
+++ b/src/util/schemas/responses/TypedResponses.ts
@@ -11,6 +11,7 @@ import {
 	Member,
 	Message,
 	PrivateUser,
+	PublicMember,
 	PublicUser,
 	Role,
 	Sticker,
@@ -68,6 +69,7 @@ export type APIChannelArray = Channel[];
 export type APIEmojiArray = Emoji[];
 
 export type APIMemberArray = Member[];
+export type APIPublicMember = PublicMember;
 
 export interface APIGuildWithJoinedAt extends Guild {
 	joined_at: string;
diff --git a/src/util/schemas/responses/UserProfileResponse.ts b/src/util/schemas/responses/UserProfileResponse.ts
index bd1f46dd..eba7cbcc 100644
--- a/src/util/schemas/responses/UserProfileResponse.ts
+++ b/src/util/schemas/responses/UserProfileResponse.ts
@@ -1,8 +1,37 @@
-import { PublicConnectedAccount, PublicUser } from "../../entities";
+import {
+	Member,
+	PublicConnectedAccount,
+	PublicMember,
+	PublicUser,
+	User,
+} from "@spacebar/util";
+
+export type MutualGuild = {
+	id: string;
+	nick?: string;
+};
+
+export type PublicMemberProfile = Pick<
+	Member,
+	"banner" | "bio" | "guild_id"
+> & {
+	accent_color: null; // TODO
+};
+
+export type UserProfile = Pick<
+	User,
+	"bio" | "accent_color" | "banner" | "pronouns" | "theme_colors"
+>;
 
 export interface UserProfileResponse {
 	user: PublicUser;
 	connected_accounts: PublicConnectedAccount;
 	premium_guild_since?: Date;
 	premium_since?: Date;
+	mutual_guilds: MutualGuild[];
+	premium_type: number;
+	profile_themes_experiment_bucket: number;
+	user_profile: UserProfile;
+	guild_member?: PublicMember;
+	guild_member_profile?: PublicMemberProfile;
 }
diff --git a/src/util/util/JSON.ts b/src/util/util/JSON.ts
index 1c39b66e..c7dcf47e 100644
--- a/src/util/util/JSON.ts
+++ b/src/util/util/JSON.ts
@@ -27,6 +27,16 @@ const JSONReplacer = function (
 		return (this[key] as Date).toISOString().replace("Z", "+00:00");
 	}
 
+	// erlpack encoding doesn't call json.stringify,
+	// so our toJSON functions don't get called.
+	// manually call it here
+	// eslint-disable-next-line @typescript-eslint/ban-ts-comment
+	//@ts-ignore
+	if (this?.[key]?.toJSON)
+		// eslint-disable-next-line @typescript-eslint/ban-ts-comment
+		//@ts-ignore
+		this[key] = this[key].toJSON();
+
 	return value;
 };
 
diff --git a/src/util/util/Token.ts b/src/util/util/Token.ts
index 90310176..eec72522 100644
--- a/src/util/util/Token.ts
+++ b/src/util/util/Token.ts
@@ -19,94 +19,66 @@
 import jwt, { VerifyOptions } from "jsonwebtoken";
 import { Config } from "./Config";
 import { User } from "../entities";
+// TODO: dont use deprecated APIs lol
+import {
+	FindOptionsRelationByString,
+	FindOptionsSelectByString,
+} from "typeorm";
 
 export const JWTOptions: VerifyOptions = { algorithms: ["HS256"] };
 
 export type UserTokenData = {
 	user: User;
-	decoded: { id: string; iat: number };
+	decoded: { id: string; iat: number; email?: string };
 };
 
-async function checkEmailToken(
-	decoded: jwt.JwtPayload,
-): Promise<UserTokenData> {
-	// eslint-disable-next-line no-async-promise-executor
-	return new Promise(async (res, rej) => {
-		if (!decoded.iat) return rej("Invalid Token"); // will never happen, just for typings.
-
-		const user = await User.findOne({
-			where: {
-				email: decoded.email,
-			},
-			select: [
-				"email",
-				"id",
-				"verified",
-				"deleted",
-				"disabled",
-				"username",
-				"data",
-			],
-		});
-
-		if (!user) return rej("Invalid Token");
-
-		if (new Date().getTime() > decoded.iat * 1000 + 86400 * 1000)
-			return rej("Invalid Token");
-
-		// Using as here because we assert `id` and `iat` are in decoded.
-		// TS just doesn't want to assume its there, though.
-		return res({ decoded, user } as UserTokenData);
-	});
-}
-
-export function checkToken(
+export const checkToken = (
 	token: string,
-	jwtSecret: string,
-	isEmailVerification = false,
-): Promise<UserTokenData> {
-	return new Promise((res, rej) => {
-		token = token.replace("Bot ", "");
-		token = token.replace("Bearer ", "");
-		/**
-		in spacebar, even with instances that have bot distinction; we won't enforce "Bot" prefix,
-		as we don't really have separate pathways for bots 
-		**/
-
-		jwt.verify(token, jwtSecret, JWTOptions, async (err, decoded) => {
-			if (err || !decoded) return rej("Invalid Token");
-			if (
-				typeof decoded == "string" ||
-				!("id" in decoded) ||
-				!decoded.iat
-			)
-				return rej("Invalid Token"); // will never happen, just for typings.
-
-			if (isEmailVerification) return res(checkEmailToken(decoded));
-
-			const user = await User.findOne({
-				where: { id: decoded.id },
-				select: ["data", "bot", "disabled", "deleted", "rights"],
-			});
-
-			if (!user) return rej("Invalid Token");
-
-			// we need to round it to seconds as it saved as seconds in jwt iat and valid_tokens_since is stored in milliseconds
-			if (
-				decoded.iat * 1000 <
-				new Date(user.data.valid_tokens_since).setSeconds(0, 0)
-			)
-				return rej("Invalid Token");
-
-			if (user.disabled) return rej("User disabled");
-			if (user.deleted) return rej("User not found");
-
-			// Using as here because we assert `id` and `iat` are in decoded.
-			// TS just doesn't want to assume its there, though.
-			return res({ decoded, user } as UserTokenData);
-		});
+	opts?: {
+		select?: FindOptionsSelectByString<User>;
+		relations?: FindOptionsRelationByString;
+	},
+): Promise<UserTokenData> =>
+	new Promise((resolve, reject) => {
+		jwt.verify(
+			token,
+			Config.get().security.jwtSecret,
+			JWTOptions,
+			async (err, out) => {
+				const decoded = out as UserTokenData["decoded"];
+				if (err || !decoded) return reject("Invalid Token");
+
+				const user = await User.findOne({
+					where: decoded.email
+						? { email: decoded.email }
+						: { id: decoded.id },
+					select: [
+						...(opts?.select || []),
+						"bot",
+						"disabled",
+						"deleted",
+						"rights",
+						"data",
+					],
+					relations: opts?.relations,
+				});
+
+				if (!user) return reject("User not found");
+
+				// we need to round it to seconds as it saved as seconds in jwt iat and valid_tokens_since is stored in milliseconds
+				if (
+					decoded.iat * 1000 <
+					new Date(user.data.valid_tokens_since).setSeconds(0, 0)
+				)
+					return reject("Invalid Token");
+
+				if (user.disabled) return reject("User disabled");
+				if (user.deleted) return reject("User not found");
+
+				return resolve({ decoded, user });
+			},
+		);
 	});
-}
 
 export async function generateToken(id: string, email?: string) {
 	const iat = Math.floor(Date.now() / 1000);