summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/Server.ts52
-rw-r--r--src/routes/auth/register.ts1
-rw-r--r--src/routes/channels/#channel_id/followers.ts2
-rw-r--r--src/routes/channels/#channel_id/index.ts8
-rw-r--r--src/routes/channels/#channel_id/messages/#message_id/crosspost.ts4
-rw-r--r--src/routes/channels/#channel_id/messages/#message_id/index.ts12
-rw-r--r--src/routes/channels/#channel_id/messages/#message_id/reactions.ts12
-rw-r--r--src/routes/channels/#channel_id/messages/bulk-delete.ts4
-rw-r--r--src/routes/channels/#channel_id/messages/index.ts55
-rw-r--r--src/routes/channels/#channel_id/permissions.ts73
-rw-r--r--src/routes/channels/#channel_id/pins.ts2
-rw-r--r--src/routes/channels/#channel_id/recipients.ts2
-rw-r--r--src/routes/channels/#channel_id/webhooks.ts4
-rw-r--r--src/routes/experiments.ts4
-rw-r--r--src/routes/gateway.ts4
-rw-r--r--src/routes/guilds/#guild_id/bans.ts24
-rw-r--r--src/routes/guilds/#guild_id/channels.ts8
-rw-r--r--src/routes/guilds/#guild_id/invites.ts17
-rw-r--r--src/routes/guilds/#guild_id/members/#member_id/index.ts2
-rw-r--r--src/routes/science.ts4
-rw-r--r--src/routes/users/#id/index.ts8
-rw-r--r--src/routes/users/@me/affinities/guilds.ts4
-rw-r--r--src/routes/users/@me/affinities/user.ts4
-rw-r--r--src/routes/users/@me/channels.ts34
-rw-r--r--src/routes/users/@me/delete.ts28
-rw-r--r--src/routes/users/@me/disable.ts4
-rw-r--r--src/routes/users/@me/index.ts30
-rw-r--r--src/routes/users/@me/library.ts4
-rw-r--r--src/routes/users/@me/relationships.ts156
-rw-r--r--src/routes/users/@me/settings.ts4
-rw-r--r--src/schema/Channel.ts12
-rw-r--r--src/schema/Message.ts1
-rw-r--r--src/schema/User.ts54
-rw-r--r--src/util/Channel.ts2
-rw-r--r--src/util/Message.ts76
-rw-r--r--src/util/User.ts12
-rw-r--r--src/util/cdn.ts24
-rw-r--r--src/util/instanceOf.ts3
38 files changed, 539 insertions, 215 deletions
diff --git a/src/Server.ts b/src/Server.ts

index b1fe3c90..5ae65918 100644 --- a/src/Server.ts +++ b/src/Server.ts
@@ -9,8 +9,8 @@ import i18nextMiddleware, { I18next } from "i18next-http-middleware"; import i18nextBackend from "i18next-node-fs-backend"; import { ErrorHandler } from "./middlewares/ErrorHandler"; import { BodyParser } from "./middlewares/BodyParser"; -import express, { Router } from "express"; -import fetch from "node-fetch"; +import express, { Router, Request, Response } from "express"; +import fetch, { Response as FetchResponse } from "node-fetch"; import mongoose from "mongoose"; import path from "path"; @@ -28,8 +28,16 @@ declare global { } } +const assetCache = new Map< + string, + { + response: FetchResponse; + buffer: Buffer; + } +>(); + export class FosscordServer extends Server { - public options: FosscordServerOptions; + public declare options: FosscordServerOptions; constructor(opts?: Partial<FosscordServerOptions>) { // @ts-ignore @@ -60,7 +68,7 @@ export class FosscordServer extends Server { this.app.use(GlobalRateLimit); this.app.use(Authentication); this.app.use(CORS); - this.app.use(BodyParser({ inflate: true })); + this.app.use(BodyParser({ inflate: true, limit: 1024 * 1024 * 2 })); const languages = await fs.readdir(path.join(__dirname, "..", "locales")); const namespaces = await fs.readdir(path.join(__dirname, "..", "locales", "en")); const ns = namespaces.filter((x) => x.endsWith(".json")).map((x) => x.slice(0, x.length - 5)); @@ -89,19 +97,27 @@ export class FosscordServer extends Server { app.use("/api/v8", prefix); this.app = app; this.app.use(ErrorHandler); - const indexHTML = await fs.readFile(path.join(__dirname, "..", "client_test", "index.html")); + const indexHTML = await fs.readFile(path.join(__dirname, "..", "client_test", "index.html"), { encoding: "utf8" }); this.app.use("/assets", express.static(path.join(__dirname, "..", "assets"))); - this.app.get("/assets/:file", async (req, res) => { + this.app.get("/assets/:file", async (req: Request, res: Response) => { delete req.headers.host; - const response = await fetch(`https://discord.com/assets/${req.params.file}`, { - // @ts-ignore - headers: { - ...req.headers - } - }); - const buffer = await response.buffer(); + var response: FetchResponse; + var buffer: Buffer; + const cache = assetCache.get(req.params.file); + if (!cache) { + response = await fetch(`https://discord.com/assets/${req.params.file}`, { + // @ts-ignore + headers: { + ...req.headers + } + }); + buffer = await response.buffer(); + } else { + response = cache.response; + buffer = cache.buffer; + } response.headers.forEach((value, name) => { if ( @@ -120,13 +136,19 @@ export class FosscordServer extends Server { } res.set(name, value); }); + assetCache.set(req.params.file, { buffer, response }); return res.send(buffer); }); - this.app.get("*", (req, res) => { + this.app.get("*", (req: Request, res: Response) => { res.set("Cache-Control", "public, max-age=" + 60 * 60 * 24); res.set("content-type", "text/html"); - res.send(indexHTML); + res.send( + indexHTML.replace( + /CDN_HOST: ".+"/, + `CDN_HOST: "${(Config.get().cdn.endpoint || "http://localhost:3003").replace(/https?:/, "")}"` + ) + ); }); return super.start(); } diff --git a/src/routes/auth/register.ts b/src/routes/auth/register.ts
index e24485da..50bac43a 100644 --- a/src/routes/auth/register.ts +++ b/src/routes/auth/register.ts
@@ -181,6 +181,7 @@ router.post( premium: false, premium_type: 0, phone: null, + bio: "", mfa_enabled: false, verified: false, disabled: false, diff --git a/src/routes/channels/#channel_id/followers.ts b/src/routes/channels/#channel_id/followers.ts
index c06db61b..641af4f8 100644 --- a/src/routes/channels/#channel_id/followers.ts +++ b/src/routes/channels/#channel_id/followers.ts
@@ -1,4 +1,4 @@ -import { Router } from "express"; +import { Router, Response, Request } from "express"; const router: Router = Router(); // TODO: diff --git a/src/routes/channels/#channel_id/index.ts b/src/routes/channels/#channel_id/index.ts
index 0e5a5124..dcc093ae 100644 --- a/src/routes/channels/#channel_id/index.ts +++ b/src/routes/channels/#channel_id/index.ts
@@ -1,5 +1,5 @@ import { ChannelDeleteEvent, ChannelModel, ChannelUpdateEvent, getPermission, GuildUpdateEvent, toObject } from "@fosscord/server-util"; -import { Router } from "express"; +import { Router, Response, Request } from "express"; import { HTTPError } from "lambert-server"; import { ChannelModifySchema } from "../../../schema/Channel"; import { emitEvent } from "../../../util/Event"; @@ -8,7 +8,7 @@ const router: Router = Router(); // TODO: delete channel // TODO: Get channel -router.get("/", async (req, res) => { +router.get("/", async (req: Request, res: Response) => { const { channel_id } = req.params; const channel = await ChannelModel.findOne({ id: channel_id }).exec(); @@ -20,7 +20,7 @@ router.get("/", async (req, res) => { return res.send(toObject(channel)); }); -router.delete("/", async (req, res) => { +router.delete("/", async (req: Request, res: Response) => { const { channel_id } = req.params; const channel = await ChannelModel.findOne({ id: channel_id }).exec(); @@ -39,7 +39,7 @@ router.delete("/", async (req, res) => { res.send(data); }); -router.patch("/", check(ChannelModifySchema), async (req, res) => { +router.patch("/", check(ChannelModifySchema), async (req: Request, res: Response) => { var payload = req.body as ChannelModifySchema; const { channel_id } = req.params; diff --git a/src/routes/channels/#channel_id/messages/#message_id/crosspost.ts b/src/routes/channels/#channel_id/messages/#message_id/crosspost.ts
index 17f36396..6753e832 100644 --- a/src/routes/channels/#channel_id/messages/#message_id/crosspost.ts +++ b/src/routes/channels/#channel_id/messages/#message_id/crosspost.ts
@@ -1,8 +1,8 @@ -import { Router } from "express"; +import { Router, Response, Request } from "express"; const router = Router(); // TODO: -// router.post("/", (req, res) => {}); +// router.post("/", (req: Request, res: Response) => {}); export default router; diff --git a/src/routes/channels/#channel_id/messages/#message_id/index.ts b/src/routes/channels/#channel_id/messages/#message_id/index.ts
index 5a61b4ad..90727f60 100644 --- a/src/routes/channels/#channel_id/messages/#message_id/index.ts +++ b/src/routes/channels/#channel_id/messages/#message_id/index.ts
@@ -1,14 +1,14 @@ import { ChannelModel, getPermission, MessageDeleteEvent, MessageModel, MessageUpdateEvent, toObject } from "@fosscord/server-util"; -import { Router } from "express"; +import { Router, Response, Request } from "express"; import { HTTPError } from "lambert-server"; import { MessageCreateSchema } from "../../../../../schema/Message"; import { emitEvent } from "../../../../../util/Event"; import { check } from "../../../../../util/instanceOf"; -import { handleMessage } from "../../../../../util/Message"; +import { handleMessage, postHandleMessage } from "../../../../../util/Message"; const router = Router(); -router.patch("/", check(MessageCreateSchema), async (req, res) => { +router.patch("/", check(MessageCreateSchema), async (req: Request, res: Response) => { const { message_id, channel_id } = req.params; var body = req.body as MessageCreateSchema; @@ -40,10 +40,14 @@ router.patch("/", check(MessageCreateSchema), async (req, res) => { data: { ...toObject(message), nonce: undefined } } as MessageUpdateEvent); + postHandleMessage(message); + return res.json(toObject(message)); }); -router.delete("/", async (req, res) => { +// TODO: delete attachments in message + +router.delete("/", async (req: Request, res: Response) => { const { message_id, channel_id } = req.params; const channel = await ChannelModel.findOne({ id: channel_id }, { guild_id: true }); diff --git a/src/routes/channels/#channel_id/messages/#message_id/reactions.ts b/src/routes/channels/#channel_id/messages/#message_id/reactions.ts
index 1bfaae39..c31be435 100644 --- a/src/routes/channels/#channel_id/messages/#message_id/reactions.ts +++ b/src/routes/channels/#channel_id/messages/#message_id/reactions.ts
@@ -13,7 +13,7 @@ import { toObject, UserModel } from "@fosscord/server-util"; -import { Router } from "express"; +import { Router, Response, Request } from "express"; import { HTTPError } from "lambert-server"; import { emitEvent } from "../../../../../util/Event"; @@ -35,7 +35,7 @@ function getEmoji(emoji: string): PartialEmoji { }; } -router.delete("/", async (req, res) => { +router.delete("/", async (req: Request, res: Response) => { const { message_id, channel_id } = req.params; const channel = await ChannelModel.findOne({ id: channel_id }, { guild_id: true }).exec(); @@ -61,7 +61,7 @@ router.delete("/", async (req, res) => { res.sendStatus(204); }); -router.delete("/:emoji", async (req, res) => { +router.delete("/:emoji", async (req: Request, res: Response) => { const { message_id, channel_id } = req.params; const emoji = getEmoji(req.params.emoji); @@ -95,7 +95,7 @@ router.delete("/:emoji", async (req, res) => { res.sendStatus(204); }); -router.get("/:emoji", async (req, res) => { +router.get("/:emoji", async (req: Request, res: Response) => { const { message_id, channel_id } = req.params; const emoji = getEmoji(req.params.emoji); @@ -112,7 +112,7 @@ router.get("/:emoji", async (req, res) => { res.json(toObject(users)); }); -router.put("/:emoji/:user_id", async (req, res) => { +router.put("/:emoji/:user_id", async (req: Request, res: Response) => { const { message_id, channel_id, user_id } = req.params; if (user_id !== "@me") throw new HTTPError("Invalid user"); const emoji = getEmoji(req.params.emoji); @@ -162,7 +162,7 @@ router.put("/:emoji/:user_id", async (req, res) => { res.sendStatus(204); }); -router.delete("/:emoji/:user_id", async (req, res) => { +router.delete("/:emoji/:user_id", async (req: Request, res: Response) => { var { message_id, channel_id, user_id } = req.params; const emoji = getEmoji(req.params.emoji); diff --git a/src/routes/channels/#channel_id/messages/bulk-delete.ts b/src/routes/channels/#channel_id/messages/bulk-delete.ts
index 24724d34..8cb672d8 100644 --- a/src/routes/channels/#channel_id/messages/bulk-delete.ts +++ b/src/routes/channels/#channel_id/messages/bulk-delete.ts
@@ -1,4 +1,4 @@ -import { Router } from "express"; +import { Router, Response, Request } from "express"; import { ChannelModel, Config, getPermission, MessageDeleteBulkEvent, MessageModel } from "@fosscord/server-util"; import { HTTPError } from "lambert-server"; import { emitEvent } from "../../../../util/Event"; @@ -11,7 +11,7 @@ export default router; // TODO: should users be able to bulk delete messages or only bots? // TODO: should this request fail, if you provide messages older than 14 days/invalid ids? // https://discord.com/developers/docs/resources/channel#bulk-delete-messages -router.post("/", check({ messages: [String] }), async (req, res) => { +router.post("/", check({ messages: [String] }), async (req: Request, res: Response) => { const { channel_id } = req.params; const channel = await ChannelModel.findOne({ id: channel_id }, { permission_overwrites: true, guild_id: true }).exec(); if (!channel?.guild_id) throw new HTTPError("Can't bulk delete dm channel messages", 400); diff --git a/src/routes/channels/#channel_id/messages/index.ts b/src/routes/channels/#channel_id/messages/index.ts
index cdc46d14..b42a886b 100644 --- a/src/routes/channels/#channel_id/messages/index.ts +++ b/src/routes/channels/#channel_id/messages/index.ts
@@ -1,24 +1,13 @@ -import { Router } from "express"; -import { - ChannelModel, - ChannelType, - getPermission, - Message, - MessageCreateEvent, - MessageDocument, - MessageModel, - Snowflake, - toObject -} from "@fosscord/server-util"; +import { Router, Response, Request } from "express"; +import { Attachment, ChannelModel, ChannelType, getPermission, MessageDocument, MessageModel, toObject } from "@fosscord/server-util"; import { HTTPError } from "lambert-server"; import { MessageCreateSchema } from "../../../../schema/Message"; import { check, instanceOf, Length } from "../../../../util/instanceOf"; -import { PublicUserProjection } from "../../../../util/User"; import multer from "multer"; -import { emitEvent } from "../../../../util/Event"; import { Query } from "mongoose"; -import { PublicMemberProjection } from "../../../../util/Member"; import { sendMessage } from "../../../../util/Message"; +import { uploadFile } from "../../../../util/cdn"; + const router: Router = Router(); export default router; @@ -39,7 +28,7 @@ export function isTextChannel(type: ChannelType): boolean { // https://discord.com/developers/docs/resources/channel#create-message // get messages -router.get("/", async (req, res) => { +router.get("/", async (req: Request, res: Response) => { const channel_id = req.params.channel_id; const channel = await ChannelModel.findOne({ id: channel_id }, { guild_id: true, type: true, permission_overwrites: true }).exec(); if (!channel) throw new HTTPError("Channel not found", 404); @@ -86,6 +75,8 @@ router.get("/", async (req, res) => { // @ts-ignore delete x.user_ids; }); + // @ts-ignore + if (!x.author) x.author = { discriminator: "0000", username: "Deleted User", public_flags: 0n, avatar: null }; return x; }) @@ -93,7 +84,14 @@ router.get("/", async (req, res) => { }); // TODO: config max upload size -const messageUpload = multer({ limits: { fieldSize: 1024 * 1024 * 1024 * 50 } }); // max upload 50 mb +const messageUpload = multer({ + limits: { + fileSize: 1024 * 1024 * 100, + fields: 10, + files: 1 + }, + storage: multer.memoryStorage() +}); // max upload 50 mb // TODO: dynamically change limit of MessageCreateSchema with config // TODO: check: sum of all characters in an embed structure must not exceed 6000 characters @@ -101,14 +99,31 @@ const messageUpload = multer({ limits: { fieldSize: 1024 * 1024 * 1024 * 50 } }) // https://discord.com/developers/docs/resources/channel#create-message // TODO: text channel slowdown // TODO: trim and replace message content and every embed field + // Send message -router.post("/", check(MessageCreateSchema), async (req, res) => { +router.post("/", check(MessageCreateSchema), messageUpload.single("file"), async (req: Request, res: Response) => { const { channel_id } = req.params; - const body = req.body as MessageCreateSchema; + var body = req.body as MessageCreateSchema; + const attachments: Attachment[] = []; + + if (req.file) { + try { + const file = await uploadFile(`/attachments/${channel_id}`, req.file); + attachments.push({ ...file, proxy_url: file.url }); + } catch (error) { + return res.status(400).json(error); + } + } + + if (body.payload_json) { + body = JSON.parse(body.payload_json); + const errors = instanceOf(MessageCreateSchema, body, { req }); + if (errors !== true) throw errors; + } const embeds = []; if (body.embed) embeds.push(body.embed); - const data = await sendMessage({ ...body, type: 0, pinned: false, author_id: req.user_id, embeds, channel_id }); + const data = await sendMessage({ ...body, type: 0, pinned: false, author_id: req.user_id, embeds, channel_id, attachments }); return res.send(data); }); diff --git a/src/routes/channels/#channel_id/permissions.ts b/src/routes/channels/#channel_id/permissions.ts
index 93c33ea5..3993c424 100644 --- a/src/routes/channels/#channel_id/permissions.ts +++ b/src/routes/channels/#channel_id/permissions.ts
@@ -1,5 +1,74 @@ -import { Router } from "express"; +import { ChannelModel, ChannelPermissionOverwrite, ChannelUpdateEvent, getPermission, MemberModel, RoleModel } from "@fosscord/server-util"; +import { Router, Response, Request } from "express"; +import { HTTPError } from "lambert-server"; +import { emitEvent } from "../../../util/Event"; +import { check } from "../../../util/instanceOf"; const router: Router = Router(); -// TODO: + +// TODO: Only permissions your bot has in the guild or channel can be allowed/denied (unless your bot has a MANAGE_ROLES overwrite in the channel) + +router.put("/:overwrite_id", check({ allow: String, deny: String, type: Number, id: String }), async (req: Request, res: Response) => { + const { channel_id, overwrite_id } = req.params; + const body = req.body as { allow: bigint; deny: bigint; type: number; id: string }; + + var channel = await ChannelModel.findOne({ id: channel_id }, { guild_id: true, permission_overwrites: true }).exec(); + if (!channel || !channel.guild_id) throw new HTTPError("Channel not found", 404); + + const permissions = await getPermission(req.user_id, channel.guild_id, channel_id); + permissions.hasThrow("MANAGE_ROLES"); + + if (body.type === 0) { + if (!(await RoleModel.exists({ id: overwrite_id }))) throw new HTTPError("role not found", 404); + } else if (body.type === 1) { + if (!(await MemberModel.exists({ id: overwrite_id }))) throw new HTTPError("user not found", 404); + } else throw new HTTPError("type not supported"); + + // @ts-ignore + var overwrite: ChannelPermissionOverwrite = channel.permission_overwrites.find((x) => x.id === overwrite_id); + if (!overwrite) { + // @ts-ignore + overwrite = { + id: overwrite_id, + type: body.type, + allow: body.allow, + deny: body.deny + }; + channel.permission_overwrites.push(overwrite); + } + overwrite.allow = body.allow; + overwrite.deny = body.deny; + + channel = await ChannelModel.findOneAndUpdate({ id: channel_id }, channel).exec(); + if (!channel) throw new HTTPError("Channel not found", 404); + + await emitEvent({ + event: "CHANNEL_UPDATE", + channel_id, + guild_id: channel.guild_id, + data: channel + } as ChannelUpdateEvent); + + return res.sendStatus(204); +}); + +// TODO: check permission hierarchy +router.delete("/:overwrite_id", async (req: Request, res: Response) => { + const { channel_id, overwrite_id } = req.params; + + const permissions = await getPermission(req.user_id, undefined, channel_id); + permissions.hasThrow("MANAGE_ROLES"); + + const channel = await ChannelModel.findOneAndUpdate({ id: channel_id }, { $pull: { permission_overwrites: { id: overwrite_id } } }); + if (!channel || !channel.guild_id) throw new HTTPError("Channel not found", 404); + + await emitEvent({ + event: "CHANNEL_UPDATE", + channel_id, + guild_id: channel.guild_id, + data: channel + } as ChannelUpdateEvent); + + return res.sendStatus(204); +}); export default router; diff --git a/src/routes/channels/#channel_id/pins.ts b/src/routes/channels/#channel_id/pins.ts
index 43c504d8..5b61f0d2 100644 --- a/src/routes/channels/#channel_id/pins.ts +++ b/src/routes/channels/#channel_id/pins.ts
@@ -53,7 +53,7 @@ router.put("/:message_id", async (req: Request, res: Response) => { res.sendStatus(204); }); -router.delete("/:message_id", async (req, res) => { +router.delete("/:message_id", async (req: Request, res: Response) => { const { channel_id, message_id } = req.params; const channel = await ChannelModel.findOne({ id: channel_id }).exec(); diff --git a/src/routes/channels/#channel_id/recipients.ts b/src/routes/channels/#channel_id/recipients.ts
index 93c33ea5..ea6bc563 100644 --- a/src/routes/channels/#channel_id/recipients.ts +++ b/src/routes/channels/#channel_id/recipients.ts
@@ -1,4 +1,4 @@ -import { Router } from "express"; +import { Router, Response, Request } from "express"; const router: Router = Router(); // TODO: diff --git a/src/routes/channels/#channel_id/webhooks.ts b/src/routes/channels/#channel_id/webhooks.ts
index a56365b8..b60f4d68 100644 --- a/src/routes/channels/#channel_id/webhooks.ts +++ b/src/routes/channels/#channel_id/webhooks.ts
@@ -1,4 +1,4 @@ -import { Router } from "express"; +import { Router, Response, Request } from "express"; import { check, Length } from "../../../util/instanceOf"; import { ChannelModel, getPermission, trimSpecial } from "@fosscord/server-util"; import { HTTPError } from "lambert-server"; @@ -8,7 +8,7 @@ const router: Router = Router(); // TODO: // TODO: use Image Data Type for avatar instead of String -router.post("/", check({ name: new Length(String, 1, 80), $avatar: String }), async (req, res) => { +router.post("/", check({ name: new Length(String, 1, 80), $avatar: String }), async (req: Request, res: Response) => { const channel_id = req.params.channel_id; const channel = await ChannelModel.findOne({ id: channel_id }, { guild_id: true, type: true }).exec(); if (!channel) throw new HTTPError("Channel not found", 404); diff --git a/src/routes/experiments.ts b/src/routes/experiments.ts
index 6bca49c5..3bdbed62 100644 --- a/src/routes/experiments.ts +++ b/src/routes/experiments.ts
@@ -1,8 +1,8 @@ -import { Router } from "express"; +import { Router, Response, Request } from "express"; const router = Router(); -router.get("/", (req, res) => { +router.get("/", (req: Request, res: Response) => { // TODO: res.send({ fingerprint: "", assignments: [] }); }); diff --git a/src/routes/gateway.ts b/src/routes/gateway.ts
index ffbbe74c..7e8146df 100644 --- a/src/routes/gateway.ts +++ b/src/routes/gateway.ts
@@ -1,9 +1,9 @@ import { Config } from "@fosscord/server-util"; -import { Router } from "express"; +import { Router, Response, Request } from "express"; const router = Router(); -router.get("/", (req, res) => { +router.get("/", (req: Request, res: Response) => { const { endpoint } = Config.get().gateway; res.send({ url: endpoint || process.env.GATEWAY || "ws://localhost:3002" }); }); diff --git a/src/routes/guilds/#guild_id/bans.ts b/src/routes/guilds/#guild_id/bans.ts
index f84950f9..87d2e7f8 100644 --- a/src/routes/guilds/#guild_id/bans.ts +++ b/src/routes/guilds/#guild_id/bans.ts
@@ -11,17 +11,17 @@ import { getPublicUser } from "../../../util/User"; const router: Router = Router(); router.get("/", async (req: Request, res: Response) => { - const guild_id = req.params.id; + const { guild_id } = req.params; const guild = await GuildModel.exists({ id: guild_id }); if (!guild) throw new HTTPError("Guild not found", 404); - var bans = await BanModel.find({ guild_id: guild_id }).exec(); + var bans = await BanModel.find({ guild_id: guild_id }, { user: true, reason: true }).exec(); return res.json(toObject(bans)); }); router.get("/:user", async (req: Request, res: Response) => { - const guild_id = req.params.id; + const { guild_id } = req.params; const user_id = req.params.ban; var ban = await BanModel.findOne({ guild_id: guild_id, user_id: user_id }).exec(); @@ -29,8 +29,8 @@ router.get("/:user", async (req: Request, res: Response) => { return res.json(ban); }); -router.post("/:user_id", check(BanCreateSchema), async (req: Request, res: Response) => { - const guild_id = req.params.id; +router.put("/:user_id", check(BanCreateSchema), async (req: Request, res: Response) => { + const { guild_id } = req.params; const banned_user_id = req.params.user_id; const banned_user = await getPublicUser(banned_user_id); @@ -45,19 +45,19 @@ router.post("/:user_id", check(BanCreateSchema), async (req: Request, res: Respo guild_id: guild_id, ip: getIpAdress(req), executor_id: req.user_id, - reason: req.body.reason, // || otherwise empty + reason: req.body.reason // || otherwise empty }).save(); await emitEvent({ event: "GUILD_BAN_ADD", data: { guild_id: guild_id, - user: banned_user, + user: banned_user }, - guild_id: guild_id, + guild_id: guild_id } as GuildBanAddEvent); - return res.json(ban).send(); + return res.json(toObject(ban)); }); router.delete("/:user_id", async (req: Request, res: Response) => { @@ -73,16 +73,16 @@ router.delete("/:user_id", async (req: Request, res: Response) => { await BanModel.deleteOne({ user_id: banned_user_id, - guild_id, + guild_id }).exec(); await emitEvent({ event: "GUILD_BAN_REMOVE", data: { guild_id, - user: banned_user, + user: banned_user }, - guild_id, + guild_id } as GuildBanRemoveEvent); return res.status(204).send(); diff --git a/src/routes/guilds/#guild_id/channels.ts b/src/routes/guilds/#guild_id/channels.ts
index 9d8a95b0..a3c6f4fe 100644 --- a/src/routes/guilds/#guild_id/channels.ts +++ b/src/routes/guilds/#guild_id/channels.ts
@@ -1,4 +1,4 @@ -import { Router } from "express"; +import { Router, Response, Request } from "express"; import { ChannelCreateEvent, ChannelModel, @@ -16,14 +16,14 @@ import { check } from "../../../util/instanceOf"; import { createChannel } from "../../../util/Channel"; const router = Router(); -router.get("/", async (req, res) => { +router.get("/", async (req: Request, res: Response) => { const { guild_id } = req.params; const channels = await ChannelModel.find({ guild_id }).exec(); res.json(toObject(channels)); }); -router.post("/", check(ChannelModifySchema), async (req, res) => { +router.post("/", check(ChannelModifySchema), async (req: Request, res: Response) => { const { guild_id } = req.params; const body = req.body as ChannelModifySchema; @@ -32,7 +32,7 @@ router.post("/", check(ChannelModifySchema), async (req, res) => { res.json(channel); }); -router.patch("/", check(ChannelModifySchema), async (req, res) => { +router.patch("/", check(ChannelModifySchema), async (req: Request, res: Response) => { const { guild_id } = req.params; const body = req.body as ChannelModifySchema; diff --git a/src/routes/guilds/#guild_id/invites.ts b/src/routes/guilds/#guild_id/invites.ts new file mode 100644
index 00000000..1894ec96 --- /dev/null +++ b/src/routes/guilds/#guild_id/invites.ts
@@ -0,0 +1,17 @@ +import { getPermission, InviteModel, toObject } from "@fosscord/server-util"; +import { Request, Response, Router } from "express"; + +const router = Router(); + +router.get("/", async (req: Request, res: Response) => { + const { guild_id } = req.params; + + const permissions = await getPermission(req.user_id, guild_id); + permissions.hasThrow("MANAGE_GUILD"); + + const invites = await InviteModel.find({ guild_id }).exec(); + + return res.json(toObject(invites)); +}); + +export default router; diff --git a/src/routes/guilds/#guild_id/members/#member_id/index.ts b/src/routes/guilds/#guild_id/members/#member_id/index.ts
index e9356c0d..12eedfb2 100644 --- a/src/routes/guilds/#guild_id/members/#member_id/index.ts +++ b/src/routes/guilds/#guild_id/members/#member_id/index.ts
@@ -28,7 +28,7 @@ router.get("/", async (req: Request, res: Response) => { return res.json(toObject(member)); }); -router.patch("/", check(MemberChangeSchema), async (req, res) => { +router.patch("/", check(MemberChangeSchema), async (req: Request, res: Response) => { const { guild_id, member_id } = req.params; const body = req.body as MemberChangeSchema; if (body.roles) { diff --git a/src/routes/science.ts b/src/routes/science.ts
index ab3ce58c..b16ef783 100644 --- a/src/routes/science.ts +++ b/src/routes/science.ts
@@ -1,8 +1,8 @@ -import { Router } from "express"; +import { Router, Response, Request } from "express"; const router = Router(); -router.post("/", (req, res) => { +router.post("/", (req: Request, res: Response) => { // TODO: res.sendStatus(204); }); diff --git a/src/routes/users/#id/index.ts b/src/routes/users/#id/index.ts
index d5f3b788..a2ad3ae6 100644 --- a/src/routes/users/#id/index.ts +++ b/src/routes/users/#id/index.ts
@@ -1,19 +1,13 @@ import { Router, Request, Response } from "express"; -import { UserModel, toObject } from "@fosscord/server-util"; import { getPublicUser } from "../../../util/User"; import { HTTPError } from "lambert-server"; -import { UserUpdateSchema } from "../../../schema/User"; -import { check } from "../../../util/instanceOf"; const router: Router = Router(); router.get("/", async (req: Request, res: Response) => { const { id } = req.params; - const user = await getPublicUser(id); - if (!user) throw new HTTPError("User not found", 404); - res.json(user); + res.json(await getPublicUser(id)); }); - export default router; diff --git a/src/routes/users/@me/affinities/guilds.ts b/src/routes/users/@me/affinities/guilds.ts
index ea0fe59d..fa6be0e7 100644 --- a/src/routes/users/@me/affinities/guilds.ts +++ b/src/routes/users/@me/affinities/guilds.ts
@@ -1,8 +1,8 @@ -import { Router } from "express"; +import { Router, Response, Request } from "express"; const router = Router(); -router.get("/", (req, res) => { +router.get("/", (req: Request, res: Response) => { // TODO: res.status(200).send({ guild_affinities: [] }); }); diff --git a/src/routes/users/@me/affinities/user.ts b/src/routes/users/@me/affinities/user.ts
index 2e435995..0790a8a4 100644 --- a/src/routes/users/@me/affinities/user.ts +++ b/src/routes/users/@me/affinities/user.ts
@@ -1,8 +1,8 @@ -import { Router } from "express"; +import { Router, Response, Request } from "express"; const router = Router(); -router.get("/", (req, res) => { +router.get("/", (req: Request, res: Response) => { // TODO: res.status(200).send({ user_affinities: [], inverse_user_affinities: [] }); }); diff --git a/src/routes/users/@me/channels.ts b/src/routes/users/@me/channels.ts
index 3b2b6781..a425a25f 100644 --- a/src/routes/users/@me/channels.ts +++ b/src/routes/users/@me/channels.ts
@@ -2,50 +2,52 @@ import { Router, Request, Response } from "express"; import { ChannelModel, ChannelCreateEvent, - DMChannel, - UserModel, toObject, ChannelType, Snowflake, trimSpecial, + Channel, + DMChannel, + UserModel } from "@fosscord/server-util"; import { HTTPError } from "lambert-server"; import { emitEvent } from "../../../util/Event"; -import { getPublicUser } from "../../../util/User"; import { DmChannelCreateSchema } from "../../../schema/Channel"; import { check } from "../../../util/instanceOf"; const router: Router = Router(); router.get("/", async (req: Request, res: Response) => { - var channels = await ChannelModel.find({ - $or: [ - { recipients: req.user_id, type: ChannelType.DM }, - { recipients: req.user_id, type: ChannelType.GROUP_DM }, - ], - }).exec(); + var channels = await ChannelModel.find({ recipient_ids: req.user_id }).exec(); res.json(toObject(channels)); }); -router.post("/", check(DmChannelCreateSchema), async (req, res) => { +router.post("/", check(DmChannelCreateSchema), async (req: Request, res: Response) => { const body = req.body as DmChannelCreateSchema; - if (body.recipients.length === 0) throw new HTTPError("You need to specify at least one recipient"); + + body.recipients = body.recipients.filter((x) => x !== req.user_id).unique(); + + if (!(await Promise.all(body.recipients.map((x) => UserModel.exists({ id: x })))).every((x) => x)) { + throw new HTTPError("Recipient not found"); + } + const type = body.recipients.length === 1 ? ChannelType.DM : ChannelType.GROUP_DM; const name = trimSpecial(body.name); - const channel = { + const channel = await new ChannelModel({ name, type, owner_id: req.user_id, id: Snowflake.generate(), created_at: new Date(), - }; - await new ChannelModel(channel).save(); + last_message_id: null, + recipient_ids: [...body.recipients, req.user_id] + }).save(); - /*Event({ event: "CHANNEL_CREATE", data: channel } as ChannelCreateEvent);*/ + await emitEvent({ event: "CHANNEL_CREATE", data: toObject(channel), user_id: req.user_id } as ChannelCreateEvent); - res.json(channel); + res.json(toObject(channel)); }); export default router; diff --git a/src/routes/users/@me/delete.ts b/src/routes/users/@me/delete.ts
index ec4cc223..edda8e2d 100644 --- a/src/routes/users/@me/delete.ts +++ b/src/routes/users/@me/delete.ts
@@ -1,29 +1,21 @@ import { Router, Request, Response } from "express"; -import { UserModel,UserDocument, toObject } from "@fosscord/server-util"; -import { getPublicUser } from "../../../util/User"; -import { HTTPError } from "lambert-server"; -import { UserUpdateSchema } from "../../../schema/User"; -import { check, FieldErrors, Length } from "../../../util/instanceOf"; -import { db } from "@fosscord/server-util"; +import { GuildModel, MemberModel, UserModel } from "@fosscord/server-util"; import bcrypt from "bcrypt"; const router = Router(); router.post("/", async (req: Request, res: Response) => { + const user = await UserModel.findOne({ id: req.user_id }).exec(); //User object - const user = await UserModel.findOne( - { id: req.user_id }, - - ).exec(); //User object - - let correctpass = await bcrypt.compare(req.body.password,user!.user_data.hash) //Not sure if user typed right password :/ - if(correctpass){ - await UserModel.deleteOne({id: req.user_id}).exec() //Yeetus user deletus + let correctpass = await bcrypt.compare(req.body.password, user!.user_data.hash); //Not sure if user typed right password :/ + if (correctpass) { + await Promise.all([ + UserModel.deleteOne({ id: req.user_id }).exec(), //Yeetus user deletus + MemberModel.deleteMany({ id: req.user_id }).exec() + ]); - res.sendStatus(204); - } - else{ + res.sendStatus(204); + } else { res.sendStatus(401); - } }); diff --git a/src/routes/users/@me/disable.ts b/src/routes/users/@me/disable.ts
index ab3ce58c..b16ef783 100644 --- a/src/routes/users/@me/disable.ts +++ b/src/routes/users/@me/disable.ts
@@ -1,8 +1,8 @@ -import { Router } from "express"; +import { Router, Response, Request } from "express"; const router = Router(); -router.post("/", (req, res) => { +router.post("/", (req: Request, res: Response) => { // TODO: res.sendStatus(204); }); diff --git a/src/routes/users/@me/index.ts b/src/routes/users/@me/index.ts
index d139203d..4f17fbee 100644 --- a/src/routes/users/@me/index.ts +++ b/src/routes/users/@me/index.ts
@@ -2,31 +2,35 @@ import { Router, Request, Response } from "express"; import { UserModel, toObject } from "@fosscord/server-util"; import { HTTPError } from "lambert-server"; import { getPublicUser } from "../../../util/User"; -import { UserModifySchema } from "../../../schema/User" +import { UserModifySchema } from "../../../schema/User"; import { check } from "../../../util/instanceOf"; +import { uploadFile } from "../../../util/cdn"; const router: Router = Router(); router.get("/", async (req: Request, res: Response) => { - const user = await UserModel.findOne({ id: req.user_id }).exec(); - if (!user) throw new HTTPError("User not found", 404); - - var publicUser = await getPublicUser(user.id); - - res.json(publicUser); + res.json(await getPublicUser(req.user_id)); }); router.patch("/", check(UserModifySchema), async (req: Request, res: Response) => { const body = req.body as UserModifySchema; - const user = await UserModel.findOne({ id: req.user_id }).exec(); - if (!user) throw new HTTPError("User not found", 404); + if (body.avatar) { + try { + const mimetype = body.avatar.split(":")[1].split(";")[0]; + const buffer = Buffer.from(body.avatar.split(",")[1], "base64"); + + // @ts-ignore + const { id } = await uploadFile(`/avatars/${req.user_id}`, { buffer, mimetype, originalname: "avatar" }); + body.avatar = id; + } catch (error) { + throw new HTTPError("Invalid avatar"); + } + } - var newuser = await UserModel.findOneAndUpdate({ id: req.user_id }, { - ...body - }).exec(); + const user = await UserModel.findOneAndUpdate({ id: req.user_id }, body).exec(); - res.json(newuser); + res.json(toObject(user)); }); export default router; diff --git a/src/routes/users/@me/library.ts b/src/routes/users/@me/library.ts
index 2ffff851..d771cb5e 100644 --- a/src/routes/users/@me/library.ts +++ b/src/routes/users/@me/library.ts
@@ -1,8 +1,8 @@ -import { Router } from "express"; +import { Router, Response, Request } from "express"; const router = Router(); -router.get("/", (req, res) => { +router.get("/", (req: Request, res: Response) => { // TODO: res.status(200).send([]); }); diff --git a/src/routes/users/@me/relationships.ts b/src/routes/users/@me/relationships.ts new file mode 100644
index 00000000..b874ec9a --- /dev/null +++ b/src/routes/users/@me/relationships.ts
@@ -0,0 +1,156 @@ +import { + RelationshipAddEvent, + UserModel, + PublicUserProjection, + toObject, + RelationshipType, + RelationshipRemoveEvent +} from "@fosscord/server-util"; +import { Router, Response, Request } from "express"; +import { check, HTTPError } from "lambert-server"; +import { emitEvent } from "../../../util/Event"; + +const router = Router(); + +const userProjection = { "user_data.relationships": true, ...PublicUserProjection }; + +router.put("/:id", check({ $type: Number }), async (req: Request, res: Response) => { + const { id } = req.params; + if (id === req.user_id) throw new HTTPError("You can't add yourself as a friend"); + const body = req.body as { type?: number }; + + const user = await UserModel.findOne({ id: req.user_id }, userProjection).exec(); + if (!user) throw new HTTPError("Invalid token", 400); + + const friend = await UserModel.findOne({ id }, userProjection).exec(); + if (!friend) throw new HTTPError("User not found", 404); + + var relationship = user.user_data.relationships.find((x) => x.id === id); + const friendRequest = friend.user_data.relationships.find((x) => x.id === req.user_id); + + if (body.type === RelationshipType.blocked) { + if (relationship) { + if (relationship.type === RelationshipType.blocked) throw new HTTPError("You already blocked the user"); + relationship.type = RelationshipType.blocked; + } else { + relationship = { id, type: RelationshipType.blocked }; + user.user_data.relationships.push(relationship); + } + + if (friendRequest && friendRequest.type !== RelationshipType.blocked) { + friend.user_data.relationships.remove(friendRequest); + await Promise.all([ + friend.save(), + emitEvent({ + event: "RELATIONSHIP_REMOVE", + data: friendRequest, + user_id: id + } as RelationshipRemoveEvent) + ]); + } + + await Promise.all([ + user.save(), + emitEvent({ + event: "RELATIONSHIP_ADD", + data: { + ...toObject(relationship), + user: { ...toObject(friend), user_data: undefined } + }, + user_id: req.user_id + } as RelationshipAddEvent) + ]); + + return res.sendStatus(204); + } + + var incoming_relationship = { id: req.user_id, nickname: undefined, type: RelationshipType.incoming }; + var outgoing_relationship = { id, nickname: undefined, type: RelationshipType.outgoing }; + + if (friendRequest) { + if (friendRequest.type === RelationshipType.blocked) throw new HTTPError("The user blocked you"); + // accept friend request + // @ts-ignore + incoming_relationship = friendRequest; + incoming_relationship.type = RelationshipType.friends; + outgoing_relationship.type = RelationshipType.friends; + } else friend.user_data.relationships.push(incoming_relationship); + + if (relationship) { + if (relationship.type === RelationshipType.outgoing) throw new HTTPError("You already sent a friend request"); + if (relationship.type === RelationshipType.blocked) throw new HTTPError("Unblock the user before sending a friend request"); + if (relationship.type === RelationshipType.friends) throw new HTTPError("You are already friends with the user"); + } else user.user_data.relationships.push(outgoing_relationship); + + await Promise.all([ + user.save(), + friend.save(), + emitEvent({ + event: "RELATIONSHIP_ADD", + data: { + ...outgoing_relationship, + user: { ...toObject(friend), user_data: undefined } + }, + user_id: req.user_id + } as RelationshipAddEvent), + emitEvent({ + event: "RELATIONSHIP_ADD", + data: { + ...toObject(incoming_relationship), + should_notify: true, + user: { ...toObject(user), user_data: undefined } + }, + user_id: id + } as RelationshipAddEvent) + ]); + + return res.sendStatus(204); +}); + +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 UserModel.findOne({ id: req.user_id }).exec(); + if (!user) throw new HTTPError("Invalid token", 400); + + const friend = await UserModel.findOne({ id }, userProjection).exec(); + if (!friend) throw new HTTPError("User not found", 404); + + const relationship = user.user_data.relationships.find((x) => x.id === id); + const friendRequest = friend.user_data.relationships.find((x) => x.id === req.user_id); + if (relationship?.type === RelationshipType.blocked) { + // unblock user + user.user_data.relationships.remove(relationship); + + await Promise.all([ + user.save(), + emitEvent({ event: "RELATIONSHIP_REMOVE", user_id: req.user_id, data: relationship } as RelationshipRemoveEvent) + ]); + return res.sendStatus(204); + } + if (!relationship || !friendRequest) throw new HTTPError("You are not friends with the user", 404); + if (friendRequest.type === RelationshipType.blocked) throw new HTTPError("The user blocked you"); + + user.user_data.relationships.remove(relationship); + friend.user_data.relationships.remove(friendRequest); + + await Promise.all([ + user.save(), + friend.save(), + emitEvent({ + event: "RELATIONSHIP_REMOVE", + data: relationship, + user_id: req.user_id + } as RelationshipRemoveEvent), + emitEvent({ + event: "RELATIONSHIP_REMOVE", + data: friendRequest, + user_id: id + } as RelationshipRemoveEvent) + ]); + + return res.sendStatus(204); +}); + +export default router; diff --git a/src/routes/users/@me/settings.ts b/src/routes/users/@me/settings.ts
index f1d95caf..cca9b3ab 100644 --- a/src/routes/users/@me/settings.ts +++ b/src/routes/users/@me/settings.ts
@@ -1,8 +1,8 @@ -import { Router } from "express"; +import { Router, Response, Request } from "express"; const router = Router(); -router.patch("/", (req, res) => { +router.patch("/", (req: Request, res: Response) => { // TODO: res.sendStatus(204); }); diff --git a/src/schema/Channel.ts b/src/schema/Channel.ts
index a89d5fb3..2cb7f7f4 100644 --- a/src/schema/Channel.ts +++ b/src/schema/Channel.ts
@@ -14,16 +14,16 @@ export const ChannelModifySchema = { id: String, type: new Length(Number, 0, 1), // either 0 (role) or 1 (member) allow: BigInt, - deny: BigInt, - }, + deny: BigInt + } ], $parent_id: String, - $nsfw: Boolean, + $nsfw: Boolean }; export const DmChannelCreateSchema = { $name: String, - recipients: [String], + recipients: new Length([String], 1, 10) }; export interface DmChannelCreateSchema { @@ -52,8 +52,8 @@ export interface ChannelModifySchema { export const ChannelGuildPositionUpdateSchema = [ { id: String, - $position: Number, - }, + $position: Number + } ]; export type ChannelGuildPositionUpdateSchema = { diff --git a/src/schema/Message.ts b/src/schema/Message.ts
index e6aa42b3..b2e4b1f7 100644 --- a/src/schema/Message.ts +++ b/src/schema/Message.ts
@@ -68,4 +68,5 @@ export interface MessageCreateSchema { fail_if_not_exists: boolean; }; payload_json?: string; + file?: any; } diff --git a/src/schema/User.ts b/src/schema/User.ts
index 15f27088..d5a7b909 100644 --- a/src/schema/User.ts +++ b/src/schema/User.ts
@@ -1,53 +1,13 @@ -export const UserUpdateSchema = { - id: String, - username: String, - discriminator: String, - avatar: String || null, - $phone: String, - desktop: Boolean, - mobile: Boolean, - premium: Boolean, - premium_type: Number, - bot: Boolean, - system: Boolean, - nsfw_allowed: Boolean, - mfa_enabled: Boolean, - created_at: Date, - verified: Boolean, - $email: String, - flags: BigInt, - public_flags: BigInt, - $guilds: [String], -}; - -export interface UserUpdateSchema { - id: string; - username: string; - discriminator: string; - avatar: string | null; - phone?: string; - desktop: boolean; - mobile: boolean; - premium: boolean; - premium_type: number; - bot: boolean; - system: boolean; - nsfw_allowed: boolean; - mfa_enabled: boolean; - created_at: Date; - verified: boolean; - email?: string; - flags: bigint; - public_flags: bigint; - guilds: string[]; -} +import { Length } from "../util/instanceOf"; export const UserModifySchema = { - username: String, - avatar: String || null, + $username: new Length(String, 2, 32), + $avatar: String, + $bio: new Length(String, 0, 190) }; export interface UserModifySchema { - username: string; - avatar: string | null; + username?: string; + avatar?: string | null; + bio?: string; } diff --git a/src/util/Channel.ts b/src/util/Channel.ts
index c8df85bc..8dfc03bc 100644 --- a/src/util/Channel.ts +++ b/src/util/Channel.ts
@@ -45,7 +45,7 @@ export async function createChannel(channel: Partial<TextChannel | VoiceChannel> id: Snowflake.generate(), created_at: new Date(), // @ts-ignore - recipients: null + recipient_ids: null }).save(); await emitEvent({ event: "CHANNEL_CREATE", data: channel, guild_id: channel.guild_id } as ChannelCreateEvent); diff --git a/src/util/Message.ts b/src/util/Message.ts
index 0d3cdac7..9b928031 100644 --- a/src/util/Message.ts +++ b/src/util/Message.ts
@@ -1,14 +1,28 @@ -import { ChannelModel, MessageCreateEvent } from "@fosscord/server-util"; +import { ChannelModel, Embed, Message, MessageCreateEvent, MessageUpdateEvent } from "@fosscord/server-util"; import { Snowflake } from "@fosscord/server-util"; import { MessageModel } from "@fosscord/server-util"; import { PublicMemberProjection } from "@fosscord/server-util"; import { toObject } from "@fosscord/server-util"; import { getPermission } from "@fosscord/server-util"; -import { Message } from "@fosscord/server-util"; import { HTTPError } from "lambert-server"; +import fetch from "node-fetch"; +import cheerio from "cheerio"; import { emitEvent } from "./Event"; // TODO: check webhook, application, system author +const LINK_REGEX = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/g; + +const DEFAULT_FETCH_OPTIONS: any = { + redirect: "follow", + follow: 1, + headers: { + "user-agent": "Mozilla/5.0 (compatible; Discordbot/2.0; +https://discordapp.com)" + }, + size: 1024 * 1024 * 1, + compress: true, + method: "GET" +}; + export async function handleMessage(opts: Partial<Message>) { const channel = await ChannelModel.findOne({ id: opts.channel_id }, { guild_id: true, type: true, permission_overwrites: true }).exec(); if (!channel || !opts.channel_id) throw new HTTPError("Channel not found", 404); @@ -36,13 +50,67 @@ export async function handleMessage(opts: Partial<Message>) { mention_channels_ids: [], mention_role_ids: [], mention_user_ids: [], - attachments: [], // TODO: message attachments + attachments: opts.attachments || [], // TODO: message attachments embeds: opts.embeds || [], reactions: opts.reactions || [], type: opts.type ?? 0 }; } +// TODO: cache link result in db +export async function postHandleMessage(message: Message) { + var links = message.content?.match(LINK_REGEX); + if (!links) return; + + const data = { ...message }; + data.embeds = data.embeds.filter((x) => x.type !== "link"); + + links = links.slice(0, 5); // embed max 5 links + + for (const link of links) { + try { + const request = await fetch(link, DEFAULT_FETCH_OPTIONS); + + const text = await request.text(); + const $ = cheerio.load(text); + + const title = $('meta[property="og:title"]').attr("content"); + const provider_name = $('meta[property="og:site_name"]').text(); + const author_name = $('meta[property="article:author"]').attr("content"); + const description = $('meta[property="og:description"]').attr("content") || $('meta[property="description"]').attr("content"); + const image = $('meta[property="og:image"]').attr("content"); + const url = $('meta[property="og:url"]').attr("content"); + // TODO: color + const embed: Embed = { + provider: { + url: link, + name: provider_name + } + }; + + if (author_name) embed.author = { name: author_name }; + if (image) embed.thumbnail = { proxy_url: image, url: image }; + if (title) embed.title = title; + if (url) embed.url = url; + if (description) embed.description = description; + + if (title || description) { + data.embeds.push(embed); + } + } catch (error) {} + } + + await Promise.all([ + emitEvent({ + event: "MESSAGE_UPDATE", + guild_id: message.guild_id, + channel_id: message.channel_id, + data + } as MessageUpdateEvent), + MessageModel.updateOne({ id: message.id, channel_id: message.channel_id }, data).exec() + ]); +} + export async function sendMessage(opts: Partial<Message>) { const message = await handleMessage({ ...opts, id: Snowflake.generate(), timestamp: new Date() }); @@ -50,5 +118,7 @@ export async function sendMessage(opts: Partial<Message>) { await emitEvent({ event: "MESSAGE_CREATE", channel_id: opts.channel_id, data, guild_id: message.guild_id } as MessageCreateEvent); + postHandleMessage(data); // no await as it shouldnt block the message send function + return data; } diff --git a/src/util/User.ts b/src/util/User.ts
index 0f3768cc..107fc759 100644 --- a/src/util/User.ts +++ b/src/util/User.ts
@@ -1,20 +1,14 @@ -import { toObject, UserModel } from "@fosscord/server-util"; +import { toObject, UserModel, PublicUserProjection } from "@fosscord/server-util"; import { HTTPError } from "lambert-server"; -export const PublicUserProjection = { - username: true, - discriminator: true, - id: true, - public_flags: true, - avatar: true, -}; +export { PublicUserProjection }; export async function getPublicUser(user_id: string, additional_fields?: any) { const user = await UserModel.findOne( { id: user_id }, { ...PublicUserProjection, - ...additional_fields, + ...additional_fields } ).exec(); if (!user) throw new HTTPError("User not found", 404); diff --git a/src/util/cdn.ts b/src/util/cdn.ts new file mode 100644
index 00000000..a66e2215 --- /dev/null +++ b/src/util/cdn.ts
@@ -0,0 +1,24 @@ +import { Config } from "@fosscord/server-util"; +import FormData from "form-data"; +import fetch from "node-fetch"; + +export async function uploadFile(path: string, file: Express.Multer.File) { + const form = new FormData(); + form.append("file", file.buffer, { + contentType: file.mimetype, + filename: file.originalname + }); + + const response = await fetch(`${Config.get().cdn.endpoint || "http://localhost:3003"}${path}`, { + headers: { + signature: Config.get().security.requestSignature, + ...form.getHeaders() + }, + method: "POST", + body: form + }); + const result = await response.json(); + + if (response.status !== 200) throw result; + return result; +} diff --git a/src/util/instanceOf.ts b/src/util/instanceOf.ts
index b67bde27..93a92805 100644 --- a/src/util/instanceOf.ts +++ b/src/util/instanceOf.ts
@@ -74,10 +74,9 @@ export function instanceOf( ): Boolean { if (!ref) ref = { obj: null, key: "" }; if (!path) path = "body"; + if (!type) return true; // no type was specified try { - if (!type) return true; // no type was specified - if (value == null) { if (optional) return true; throw new FieldError("BASE_TYPE_REQUIRED", req.t("common:field.BASE_TYPE_REQUIRED"));