summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--Status.ts7
-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
-rw-r--r--bundle/package-lock.json13
-rw-r--r--bundle/package.json1
-rw-r--r--gateway/package-lock.json4
-rw-r--r--gateway/src/listener/listener.ts10
-rw-r--r--gateway/src/opcodes/Identify.ts42
-rw-r--r--util/package-lock.json14
-rw-r--r--util/package.json2
-rw-r--r--util/src/entities/Application.ts16
-rw-r--r--util/src/entities/Attachment.ts34
-rw-r--r--util/src/entities/AuditLog.ts8
-rw-r--r--util/src/entities/Ban.ts9
-rw-r--r--util/src/entities/BaseClass.ts34
-rw-r--r--util/src/entities/Channel.ts98
-rw-r--r--util/src/entities/Config.ts8
-rw-r--r--util/src/entities/ConnectedAccount.ts3
-rw-r--r--util/src/entities/Emoji.ts11
-rw-r--r--util/src/entities/Guild.ts68
-rw-r--r--util/src/entities/Invite.ts12
-rw-r--r--util/src/entities/Member.ts86
-rw-r--r--util/src/entities/Message.ts78
-rw-r--r--util/src/entities/ReadState.ts9
-rw-r--r--util/src/entities/Recipient.ts19
-rw-r--r--util/src/entities/Relationship.ts3
-rw-r--r--util/src/entities/Role.ts3
-rw-r--r--util/src/entities/Sticker.ts2
-rw-r--r--util/src/entities/Team.ts10
-rw-r--r--util/src/entities/TeamMember.ts6
-rw-r--r--util/src/entities/Template.ts6
-rw-r--r--util/src/entities/User.ts22
-rw-r--r--util/src/entities/VoiceState.ts11
-rw-r--r--util/src/entities/Webhook.ts15
-rw-r--r--util/src/entities/index.ts2
-rw-r--r--util/src/interfaces/Event.ts13
-rw-r--r--util/src/tes.ts12
-rw-r--r--util/src/util/Config.ts2
-rw-r--r--util/src/util/Permissions.ts7
58 files changed, 586 insertions, 433 deletions
diff --git a/Status.ts b/Status.ts
deleted file mode 100644

index c4dab586..00000000 --- a/Status.ts +++ /dev/null
@@ -1,7 +0,0 @@ -export type Status = "idle" | "dnd" | "online" | "offline"; - -export interface ClientStatus { - desktop?: string; // e.g. Windows/Linux/Mac - mobile?: string; // e.g. iOS/Android - web?: string; // e.g. browser, bot account -} 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 5fc36f33..f061172a 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"; import {ApiError} from "../util/ApiError"; @@ -19,12 +20,18 @@ export function ErrorHandler(error: Error, req: Request, res: Response, next: Ne message = error.message; httpcode = error.httpStatus; } - 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; @@ -32,8 +39,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; } diff --git a/bundle/package-lock.json b/bundle/package-lock.json
index 9fcb5490..59fd96ee 100644 --- a/bundle/package-lock.json +++ b/bundle/package-lock.json
@@ -16,6 +16,7 @@ "@fosscord/util": "file:../util", "async-exit-hook": "^2.0.1", "express": "^4.17.1", + "missing-native-js-functions": "^1.2.13", "mongodb-memory-server": "^7.3.6", "node-os-utils": "^1.3.5" }, @@ -70,6 +71,7 @@ "mongoose-long": "^0.3.2", "multer": "^1.4.2", "node-fetch": "^2.6.1", + "supertest": "^6.1.6", "typeorm": "^0.2.37" }, "devDependencies": { @@ -1224,6 +1226,11 @@ "node": "*" } }, + "node_modules/missing-native-js-functions": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/missing-native-js-functions/-/missing-native-js-functions-1.2.13.tgz", + "integrity": "sha512-1RAArfUkrGkj5N3xJVW251F2PvfP2ozAcxsLLDR6uiiAixTP5Abh8zzGMadepbqgiHC0FGlTSAUNbh9abN4Osg==" + }, "node_modules/mkdirp": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", @@ -1916,6 +1923,7 @@ "multer": "^1.4.2", "node-fetch": "^2.6.1", "saslprep": "^1.0.3", + "supertest": "^6.1.6", "ts-node": "^9.1.1", "ts-node-dev": "^1.1.6", "typeorm": "^0.2.37", @@ -2800,6 +2808,11 @@ "brace-expansion": "^1.1.7" } }, + "missing-native-js-functions": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/missing-native-js-functions/-/missing-native-js-functions-1.2.13.tgz", + "integrity": "sha512-1RAArfUkrGkj5N3xJVW251F2PvfP2ozAcxsLLDR6uiiAixTP5Abh8zzGMadepbqgiHC0FGlTSAUNbh9abN4Osg==" + }, "mkdirp": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", diff --git a/bundle/package.json b/bundle/package.json
index 71d7efea..63945135 100644 --- a/bundle/package.json +++ b/bundle/package.json
@@ -51,6 +51,7 @@ "@fosscord/util": "file:../util", "async-exit-hook": "^2.0.1", "express": "^4.17.1", + "missing-native-js-functions": "^1.2.13", "mongodb-memory-server": "^7.3.6", "node-os-utils": "^1.3.5" } diff --git a/gateway/package-lock.json b/gateway/package-lock.json
index 5190b6b0..40606ecd 100644 --- a/gateway/package-lock.json +++ b/gateway/package-lock.json
@@ -55,7 +55,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": { @@ -1949,7 +1949,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" } }, diff --git a/gateway/src/listener/listener.ts b/gateway/src/listener/listener.ts
index 8fa96555..75ca1680 100644 --- a/gateway/src/listener/listener.ts +++ b/gateway/src/listener/listener.ts
@@ -14,7 +14,8 @@ import { Send } from "../util/Send"; import WebSocket from "../util/WebSocket"; import "missing-native-js-functions"; import { Channel as AMQChannel } from "amqplib"; -import { In } from "../../../util/node_modules/typeorm"; +import { In, Like } from "../../../util/node_modules/typeorm"; +import { Recipient } from "../../../util/dist/entities/Recipient"; // TODO: close connection on Invalidated Token // TODO: check intent @@ -28,10 +29,9 @@ export async function setupListener(this: WebSocket) { const members = await Member.find({ where: { id: this.user_id } }); const guild_ids = members.map((x) => x.guild_id); const user = await User.findOneOrFail({ id: this.user_id }); - const channels = await Channel.find({ - where: [{ recipient_ids: this.user_id }, { guild_id: In(guild_ids) }], - }); - const dm_channels = channels.filter((x) => !x.guild_id); + const recipients = await Recipient.find({ where: { 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 }; diff --git a/gateway/src/opcodes/Identify.ts b/gateway/src/opcodes/Identify.ts
index 87008998..958f1b73 100644 --- a/gateway/src/opcodes/Identify.ts +++ b/gateway/src/opcodes/Identify.ts
@@ -1,12 +1,25 @@ import { CLOSECODES, Payload, OPCODES } from "../util/Constants"; import WebSocket from "../util/WebSocket"; -import { Channel, checkToken, Guild, Intents, Member, ReadyEventData, User, EVENTEnum, Config } from "@fosscord/util"; +import { + Channel, + checkToken, + Guild, + Intents, + Member, + ReadyEventData, + User, + EVENTEnum, + Config, + dbConnection, +} from "@fosscord/util"; import { setupListener } from "../listener/listener"; import { IdentifySchema } from "../schema/Identify"; import { Send } from "../util/Send"; // import experiments from "./experiments.json"; const experiments: any = []; import { check } from "./instanceOf"; +import { Like } from "../../../util/node_modules/typeorm"; +import { Recipient } from "../../../util/dist/entities/Recipient"; // TODO: bot sharding // TODO: check priviliged intents @@ -42,17 +55,21 @@ export async function onIdentify(this: WebSocket, data: Payload) { } } - const members = await Member.find({ where: { id: this.user_id }, relations: ["guild"] }); + const members = await Member.find({ + where: { id: this.user_id }, + relations: ["guild", "guild.channels", "guild.emojis", "guild.roles", "guild.stickers", "user", "roles"], + }); const merged_members = members.map((x: any) => { - const y = { ...x, user_id: x.id }; - delete y.settings; - delete y.id; - return [y]; + return [x]; }) as Member[][]; - const guilds = members.map((x) => x.guild); + const guilds = members.map((x) => ({ ...x.guild, joined_at: x.joined_at })); const user_guild_settings_entries = members.map((x) => x.settings); - const channels = await Channel.find({ where: { recipient_ids: this.user_id } }); + const recipients = await Recipient.find({ + where: { id: this.user_id }, + relations: ["channel", "channel.recipients"], + }); + const channels = recipients.map((x) => x.channel); const user = await User.findOneOrFail({ id: this.user_id }); if (!user) return this.close(CLOSECODES.Authentication_failed); @@ -63,6 +80,7 @@ export async function onIdentify(this: WebSocket, data: Payload) { public_flags: user.public_flags, avatar: user.avatar, bot: user.bot, + bio: user.bio, }; const privateUser = { @@ -116,11 +134,7 @@ export async function onIdentify(this: WebSocket, data: Payload) { partial: false, // TODO partial version: 642, }, - // @ts-ignore - private_channels: channels.map((x): ChannelDocument => { - delete x.recipients; - return x; - }), + private_channels: channels, session_id: "", // TODO analytics_token: "", // TODO connected_accounts: [], // TODO @@ -134,7 +148,7 @@ export async function onIdentify(this: WebSocket, data: Payload) { // @ts-ignore experiments: experiments, // TODO guild_join_requests: [], // TODO what is this? - users: [public_user, ...channels.map((x: any) => x.recipients).flat()].unique(), // TODO + users: [public_user].unique(), // TODO merged_members: merged_members, // shard // TODO: only for bots sharding // application // TODO for applications diff --git a/util/package-lock.json b/util/package-lock.json
index bb04d0bc..47aca2d1 100644 --- a/util/package-lock.json +++ b/util/package-lock.json
@@ -24,7 +24,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": { @@ -8235,9 +8235,9 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "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==", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -15150,9 +15150,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==" }, "typescript-json-schema": { "version": "0.50.1", diff --git a/util/package.json b/util/package.json
index af14a521..39af7526 100644 --- a/util/package.json +++ b/util/package.json
@@ -51,7 +51,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" }, "jest": { diff --git a/util/src/entities/Application.ts b/util/src/entities/Application.ts
index b179d171..2092cd4e 100644 --- a/util/src/entities/Application.ts +++ b/util/src/entities/Application.ts
@@ -2,6 +2,7 @@ import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm"; import { BaseClass } from "./BaseClass"; import { Guild } from "./Guild"; import { Team } from "./Team"; +import { User } from "./User"; @Entity("applications") export class Application extends BaseClass { @@ -29,8 +30,9 @@ export class Application extends BaseClass { @Column({ nullable: true }) privacy_policy_url?: string; - @Column() - owner_id: string; + @JoinColumn({ name: "owner_id" }) + @ManyToOne(() => User) + owner?: User; @Column({ nullable: true }) summary?: string; @@ -38,18 +40,12 @@ export class Application extends BaseClass { @Column() verify_key: string; - @RelationId((application: Application) => application.team) - team_id: string; - @JoinColumn({ name: "team_id" }) - @ManyToOne(() => Team, (team: Team) => team.id) + @ManyToOne(() => Team) team?: Team; - @RelationId((application: Application) => application.guild) - guild_id: string; - @JoinColumn({ name: "guild_id" }) - @ManyToOne(() => Guild, (guild: Guild) => guild.id) + @ManyToOne(() => Guild) guild: Guild; // if this application is a game sold, this field will be the guild to which it has been linked @Column({ nullable: true }) diff --git a/util/src/entities/Attachment.ts b/util/src/entities/Attachment.ts new file mode 100644
index 00000000..ca893400 --- /dev/null +++ b/util/src/entities/Attachment.ts
@@ -0,0 +1,34 @@ +import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm"; +import { BaseClass } from "./BaseClass"; + +@Entity("attachments") +export class Attachment extends BaseClass { + @Column() + filename: string; // name of file attached + + @Column() + size: number; // size of file in bytes + + @Column() + url: string; // source url of file + + @Column() + proxy_url: string; // a proxied url of file + + @Column({ nullable: true }) + height?: number; // height of file (if image) + + @Column({ nullable: true }) + width?: number; // width of file (if image) + + @Column({ nullable: true }) + content_type?: string; + + @Column({ nullable: true }) + @RelationId((attachment: Attachment) => attachment.message) + message_id: string; + + @JoinColumn({ name: "message_id" }) + @ManyToOne(() => require("./Message").Message, (message: import("./Message").Message) => message.attachments) + message: import("./Message").Message; +} diff --git a/util/src/entities/AuditLog.ts b/util/src/entities/AuditLog.ts
index ea8aa641..ceeb21fd 100644 --- a/util/src/entities/AuditLog.ts +++ b/util/src/entities/AuditLog.ts
@@ -43,18 +43,16 @@ export enum AuditLogEvents { @Entity("audit_logs") export class AuditLogEntry extends BaseClass { - @RelationId((auditlog: AuditLogEntry) => auditlog.target) - target_id: string; - @JoinColumn({ name: "target_id" }) - @ManyToOne(() => User, (user: User) => user.id) + @ManyToOne(() => User) target?: User; + @Column({ nullable: true }) @RelationId((auditlog: AuditLogEntry) => auditlog.user) user_id: string; @JoinColumn({ name: "user_id" }) - @ManyToOne(() => User, (user: User) => user.id) + @ManyToOne(() => User) user: User; @Column({ diff --git a/util/src/entities/Ban.ts b/util/src/entities/Ban.ts
index f1cbd849..e8a6d648 100644 --- a/util/src/entities/Ban.ts +++ b/util/src/entities/Ban.ts
@@ -5,25 +5,28 @@ import { User } from "./User"; @Entity("bans") export class Ban extends BaseClass { + @Column({ nullable: true }) @RelationId((ban: Ban) => ban.user) user_id: string; @JoinColumn({ name: "user_id" }) - @ManyToOne(() => User, (user: User) => user.id) + @ManyToOne(() => User) user: User; + @Column({ nullable: true }) @RelationId((ban: Ban) => ban.guild) guild_id: string; @JoinColumn({ name: "guild_id" }) - @ManyToOne(() => Guild, (guild: Guild) => guild.id) + @ManyToOne(() => Guild) guild: Guild; + @Column({ nullable: true }) @RelationId((ban: Ban) => ban.executor) executor_id: string; @JoinColumn({ name: "executor_id" }) - @ManyToOne(() => User, (user: User) => user.id) + @ManyToOne(() => User) executor: User; @Column() diff --git a/util/src/entities/BaseClass.ts b/util/src/entities/BaseClass.ts
index 63ce5836..0856ccd1 100644 --- a/util/src/entities/BaseClass.ts +++ b/util/src/entities/BaseClass.ts
@@ -1,13 +1,5 @@ import "reflect-metadata"; -import { - BaseEntity, - BeforeInsert, - BeforeUpdate, - EntityMetadata, - FindConditions, - FindManyOptions, - PrimaryColumn, -} from "typeorm"; +import { BaseEntity, BeforeInsert, BeforeUpdate, EntityMetadata, FindConditions, PrimaryColumn } from "typeorm"; import { Snowflake } from "../util/Snowflake"; import "missing-native-js-functions"; @@ -16,13 +8,12 @@ import "missing-native-js-functions"; export class BaseClass extends BaseEntity { @PrimaryColumn() - id: string; + id: string = Snowflake.generate(); // @ts-ignore - constructor(public props?: any, public opts: { id?: string } = {}) { + constructor(public props?: any) { super(); - - this.id = this.opts.id || Snowflake.generate(); + this.assign(props); } get construct(): any { @@ -35,13 +26,15 @@ export class BaseClass extends BaseEntity { assign(props: any) { if (!props || typeof props !== "object") return; - - delete props.id; delete props.opts; delete props.props; - const properties = new Set(this.metadata.columns.map((x: any) => x.propertyName)); - // will not include relational properties (e.g. @RelationId @ManyToMany) + const properties = new Set( + this.metadata.columns + .map((x: any) => x.propertyName) + .concat(this.metadata.relations.map((x) => x.propertyName)) + ); + // will not include relational properties for (const key in props) { if (!properties.has(key)) continue; @@ -65,8 +58,11 @@ export class BaseClass extends BaseEntity { } toJSON(): any { - // @ts-ignore - return Object.fromEntries(this.metadata.columns.map((x) => [x.propertyName, this[x.propertyName]])); + return Object.fromEntries( + this.metadata.columns // @ts-ignore + .map((x) => [x.propertyName, this[x.propertyName]]) // @ts-ignore + .concat(this.metadata.relations.map((x) => [x.propertyName, this[x.propertyName]])) + ); } static increment<T extends BaseClass>(conditions: FindConditions<T>, propertyPath: string, value: number | string) { diff --git a/util/src/entities/Channel.ts b/util/src/entities/Channel.ts
index 4900f485..e3586dfc 100644 --- a/util/src/entities/Channel.ts +++ b/util/src/entities/Channel.ts
@@ -1,8 +1,12 @@ -import { Column, Entity, JoinColumn, ManyToMany, ManyToOne, RelationId } from "typeorm"; +import { Column, Entity, JoinColumn, JoinTable, ManyToMany, ManyToOne, OneToMany, RelationId } from "typeorm"; import { BaseClass } from "./BaseClass"; import { Guild } from "./Guild"; import { Message } from "./Message"; import { User } from "./User"; +import { HTTPError } from "lambert-server"; +import { emitEvent, getPermission, Snowflake } from "../util"; +import { ChannelCreateEvent } from "../interfaces"; +import { Recipient } from "./Recipient"; export enum ChannelType { GUILD_TEXT = 0, // a text channel within a server @@ -25,40 +29,39 @@ export class Channel extends BaseClass { @Column({ type: "simple-enum", enum: ChannelType }) type: ChannelType; - @Column("simple-array") - @RelationId((channel: Channel) => channel.recipients) - recipient_ids: string[]; - - @JoinColumn({ name: "recipient_ids" }) - @ManyToMany(() => User, (user: User) => user.id) - recipients?: User[]; + @OneToMany(() => Recipient, (recipient: Recipient) => recipient.channel, { cascade: true }) + recipients?: Recipient[]; + @Column({ nullable: true }) @RelationId((channel: Channel) => channel.last_message) last_message_id: string; @JoinColumn({ name: "last_message_id" }) - @ManyToOne(() => Message, (message: Message) => message.id) + @ManyToOne(() => Message) last_message?: Message; + @Column({ nullable: true }) @RelationId((channel: Channel) => channel.guild) guild_id?: string; @JoinColumn({ name: "guild_id" }) - @ManyToOne(() => Guild, (guild: Guild) => guild.id) + @ManyToOne(() => Guild) guild: Guild; + @Column({ nullable: true }) @RelationId((channel: Channel) => channel.parent) parent_id: string; @JoinColumn({ name: "parent_id" }) - @ManyToOne(() => Channel, (channel: Channel) => channel.id) + @ManyToOne(() => Channel) parent?: Channel; + @Column({ nullable: true }) @RelationId((channel: Channel) => channel.owner) owner_id: string; @JoinColumn({ name: "owner_id" }) - @ManyToOne(() => User, (user: User) => user.id) + @ManyToOne(() => User) owner: User; @Column({ nullable: true }) @@ -82,14 +85,77 @@ export class Channel extends BaseClass { @Column({ nullable: true }) user_limit?: number; - @Column() - nsfw: boolean; + @Column({ nullable: true }) + nsfw?: boolean; - @Column() - rate_limit_per_user: number; + @Column({ nullable: true }) + rate_limit_per_user?: number; @Column({ nullable: true }) topic?: string; + + // TODO: DM channel + static async createChannel( + channel: Partial<Channel>, + user_id: string = "0", + opts?: { + keepId?: boolean; + skipExistsCheck?: boolean; + skipPermissionCheck?: boolean; + skipEventEmit?: boolean; + } + ) { + if (!opts?.skipPermissionCheck) { + // 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 = { + ...channel, + ...(!opts?.keepId && { id: Snowflake.generate() }), + created_at: new Date(), + position: channel.position || 0, + }; + + await Promise.all([ + Channel.insert(channel), + !opts?.skipEventEmit + ? emitEvent({ + event: "CHANNEL_CREATE", + data: channel, + guild_id: channel.guild_id, + } as ChannelCreateEvent) + : Promise.resolve(), + ]); + + return channel; + } } export interface ChannelPermissionOverwrite { diff --git a/util/src/entities/Config.ts b/util/src/entities/Config.ts
index f030b167..5eb55933 100644 --- a/util/src/entities/Config.ts +++ b/util/src/entities/Config.ts
@@ -204,12 +204,12 @@ export const DefaultConfigOptions: ConfigValue = { window: 5, }, webhook: { - count: 5, - window: 20, + count: 10, + window: 5, }, channel: { - count: 5, - window: 20, + count: 10, + window: 5, }, auth: { login: { diff --git a/util/src/entities/ConnectedAccount.ts b/util/src/entities/ConnectedAccount.ts
index e48bc322..75982d01 100644 --- a/util/src/entities/ConnectedAccount.ts +++ b/util/src/entities/ConnectedAccount.ts
@@ -4,11 +4,12 @@ import { User } from "./User"; @Entity("connected_accounts") export class ConnectedAccount extends BaseClass { + @Column({ nullable: true }) @RelationId((account: ConnectedAccount) => account.user) user_id: string; @JoinColumn({ name: "user_id" }) - @ManyToOne(() => User, (user: User) => user.connected_accounts) + @ManyToOne(() => User) user: User; @Column({ select: false }) diff --git a/util/src/entities/Emoji.ts b/util/src/entities/Emoji.ts
index 4c0fccd3..181aff2c 100644 --- a/util/src/entities/Emoji.ts +++ b/util/src/entities/Emoji.ts
@@ -1,4 +1,4 @@ -import { Column, Entity, JoinColumn, ManyToMany, ManyToOne, RelationId } from "typeorm"; +import { Column, Entity, JoinColumn, ManyToOne } from "typeorm"; import { BaseClass } from "./BaseClass"; import { Guild } from "./Guild"; import { Role } from "./Role"; @@ -15,7 +15,7 @@ export class Emoji extends BaseClass { guild_id: string; @JoinColumn({ name: "guild_id" }) - @ManyToOne(() => Guild, (guild: Guild) => guild.emojis) + @ManyToOne(() => Guild) guild: Guild; @Column() @@ -26,11 +26,4 @@ export class Emoji extends BaseClass { @Column() require_colons: boolean; - - @RelationId((emoji: Emoji) => emoji.roles) - role_ids: string[]; - - @JoinColumn({ name: "role_ids" }) - @ManyToMany(() => Role, (role: Role) => role.id) - roles: Role[]; // roles this emoji is whitelisted to (new discord feature?) } diff --git a/util/src/entities/Guild.ts b/util/src/entities/Guild.ts
index 21b059a3..032a9415 100644 --- a/util/src/entities/Guild.ts +++ b/util/src/entities/Guild.ts
@@ -1,20 +1,30 @@ import { Column, Entity, JoinColumn, ManyToMany, ManyToOne, OneToMany, OneToOne, RelationId } from "typeorm"; +import { Ban } from "./Ban"; import { BaseClass } from "./BaseClass"; import { Channel } from "./Channel"; import { Emoji } from "./Emoji"; import { Invite } from "./Invite"; import { Member } from "./Member"; import { Role } from "./Role"; +import { Sticker } from "./Sticker"; +import { Template } from "./Template"; import { User } from "./User"; import { VoiceState } from "./VoiceState"; +import { Webhook } from "./Webhook"; + +// TODO: application_command_count, application_command_counts: {1: 0, 2: 0, 3: 0} +// TODO: guild_scheduled_events +// TODO: stage_instances +// TODO: threads @Entity("guilds") export class Guild extends BaseClass { + @Column({ nullable: true }) @RelationId((guild: Guild) => guild.afk_channel) afk_channel_id?: string; @JoinColumn({ name: "afk_channel_id" }) - @ManyToOne(() => Channel, (channel: Channel) => channel.id) + @ManyToOne(() => Channel) afk_channel?: Channel; @Column({ nullable: true }) @@ -25,6 +35,10 @@ export class Guild extends BaseClass { // @Column({ nullable: true }) // application?: string; + @JoinColumn({ name: "ban_ids" }) + @OneToMany(() => Ban, (ban: Ban) => ban.guild) + bans: Ban[]; + @Column({ nullable: true }) banner?: string; @@ -64,52 +78,57 @@ export class Guild extends BaseClass { @Column({ nullable: true }) presence_count?: number; // users online - @RelationId((guild: Guild) => guild.members) - member_ids: string[]; - - @JoinColumn({ name: "member_ids" }) @OneToMany(() => Member, (member: Member) => member.guild) members: Member[]; - @RelationId((guild: Guild) => guild.roles) - role_ids: string[]; - @JoinColumn({ name: "role_ids" }) @OneToMany(() => Role, (role: Role) => role.guild) roles: Role[]; - @RelationId((guild: Guild) => guild.channels) - channel_ids: string[]; - @JoinColumn({ name: "channel_ids" }) @OneToMany(() => Channel, (channel: Channel) => channel.guild) channels: Channel[]; - @RelationId((guild: Guild) => guild.emojis) - emoji_ids: string[]; + @Column({ nullable: true }) + @RelationId((guild: Guild) => guild.template) + template_id: string; + + @JoinColumn({ name: "template_id" }) + @ManyToOne(() => Template) + template: Template; @JoinColumn({ name: "emoji_ids" }) @OneToMany(() => Emoji, (emoji: Emoji) => emoji.guild) emojis: Emoji[]; - @RelationId((guild: Guild) => guild.voice_states) - voice_state_ids: string[]; + @JoinColumn({ name: "sticker_ids" }) + @OneToMany(() => Sticker, (sticker: Sticker) => sticker.guild) + stickers: Sticker[]; + + @JoinColumn({ name: "invite_ids" }) + @OneToMany(() => Invite, (invite: Invite) => invite.guild) + invites: Invite[]; @JoinColumn({ name: "voice_state_ids" }) @OneToMany(() => VoiceState, (voicestate: VoiceState) => voicestate.guild) voice_states: VoiceState[]; + @JoinColumn({ name: "webhook_ids" }) + @OneToMany(() => Webhook, (webhook: Webhook) => webhook.guild) + webhooks: Webhook[]; + @Column({ nullable: true }) mfa_level?: number; @Column() name: string; + @Column({ nullable: true }) @RelationId((guild: Guild) => guild.owner) owner_id: string; - @JoinColumn({ name: "owner_id" }) - @OneToOne(() => User) + @JoinColumn([{ name: "owner_id", referencedColumnName: "id" }]) + @ManyToOne(() => User) owner: User; @Column({ nullable: true }) @@ -121,18 +140,20 @@ export class Guild extends BaseClass { @Column({ nullable: true }) premium_tier?: number; // nitro boost level + @Column({ nullable: true }) @RelationId((guild: Guild) => guild.public_updates_channel) public_updates_channel_id: string; @JoinColumn({ name: "public_updates_channel_id" }) - @OneToOne(() => Channel, (channel: Channel) => channel.id) + @ManyToOne(() => Channel) public_updates_channel?: Channel; + @Column({ nullable: true }) @RelationId((guild: Guild) => guild.rules_channel) rules_channel_id?: string; @JoinColumn({ name: "rules_channel_id" }) - @OneToOne(() => Channel, (channel: Channel) => channel.id) + @ManyToOne(() => Channel) rules_channel?: string; @Column({ nullable: true }) @@ -141,11 +162,12 @@ export class Guild extends BaseClass { @Column({ nullable: true }) splash?: string; + @Column({ nullable: true }) @RelationId((guild: Guild) => guild.system_channel) system_channel_id?: string; @JoinColumn({ name: "system_channel_id" }) - @OneToOne(() => Channel, (channel: Channel) => channel.id) + @ManyToOne(() => Channel) system_channel?: Channel; @Column({ nullable: true }) @@ -154,11 +176,12 @@ export class Guild extends BaseClass { @Column({ nullable: true }) unavailable?: boolean; + @Column({ nullable: true }) @RelationId((guild: Guild) => guild.vanity_url) vanity_url_code?: string; @JoinColumn({ name: "vanity_url_code" }) - @OneToOne(() => Invite) + @ManyToOne(() => Invite) vanity_url?: Invite; @Column({ nullable: true }) @@ -176,11 +199,12 @@ export class Guild extends BaseClass { }[]; }; + @Column({ nullable: true }) @RelationId((guild: Guild) => guild.widget_channel) widget_channel_id?: string; @JoinColumn({ name: "widget_channel_id" }) - @OneToOne(() => Channel, (channel: Channel) => channel.id) + @ManyToOne(() => Channel) widget_channel?: Channel; @Column({ nullable: true }) diff --git a/util/src/entities/Invite.ts b/util/src/entities/Invite.ts
index d8c6d69c..01e22294 100644 --- a/util/src/entities/Invite.ts +++ b/util/src/entities/Invite.ts
@@ -27,32 +27,36 @@ export class Invite extends BaseClass { @Column() expires_at: Date; + @Column({ nullable: true }) @RelationId((invite: Invite) => invite.guild) guild_id: string; @JoinColumn({ name: "guild_id" }) - @ManyToOne(() => Guild, (guild: Guild) => guild.id) + @ManyToOne(() => Guild) guild: Guild; + @Column({ nullable: true }) @RelationId((invite: Invite) => invite.channel) channel_id: string; @JoinColumn({ name: "channel_id" }) - @ManyToOne(() => Channel, (channel: Channel) => channel.id) + @ManyToOne(() => Channel) channel: Channel; + @Column({ nullable: true }) @RelationId((invite: Invite) => invite.inviter) inviter_id: string; @JoinColumn({ name: "inviter_id" }) - @ManyToOne(() => User, (user: User) => user.id) + @ManyToOne(() => User) inviter: User; + @Column({ nullable: true }) @RelationId((invite: Invite) => invite.target_user) target_user_id: string; @JoinColumn({ name: "target_user_id" }) - @ManyToOne(() => User, (user: User) => user.id) + @ManyToOne(() => User) target_user?: string; // could be used for "User specific invites" https://github.com/fosscord/fosscord/issues/62 @Column({ nullable: true }) diff --git a/util/src/entities/Member.ts b/util/src/entities/Member.ts
index c5d289ef..d2d78bb9 100644 --- a/util/src/entities/Member.ts +++ b/util/src/entities/Member.ts
@@ -15,27 +15,22 @@ import { Role } from "./Role"; @Entity("members") export class Member extends BaseClass { - @RelationId((member: Member) => member.user) - user_id: string; - - @JoinColumn({ name: "user_id" }) - @ManyToOne(() => User, (user: User) => user.id) + @JoinColumn({ name: "id" }) + @ManyToOne(() => User) user: User; + @Column({ nullable: true }) @RelationId((member: Member) => member.guild) guild_id: string; @JoinColumn({ name: "guild_id" }) - @ManyToOne(() => Guild, (guild: Guild) => guild.members) + @ManyToOne(() => Guild) guild: Guild; @Column({ nullable: true }) nick?: string; - @RelationId((member: Member) => member.roles) - role_ids: string[]; - - @JoinTable() + @JoinTable({ name: "member_roles" }) @ManyToMany(() => Role) roles: Role[]; @@ -62,7 +57,7 @@ export class Member extends BaseClass { read_state: Record<string, string | null>; static async IsInGuildOrFail(user_id: string, guild_id: string) { - if (await Member.count({ id: user_id, guild_id })) return true; + if (await Member.count({ id: user_id, guild: { id: guild_id } })) return true; throw new HTTPError("You are not member of this guild", 403); } @@ -99,46 +94,54 @@ export class Member extends BaseClass { static async addRole(user_id: string, guild_id: string, role_id: string) { const [member] = await Promise.all([ + // @ts-ignore Member.findOneOrFail({ where: { id: user_id, guild_id: guild_id }, - relations: ["user"], // we don't want to load the role objects just the ids + relations: ["user", "roles"], // we don't want to load the role objects just the ids + select: ["roles.id"], }), await Role.findOneOrFail({ id: role_id, guild_id: guild_id }), ]); - member.role_ids.push(role_id); - member.save(); + member.roles.push(new Role({ id: role_id })); - await emitEvent({ - event: "GUILD_MEMBER_UPDATE", - data: { + await Promise.all([ + member.save(), + emitEvent({ + event: "GUILD_MEMBER_UPDATE", + data: { + guild_id: guild_id, + user: member.user, + roles: member.roles.map((x) => x.id), + }, guild_id: guild_id, - user: member.user, - roles: member.role_ids, - }, - guild_id: guild_id, - } as GuildMemberUpdateEvent); + } as GuildMemberUpdateEvent), + ]); } static async removeRole(user_id: string, guild_id: string, role_id: string) { const [member] = await Promise.all([ + // @ts-ignore Member.findOneOrFail({ where: { id: user_id, guild_id: guild_id }, - relations: ["user"], // we don't want to load the role objects just the ids + relations: ["user", "roles"], // we don't want to load the role objects just the ids + select: ["roles.id"], }), await Role.findOneOrFail({ id: role_id, guild_id: guild_id }), ]); - member.role_ids.remove(role_id); - member.save(); + member.roles = member.roles.filter((x) => x.id == role_id); - await emitEvent({ - event: "GUILD_MEMBER_UPDATE", - data: { + await Promise.all([ + member.save(), + emitEvent({ + event: "GUILD_MEMBER_UPDATE", + data: { + guild_id: guild_id, + user: member.user, + roles: member.roles.map((x) => x.id), + }, guild_id: guild_id, - user: member.user, - roles: member.role_ids, - }, - guild_id: guild_id, - } as GuildMemberUpdateEvent); + } as GuildMemberUpdateEvent), + ]); } static async changeNickname(user_id: string, guild_id: string, nickname: string) { @@ -175,11 +178,14 @@ export class Member extends BaseClass { throw new HTTPError(`You are at the ${maxGuilds} server limit.`, 403); } - const guild = await Guild.findOneOrFail(guild_id, { + const guild = await Guild.findOneOrFail({ + where: { + id: guild_id, + }, relations: ["channels", "emojis", "members", "roles", "stickers"], }); - if (await Member.count({ id: user.id, guild_id })) + if (await Member.count({ id: user.id, guild: { id: guild_id } })) throw new HTTPError("You are already a member of this guild", 400); const member = { @@ -194,23 +200,23 @@ export class Member extends BaseClass { pending: false, }; // @ts-ignore - guild.joined_at = member.joined_at; + guild.joined_at = member.joined_at.toISOString(); await Promise.all([ - new Member({ + Member.insert({ ...member, + roles: undefined, read_state: {}, settings: { channel_overrides: [], message_notifications: 0, mobile_push: true, - mute_config: null, muted: false, suppress_everyone: false, suppress_roles: false, version: 0, }, - }).save(), + }), Guild.increment({ id: guild_id }, "member_count", 1), emitEvent({ event: "GUILD_MEMBER_ADD", @@ -223,7 +229,7 @@ export class Member extends BaseClass { } as GuildMemberAddEvent), emitEvent({ event: "GUILD_CREATE", - data: guild, + data: { ...guild, members: [...guild.members, member] }, user_id, } as GuildCreateEvent), ]); diff --git a/util/src/entities/Message.ts b/util/src/entities/Message.ts
index 43d0f9d0..542b2b55 100644 --- a/util/src/entities/Message.ts +++ b/util/src/entities/Message.ts
@@ -9,8 +9,10 @@ import { CreateDateColumn, Entity, JoinColumn, + JoinTable, ManyToMany, ManyToOne, + OneToMany, RelationId, UpdateDateColumn, } from "typeorm"; @@ -18,6 +20,7 @@ import { BaseClass } from "./BaseClass"; import { Guild } from "./Guild"; import { Webhook } from "./Webhook"; import { Sticker } from "./Sticker"; +import { Attachment } from "./Attachment"; export enum MessageType { DEFAULT = 0, @@ -44,46 +47,52 @@ export class Message extends BaseClass { @Column() id: string; + @Column({ nullable: true }) @RelationId((message: Message) => message.channel) channel_id: string; @JoinColumn({ name: "channel_id" }) - @ManyToOne(() => Channel, (channel: Channel) => channel.id) + @ManyToOne(() => Channel) channel: Channel; + @Column({ nullable: true }) @RelationId((message: Message) => message.guild) guild_id?: string; @JoinColumn({ name: "guild_id" }) - @ManyToOne(() => Guild, (guild: Guild) => guild.id) + @ManyToOne(() => Guild) guild?: Guild; + @Column({ nullable: true }) @RelationId((message: Message) => message.author) author_id: string; - @JoinColumn({ name: "author_id" }) - @ManyToOne(() => User, (user: User) => user.id) + @JoinColumn({ name: "author_id", referencedColumnName: "id" }) + @ManyToOne(() => User) author?: User; + @Column({ nullable: true }) @RelationId((message: Message) => message.member) member_id: string; @JoinColumn({ name: "member_id" }) - @ManyToOne(() => Member, (member: Member) => member.id) + @ManyToOne(() => Member) member?: Member; + @Column({ nullable: true }) @RelationId((message: Message) => message.webhook) webhook_id: string; @JoinColumn({ name: "webhook_id" }) - @ManyToOne(() => Webhook, (webhook: Webhook) => webhook.id) + @ManyToOne(() => Webhook) webhook?: Webhook; + @Column({ nullable: true }) @RelationId((message: Message) => message.application) application_id: string; @JoinColumn({ name: "application_id" }) - @ManyToOne(() => Application, (application: Application) => application.id) + @ManyToOne(() => Application) application?: Application; @Column({ nullable: true }) @@ -103,29 +112,25 @@ export class Message extends BaseClass { @Column({ nullable: true }) mention_everyone?: boolean; - @RelationId((message: Message) => message.mentions) - mention_user_ids: string[]; - - @JoinColumn({ name: "mention_user_ids" }) - @ManyToMany(() => User, (user: User) => user.id) + @JoinTable({ name: "message_user_mentions" }) + @ManyToMany(() => User) mentions: User[]; - @RelationId((message: Message) => message.mention_roles) - mention_role_ids: string[]; - - @JoinColumn({ name: "mention_role_ids" }) - @ManyToMany(() => Role, (role: Role) => role.id) + @JoinTable({ name: "message_role_mentions" }) + @ManyToMany(() => Role) mention_roles: Role[]; - @RelationId((message: Message) => message.mention_channels) - mention_channel_ids: string[]; - - @JoinColumn({ name: "mention_channel_ids" }) - @ManyToMany(() => Channel, (channel: Channel) => channel.id) + @JoinTable({ name: "message_channel_mentions" }) + @ManyToMany(() => Channel) mention_channels: Channel[]; - @Column({ type: "simple-json" }) - attachments: Attachment[]; + @JoinTable({ name: "message_stickers" }) + @ManyToMany(() => Sticker) + sticker_items?: Sticker[]; + + @JoinColumn({ name: "attachment_ids" }) + @OneToMany(() => Attachment, (attachment: Attachment) => attachment.message) + attachments?: Attachment[]; @Column({ type: "simple-json" }) embeds: Embed[]; @@ -150,14 +155,6 @@ export class Message extends BaseClass { @Column({ nullable: true }) flags?: string; - - @RelationId((message: Message) => message.stickers) - sticker_ids: string[]; - - @JoinColumn({ name: "sticker_ids" }) - @ManyToMany(() => Sticker, (sticker: Sticker) => sticker.id) - stickers?: Sticker[]; - @Column({ type: "simple-json", nullable: true }) message_reference?: { message_id: string; @@ -166,7 +163,7 @@ export class Message extends BaseClass { }; @JoinColumn({ name: "message_reference_id" }) - @ManyToOne(() => Message, (message: Message) => message.id) + @ManyToOne(() => Message) referenced_message?: Message; @Column({ type: "simple-json", nullable: true }) @@ -178,8 +175,8 @@ export class Message extends BaseClass { // user: User; // TODO: autopopulate user }; - @Column({ type: "simple-json" }) - components: MessageComponent[]; + @Column({ type: "simple-json", nullable: true }) + components?: MessageComponent[]; } export interface MessageComponent { @@ -198,17 +195,6 @@ export enum MessageComponentType { Button = 2, } -export interface Attachment { - id: string; // attachment id - filename: string; // name of file attached - size: number; // size of file in bytes - url: string; // source url of file - proxy_url: string; // a proxied url of file - height?: number; // height of file (if image) - width?: number; // width of file (if image) - content_type?: string; -} - export interface Embed { title?: string; //title of embed type?: EmbedType; // type of embed (always "rich" for webhook embeds) diff --git a/util/src/entities/ReadState.ts b/util/src/entities/ReadState.ts
index 650ee4c3..8dd05b21 100644 --- a/util/src/entities/ReadState.ts +++ b/util/src/entities/ReadState.ts
@@ -10,25 +10,28 @@ import { User } from "./User"; @Entity("read_states") export class ReadState extends BaseClass { + @Column({ nullable: true }) @RelationId((read_state: ReadState) => read_state.channel) channel_id: string; @JoinColumn({ name: "channel_id" }) - @ManyToOne(() => Channel, (channel: Channel) => channel.id) + @ManyToOne(() => Channel) channel: Channel; + @Column({ nullable: true }) @RelationId((read_state: ReadState) => read_state.user) user_id: string; @JoinColumn({ name: "user_id" }) - @ManyToOne(() => User, (user: User) => user.id) + @ManyToOne(() => User) user: User; + @Column({ nullable: true }) @RelationId((read_state: ReadState) => read_state.last_message) last_message_id: string; @JoinColumn({ name: "last_message_id" }) - @ManyToOne(() => Message, (message: Message) => message.id) + @ManyToOne(() => Message) last_message?: Message; @Column({ nullable: true }) diff --git a/util/src/entities/Recipient.ts b/util/src/entities/Recipient.ts new file mode 100644
index 00000000..75d5b94d --- /dev/null +++ b/util/src/entities/Recipient.ts
@@ -0,0 +1,19 @@ +import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm"; +import { BaseClass } from "./BaseClass"; + +@Entity("recipients") +export class Recipient extends BaseClass { + @Column() + @RelationId((recipient: Recipient) => recipient.channel) + channel_id: string; + + @JoinColumn({ name: "channel_id" }) + @ManyToOne(() => require("./Channel").Channel) + channel: import("./Channel").Channel; + + @JoinColumn({ name: "id" }) + @ManyToOne(() => require("./User").User) + user: import("./User").User; + + // TODO: settings/mute/nick/added at/encryption keys/read_state +} diff --git a/util/src/entities/Relationship.ts b/util/src/entities/Relationship.ts
index cf3624bf..5935f5b6 100644 --- a/util/src/entities/Relationship.ts +++ b/util/src/entities/Relationship.ts
@@ -11,11 +11,12 @@ export enum RelationshipType { @Entity("relationships") export class Relationship extends BaseClass { + @Column({ nullable: true }) @RelationId((relationship: Relationship) => relationship.user) user_id: string; @JoinColumn({ name: "user_id" }) - @ManyToOne(() => User, (user: User) => user.relationships) + @ManyToOne(() => User) user: User; @Column({ nullable: true }) diff --git a/util/src/entities/Role.ts b/util/src/entities/Role.ts
index be8c731d..33c8d272 100644 --- a/util/src/entities/Role.ts +++ b/util/src/entities/Role.ts
@@ -5,11 +5,12 @@ import { Guild } from "./Guild"; @Entity("roles") export class Role extends BaseClass { + @Column({ nullable: true }) @RelationId((role: Role) => role.guild) guild_id: string; @JoinColumn({ name: "guild_id" }) - @ManyToOne(() => Guild, (guild: Guild) => guild.id) + @ManyToOne(() => Guild) guild: Guild; @Column() diff --git a/util/src/entities/Sticker.ts b/util/src/entities/Sticker.ts
index 12394207..7730a86a 100644 --- a/util/src/entities/Sticker.ts +++ b/util/src/entities/Sticker.ts
@@ -31,7 +31,7 @@ export class Sticker extends BaseClass { guild_id?: string; @JoinColumn({ name: "guild_id" }) - @ManyToOne(() => Guild, (guild: Guild) => guild.id) + @ManyToOne(() => Guild) guild?: Guild; @Column({ type: "simple-enum", enum: StickerType }) diff --git a/util/src/entities/Team.ts b/util/src/entities/Team.ts
index b37f368c..beb8bf68 100644 --- a/util/src/entities/Team.ts +++ b/util/src/entities/Team.ts
@@ -1,4 +1,4 @@ -import { Column, Entity, JoinColumn, ManyToMany, ManyToOne, RelationId } from "typeorm"; +import { Column, Entity, JoinColumn, ManyToMany, ManyToOne, OneToMany, RelationId } from "typeorm"; import { BaseClass } from "./BaseClass"; import { TeamMember } from "./TeamMember"; import { User } from "./User"; @@ -8,20 +8,18 @@ export class Team extends BaseClass { @Column({ nullable: true }) icon?: string; - @RelationId((team: Team) => team.members) - member_ids: string[]; - @JoinColumn({ name: "member_ids" }) - @ManyToMany(() => TeamMember, (member: TeamMember) => member.id) + @OneToMany(() => TeamMember, (member: TeamMember) => member.team) members: TeamMember[]; @Column() name: string; + @Column({ nullable: true }) @RelationId((team: Team) => team.owner_user) owner_user_id: string; @JoinColumn({ name: "owner_user_id" }) - @ManyToOne(() => User, (user: User) => user.id) + @ManyToOne(() => User) owner_user: User; } diff --git a/util/src/entities/TeamMember.ts b/util/src/entities/TeamMember.ts
index d4c95397..6b184d08 100644 --- a/util/src/entities/TeamMember.ts +++ b/util/src/entities/TeamMember.ts
@@ -15,17 +15,19 @@ export class TeamMember extends BaseClass { @Column({ type: "simple-array" }) permissions: string[]; + @Column({ nullable: true }) @RelationId((member: TeamMember) => member.team) team_id: string; @JoinColumn({ name: "team_id" }) - @ManyToOne(() => require("./Team").Team, (team: import("./Team").Team) => team.id) + @ManyToOne(() => require("./Team").Team, (team: import("./Team").Team) => team.members) team: import("./Team").Team; + @Column({ nullable: true }) @RelationId((member: TeamMember) => member.user) user_id: string; @JoinColumn({ name: "user_id" }) - @ManyToOne(() => User, (user: User) => user.id) + @ManyToOne(() => User) user: User; } diff --git a/util/src/entities/Template.ts b/util/src/entities/Template.ts
index 2d8c9b6c..76f77ba6 100644 --- a/util/src/entities/Template.ts +++ b/util/src/entities/Template.ts
@@ -17,11 +17,12 @@ export class Template extends BaseClass { @Column({ nullable: true }) usage_count?: number; + @Column({ nullable: true }) @RelationId((template: Template) => template.creator) creator_id: string; @JoinColumn({ name: "creator_id" }) - @ManyToOne(() => User, (user: User) => user.id) + @ManyToOne(() => User) creator: User; @Column() @@ -30,11 +31,12 @@ export class Template extends BaseClass { @Column() updated_at: Date; + @Column({ nullable: true }) @RelationId((template: Template) => template.source_guild) source_guild_id: string; @JoinColumn({ name: "source_guild_id" }) - @ManyToOne(() => Guild, (guild: Guild) => guild.id) + @ManyToOne(() => Guild) source_guild: Guild; @Column({ type: "simple-json" }) diff --git a/util/src/entities/User.ts b/util/src/entities/User.ts
index 73afba67..39f654be 100644 --- a/util/src/entities/User.ts +++ b/util/src/entities/User.ts
@@ -1,9 +1,10 @@ -import { Column, Entity, FindOneOptions, JoinColumn, OneToMany, RelationId } from "typeorm"; +import { Column, Entity, FindOneOptions, JoinColumn, ManyToMany, OneToMany, RelationId } from "typeorm"; import { BaseClass } from "./BaseClass"; import { BitField } from "../util/BitField"; import { Relationship } from "./Relationship"; import { ConnectedAccount } from "./ConnectedAccount"; import { HTTPError } from "lambert-server"; +import { Channel } from "./Channel"; type PublicUserKeys = | "username" @@ -105,16 +106,10 @@ export class User extends BaseClass { @Column() public_flags: string; - @RelationId((user: User) => user.relationships) - relationship_ids: string[]; // array of guild ids the user is part of - @JoinColumn({ name: "relationship_ids" }) @OneToMany(() => Relationship, (relationship: Relationship) => relationship.user, { cascade: true }) relationships: Relationship[]; - @RelationId((user: User) => user.connected_accounts) - connected_account_ids: string[]; // array of guild ids the user is part of - @JoinColumn({ name: "connected_account_ids" }) @OneToMany(() => ConnectedAccount, (account: ConnectedAccount) => account.user) connected_accounts: ConnectedAccount[]; @@ -126,16 +121,19 @@ export class User extends BaseClass { }; @Column({ type: "simple-array" }) - fingerprints: string[] = []; // array of fingerprints -> used to prevent multiple accounts + fingerprints: string[]; // array of fingerprints -> used to prevent multiple accounts @Column({ type: "simple-json" }) settings: UserSettings; static async getPublicUser(user_id: string, opts?: FindOneOptions<User>) { - const user = await User.findOne(user_id, { - ...opts, - select: [...PublicUserProjection, ...(opts?.select || [])], - }); + const user = await User.findOne( + { id: user_id }, + { + ...opts, + select: [...PublicUserProjection, ...(opts?.select || [])], + } + ); if (!user) throw new HTTPError("User not found", 404); return user; } diff --git a/util/src/entities/VoiceState.ts b/util/src/entities/VoiceState.ts
index e9d3dfa2..c5040cf1 100644 --- a/util/src/entities/VoiceState.ts +++ b/util/src/entities/VoiceState.ts
@@ -1,4 +1,4 @@ -import { Column, Entity, JoinColumn, ManyToOne, OneToOne, RelationId } from "typeorm"; +import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm"; import { BaseClass } from "./BaseClass"; import { Channel } from "./Channel"; import { Guild } from "./Guild"; @@ -6,25 +6,28 @@ import { User } from "./User"; @Entity("voice_states") export class VoiceState extends BaseClass { + @Column({ nullable: true }) @RelationId((voice_state: VoiceState) => voice_state.guild) guild_id: string; @JoinColumn({ name: "guild_id" }) - @ManyToOne(() => Guild, (guild: Guild) => guild.id) + @ManyToOne(() => Guild) guild?: Guild; + @Column({ nullable: true }) @RelationId((voice_state: VoiceState) => voice_state.channel) channel_id: string; @JoinColumn({ name: "channel_id" }) - @ManyToOne(() => Channel, (channel: Channel) => channel.id) + @ManyToOne(() => Channel) channel: Channel; + @Column({ nullable: true }) @RelationId((voice_state: VoiceState) => voice_state.user) user_id: string; @JoinColumn({ name: "user_id" }) - @ManyToOne(() => User, (user: User) => user.id) + @ManyToOne(() => User) user: User; @Column() diff --git a/util/src/entities/Webhook.ts b/util/src/entities/Webhook.ts
index a75cb959..12ba0d08 100644 --- a/util/src/entities/Webhook.ts +++ b/util/src/entities/Webhook.ts
@@ -27,38 +27,43 @@ export class Webhook extends BaseClass { @Column({ nullable: true }) token?: string; + @Column({ nullable: true }) @RelationId((webhook: Webhook) => webhook.guild) guild_id: string; @JoinColumn({ name: "guild_id" }) - @ManyToOne(() => Guild, (guild: Guild) => guild.id) + @ManyToOne(() => Guild) guild: Guild; + @Column({ nullable: true }) @RelationId((webhook: Webhook) => webhook.channel) channel_id: string; @JoinColumn({ name: "channel_id" }) - @ManyToOne(() => Channel, (channel: Channel) => channel.id) + @ManyToOne(() => Channel) channel: Channel; + @Column({ nullable: true }) @RelationId((webhook: Webhook) => webhook.application) application_id: string; @JoinColumn({ name: "application_id" }) - @ManyToOne(() => Application, (application: Application) => application.id) + @ManyToOne(() => Application) application: Application; + @Column({ nullable: true }) @RelationId((webhook: Webhook) => webhook.user) user_id: string; @JoinColumn({ name: "user_id" }) - @ManyToOne(() => User, (user: User) => user.id) + @ManyToOne(() => User) user: User; + @Column({ nullable: true }) @RelationId((webhook: Webhook) => webhook.guild) source_guild_id: string; @JoinColumn({ name: "source_guild_id" }) - @ManyToOne(() => Guild, (guild: Guild) => guild.id) + @ManyToOne(() => Guild) source_guild: Guild; } diff --git a/util/src/entities/index.ts b/util/src/entities/index.ts
index e0246a10..aa37ae2e 100644 --- a/util/src/entities/index.ts +++ b/util/src/entities/index.ts
@@ -1,4 +1,5 @@ export * from "./Application"; +export * from "./Attachment"; export * from "./AuditLog"; export * from "./Ban"; export * from "./BaseClass"; @@ -12,6 +13,7 @@ export * from "./Member"; export * from "./Message"; export * from "./RateLimit"; export * from "./ReadState"; +export * from "./Recipient"; export * from "./Relationship"; export * from "./Role"; export * from "./Sticker"; diff --git a/util/src/interfaces/Event.ts b/util/src/interfaces/Event.ts
index 814a8beb..7ea1bd49 100644 --- a/util/src/interfaces/Event.ts +++ b/util/src/interfaces/Event.ts
@@ -89,14 +89,7 @@ export interface ReadyEventData { }; merged_members?: Omit<Member, "settings" | "user">[][]; // probably all users who the user is in contact with - users?: { - avatar: string | null; - discriminator: string; - id: string; - username: string; - bot: boolean; - public_flags: string; - }[]; + users?: PublicUser[]; } export interface ReadyEvent extends Event { @@ -130,7 +123,9 @@ export interface ChannelPinsUpdateEvent extends Event { export interface GuildCreateEvent extends Event { event: "GUILD_CREATE"; - data: Guild; + data: Guild & { + joined_at: Date; + }; } export interface GuildUpdateEvent extends Event { diff --git a/util/src/tes.ts b/util/src/tes.ts
index b469f1d8..e326dee1 100644 --- a/util/src/tes.ts +++ b/util/src/tes.ts
@@ -5,10 +5,14 @@ import { initDatabase } from "./util"; initDatabase().then(async (x) => { try { - const user = await new User( - { guilds: [], discriminator: "1", username: "test", flags: "0", public_flags: "0" }, - { id: "0" } - ).save(); + const user = await new User({ + guilds: [], + discriminator: "1", + username: "test", + flags: "0", + public_flags: "0", + id: "0", + }).save(); user.relationships = [new Relationship({ type: RelationshipType.friends })]; diff --git a/util/src/util/Config.ts b/util/src/util/Config.ts
index 508a8901..1ec71ad0 100644 --- a/util/src/util/Config.ts +++ b/util/src/util/Config.ts
@@ -7,7 +7,7 @@ var config: ConfigEntity; export const Config = { init: async function init() { if (config) return config; - config = (await ConfigEntity.findOne({ id: "0" })) || new ConfigEntity({}, { id: "0" }); + config = (await ConfigEntity.findOne({ id: "0" })) || new ConfigEntity({ id: "0" }); return this.set((config.value || {}).merge(DefaultConfigOptions)); }, diff --git a/util/src/util/Permissions.ts b/util/src/util/Permissions.ts
index ab6aa795..89b316ee 100644 --- a/util/src/util/Permissions.ts +++ b/util/src/util/Permissions.ts
@@ -220,13 +220,14 @@ export async function getPermission(user_id?: string, guild_id?: string, channel guild = await Guild.findOneOrFail({ id: guild_id }); if (guild.owner_id === user_id) return new Permissions(Permissions.FLAGS.ADMINISTRATOR); - member = await Member.findOneOrFail({ where: { guild_id, id: user_id }, relations: ["roles"] }); + member = await Member.findOneOrFail({ where: { guild: guild_id, id: user_id }, relations: ["roles"] }); } + // TODO: remove guild.roles and convert recipient_ids to recipients var permission = Permissions.finalPermission({ user: { id: user_id, - roles: member?.role_ids || [], + roles: member?.roles.map((x) => x.id) || [], }, guild: { roles: member?.roles || [], @@ -234,7 +235,7 @@ export async function getPermission(user_id?: string, guild_id?: string, channel channel: { overwrites: channel?.permission_overwrites, owner_id: channel?.owner_id, - recipient_ids: channel?.recipient_ids, + recipient_ids: channel?.recipients?.map((x) => x.id), }, });