summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--api/src/routes/channels/#channel_id/messages/#message_id/reactions.ts2
-rw-r--r--api/src/routes/channels/#channel_id/permissions.ts2
-rw-r--r--api/src/routes/channels/#channel_id/typing.ts2
-rw-r--r--api/src/routes/guilds/#guild_id/bans.ts1
-rw-r--r--api/src/routes/guilds/#guild_id/index.ts4
-rw-r--r--api/src/routes/guilds/#guild_id/members/#member_id/index.ts4
-rw-r--r--api/src/routes/guilds/index.ts84
-rw-r--r--api/src/routes/guilds/templates/index.ts2
-rw-r--r--api/src/routes/users/@me/channels.ts2
-rw-r--r--api/src/routes/users/@me/delete.ts2
-rw-r--r--api/src/routes/users/@me/guilds.ts4
-rw-r--r--api/src/schema/Message.ts78
-rw-r--r--gateway/src/events/Close.ts1
-rw-r--r--gateway/src/listener/listener.ts44
-rw-r--r--gateway/src/opcodes/Identify.ts14
-rw-r--r--gateway/src/opcodes/LazyRequest.ts93
16 files changed, 183 insertions, 156 deletions
diff --git a/api/src/routes/channels/#channel_id/messages/#message_id/reactions.ts b/api/src/routes/channels/#channel_id/messages/#message_id/reactions.ts
index 37168940..f60484b5 100644
--- a/api/src/routes/channels/#channel_id/messages/#message_id/reactions.ts
+++ b/api/src/routes/channels/#channel_id/messages/#message_id/reactions.ts
@@ -136,7 +136,7 @@ router.put("/:emoji/:user_id", async (req: Request, res: Response) => {
 
 	await Message.update({ id: message_id, channel_id }, message);
 
-	const member = channel.guild_id && (await Member.findOneOrFail({ user_id: req.user_id }));
+	const member = channel.guild_id && (await Member.findOneOrFail({ id: req.user_id }));
 
 	await emitEvent({
 		event: "MESSAGE_REACTION_ADD",
diff --git a/api/src/routes/channels/#channel_id/permissions.ts b/api/src/routes/channels/#channel_id/permissions.ts
index 97f21659..9c49542b 100644
--- a/api/src/routes/channels/#channel_id/permissions.ts
+++ b/api/src/routes/channels/#channel_id/permissions.ts
@@ -20,7 +20,7 @@ router.put("/:overwrite_id", check({ allow: String, deny: String, type: Number,
 	if (body.type === 0) {
 		if (!(await Role.count({ id: overwrite_id }))) throw new HTTPError("role not found", 404);
 	} else if (body.type === 1) {
-		if (!(await Member.count({ user_id: overwrite_id }))) throw new HTTPError("user not found", 404);
+		if (!(await Member.count({ id: overwrite_id }))) throw new HTTPError("user not found", 404);
 	} else throw new HTTPError("type not supported", 501);
 
 	// @ts-ignore
diff --git a/api/src/routes/channels/#channel_id/typing.ts b/api/src/routes/channels/#channel_id/typing.ts
index aef99103..f1fb3c86 100644
--- a/api/src/routes/channels/#channel_id/typing.ts
+++ b/api/src/routes/channels/#channel_id/typing.ts
@@ -10,7 +10,7 @@ router.post("/", async (req: Request, res: Response) => {
 	const user_id = req.user_id;
 	const timestamp = Date.now();
 	const channel = await Channel.findOneOrFail({ id: channel_id });
-	const member = await Member.findOneOrFail({ user_id: user_id });
+	const member = await Member.findOneOrFail({ id: user_id });
 
 	await emitEvent({
 		event: "TYPING_START",
diff --git a/api/src/routes/guilds/#guild_id/bans.ts b/api/src/routes/guilds/#guild_id/bans.ts
index b84a68a7..31aa2385 100644
--- a/api/src/routes/guilds/#guild_id/bans.ts
+++ b/api/src/routes/guilds/#guild_id/bans.ts
@@ -3,7 +3,6 @@ import { emitEvent, getPermission, GuildBanAddEvent, GuildBanRemoveEvent, Guild,
 import { HTTPError } from "lambert-server";
 import { getIpAdress } from "../../../util/ipAddress";
 import { BanCreateSchema } from "../../../schema/Ban";
-
 import { check } from "../../../util/instanceOf";
 
 const router: Router = Router();
diff --git a/api/src/routes/guilds/#guild_id/index.ts b/api/src/routes/guilds/#guild_id/index.ts
index 80b5c609..567898dd 100644
--- a/api/src/routes/guilds/#guild_id/index.ts
+++ b/api/src/routes/guilds/#guild_id/index.ts
@@ -14,8 +14,8 @@ router.get("/", async (req: Request, res: Response) => {
 
 	const [guild, member_count, member] = await Promise.all([
 		Guild.findOneOrFail({ id: guild_id }),
-		Member.count({ guild_id: guild_id, user_id: req.user_id }),
-		Member.findOneOrFail({ user_id: req.user_id })
+		Member.count({ guild_id: guild_id, id: req.user_id }),
+		Member.findOneOrFail({ id: req.user_id })
 	]);
 	if (!member_count) throw new HTTPError("You are not a member of the guild you are trying to access", 401);
 
diff --git a/api/src/routes/guilds/#guild_id/members/#member_id/index.ts b/api/src/routes/guilds/#guild_id/members/#member_id/index.ts
index 733a64c4..8b74a524 100644
--- a/api/src/routes/guilds/#guild_id/members/#member_id/index.ts
+++ b/api/src/routes/guilds/#guild_id/members/#member_id/index.ts
@@ -21,7 +21,7 @@ router.get("/", async (req: Request, res: Response) => {
 	const { guild_id, member_id } = req.params;
 	await Member.IsInGuildOrFail(req.user_id, guild_id);
 
-	const member = await Member.findOneOrFail({ user_id: member_id, guild_id });
+	const member = await Member.findOneOrFail({ id: member_id, guild_id });
 
 	return res.json(member);
 });
@@ -39,7 +39,7 @@ router.patch("/", check(MemberChangeSchema), async (req: Request, res: Response)
 		permission.hasThrow("MANAGE_ROLES");
 	}
 
-	const member = await Member.findOneOrFail({ user_id: member_id, guild_id });
+	const member = await Member.findOneOrFail({ id: member_id, guild_id });
 	member.assign(req.body);
 
 	Promise.all([
diff --git a/api/src/routes/guilds/index.ts b/api/src/routes/guilds/index.ts
index a87f926c..51dcf96a 100644
--- a/api/src/routes/guilds/index.ts
+++ b/api/src/routes/guilds/index.ts
@@ -13,54 +13,54 @@ router.post("/", check(GuildCreateSchema), async (req: Request, res: Response) =
 	const body = req.body as GuildCreateSchema;
 
 	const { maxGuilds } = Config.get().limits.user;
-	const guild_count = await Member.count({ user_id: req.user_id });
+	const guild_count = await Member.count({ id: req.user_id });
 	if (guild_count >= maxGuilds) {
 		throw DiscordApiErrors.MAXIMUM_GUILDS.withParams(maxGuilds);
 	}
 
 	const guild_id = Snowflake.generate();
 
-	const [guild, role] = await Promise.all([
-		new Guild({
-			name: body.name,
-			region: Config.get().regions.default,
-			owner_id: req.user_id,
-			afk_timeout: 300,
-			default_message_notifications: 0,
-			explicit_content_filter: 0,
-			features: [],
-			id: guild_id,
-			max_members: 250000,
-			max_presences: 250000,
-			max_video_channel_users: 25,
-			presence_count: 0,
-			member_count: 0, // will automatically be increased by addMember()
-			mfa_level: 0,
-			preferred_locale: "en-US",
-			premium_subscription_count: 0,
-			premium_tier: 0,
-			system_channel_flags: 0,
-			unavailable: false,
-			verification_level: 0,
-			welcome_screen: {
-				enabled: false,
-				description: "No description",
-				welcome_channels: []
-			},
-			widget_enabled: false
-		}).save(),
-		new Role({
-			id: guild_id,
-			guild_id: guild_id,
-			color: 0,
-			hoist: false,
-			managed: false,
-			mentionable: false,
-			name: "@everyone",
-			permissions: String("2251804225"),
-			position: 0
-		}).save()
-	]);
+	const guild = await new Guild({
+		name: body.name,
+		region: Config.get().regions.default,
+		owner_id: req.user_id,
+		afk_timeout: 300,
+		default_message_notifications: 0,
+		explicit_content_filter: 0,
+		features: [],
+		id: guild_id,
+		max_members: 250000,
+		max_presences: 250000,
+		max_video_channel_users: 25,
+		presence_count: 0,
+		member_count: 0, // will automatically be increased by addMember()
+		mfa_level: 0,
+		preferred_locale: "en-US",
+		premium_subscription_count: 0,
+		premium_tier: 0,
+		system_channel_flags: 0,
+		unavailable: false,
+		verification_level: 0,
+		welcome_screen: {
+			enabled: false,
+			description: "No description",
+			welcome_channels: []
+		},
+		widget_enabled: false
+	}).save();
+
+	// we have to create the role _after_ the guild because else we would get a "SQLITE_CONSTRAINT: FOREIGN KEY constraint failed" error
+	const role = await new Role({
+		id: guild_id,
+		guild_id: guild_id,
+		color: 0,
+		hoist: false,
+		managed: false,
+		mentionable: false,
+		name: "@everyone",
+		permissions: String("2251804225"),
+		position: 0
+	}).save();
 
 	if (!body.channels || !body.channels.length) body.channels = [{ id: "01", type: 0, name: "general" }];
 
diff --git a/api/src/routes/guilds/templates/index.ts b/api/src/routes/guilds/templates/index.ts
index 16b65c65..3a619278 100644
--- a/api/src/routes/guilds/templates/index.ts
+++ b/api/src/routes/guilds/templates/index.ts
@@ -20,7 +20,7 @@ router.post("/:code", check(GuildTemplateCreateSchema), async (req: Request, res
 
 	const { maxGuilds } = Config.get().limits.user;
 
-	const guild_count = await Member.count({ user_id: req.user_id });
+	const guild_count = await Member.count({ id: req.user_id });
 	if (guild_count >= maxGuilds) {
 		throw DiscordApiErrors.MAXIMUM_GUILDS.withParams(maxGuilds);
 	}
diff --git a/api/src/routes/users/@me/channels.ts b/api/src/routes/users/@me/channels.ts
index 880e09c1..6fd396b8 100644
--- a/api/src/routes/users/@me/channels.ts
+++ b/api/src/routes/users/@me/channels.ts
@@ -10,7 +10,7 @@ import { Recipient } from "../../../../../util/dist/entities/Recipient";
 const router: Router = Router();
 
 router.get("/", async (req: Request, res: Response) => {
-	const recipients = await Recipient.find({ where: { id: req.user_id }, relations: ["channel"] });
+	const recipients = await Recipient.find({ where: { user_id: req.user_id }, relations: ["channel"] });
 
 	res.json(recipients.map((x) => x.channel));
 });
diff --git a/api/src/routes/users/@me/delete.ts b/api/src/routes/users/@me/delete.ts
index e3b54607..e5fda948 100644
--- a/api/src/routes/users/@me/delete.ts
+++ b/api/src/routes/users/@me/delete.ts
@@ -19,7 +19,7 @@ router.post("/", async (req: Request, res: Response) => {
 	// TODO: decrement guild member count
 
 	if (correctpass) {
-		await Promise.all([User.delete({ id: req.user_id }), Member.delete({ user_id: req.user_id })]);
+		await Promise.all([User.delete({ id: req.user_id }), Member.delete({ id: req.user_id })]);
 
 		res.sendStatus(204);
 	} else {
diff --git a/api/src/routes/users/@me/guilds.ts b/api/src/routes/users/@me/guilds.ts
index 1edb0eb1..fb88281b 100644
--- a/api/src/routes/users/@me/guilds.ts
+++ b/api/src/routes/users/@me/guilds.ts
@@ -6,7 +6,7 @@ import { In } from "typeorm";
 const router: Router = Router();
 
 router.get("/", async (req: Request, res: Response) => {
-	const members = await Member.find({ relations: ["guild"], where: { user_id: req.user_id } });
+	const members = await Member.find({ relations: ["guild"], where: { id: req.user_id } });
 
 	res.json(members.map((x) => x.guild));
 });
@@ -20,7 +20,7 @@ router.delete("/:id", async (req: Request, res: Response) => {
 	if (guild.owner_id === req.user_id) throw new HTTPError("You can't leave your own guild", 400);
 
 	await Promise.all([
-		Member.delete({ user_id: req.user_id, guild_id: guild_id }),
+		Member.delete({ id: req.user_id, guild_id: guild_id }),
 		emitEvent({
 			event: "GUILD_DELETE",
 			data: {
diff --git a/api/src/schema/Message.ts b/api/src/schema/Message.ts
index 742542df..d39f685a 100644
--- a/api/src/schema/Message.ts
+++ b/api/src/schema/Message.ts
@@ -7,48 +7,52 @@ export const EmbedImage = {
 	$height: Number
 };
 
+const embed = {
+	$title: new Length(String, 0, 256), //title of embed
+	$type: String, // type of embed (always "rich" for webhook embeds)
+	$description: new Length(String, 0, 2048), // description of embed
+	$url: String, // url of embed
+	$timestamp: String, // ISO8601 timestamp
+	$color: Number, // color code of the embed
+	$footer: {
+		text: new Length(String, 0, 2048),
+		icon_url: String,
+		proxy_icon_url: String
+	}, // footer object	footer information
+	$image: EmbedImage, // image object	image information
+	$thumbnail: EmbedImage, // thumbnail object	thumbnail information
+	$video: EmbedImage, // video object	video information
+	$provider: {
+		name: String,
+		url: String
+	}, // provider object	provider information
+	$author: {
+		name: new Length(String, 0, 256),
+		url: String,
+		icon_url: String,
+		proxy_icon_url: String
+	}, // author object	author information
+	$fields: new Length(
+		[
+			{
+				name: new Length(String, 0, 256),
+				value: new Length(String, 0, 1024),
+				$inline: Boolean
+			}
+		],
+		0,
+		25
+	)
+};
+
 export const MessageCreateSchema = {
 	$content: new Length(String, 0, 2000),
 	$nonce: String,
 	$tts: Boolean,
 	$flags: String,
-	$embed: {
-		$title: new Length(String, 0, 256), //title of embed
-		$type: String, // type of embed (always "rich" for webhook embeds)
-		$description: new Length(String, 0, 2048), // description of embed
-		$url: String, // url of embed
-		$timestamp: String, // ISO8601 timestamp
-		$color: Number, // color code of the embed
-		$footer: {
-			text: new Length(String, 0, 2048),
-			icon_url: String,
-			proxy_icon_url: String
-		}, // footer object	footer information
-		$image: EmbedImage, // image object	image information
-		$thumbnail: EmbedImage, // thumbnail object	thumbnail information
-		$video: EmbedImage, // video object	video information
-		$provider: {
-			name: String,
-			url: String
-		}, // provider object	provider information
-		$author: {
-			name: new Length(String, 0, 256),
-			url: String,
-			icon_url: String,
-			proxy_icon_url: String
-		}, // author object	author information
-		$fields: new Length(
-			[
-				{
-					name: new Length(String, 0, 256),
-					value: new Length(String, 0, 1024),
-					$inline: Boolean
-				}
-			],
-			0,
-			25
-		)
-	},
+	$embed: embed,
+	// TODO: ^ embed is deprecated in favor of embeds (https://discord.com/developers/docs/resources/channel#message-object)
+	// $embeds: [embed],
 	$allowed_mentions: {
 		$parse: [String],
 		$roles: [String],
diff --git a/gateway/src/events/Close.ts b/gateway/src/events/Close.ts
index d68fc751..b4fed316 100644
--- a/gateway/src/events/Close.ts
+++ b/gateway/src/events/Close.ts
@@ -3,6 +3,7 @@ import { Message } from "./Message";
 import { Session } from "@fosscord/util";
 
 export async function Close(this: WebSocket, code: number, reason: string) {
+	console.log("[WebSocket] closed", code, reason);
 	await Session.delete({ session_id: this.session_id });
 	// @ts-ignore
 	this.off("message", Message);
diff --git a/gateway/src/listener/listener.ts b/gateway/src/listener/listener.ts
index 67837e8d..0b6fa50c 100644
--- a/gateway/src/listener/listener.ts
+++ b/gateway/src/listener/listener.ts
@@ -26,15 +26,20 @@ import { Recipient } from "../../../util/dist/entities/Recipient";
 
 // TODO: use already queried guilds/channels of Identify and don't fetch them again
 export async function setupListener(this: WebSocket) {
-	const members = await Member.find({ user_id: this.user_id });
+	const members = await Member.find({ id: this.user_id });
 	const guild_ids = members.map((x) => x.guild_id);
 	const user = await User.findOneOrFail({ id: this.user_id });
-	const recipients = await Recipient.find({ where: { id: this.user_id }, relations: ["channel"] });
+	const recipients = await Recipient.find({
+		where: { user_id: this.user_id },
+		relations: ["channel"],
+	});
 	const channels = await Channel.find({ guild_id: In(guild_ids) });
 	const dm_channels = recipients.map((x) => x.channel);
 	const guild_channels = channels.filter((x) => x.guild_id);
 
-	const opts: { acknowledge: boolean; channel?: AMQChannel } = { acknowledge: true };
+	const opts: { acknowledge: boolean; channel?: AMQChannel } = {
+		acknowledge: true,
+	};
 	const consumer = consume.bind(this);
 
 	if (RabbitMQ.connection) {
@@ -58,13 +63,25 @@ export async function setupListener(this: WebSocket) {
 				this.listeners;
 				this.events[guild] = await listenEvent(guild, consumer, opts);
 
-				for (const channel of guild_channels.filter((c) => c.guild_id === guild)) {
-					if (x.overwriteChannel(channel.permission_overwrites).has("VIEW_CHANNEL")) {
-						this.events[channel.id] = await listenEvent(channel.id, consumer, opts);
+				for (const channel of guild_channels.filter(
+					(c) => c.guild_id === guild
+				)) {
+					if (
+						x
+							.overwriteChannel(channel.permission_overwrites)
+							.has("VIEW_CHANNEL")
+					) {
+						this.events[channel.id] = await listenEvent(
+							channel.id,
+							consumer,
+							opts
+						);
 					}
 				}
 			})
-			.catch((e) => console.log("couldn't get permission for guild " + guild, e));
+			.catch((e) =>
+				console.log("couldn't get permission for guild " + guild, e)
+			);
 	}
 
 	this.once("close", () => {
@@ -91,7 +108,12 @@ async function consume(this: WebSocket, opts: EventOpts) {
 			opts.cancel();
 			break;
 		case "CHANNEL_CREATE":
-			if (!permission.overwriteChannel(data.permission_overwrites).has("VIEW_CHANNEL")) return;
+			if (
+				!permission
+					.overwriteChannel(data.permission_overwrites)
+					.has("VIEW_CHANNEL")
+			)
+				return;
 		// TODO: check if user has permission to channel
 		case "GUILD_CREATE":
 			this.events[id] = await listenEvent(id, consumer, listenOpts);
@@ -99,7 +121,11 @@ async function consume(this: WebSocket, opts: EventOpts) {
 		case "CHANNEL_UPDATE":
 			const exists = this.events[id];
 			// @ts-ignore
-			if (permission.overwriteChannel(data.permission_overwrites).has("VIEW_CHANNEL")) {
+			if (
+				permission
+					.overwriteChannel(data.permission_overwrites)
+					.has("VIEW_CHANNEL")
+			) {
 				if (exists) break;
 				this.events[id] = await listenEvent(id, consumer, listenOpts);
 			} else {
diff --git a/gateway/src/opcodes/Identify.ts b/gateway/src/opcodes/Identify.ts
index 73c9a29e..2b2c31c5 100644
--- a/gateway/src/opcodes/Identify.ts
+++ b/gateway/src/opcodes/Identify.ts
@@ -57,8 +57,16 @@ export async function onIdentify(this: WebSocket, data: Payload) {
 	}
 
 	const members = await Member.find({
-		where: { user_id: this.user_id },
-		relations: ["guild", "guild.channels", "guild.emojis", "guild.roles", "guild.stickers", "user", "roles"],
+		where: { id: this.user_id },
+		relations: [
+			"guild",
+			"guild.channels",
+			"guild.emojis",
+			"guild.roles",
+			"guild.stickers",
+			"user",
+			"roles",
+		],
 	});
 	const merged_members = members.map((x: any) => {
 		return [x];
@@ -67,7 +75,7 @@ export async function onIdentify(this: WebSocket, data: Payload) {
 	const user_guild_settings_entries = members.map((x) => x.settings);
 
 	const recipients = await Recipient.find({
-		where: { id: this.user_id },
+		where: { user_id: this.user_id },
 		relations: ["channel", "channel.recipients"],
 	});
 	const channels = recipients.map((x) => x.channel);
diff --git a/gateway/src/opcodes/LazyRequest.ts b/gateway/src/opcodes/LazyRequest.ts
index 146175b5..e035e6bb 100644
--- a/gateway/src/opcodes/LazyRequest.ts
+++ b/gateway/src/opcodes/LazyRequest.ts
@@ -1,13 +1,19 @@
-// @ts-nocheck WIP
-import { db, getPermission, PublicUserProjection, toObject } from "@fosscord/util";
+import {
+	getPermission,
+	Member,
+	PublicMemberProjection,
+	PublicUserProjection,
+	Role,
+} from "@fosscord/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";
+import "missing-native-js-functions";
 
 // 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
+// TODO: config: to list all members (even those who are offline) sorted by role, or just those who are online
 // TODO: rewrite typeorm
 
 export async function onLazyRequest(this: WebSocket, { d }: Payload) {
@@ -18,60 +24,43 @@ export async function onLazyRequest(this: WebSocket, { d }: Payload) {
 	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 = 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();
+	var members = await Member.find({
+		where: { guild_id: guild_id },
+		relations: ["roles", "user"],
+		select: PublicMemberProjection,
+	});
+
+	const roles = await Role.find({
+		where: { guild_id: guild_id },
+		order: {
+			position: "ASC",
+		},
+	});
 
-	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 groups = [] as any[];
+	var member_count = 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 });
+		const [role_members, other_members] = members.partition((m) =>
+			m.roles.find((r) => r.id === role.id)
+		);
+		const group = {
+			count: role_members.length,
+			id: role.id === guild_id ? "online" : role.name,
+		};
+
+		items.push({ group });
+		groups.push(group);
+
+		for (const member of role_members) {
+			member.roles = member.roles.filter((x) => x.id !== guild_id);
+			items.push({
+				member: { ...member, roles: member.roles.map((x) => x.id) },
+			});
 		}
+		members = other_members;
+		member_count += role_members.length;
 	}
 
 	return Send(this, {