diff options
Diffstat (limited to 'api')
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; } |