summary refs log tree commit diff
path: root/api
diff options
context:
space:
mode:
Diffstat (limited to 'api')
-rw-r--r--api/package-lock.json18
-rw-r--r--api/package.json2
-rw-r--r--api/src/middlewares/Authentication.ts8
-rw-r--r--api/src/middlewares/ErrorHandler.ts11
-rw-r--r--api/src/routes/auth/register.ts9
-rw-r--r--api/src/routes/channels/#channel_id/messages/#message_id/index.ts7
-rw-r--r--api/src/routes/channels/#channel_id/messages/index.ts36
-rw-r--r--api/src/routes/channels/#channel_id/pins.ts2
-rw-r--r--api/src/routes/channels/#channel_id/typing.ts2
-rw-r--r--api/src/routes/guilds/#guild_id/channels.ts3
-rw-r--r--api/src/routes/guilds/#guild_id/index.ts4
-rw-r--r--api/src/routes/guilds/#guild_id/members/#member_id/index.ts2
-rw-r--r--api/src/routes/guilds/#guild_id/roles.ts2
-rw-r--r--api/src/routes/guilds/index.ts41
-rw-r--r--api/src/routes/users/@me/channels.ts7
-rw-r--r--api/src/routes/users/@me/disable.ts2
-rw-r--r--api/src/routes/users/@me/index.ts2
-rw-r--r--api/src/routes/users/@me/relationships.ts12
-rw-r--r--api/src/schema/Message.ts2
-rw-r--r--api/src/util/Channel.ts52
-rw-r--r--api/src/util/Message.ts95
21 files changed, 155 insertions, 164 deletions
diff --git a/api/package-lock.json b/api/package-lock.json
index 758e51c8..63724688 100644
--- a/api/package-lock.json
+++ b/api/package-lock.json
@@ -60,7 +60,7 @@
 				"saslprep": "^1.0.3",
 				"ts-node": "^9.1.1",
 				"ts-node-dev": "^1.1.6",
-				"typescript": "^4.1.2"
+				"typescript": "^4.4.2"
 			}
 		},
 		"../util": {
@@ -83,7 +83,7 @@
 				"reflect-metadata": "^0.1.13",
 				"sqlite3": "^5.0.2",
 				"typeorm": "^0.2.37",
-				"typescript": "^4.3.5",
+				"typescript": "^4.4.2",
 				"typescript-json-schema": "^0.50.1"
 			},
 			"devDependencies": {
@@ -11430,9 +11430,9 @@
 			}
 		},
 		"node_modules/typescript": {
-			"version": "4.3.5",
-			"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.5.tgz",
-			"integrity": "sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA==",
+			"version": "4.4.2",
+			"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.2.tgz",
+			"integrity": "sha512-gzP+t5W4hdy4c+68bfcv0t400HVJMMd2+H9B7gae1nQlBzCqvrXX+6GL/b3GAgyTH966pzrZ70/fRjwAtZksSQ==",
 			"dev": true,
 			"bin": {
 				"tsc": "bin/tsc",
@@ -12637,7 +12637,7 @@
 				"reflect-metadata": "^0.1.13",
 				"sqlite3": "^5.0.2",
 				"typeorm": "^0.2.37",
-				"typescript": "^4.3.5",
+				"typescript": "^4.4.2",
 				"typescript-json-schema": "^0.50.1"
 			}
 		},
@@ -21387,9 +21387,9 @@
 			}
 		},
 		"typescript": {
-			"version": "4.3.5",
-			"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.5.tgz",
-			"integrity": "sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA==",
+			"version": "4.4.2",
+			"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.2.tgz",
+			"integrity": "sha512-gzP+t5W4hdy4c+68bfcv0t400HVJMMd2+H9B7gae1nQlBzCqvrXX+6GL/b3GAgyTH966pzrZ70/fRjwAtZksSQ==",
 			"dev": true
 		},
 		"umd": {
diff --git a/api/package.json b/api/package.json
index b8bb4d45..37db6ff3 100644
--- a/api/package.json
+++ b/api/package.json
@@ -52,7 +52,7 @@
 		"saslprep": "^1.0.3",
 		"ts-node": "^9.1.1",
 		"ts-node-dev": "^1.1.6",
-		"typescript": "^4.1.2"
+		"typescript": "^4.4.2"
 	},
 	"dependencies": {
 		"@fosscord/util": "file:../util",
diff --git a/api/src/middlewares/Authentication.ts b/api/src/middlewares/Authentication.ts
index 34a66a6b..a300c786 100644
--- a/api/src/middlewares/Authentication.ts
+++ b/api/src/middlewares/Authentication.ts
@@ -18,9 +18,9 @@ export const API_PREFIX_TRAILING_SLASH = /^\/api(\/v\d+)?\//;
 declare global {
 	namespace Express {
 		interface Request {
-			user_id: any;
+			user_id: string;
 			user_bot: boolean;
-			token: any;
+			token: string;
 		}
 	}
 }
@@ -47,7 +47,7 @@ export async function Authentication(req: Request, res: Response, next: NextFunc
 		req.user_id = decoded.id;
 		req.user_bot = user.bot;
 		return next();
-	} catch (error) {
-		return next(new HTTPError(error.toString(), 400));
+	} catch (error: any) {
+		return next(new HTTPError(error?.toString(), 400));
 	}
 }
diff --git a/api/src/middlewares/ErrorHandler.ts b/api/src/middlewares/ErrorHandler.ts
index 0ed37bb4..e1ab592c 100644
--- a/api/src/middlewares/ErrorHandler.ts
+++ b/api/src/middlewares/ErrorHandler.ts
@@ -1,5 +1,6 @@
 import { NextFunction, Request, Response } from "express";
 import { HTTPError } from "lambert-server";
+import { EntityNotFoundError } from "typeorm";
 import { FieldError } from "../util/instanceOf";
 
 // TODO: update with new body/typorm validation
@@ -13,12 +14,18 @@ export function ErrorHandler(error: Error, req: Request, res: Response, next: Ne
 		let errors = undefined;
 
 		if (error instanceof HTTPError && error.code) code = httpcode = error.code;
-		else if (error instanceof FieldError) {
+		else if (error instanceof EntityNotFoundError) {
+			message = `${(error as any).stringifyTarget} can not be found`;
+			code = 404;
+		} else if (error instanceof FieldError) {
 			code = Number(error.code);
 			message = error.message;
 			errors = error.errors;
 		} else {
+			console.error(`[Error] ${code} ${req.url}`, errors || error, "body:", req.body);
+
 			if (req.server?.options?.production) {
+				// don't expose internal errors to the user, instead human errors should be thrown as HTTPError
 				message = "Internal Server Error";
 			}
 			code = httpcode = 500;
@@ -26,8 +33,6 @@ export function ErrorHandler(error: Error, req: Request, res: Response, next: Ne
 
 		if (httpcode > 511) httpcode = 400;
 
-		console.error(`[Error] ${code} ${req.url}`, errors || error, "body:", req.body);
-
 		res.status(httpcode).json({ code: code, message, errors });
 	} catch (error) {
 		console.error(`[Internal Server Error] 500`, error);
diff --git a/api/src/routes/auth/register.ts b/api/src/routes/auth/register.ts
index b41ef82c..8bcecda1 100644
--- a/api/src/routes/auth/register.ts
+++ b/api/src/routes/auth/register.ts
@@ -181,10 +181,11 @@ router.post(
 		// appearently discord doesn't save the date of birth and just calculate if nsfw is allowed
 		// if nsfw_allowed is null/undefined it'll require date_of_birth to set it to true/false
 
-		const user = await new User({
+		const user = {
 			created_at: new Date(),
 			username: adjusted_username,
 			discriminator,
+			id: Snowflake.generate(),
 			bot: false,
 			system: false,
 			desktop: false,
@@ -204,8 +205,10 @@ router.post(
 				hash: adjusted_password,
 				valid_tokens_since: new Date()
 			},
-			settings: defaultSettings
-		}).save();
+			settings: defaultSettings,
+			fingerprints: []
+		};
+		await User.insert(user);
 
 		return res.json({ token: await generateToken(user.id) });
 	}
diff --git a/api/src/routes/channels/#channel_id/messages/#message_id/index.ts b/api/src/routes/channels/#channel_id/messages/#message_id/index.ts
index 32478763..b9d46c4f 100644
--- a/api/src/routes/channels/#channel_id/messages/#message_id/index.ts
+++ b/api/src/routes/channels/#channel_id/messages/#message_id/index.ts
@@ -20,7 +20,9 @@ router.patch("/", check(MessageCreateSchema), async (req: Request, res: Response
 		body = { flags: body.flags }; // admins can only suppress embeds of other messages
 	}
 
-	const opts = await handleMessage({
+	const new_message = await handleMessage({
+		// TODO: should be message_reference overridable?
+		// @ts-ignore
 		message_reference: message.message_reference,
 		...body,
 		author_id: message.author_id,
@@ -28,10 +30,9 @@ router.patch("/", check(MessageCreateSchema), async (req: Request, res: Response
 		id: message_id,
 		edited_timestamp: new Date()
 	});
-	message.assign(opts);
 
 	await Promise.all([
-		message.save(),
+		new_message.save(),
 		await emitEvent({
 			event: "MESSAGE_UPDATE",
 			channel_id,
diff --git a/api/src/routes/channels/#channel_id/messages/index.ts b/api/src/routes/channels/#channel_id/messages/index.ts
index 17944548..86de6de8 100644
--- a/api/src/routes/channels/#channel_id/messages/index.ts
+++ b/api/src/routes/channels/#channel_id/messages/index.ts
@@ -31,10 +31,7 @@ export function isTextChannel(type: ChannelType): boolean {
 // get messages
 router.get("/", async (req: Request, res: Response) => {
 	const channel_id = req.params.channel_id;
-	const channel = await Channel.findOneOrFail(
-		{ id: channel_id },
-		{ select: ["guild_id", "type", "permission_overwrites", "recipient_ids", "owner_id"] }
-	); // lean is needed, because we don't want to populate .recipients that also auto deletes .recipient_ids
+	const channel = await Channel.findOneOrFail({ id: channel_id });
 	if (!channel) throw new HTTPError("Channel not found", 404);
 
 	isTextChannel(channel.type);
@@ -56,7 +53,12 @@ router.get("/", async (req: Request, res: Response) => {
 	permissions.hasThrow("VIEW_CHANNEL");
 	if (!permissions.has("READ_MESSAGE_HISTORY")) return res.json([]);
 
-	var query: FindManyOptions<Message> & { where: { id?: any } } = { order: { id: "DESC" }, take: limit, where: { channel_id } };
+	var query: FindManyOptions<Message> & { where: { id?: any } } = {
+		order: { id: "DESC" },
+		take: limit,
+		where: { channel_id },
+		relations: ["author", "webhook", "application", "mentions", "mention_roles", "mention_channels", "sticker_items", "attachments"]
+	};
 
 	if (after) query.where.id = MoreThan(after);
 	else if (before) query.where.id = LessThan(before);
@@ -69,18 +71,20 @@ router.get("/", async (req: Request, res: Response) => {
 
 	const messages = await Message.find(query);
 
-	return res.json(messages).map((x) => {
-		(x.reactions || []).forEach((x: any) => {
+	return res.json(
+		messages.map((x) => {
+			(x.reactions || []).forEach((x: any) => {
+				// @ts-ignore
+				if ((x.user_ids || []).includes(req.user_id)) x.me = true;
+				// @ts-ignore
+				delete x.user_ids;
+			});
 			// @ts-ignore
-			if ((x.user_ids || []).includes(req.user_id)) x.me = true;
-			// @ts-ignore
-			delete x.user_ids;
-		});
-		// @ts-ignore
-		if (!x.author) x.author = { discriminator: "0000", username: "Deleted User", public_flags: "0", avatar: null };
+			if (!x.author) x.author = { discriminator: "0000", username: "Deleted User", public_flags: "0", avatar: null };
 
-		return x;
-	});
+			return x;
+		})
+	);
 });
 
 // TODO: config max upload size
@@ -136,5 +140,5 @@ router.post("/", messageUpload.single("file"), async (req: Request, res: Respons
 		edited_timestamp: undefined
 	});
 
-	return res.send(data);
+	return res.json(data);
 });
diff --git a/api/src/routes/channels/#channel_id/pins.ts b/api/src/routes/channels/#channel_id/pins.ts
index d83e36ed..96a3fdbf 100644
--- a/api/src/routes/channels/#channel_id/pins.ts
+++ b/api/src/routes/channels/#channel_id/pins.ts
@@ -14,7 +14,7 @@ router.put("/:message_id", async (req: Request, res: Response) => {
 	// * in dm channels anyone can pin messages -> only check for guilds
 	if (message.guild_id) permission.hasThrow("MANAGE_MESSAGES");
 
-	const pinned_count = await Message.count({ channel_id, pinned: true });
+	const pinned_count = await Message.count({ channel: { id: channel_id }, pinned: true });
 	const { maxPins } = Config.get().limits.channel;
 	if (pinned_count >= maxPins) throw new HTTPError("Max pin count reached: " + maxPins);
 
diff --git a/api/src/routes/channels/#channel_id/typing.ts b/api/src/routes/channels/#channel_id/typing.ts
index 2305e8e8..f1fb3c86 100644
--- a/api/src/routes/channels/#channel_id/typing.ts
+++ b/api/src/routes/channels/#channel_id/typing.ts
@@ -17,7 +17,7 @@ router.post("/", async (req: Request, res: Response) => {
 		channel_id: channel_id,
 		data: {
 			// this is the paylod
-			member: { ...member, roles: member.role_ids },
+			member: { ...member, roles: member.roles.map((x) => x.id) },
 			channel_id,
 			timestamp,
 			user_id,
diff --git a/api/src/routes/guilds/#guild_id/channels.ts b/api/src/routes/guilds/#guild_id/channels.ts
index 7b0e94b6..5aa1d33d 100644
--- a/api/src/routes/guilds/#guild_id/channels.ts
+++ b/api/src/routes/guilds/#guild_id/channels.ts
@@ -4,7 +4,6 @@ import { HTTPError } from "lambert-server";
 import { ChannelModifySchema } from "../../../schema/Channel";
 
 import { check } from "../../../util/instanceOf";
-import { createChannel } from "../../../util/Channel";
 const router = Router();
 
 router.get("/", async (req: Request, res: Response) => {
@@ -22,7 +21,7 @@ router.post("/", check(ChannelModifySchema), async (req: Request, res: Response)
 	const { guild_id } = req.params;
 	const body = req.body as ChannelModifySchema;
 
-	const channel = await createChannel({ ...body, guild_id }, req.user_id);
+	const channel = await Channel.createChannel({ ...body, guild_id }, req.user_id);
 
 	res.status(201).json(channel);
 });
diff --git a/api/src/routes/guilds/#guild_id/index.ts b/api/src/routes/guilds/#guild_id/index.ts
index d205b164..6f55be3b 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, id: req.user_id }),
-		Member.findOneOrFail(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 db29cd08..d9ce91c0 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
@@ -43,7 +43,7 @@ router.patch("/", check(MemberChangeSchema), async (req: Request, res: Response)
 		emitEvent({
 			event: "GUILD_MEMBER_UPDATE",
 			guild_id,
-			data: { ...member, roles: member.role_ids }
+			data: { ...member, roles: member.roles.map((x) => x.id) }
 		} as GuildMemberUpdateEvent)
 	]);
 
diff --git a/api/src/routes/guilds/#guild_id/roles.ts b/api/src/routes/guilds/#guild_id/roles.ts
index 796a8eb8..f6ac8caa 100644
--- a/api/src/routes/guilds/#guild_id/roles.ts
+++ b/api/src/routes/guilds/#guild_id/roles.ts
@@ -91,7 +91,7 @@ router.patch("/:role_id", check(RoleModifySchema), async (req: Request, res: Res
 	const perms = await getPermission(req.user_id, guild_id);
 	perms.hasThrow("MANAGE_ROLES");
 
-	const role = new Role({ ...body, role_id, guild_id, permissions: perms.bitfield & (body.permissions || 0n) });
+	const role = new Role({ ...body, id: role_id, guild_id, permissions: perms.bitfield & (body.permissions || 0n) });
 
 	await Promise.all([
 		role.save(),
diff --git a/api/src/routes/guilds/index.ts b/api/src/routes/guilds/index.ts
index 020aba6a..e4157384 100644
--- a/api/src/routes/guilds/index.ts
+++ b/api/src/routes/guilds/index.ts
@@ -1,9 +1,8 @@
 import { Router, Request, Response } from "express";
-import { Role, Guild, Snowflake, Config, User, Member } from "@fosscord/util";
+import { Role, Guild, Snowflake, Config, User, Member, Channel } from "@fosscord/util";
 import { HTTPError } from "lambert-server";
 import { check } from "./../../util/instanceOf";
 import { GuildCreateSchema } from "../../schema/Guild";
-import { createChannel } from "../../util/Channel";
 
 const router: Router = Router();
 
@@ -13,14 +12,15 @@ 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({ where: { id: req.user_id } });
+	const guild_count = await Member.count({ id: req.user_id });
 	if (guild_count >= maxGuilds) {
 		throw new HTTPError(`Maximum number of guilds reached ${maxGuilds}`, 403);
 	}
 
 	const guild_id = Snowflake.generate();
-	const guild = new Guild(
-		{
+
+	const [guild, role] = await Promise.all([
+		Guild.insert({
 			name: body.name,
 			region: Config.get().regions.default,
 			owner_id: req.user_id,
@@ -38,7 +38,7 @@ router.post("/", check(GuildCreateSchema), async (req: Request, res: Response) =
 			preferred_locale: "en-US",
 			premium_subscription_count: 0,
 			premium_tier: 0,
-			system_channel_flags: "0",
+			system_channel_flags: 0,
 			unavailable: false,
 			verification_level: 0,
 			welcome_screen: {
@@ -47,11 +47,9 @@ router.post("/", check(GuildCreateSchema), async (req: Request, res: Response) =
 				welcome_channels: []
 			},
 			widget_enabled: false
-		},
-		{ id: guild_id }
-	);
-	const role = new Role(
-		{
+		}),
+		Role.insert({
+			id: guild_id,
 			guild_id: guild_id,
 			color: 0,
 			hoist: false,
@@ -59,15 +57,9 @@ router.post("/", check(GuildCreateSchema), async (req: Request, res: Response) =
 			mentionable: false,
 			name: "@everyone",
 			permissions: String("2251804225"),
-			position: 0,
-			tags: null
-		},
-		{
-			id: guild_id
-		}
-	);
-
-	await Promise.all([guild.save(), role.save()]);
+			position: 0
+		})
+	]);
 
 	if (!body.channels || !body.channels.length) body.channels = [{ id: "01", type: 0, name: "general" }];
 
@@ -86,13 +78,18 @@ router.post("/", check(GuildCreateSchema), async (req: Request, res: Response) =
 			// TODO: should we abort if parent_id is a category? (to disallow sub category channels)
 			var parent_id = ids.get(x.parent_id);
 
-			return createChannel({ ...x, guild_id, id, parent_id }, req.user_id, { keepId: true, skipExistsCheck: true });
+			return Channel.createChannel({ ...x, guild_id, id, parent_id }, req.user_id, {
+				keepId: true,
+				skipExistsCheck: true,
+				skipPermissionCheck: true,
+				skipEventEmit: true
+			});
 		})
 	);
 
 	await Member.addToGuild(req.user_id, guild_id);
 
-	res.status(201).json({ id: guild.id });
+	res.status(201).json({ id: guild_id });
 });
 
 export default router;
diff --git a/api/src/routes/users/@me/channels.ts b/api/src/routes/users/@me/channels.ts
index ab203571..880e09c1 100644
--- a/api/src/routes/users/@me/channels.ts
+++ b/api/src/routes/users/@me/channels.ts
@@ -5,13 +5,14 @@ import { HTTPError } from "lambert-server";
 import { DmChannelCreateSchema } from "../../../schema/Channel";
 import { check } from "../../../util/instanceOf";
 import { In } from "typeorm";
+import { Recipient } from "../../../../../util/dist/entities/Recipient";
 
 const router: Router = Router();
 
 router.get("/", async (req: Request, res: Response) => {
-	var channels = await Channel.find({ recipient_ids: req.user_id });
+	const recipients = await Recipient.find({ where: { id: req.user_id }, relations: ["channel"] });
 
-	res.json(channels);
+	res.json(recipients.map((x) => x.channel));
 });
 
 router.post("/", check(DmChannelCreateSchema), async (req: Request, res: Response) => {
@@ -34,7 +35,7 @@ router.post("/", check(DmChannelCreateSchema), async (req: Request, res: Respons
 		owner_id: req.user_id,
 		created_at: new Date(),
 		last_message_id: null,
-		recipient_ids: [...body.recipients, req.user_id]
+		recipients: [...body.recipients.map((x) => new Recipient({ id: x })), new Recipient({ id: req.user_id })]
 	}).save();
 
 	await emitEvent({ event: "CHANNEL_CREATE", data: channel, user_id: req.user_id } as ChannelCreateEvent);
diff --git a/api/src/routes/users/@me/disable.ts b/api/src/routes/users/@me/disable.ts
index ed1dedcc..7b8a130c 100644
--- a/api/src/routes/users/@me/disable.ts
+++ b/api/src/routes/users/@me/disable.ts
@@ -5,7 +5,7 @@ import bcrypt from "bcrypt";
 const router = Router();
 
 router.post("/", async (req: Request, res: Response) => {
-	const user = await User.findOneOrFail(req.user_id); //User object
+	const user = await User.findOneOrFail({ id: req.user_id }); //User object
 	let correctpass = true;
 
 	if (user.data.hash) {
diff --git a/api/src/routes/users/@me/index.ts b/api/src/routes/users/@me/index.ts
index 274cfb24..d5a5723c 100644
--- a/api/src/routes/users/@me/index.ts
+++ b/api/src/routes/users/@me/index.ts
@@ -37,7 +37,7 @@ router.patch("/", check(UserModifySchema), async (req: Request, res: Response) =
 	if (body.avatar) body.avatar = await handleFile(`/avatars/${req.user_id}`, body.avatar as string);
 	if (body.banner) body.banner = await handleFile(`/banners/${req.user_id}`, body.banner as string);
 
-	const user = await new User({ ...body }, { id: req.user_id }).save();
+	const user = await new User({ ...body, id: req.user_id }).save();
 	// TODO: dispatch user update event
 
 	res.json(user);
diff --git a/api/src/routes/users/@me/relationships.ts b/api/src/routes/users/@me/relationships.ts
index 1a89b110..0b864d88 100644
--- a/api/src/routes/users/@me/relationships.ts
+++ b/api/src/routes/users/@me/relationships.ts
@@ -26,7 +26,7 @@ async function updateRelationship(req: Request, res: Response, friend: User, typ
 	const id = friend.id;
 	if (id === req.user_id) throw new HTTPError("You can't add yourself as a friend");
 
-	const user = await User.findOneOrFail(req.user_id, { relations: ["relationships"], select: userProjection });
+	const user = await User.findOneOrFail({ id: req.user_id }, { relations: ["relationships"], select: userProjection });
 
 	var relationship = user.relationships.find((x) => x.id === id);
 	const friendRequest = friend.relationships.find((x) => x.id === req.user_id);
@@ -67,8 +67,8 @@ async function updateRelationship(req: Request, res: Response, friend: User, typ
 		return res.sendStatus(204);
 	}
 
-	var incoming_relationship = new Relationship({ nickname: undefined, type: RelationshipType.incoming }, { id: req.user_id });
-	var outgoing_relationship = new Relationship({ nickname: undefined, type: RelationshipType.outgoing }, { id });
+	var incoming_relationship = new Relationship({ nickname: undefined, type: RelationshipType.incoming, id: req.user_id });
+	var outgoing_relationship = new Relationship({ nickname: undefined, type: RelationshipType.outgoing, id });
 
 	if (friendRequest) {
 		if (friendRequest.type === RelationshipType.blocked) throw new HTTPError("The user blocked you");
@@ -113,7 +113,7 @@ router.put("/:id", check({ $type: new Length(Number, 1, 4) }), async (req: Reque
 	return await updateRelationship(
 		req,
 		res,
-		await User.findOneOrFail(req.params.id, { relations: ["relationships"], select: userProjection }),
+		await User.findOneOrFail({ id: req.params.id }, { relations: ["relationships"], select: userProjection }),
 		req.body.type
 	);
 });
@@ -135,8 +135,8 @@ router.delete("/:id", async (req: Request, res: Response) => {
 	const { id } = req.params;
 	if (id === req.user_id) throw new HTTPError("You can't remove yourself as a friend");
 
-	const user = await User.findOneOrFail(req.user_id, { select: userProjection, relations: ["relationships"] });
-	const friend = await User.findOneOrFail(id, { select: userProjection, relations: ["relationships"] });
+	const user = await User.findOneOrFail({ id: req.user_id }, { select: userProjection, relations: ["relationships"] });
+	const friend = await User.findOneOrFail({ id: id }, { select: userProjection, relations: ["relationships"] });
 
 	const relationship = user.relationships.find((x) => x.id === id);
 	const friendRequest = friend.relationships.find((x) => x.id === req.user_id);
diff --git a/api/src/schema/Message.ts b/api/src/schema/Message.ts
index bf10c037..742542df 100644
--- a/api/src/schema/Message.ts
+++ b/api/src/schema/Message.ts
@@ -81,7 +81,7 @@ export interface MessageCreateSchema {
 		message_id: string;
 		channel_id: string;
 		guild_id?: string;
-		fail_if_not_exists: boolean;
+		fail_if_not_exists?: boolean;
 	};
 	payload_json?: string;
 	file?: any;
diff --git a/api/src/util/Channel.ts b/api/src/util/Channel.ts
deleted file mode 100644
index bc9217ce..00000000
--- a/api/src/util/Channel.ts
+++ /dev/null
@@ -1,52 +0,0 @@
-import { ChannelCreateEvent, Channel, ChannelType, emitEvent, getPermission, Snowflake } from "@fosscord/util";
-import { HTTPError } from "lambert-server";
-
-// TODO: DM channel
-export async function createChannel(
-	channel: Partial<Channel>,
-	user_id: string = "0",
-	opts?: {
-		keepId?: boolean;
-		skipExistsCheck?: boolean;
-	}
-) {
-	// Always check if user has permission first
-	const permissions = await getPermission(user_id, channel.guild_id);
-	permissions.hasThrow("MANAGE_CHANNELS");
-
-	switch (channel.type) {
-		case ChannelType.GUILD_TEXT:
-		case ChannelType.GUILD_VOICE:
-			if (channel.parent_id && !opts?.skipExistsCheck) {
-				const exists = await Channel.findOneOrFail({ id: channel.parent_id });
-				if (!exists) throw new HTTPError("Parent id channel doesn't exist", 400);
-				if (exists.guild_id !== channel.guild_id) throw new HTTPError("The category channel needs to be in the guild");
-			}
-			break;
-		case ChannelType.GUILD_CATEGORY:
-			break;
-		case ChannelType.DM:
-		case ChannelType.GROUP_DM:
-			throw new HTTPError("You can't create a dm channel in a guild");
-		// TODO: check if guild is community server
-		case ChannelType.GUILD_STORE:
-		case ChannelType.GUILD_NEWS:
-		default:
-			throw new HTTPError("Not yet supported");
-	}
-
-	if (!channel.permission_overwrites) channel.permission_overwrites = [];
-	// TODO: auto generate position
-
-	channel = await new Channel({
-		...channel,
-		...(!opts?.keepId && { id: Snowflake.generate() }),
-		created_at: new Date(),
-		// @ts-ignore
-		recipient_ids: null
-	}).save();
-
-	await emitEvent({ event: "CHANNEL_CREATE", data: channel, guild_id: channel.guild_id } as ChannelCreateEvent);
-
-	return channel;
-}
diff --git a/api/src/util/Message.ts b/api/src/util/Message.ts
index 70989301..fea553bc 100644
--- a/api/src/util/Message.ts
+++ b/api/src/util/Message.ts
@@ -13,11 +13,16 @@ import {
 	Role,
 	EVERYONE_MENTION,
 	HERE_MENTION,
-	MessageType
+	MessageType,
+	User,
+	Application,
+	Webhook,
+	Attachment
 } from "@fosscord/util";
 import { HTTPError } from "lambert-server";
 import fetch from "node-fetch";
 import cheerio from "cheerio";
+import { MessageCreateSchema } from "../schema/Message";
 
 // TODO: check webhook, application, system author
 
@@ -34,17 +39,37 @@ const DEFAULT_FETCH_OPTIONS: any = {
 	method: "GET"
 };
 
-export async function handleMessage(opts: Partial<Message>): Promise<Message> {
-	const channel = await Channel.findOneOrFail(
-		{ id: opts.channel_id },
-		{ select: ["guild_id", "type", "permission_overwrites", "recipient_ids", "owner_id"] }
-	); // lean is needed, because we don't want to populate .recipients that also auto deletes .recipient_ids
+export async function handleMessage(opts: MessageOptions): Promise<Message> {
+	const channel = await Channel.findOneOrFail({ where: { id: opts.channel_id }, relations: ["recipients"] });
 	if (!channel || !opts.channel_id) throw new HTTPError("Channel not found", 404);
+
+	const message = new Message({
+		...opts,
+		guild_id: channel.guild_id,
+		channel_id: opts.channel_id,
+		attachments: opts.attachments || [],
+		embeds: opts.embeds || [],
+		reactions: /*opts.reactions ||*/ [],
+		type: opts.type ?? 0
+	});
+
 	// TODO: are tts messages allowed in dm channels? should permission be checked?
+	if (opts.author_id) {
+		message.author = await User.getPublicUser(opts.author_id);
+	}
+	if (opts.application_id) {
+		message.application = await Application.findOneOrFail({ id: opts.application_id });
+	}
+	if (opts.webhook_id) {
+		message.webhook = await Webhook.findOneOrFail({ id: opts.webhook_id });
+	}
 
-	// @ts-ignore
-	const permission = await getPermission(opts.author_id, channel.guild_id, opts.channel_id, { channel });
+	const permission = await getPermission(opts.author_id, channel.guild_id, opts.channel_id);
 	permission.hasThrow("SEND_MESSAGES");
+	if (permission.cache.member) {
+		message.member = permission.cache.member;
+	}
+
 	if (opts.tts) permission.hasThrow("SEND_TTS_MESSAGES");
 	if (opts.message_reference) {
 		permission.hasThrow("READ_MESSAGE_HISTORY");
@@ -52,10 +77,11 @@ export async function handleMessage(opts: Partial<Message>): Promise<Message> {
 		if (opts.message_reference.channel_id !== opts.channel_id) throw new HTTPError("You can only reference messages from this channel");
 		// TODO: should be checked if the referenced message exists?
 		// @ts-ignore
-		opts.type = MessageType.REPLY;
+		message.type = MessageType.REPLY;
 	}
 
-	if (!opts.content && !opts.embeds?.length && !opts.attachments?.length && !opts.stickers?.length && !opts.activity) {
+	// TODO: stickers/activity
+	if (!opts.content && !opts.embeds?.length && !opts.attachments?.length) {
 		throw new HTTPError("Empty messages are not allowed", 50006);
 	}
 
@@ -64,10 +90,9 @@ export async function handleMessage(opts: Partial<Message>): Promise<Message> {
 	var mention_role_ids = [] as string[];
 	var mention_user_ids = [] as string[];
 	var mention_everyone = false;
-	var mention_everyone = false;
 
 	if (content) {
-		content = content.trim();
+		message.content = content.trim();
 		for (const [_, mention] of content.matchAll(CHANNEL_MENTION)) {
 			if (!mention_channel_ids.includes(mention)) mention_channel_ids.push(mention);
 		}
@@ -90,21 +115,14 @@ export async function handleMessage(opts: Partial<Message>): Promise<Message> {
 		}
 	}
 
+	message.mention_channels = mention_channel_ids.map((x) => new Channel({ id: x }));
+	message.mention_roles = mention_role_ids.map((x) => new Role({ id: x }));
+	message.mentions = mention_user_ids.map((x) => new User({ id: x }));
+	message.mention_everyone = mention_everyone;
+
 	// TODO: check and put it all in the body
 
-	return {
-		...opts,
-		guild_id: channel.guild_id,
-		channel_id: opts.channel_id,
-		mention_channel_ids,
-		mention_role_ids,
-		mention_user_ids,
-		mention_everyone,
-		attachments: opts.attachments || [],
-		embeds: opts.embeds || [],
-		reactions: opts.reactions || [],
-		type: opts.type ?? 0
-	} as Message;
+	return message;
 }
 
 // TODO: cache link result in db
@@ -160,14 +178,29 @@ export async function postHandleMessage(message: Message) {
 	]);
 }
 
-export async function sendMessage(opts: Partial<Message>) {
-	const message = await handleMessage({ ...opts, id: Snowflake.generate(), timestamp: new Date() });
+export async function sendMessage(opts: MessageOptions) {
+	const message = await handleMessage({ ...opts, timestamp: new Date() });
 
-	const data = await new Message(message).save();
+	await Promise.all([
+		message.save(),
+		emitEvent({ event: "MESSAGE_CREATE", channel_id: opts.channel_id, data: message.toJSON() } as MessageCreateEvent)
+	]);
 
-	await emitEvent({ event: "MESSAGE_CREATE", channel_id: opts.channel_id, data } as MessageCreateEvent);
+	postHandleMessage(message).catch((e) => {}); // no await as it shouldnt block the message send function and silently catch error
 
-	postHandleMessage(data).catch((e) => {}); // no await as it shouldnt block the message send function and silently catch error
+	return message;
+}
 
-	return data;
+interface MessageOptions extends MessageCreateSchema {
+	id?: string;
+	type?: MessageType;
+	pinned?: boolean;
+	author_id?: string;
+	webhook_id?: string;
+	application_id?: string;
+	embeds?: Embed[];
+	channel_id?: string;
+	attachments?: Attachment[];
+	edited_timestamp?: Date;
+	timestamp?: Date;
 }