This commit is contained in:
Flam3rboy
2021-04-14 15:01:53 +02:00
58 changed files with 944 additions and 971 deletions
+44 -5
View File
@@ -10,6 +10,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 { Router } from "express";
import fetch from "node-fetch";
export interface DiscordServerOptions extends ServerOptions {}
@@ -69,15 +71,52 @@ export class DiscordServer extends Server {
});
this.app.use(i18nextMiddleware.handle(i18next, {}));
const app = this.app;
const prefix = Router();
// @ts-ignore
this.app = prefix;
this.routes = await this.registerRoutes(__dirname + "/routes/");
app.use("/api/v8", prefix);
this.app = app;
this.app.use(ErrorHandler);
const indexHTML = await fs.readFile(__dirname + "/../client_test/index.html");
// this.app.get("*", (req, res) => {
// res.set("Cache-Control", "public, max-age=" + 60 * 60 * 24);
// res.set("content-type", "text/html");
// res.send(indexHTML);
// });
this.app.get("/assets/:file", async (req, res) => {
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();
response.headers.forEach((value, name) => {
if (
[
"content-length",
"content-security-policy",
"strict-transport-security",
"set-cookie",
"transfer-encoding",
"expect-ct",
"access-control-allow-origin",
"content-encoding",
].includes(name.toLowerCase())
) {
return;
}
res.set(name, value);
});
return res.send(buffer);
});
this.app.get("*", (req, res) => {
res.set("Cache-Control", "public, max-age=" + 60 * 60 * 24);
res.set("content-type", "text/html");
res.send(indexHTML);
});
return super.start();
}
}
+10 -2
View File
@@ -2,7 +2,13 @@ import { NextFunction, Request, Response } from "express";
import { HTTPError } from "lambert-server";
import { checkToken } from "fosscord-server-util";
export const NO_AUTHORIZATION_ROUTES = ["/api/v8/auth/login", "/api/v8/auth/register", "/api/v8/webhooks/"];
export const NO_AUTHORIZATION_ROUTES = [
"/api/v8/auth/login",
"/api/v8/auth/register",
"/api/v8/webhooks/",
"/api/v8/gateway",
"/api/v8/experiments",
];
declare global {
namespace Express {
@@ -14,6 +20,8 @@ declare global {
}
export async function Authentication(req: Request, res: Response, next: NextFunction) {
if (!req.url.startsWith("/api")) return next();
if (req.url.startsWith("/api/v8/invites") && req.method === "GET") return next();
if (NO_AUTHORIZATION_ROUTES.some((x) => req.url.startsWith(x))) return next();
if (!req.headers.authorization) return next(new HTTPError("Missing Authorization Header", 401));
// TODO: check if user is banned/token expired
@@ -22,7 +30,7 @@ export async function Authentication(req: Request, res: Response, next: NextFunc
const decoded: any = await checkToken(req.headers.authorization);
req.token = decoded;
req.user_id = BigInt(decoded.id);
req.user_id = decoded.id;
return next();
} catch (error) {
return next(new HTTPError(error.toString(), 400));
+3 -2
View File
@@ -16,14 +16,15 @@ export function ErrorHandler(error: Error, req: Request, res: Response, next: Ne
errors = error.errors;
} else {
console.error(error);
if (req.server.options.production) {
if (req.server?.options?.production) {
message = "Internal Server Error";
}
code = httpcode = 500;
}
res.status(httpcode).json({ code: code, message, errors });
return next();
return;
} catch (error) {
console.error(error);
return res.status(500).json({ code: 500, message: "Internal Server Error" });
@@ -1,48 +0,0 @@
import { Router, Request, Response } from "express";
import { HTTPError } from "lambert-server";
import { check } from "@util/instanceOf";
import { random } from "@util/RandomInviteID";
import { emitEvent } from "@util/Event";
import { InviteCreateSchema } from "@schema/Invite";
import { getPermission, ChannelModel, InviteModel, InviteCreateEvent } from "fosscord-server-util";
const router: Router = Router();
router.post("/", check(InviteCreateSchema), async (req: Request, res: Response) => {
const usID = req.user_id;
const chID = BigInt(req.params.channel_id);
const channel = await ChannelModel.findOne({ id: chID }).exec();
if (!channel || !channel.guild_id) {
throw new HTTPError("This channel doesn't exist", 404);
}
const { guild_id: guID } = channel;
const permission = await getPermission(usID, guID);
if (!permission.has("CREATE_INSTANT_INVITE")) {
throw new HTTPError("You aren't authorised to access this endpoint", 401);
}
const invite = {
code: random(),
temporary: req.body.temporary,
uses: 0,
max_uses: req.body.max_uses,
max_age: req.body.max_age,
created_at: new Date(),
guild_id: guID,
channel_id: chID,
inviter_id: usID,
};
await new InviteModel(invite).save();
await emitEvent({ event: "INVITE_CREATE", data: invite } as InviteCreateEvent);
res.status(201).send(invite);
});
export default router;
-4
View File
@@ -1,4 +0,0 @@
import { Router } from "express";
const router: Router = Router();
export default router;
-34
View File
@@ -1,34 +0,0 @@
/**
* * patch to redirect requests from cloned client
* (../../client/index.html)
*/
import { Router } from "express";
import fetch, { Response } from "node-fetch";
const router: Router = Router();
const cache = new Map<string, Response>();
const assetEndpoint = "https://discord.com/assets/";
export async function getCache(key: string): Promise<Response> {
let cachedRessource = cache.get(key);
if (!cachedRessource) {
const res = await fetch(assetEndpoint + key);
// @ts-ignore
res.bufferResponse = await res.buffer();
cache.set(key, res);
cachedRessource = res;
}
return cachedRessource;
}
router.get("/:hash", async (req, res) => {
res.set("Cache-Control", "public, max-age=" + 60 * 60 * 24);
const cache = await getCache(req.params.hash);
res.set("content-type", <string>cache.headers.get("content-type"));
// @ts-ignore
res.send(cache.bufferResponse);
});
export default router;
@@ -1,9 +1,9 @@
import { Request, Response, Router } from "express";
import { check, FieldErrors, Length } from "../../../../util/instanceOf";
import { check, FieldErrors, Length } from "../../util/instanceOf";
import bcrypt from "bcrypt";
import jwt from "jsonwebtoken";
import { User, UserModel } from "fosscord-server-util";
import Config from "../../../../util/Config";
import Config from "../../util/Config";
import { adjustEmail } from "./register";
const router: Router = Router();
@@ -25,12 +25,11 @@ router.post(
const query: any[] = [{ phone: login }];
if (email) query.push({ email });
// * MongoDB Specific query for user with same email or phone number
const user = await UserModel.findOne(
{
$or: query,
},
`hash id user_settings.locale user_settings.theme`
`user_data.hash id user_settings.locale user_settings.theme`
).exec();
if (!user) {
@@ -40,7 +39,7 @@ router.post(
}
// the salt is saved in the password refer to bcrypt docs
const same_password = await bcrypt.compare(password, user.hash);
const same_password = await bcrypt.compare(password, user.user_data.hash);
if (!same_password) {
throw FieldErrors({
password: { message: req.t("auth:login.INVALID_PASSWORD"), code: "INVALID_PASSWORD" },
@@ -57,13 +56,13 @@ router.post(
}
);
export async function generateToken(id: bigint) {
export async function generateToken(id: string) {
const iat = Math.floor(Date.now() / 1000);
const algorithm = "HS256";
return new Promise((res, rej) => {
jwt.sign(
{ id: `${id}`, iat },
{ id: id, iat },
Config.get().security.jwtSecret,
{
algorithm,
@@ -80,7 +79,6 @@ export async function generateToken(id: bigint) {
* POST /auth/login
* @argument { login: "email@gmail.com", password: "cleartextpassword", undelete: false, captcha_key: null, login_source: null, gift_code_sku_id: null, }
* MFA required:
* @returns {"token": null, "mfa": true, "sms": true, "ticket": "SOME TICKET JWT TOKEN"}
@@ -1,8 +1,8 @@
import { Request, Response, Router } from "express";
import Config from "../../../../util/Config";
import Config from "../../util/Config";
import { trimSpecial, User, Snowflake, UserModel } from "fosscord-server-util";
import bcrypt from "bcrypt";
import { check, Email, EMAIL_REGEX, FieldErrors, Length } from "../../../../util/instanceOf";
import { check, Email, EMAIL_REGEX, FieldErrors, Length } from "../../util/instanceOf";
import "missing-native-js-functions";
import { generateToken } from "./login";
@@ -52,7 +52,7 @@ router.post(
let discriminator = "";
// get register Config
const { register } = Config.get();
const { register, security } = Config.get();
// check if registration is allowed
if (!register.allowNewRegistration) {
@@ -129,9 +129,9 @@ router.post(
}
}
if (register.requireCaptcha) {
if (register.requireCaptcha && security.captcha.enabled) {
if (!captcha_key) {
const { sitekey, service } = Config.get().security.captcha;
const { sitekey, service } = security.captcha;
return res.status(400).json({
captcha_key: ["captcha-required"],
captcha_sitekey: sitekey,
@@ -165,9 +165,10 @@ router.post(
});
}
// constructing final user object
// TODO fix:
// @ts-ignore
// TODO: save date_of_birth
// 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: User = {
id: Snowflake.generate(),
created_at: new Date(),
@@ -176,13 +177,34 @@ router.post(
avatar: null,
bot: false,
system: false,
desktop: false,
mobile: false,
premium: false,
premium_type: 0,
phone: undefined,
mfa_enabled: false,
verified: false,
presence: {
activities: [],
client_status: {
desktop: undefined,
mobile: undefined,
web: undefined,
},
status: "offline",
},
email: adjusted_email,
nsfw_allowed: true, // TODO: depending on age
public_flags: 0n,
flags: 0n, // TODO: generate default flags
hash: adjusted_password,
guilds: [],
valid_tokens_since: new Date(),
user_data: {
hash: adjusted_password,
valid_tokens_since: new Date(),
relationships: [],
connected_accounts: [],
fingerprints: [],
},
user_settings: {
afk_timeout: 300,
allow_accessibility_detection: true,
@@ -225,7 +247,7 @@ router.post(
};
// insert user into database
await new UserModel(user).save({});
await new UserModel(user).save();
return res.json({ token: await generateToken(user.id) });
}
@@ -0,0 +1,68 @@
import { Router, Request, Response } from "express";
import { HTTPError } from "lambert-server";
import { check } from "../../../util/instanceOf";
import { random } from "../../../util/RandomInviteID";
import { emitEvent } from "../../../util/Event";
import { InviteCreateSchema } from "../../../schema/Invite";
import { getPermission, ChannelModel, InviteModel, InviteCreateEvent, toObject } from "fosscord-server-util";
const router: Router = Router();
router.post("/", check(InviteCreateSchema), async (req: Request, res: Response) => {
const { user_id } = req;
const { channel_id } = req.params;
const channel = await ChannelModel.findOne({ id: channel_id }).exec();
if (!channel || !channel.guild_id) {
throw new HTTPError("This channel doesn't exist", 404);
}
const { guild_id } = channel;
const permission = await getPermission(user_id, guild_id);
if (!permission.has("CREATE_INSTANT_INVITE")) {
throw new HTTPError("You aren't authorised to access this endpoint", 401);
}
const invite = {
code: random(),
temporary: req.body.temporary,
uses: 0,
max_uses: req.body.max_uses,
max_age: req.body.max_age,
created_at: new Date(),
guild_id,
channel_id: channel_id,
inviter_id: user_id,
};
await new InviteModel(invite).save();
await emitEvent({ event: "INVITE_CREATE", data: invite, guild_id } as InviteCreateEvent);
res.status(201).send(invite);
});
router.get("/", async (req: Request, res: Response) => {
const { user_id } = req;
const { channel_id } = req.params;
const channel = await ChannelModel.findOne({ id: channel_id }).exec();
if (!channel || !channel.guild_id) {
throw new HTTPError("This channel doesn't exist", 404);
}
const { guild_id } = channel;
const permission = await getPermission(user_id, guild_id);
if (!permission.has("MANAGE_CHANNELS")) {
throw new HTTPError("You aren't authorised to access this endpoint", 401);
}
const invites = await InviteModel.find({ guild_id }).exec();
res.status(200).send(toObject(invites));
});
export default router;
@@ -1,9 +1,9 @@
import { Router } from "express";
import { ChannelModel, getPermission, MessageDeleteBulkEvent, MessageModel } from "fosscord-server-util";
import { HTTPError } from "lambert-server";
import Config from "@util/Config";
import { emitEvent } from "@util/Event";
import { check } from "@util/instanceOf";
import Config from "../../../../util/Config";
import { emitEvent } from "../../../../util/Event";
import { check } from "../../../../util/instanceOf";
const router: Router = Router();
@@ -12,8 +12,8 @@ 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: [BigInt] }), async (req, res) => {
const channel_id = BigInt(req.params.channel_id);
router.post("/", check({ messages: [String] }), async (req, res) => {
const channel_id = req.params.channel_id
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);
@@ -22,7 +22,7 @@ router.post("/", check({ messages: [BigInt] }), async (req, res) => {
const { maxBulkDelete } = Config.get().limits.message;
const { messages } = req.body as { messages: bigint[] };
const { messages } = req.body as { messages: string[] };
if (messages.length < 2) throw new HTTPError("You must at least specify 2 messages to bulk delete");
if (messages.length > maxBulkDelete) throw new HTTPError(`You cannot delete more than ${maxBulkDelete} messages`);
@@ -1,11 +1,23 @@
import { Router } from "express";
import { ChannelModel, ChannelType, getPermission, Message, MessageCreateEvent, MessageModel, Snowflake } from "fosscord-server-util";
import {
ChannelModel,
ChannelType,
getPermission,
Message,
MessageCreateEvent,
MessageDocument,
MessageModel,
Snowflake,
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 { 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 { emitEvent } from "../../../../util/Event";
import { Query } from "mongoose";
import { PublicMemberProjection } from "../../../../util/Member";
const router: Router = Router();
export default router;
@@ -27,23 +39,23 @@ function isTextChannel(type: ChannelType): boolean {
// https://discord.com/developers/docs/resources/channel#create-message
// get messages
router.get("/", async (req, res) => {
const channel_id = BigInt(req.params.channel_id);
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);
isTextChannel(channel.type);
try {
instanceOf({ $around: BigInt, $after: BigInt, $before: BigInt, $limit: new Length(Number, 1, 100) }, req.query, {
instanceOf({ $around: String, $after: String, $before: String, $limit: new Length(Number, 1, 100) }, req.query, {
path: "query",
req,
});
} catch (error) {
return res.status(400).json({ code: 50035, message: "Invalid Query", success: false, errors: error });
}
var { around, after, before, limit }: { around?: bigint; after?: bigint; before?: bigint; limit?: number } = req.query;
var { around, after, before, limit }: { around?: string; after?: string; before?: string; limit?: number } = req.query;
if (!limit) limit = 50;
var halfLimit = BigInt(Math.floor(limit / 2));
var halfLimit = Math.floor(limit / 2);
if ([ChannelType.GUILD_VOICE, ChannelType.GUILD_CATEGORY, ChannelType.GUILD_STORE].includes(channel.type))
throw new HTTPError("Not a text channel");
@@ -57,24 +69,21 @@ router.get("/", async (req, res) => {
if (!channel.recipients.includes(req.user_id)) throw new HTTPError("You don't have permission to view this channel", 401);
}
var query: any;
var query: Query<MessageDocument[], MessageDocument, {}>;
if (after) query = MessageModel.find({ channel_id, id: { $gt: after } });
else if (before) query = MessageModel.find({ channel_id, id: { $lt: before } });
else if (around) query = MessageModel.find({ channel_id, id: { $gt: around - halfLimit, $lt: around + halfLimit } });
else if (around)
query = MessageModel.find({
channel_id,
id: { $gt: (BigInt(around) - BigInt(halfLimit)).toString(), $lt: (BigInt(around) + BigInt(halfLimit)).toString() },
});
else {
query = MessageModel.find({ channel_id }).sort({ id: -1 });
}
const messages = await query
.limit(limit)
.populate({ path: "author", select: PublicUserProjection })
.populate({ path: "mentions", select: PublicUserProjection })
.populate({ path: "mention_channels", select: { id: true, guild_id: true, type: true, name: true } })
.populate("mention_roles")
// .populate({ path: "member", select: PublicMemberProjection })
.exec();
const messages = await query.limit(limit).exec();
return res.json(messages);
return res.json(toObject(messages));
});
// TODO: config max upload size
@@ -88,11 +97,12 @@ const messageUpload = multer({ limits: { fieldSize: 1024 * 1024 * 1024 * 50 } })
// TODO: trim and replace message content and every embed field
// Send message
router.post("/", check(MessageCreateSchema), async (req, res) => {
const channel_id = BigInt(req.params.channel_id);
const channel_id = req.params.channel_id;
const body = req.body as MessageCreateSchema;
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);
// TODO: are tts messages allowed in dm channels? should permission be checked?
if (channel.guild_id) {
const permissions = await getPermission(req.user_id, channel.guild_id, channel_id, { channel });
@@ -108,24 +118,36 @@ router.post("/", check(MessageCreateSchema), async (req, res) => {
if (body.message_reference) {
if (body.message_reference.channel_id !== channel_id) throw new HTTPError("You can only reference messages from this channel");
// TODO: should it be checked if the message exists?
// TODO: should be checked if the referenced message exists?
}
const embeds = [];
if (body.embed) embeds.push(body.embed);
// TODO: check and put all in body in it
const message: Message = {
id: Snowflake.generate(),
channel_id,
guild_id: channel.guild_id,
author_id: req.user_id,
content: req.body,
content: body.content,
timestamp: new Date(),
embeds,
mention_channels_ids: [],
mention_role_ids: [],
mention_user_ids: [],
attachments: [],
embeds: [],
reactions: [],
type: 0,
tts: body.tts,
nonce: body.nonce,
pinned: false,
};
await new MessageModel(message).save();
const doc = await new MessageModel(message).populate({ path: "member", select: PublicMemberProjection }).save();
const data = toObject(doc);
await emitEvent({ event: "MESSAGE_CREATE", channel_id, data: {} } as MessageCreateEvent);
await emitEvent({ event: "MESSAGE_CREATE", channel_id, data, guild_id: channel.guild_id } as MessageCreateEvent);
return res.send(data);
});
+10
View File
@@ -0,0 +1,10 @@
import { Router } from "express";
const router = Router();
router.get("/", (req, res) => {
// TODO:
res.send({ fingerprint: "", assignments: [] });
});
export default router;
+9
View File
@@ -0,0 +1,9 @@
import { Router } from "express";
const router = Router();
router.get("/", (req, res) => {
res.send({ url: "ws://localhost:8080" });
});
export default router;
@@ -1,28 +1,28 @@
import { Request, Response, Router } from "express";
import { BanModel, getPermission, GuildBanAddEvent, GuildBanRemoveEvent, GuildModel } from "fosscord-server-util";
import { BanModel, getPermission, GuildBanAddEvent, GuildBanRemoveEvent, GuildModel, toObject } from "fosscord-server-util";
import { HTTPError } from "lambert-server";
import { getIpAdress } from "@middlewares/GlobalRateLimit";
import { BanCreateSchema } from "@schema/Ban";
import { emitEvent } from "@util/Event";
import { check } from "@util/instanceOf";
import { removeMember } from "@util/Member";
import { getPublicUser } from "@util/User";
import { getIpAdress } from "../../../middlewares/GlobalRateLimit";
import { BanCreateSchema } from "../../../schema/Ban";
import { emitEvent } from "../../../util/Event";
import { check } from "../../../util/instanceOf";
import { removeMember } from "../../../util/Member";
import { getPublicUser } from "../../../util/User";
const router: Router = Router();
router.get("/", async (req: Request, res: Response) => {
const guild_id = BigInt(req.params.id);
const guild_id = req.params.id;
const guild = await GuildModel.findOne({ id: guild_id }).exec();
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();
return res.json(bans);
return res.json(toObject(bans));
});
router.get("/:user", async (req: Request, res: Response) => {
const guild_id = BigInt(req.params.id);
const user_id = BigInt(req.params.ban);
const guild_id = req.params.id;
const user_id = req.params.ban;
var ban = await BanModel.findOne({ guild_id: guild_id, user_id: user_id }).exec();
if (!ban) throw new HTTPError("Ban not found", 404);
@@ -30,8 +30,8 @@ router.get("/:user", async (req: Request, res: Response) => {
});
router.post("/:user_id", check(BanCreateSchema), async (req: Request, res: Response) => {
const guild_id = BigInt(req.params.id);
const banned_user_id = BigInt(req.params.user_id);
const guild_id = req.params.id;
const banned_user_id = req.params.user_id;
const banned_user = await getPublicUser(banned_user_id);
const perms = await getPermission(req.user_id, guild_id);
@@ -61,30 +61,30 @@ router.post("/:user_id", check(BanCreateSchema), async (req: Request, res: Respo
});
router.delete("/:user_id", async (req: Request, res: Response) => {
var guild_id = BigInt(req.params.id);
var banned_user_id = BigInt(req.params.user_id);
var { guild_id } = req.params;
var banned_user_id = req.params.user_id;
const banned_user = await getPublicUser(banned_user_id);
const guild = await GuildModel.findOne({ id: guild_id }, { id: true }).exec();
const guild = await GuildModel.exists({ id: guild_id });
if (!guild) throw new HTTPError("Guild not found", 404);
const perms = await getPermission(req.user_id, guild.id);
const perms = await getPermission(req.user_id, guild_id);
if (!perms.has("BAN_MEMBERS")) {
throw new HTTPError("No permissions", 403);
}
await BanModel.deleteOne({
user_id: banned_user_id,
guild_id: guild.id,
guild_id,
}).exec();
await emitEvent({
event: "GUILD_BAN_REMOVE",
data: {
guild_id: guild.id,
guild_id,
user: banned_user,
},
guild_id: guild.id,
guild_id,
} as GuildBanRemoveEvent);
return res.status(204).send();
@@ -1,19 +1,20 @@
import { Router } from "express";
import { ChannelModel, ChannelType, GuildModel, Snowflake } from "fosscord-server-util";
import { ChannelCreateEvent, ChannelModel, ChannelType, GuildModel, Snowflake, toObject } from "fosscord-server-util";
import { HTTPError } from "lambert-server";
import { ChannelModifySchema } from "../../../../../schema/Channel";
import { check } from "../../../../../util/instanceOf";
import { ChannelModifySchema } from "../../../schema/Channel";
import { emitEvent } from "../../../util/Event";
import { check } from "../../../util/instanceOf";
const router = Router();
router.get("/", async (req, res) => {
const guild_id = BigInt(req.params.id);
const { guild_id } = req.params;
const channels = await ChannelModel.find({ guild_id }).exec();
res.json(channels);
res.json(toObject(channels));
});
router.post("/", check(ChannelModifySchema), async (req, res) => {
const guild_id = BigInt(req.params.id);
const { guild_id } = req.params;
const body = req.body as ChannelModifySchema;
if (!body.permission_overwrites) body.permission_overwrites = [];
if (!body.topic) body.topic = "";
@@ -45,6 +46,8 @@ router.post("/", check(ChannelModifySchema), async (req, res) => {
};
await new ChannelModel(channel).save();
await emitEvent({ event: "CHANNEL_CREATE", data: channel, guild_id } as ChannelCreateEvent);
res.json(channel);
});
@@ -5,26 +5,31 @@ import {
getPermission,
GuildDeleteEvent,
GuildModel,
GuildUpdateEvent,
InviteModel,
MemberModel,
MessageModel,
RoleModel,
toObject,
UserModel,
} from "fosscord-server-util";
import { HTTPError } from "lambert-server";
import { GuildUpdateSchema } from "../../../../../schema/Guild";
import { emitEvent } from "../../../../../util/Event";
import { check } from "../../../../../util/instanceOf";
import { GuildUpdateSchema } from "../../../schema/Guild";
import { emitEvent } from "../../../util/Event";
import { check } from "../../../util/instanceOf";
import "missing-native-js-functions";
const router = Router();
router.get("/", async (req: Request, res: Response) => {
const guild_id = BigInt(req.params.id);
const { guild_id } = req.params;
const guild = await GuildModel.findOne({ id: guild_id }).exec();
const guild = await GuildModel.findOne({ id: guild_id })
.populate({ path: "joined_at", match: { id: req.user_id } })
.exec();
if (!guild) throw new HTTPError("Guild does not exist", 404);
const member = await MemberModel.findOne({ guild_id: guild_id, id: req.user_id }, "id").exec();
const member = await MemberModel.exists({ guild_id: guild_id, id: req.user_id });
if (!member) throw new HTTPError("You are not a member of the guild you are trying to access", 401);
return res.json(guild);
@@ -32,20 +37,27 @@ router.get("/", async (req: Request, res: Response) => {
router.patch("/", check(GuildUpdateSchema), async (req: Request, res: Response) => {
const body = req.body as GuildUpdateSchema;
const guild_id = BigInt(req.params.id);
const guild = await GuildModel.findOne({ id: guild_id }).exec();
if (!guild) throw new HTTPError("This guild does not exist", 404);
const { guild_id } = req.params;
// TODO: guild update check image
const perms = await getPermission(req.user_id, guild_id);
if (!perms.has("MANAGE_GUILD")) throw new HTTPError("You do not have the MANAGE_GUILD permission", 401);
await GuildModel.updateOne({ id: guild_id }, body).exec();
return res.status(204);
const guild = await GuildModel.findOneAndUpdate({ id: guild_id }, body)
.populate({ path: "joined_at", match: { id: req.user_id } })
.exec();
const data = toObject(guild);
emitEvent({ event: "GUILD_UPDATE", data: data, guild_id } as GuildUpdateEvent);
return res.send(data);
});
router.delete("/", async (req: Request, res: Response) => {
var guild_id = BigInt(req.params.id);
// discord prefixes this route with /delete instead of using the delete method
// docs are wrong https://discord.com/developers/docs/resources/guild#delete-guild
router.post("/delete", async (req: Request, res: Response) => {
var { guild_id } = req.params;
const guild = await GuildModel.findOne({ id: guild_id }, "owner_id").exec();
if (!guild) throw new HTTPError("This guild does not exist", 404);
@@ -67,7 +79,7 @@ router.delete("/", async (req: Request, res: Response) => {
await InviteModel.deleteMany({ guild_id }).exec();
await MessageModel.deleteMany({ guild_id }).exec();
return res.status(204).send();
return res.sendStatus(204);
});
export default router;
@@ -1,21 +1,21 @@
import { Request, Response, Router } from "express";
import { GuildModel, MemberModel } from "fosscord-server-util";
import { GuildModel, MemberModel, toObject } from "fosscord-server-util";
import { HTTPError } from "lambert-server";
import { instanceOf, Length } from "../../../../../util/instanceOf";
import { PublicMemberProjection } from "../../../../../util/Member";
import { PublicUserProjection } from "../../../../../util/User";
import { instanceOf, Length } from "../../../util/instanceOf";
import { PublicMemberProjection } from "../../../util/Member";
import { PublicUserProjection } from "../../../util/User";
const router = Router();
// TODO: not allowed for user -> only allowed for bots with privileged intents
// TODO: send over websocket
router.get("/", async (req: Request, res: Response) => {
const guild_id = BigInt(req.params.id);
const { guild_id } = req.params;
const guild = await GuildModel.findOne({ id: guild_id }).exec();
if (!guild) throw new HTTPError("Guild not found", 404);
try {
instanceOf({ $limit: new Length(Number, 1, 1000), $after: BigInt }, req.query, {
instanceOf({ $limit: new Length(Number, 1, 1000), $after: String }, req.query, {
path: "query",
req,
ref: { obj: null, key: "" },
@@ -26,22 +26,21 @@ router.get("/", async (req: Request, res: Response) => {
// @ts-ignore
if (!req.query.limit) req.query.limit = 1;
const { limit, after } = (<unknown>req.query) as { limit: number; after: bigint };
const { limit, after } = (<unknown>req.query) as { limit: number; after: string };
const query = after ? { id: { $gt: after } } : {};
var members = await MemberModel.find({ guild_id, ...query }, PublicMemberProjection)
.limit(limit)
.populate({ path: "user", select: PublicUserProjection })
.exec();
return res.json(members);
return res.json(toObject(members));
});
router.get("/:member", async (req: Request, res: Response) => {
const guild_id = BigInt(req.params.id);
const user_id = BigInt(req.params.member);
const { guild_id } = req.params;
const user_id = req.params.member;
const member = await MemberModel.findOne({ id: user_id, guild_id }).populate({ path: "user", select: PublicUserProjection }).exec();
const member = await MemberModel.findOne({ id: user_id, guild_id }).exec();
if (!member) throw new HTTPError("Member not found", 404);
return res.json(member);
@@ -1,11 +1,11 @@
import { Router, Request, Response } from "express";
import { RoleModel, GuildModel, Snowflake, Guild } from "fosscord-server-util";
import { HTTPError } from "lambert-server";
import { check } from "./../../../../util/instanceOf";
import { GuildCreateSchema } from "../../../../schema/Guild";
import Config from "../../../../util/Config";
import { getPublicUser } from "../../../../util/User";
import { addMember } from "../../../../util/Member";
import { check } from "./../../util/instanceOf";
import { GuildCreateSchema } from "../../schema/Guild";
import Config from "../../util/Config";
import { getPublicUser } from "../../util/User";
import { addMember } from "../../util/Member";
const router: Router = Router();
@@ -29,11 +29,11 @@ router.post("/", check(GuildCreateSchema), async (req: Request, res: Response) =
afk_timeout: 300,
application_id: undefined,
banner: undefined,
default_message_notifications: undefined,
default_message_notifications: 0,
description: undefined,
splash: undefined,
discovery_splash: undefined,
explicit_content_filter: undefined,
explicit_content_filter: 0,
features: [],
id: guild_id,
large: undefined,
@@ -48,11 +48,11 @@ router.post("/", check(GuildCreateSchema), async (req: Request, res: Response) =
premium_tier: 0,
public_updates_channel_id: undefined,
rules_channel_id: undefined,
system_channel_flags: undefined,
system_channel_flags: 0,
system_channel_id: undefined,
unavailable: false,
vanity_url_code: undefined,
verification_level: undefined,
verification_level: 0,
welcome_screen: [],
widget_channel_id: undefined,
widget_enabled: false,
+28
View File
@@ -0,0 +1,28 @@
import { Router, Request, Response } from "express";
import { getPermission, InviteModel, toObject } from "fosscord-server-util";
import { HTTPError } from "lambert-server";
const router: Router = Router();
router.get("/:invite_code", async (req: Request, res: Response) => {
const { invite_code: code } = req.params;
const invite = await InviteModel.findOne({ code }).exec();
if (!invite) throw new HTTPError("Unknown Invite", 404);
res.status(200).send({ invite: toObject(invite) });
});
router.delete("/:invite_code", async (req: Request, res: Response) => {
const { invite_code: code } = req.params;
const invite = await InviteModel.findOne({ code }).exec();
if (!invite) throw new HTTPError("Unknown Invite", 404);
const { guild_id, channel_id } = invite;
const perms = await getPermission(req.user_id, guild_id, channel_id);
if (!perms.has("MANAGE_GUILD") || !perms.has("MANAGE_CHANNELS")) throw new HTTPError("You aren't allow", 401);
res.status(200).send({ invite: toObject(invite) });
});
export default router;
+10
View File
@@ -0,0 +1,10 @@
import { Router } from "express";
const router = Router();
router.post("/", (req, res) => {
// TODO:
res.sendStatus(204);
});
export default router;
-14
View File
@@ -1,14 +0,0 @@
import { Router } from "express";
import { getPermission, MemberModel, db } from "fosscord-server-util";
import { Types } from "mongoose";
const router: Router = Router();
router.get("/", async (req, res) => {
// @ts-ignore
const perm = await getPermission(813185668657184768n, 813189959920910336n);
console.log(perm);
if (perm.has("ADD_REACTIONS")) console.log("add");
res.send("OK");
});
export default router;
+10
View File
@@ -0,0 +1,10 @@
import { Router } from "express";
const router = Router();
router.get("/", (req, res) => {
// TODO:
res.status(200).send({ guild_affinities: [] });
});
export default router;
@@ -1,8 +1,8 @@
import { Router, Request, Response } from "express";
import { GuildModel, MemberModel, UserModel, GuildDeleteEvent, GuildMemberRemoveEvent } from "fosscord-server-util";
import { GuildModel, MemberModel, UserModel, GuildDeleteEvent, GuildMemberRemoveEvent, toObject } from "fosscord-server-util";
import { HTTPError } from "lambert-server";
import { emitEvent } from "../../../../../util/Event";
import { getPublicUser } from "../../../../../util/User";
import { emitEvent } from "../../../util/Event";
import { getPublicUser } from "../../../util/User";
const router: Router = Router();
@@ -11,37 +11,42 @@ router.get("/", async (req: Request, res: Response) => {
if (!user) throw new HTTPError("User not found", 404);
var guildIDs = user.guilds || [];
var guild = await GuildModel.find({ id: { $in: guildIDs } }).exec();
res.json(guild);
var guild = await GuildModel.find({ id: { $in: guildIDs } })
.populate({ path: "joined_at", match: { id: req.user_id } })
.exec();
res.json(toObject(guild));
});
// user send to leave a certain guild
router.delete("/:id", async (req: Request, res: Response) => {
const guildID = BigInt(req.params.id);
const guild = await GuildModel.findOne({ id: guildID }).exec();
const guild_id = req.params.id;
const guild = await GuildModel.findOne({ id: guild_id }, { guild_id: true }).exec();
if (!guild) throw new HTTPError("Guild doesn't exist", 404);
if (guild.owner_id === req.user_id) throw new HTTPError("You can't leave your own guild", 400);
await MemberModel.deleteOne({ id: req.user_id, guild_id: guildID }).exec();
await UserModel.updateOne({ id: req.user_id }, { $pull: { guilds: guildID } }).exec();
const user = await getPublicUser(req.user_id);
await Promise.all([
MemberModel.deleteOne({ id: req.user_id, guild_id: guild_id }).exec(),
UserModel.updateOne({ id: req.user_id }, { $pull: { guilds: guild_id } }).exec(),
emitEvent({
event: "GUILD_DELETE",
data: {
id: guild_id,
},
user_id: req.user_id,
} as GuildDeleteEvent),
]);
await emitEvent({
event: "GUILD_DELETE",
data: {
id: guildID,
},
user_id: req.user_id,
} as GuildDeleteEvent);
const user = await getPublicUser(req.user_id);
await emitEvent({
event: "GUILD_MEMBER_REMOVE",
data: {
guild_id: guildID,
guild_id: guild_id,
user: user,
},
guild_id: guildID,
guild_id: guild_id,
} as GuildMemberRemoveEvent);
return res.status(204).send();
+10
View File
@@ -0,0 +1,10 @@
import { Router } from "express";
const router = Router();
router.get("/", (req, res) => {
// TODO:
res.status(200).send([]);
});
export default router;
+10
View File
@@ -0,0 +1,10 @@
import { Router } from "express";
const router = Router();
router.patch("/", (req, res) => {
// TODO:
res.sendStatus(204);
});
export default router;
+5 -5
View File
@@ -10,7 +10,7 @@ export const ChannelModifySchema = {
$position: Number,
$permission_overwrites: [
{
id: BigInt,
id: String,
type: new Length(Number, 0, 1), // either 0 (role) or 1 (member)
allow: BigInt,
deny: BigInt,
@@ -29,23 +29,23 @@ export interface ChannelModifySchema {
rate_limit_per_user?: Number;
position?: number;
permission_overwrites?: {
id: bigint;
id: string;
type: number;
allow: bigint;
deny: bigint;
}[];
parent_id?: bigint;
parent_id?: string;
nsfw?: boolean;
}
export const ChannelGuildPositionUpdateSchema = [
{
id: BigInt,
id: String,
$position: Number,
},
];
export type ChannelGuildPositionUpdateSchema = {
id: bigint;
id: string;
position?: number;
}[];
+18 -9
View File
@@ -1,13 +1,22 @@
import { ChannelSchema, GuildChannel } from "fosscord-server-util";
import { Length } from "../util/instanceOf";
export const GuildCreateSchema = {
name: new Length(String, 2, 100), // ! 2-100 chars
name: new Length(String, 2, 100),
$region: String, // auto complete voice region of the user
$icon: String,
$channels: [Object],
$guild_template_code: String,
$system_channel_id: String,
};
export interface GuildCreateSchema {
name: string;
region?: string;
icon?: string;
channels?: GuildChannel[];
guild_template_code?: string;
system_channel_id?: string;
}
export const GuildUpdateSchema = {
@@ -20,27 +29,27 @@ export const GuildUpdateSchema = {
$verification_level: Number,
$default_message_notifications: Number,
$system_channel_flags: Number,
$system_channel_id: BigInt,
$system_channel_id: String,
$explicit_content_filter: Number,
$public_updates_channel_id: BigInt,
$public_updates_channel_id: String,
$afk_timeout: Number,
$afk_channel_id: BigInt,
$afk_channel_id: String,
};
// @ts-ignore
delete GuildUpdateSchema.$channels;
export interface GuildUpdateSchema extends GuildCreateSchema {
export interface GuildUpdateSchema extends Omit<GuildCreateSchema, "channels"> {
banner?: string;
splash?: string;
description?: string;
features?: [string];
icon?: string;
verification_level?: number;
default_message_notifications?: number;
system_channel_flags?: number;
system_channel_id?: bigint;
explicit_content_filter?: number;
public_updates_channel_id?: bigint;
public_updates_channel_id?: string;
afk_timeout?: number;
afk_channel_id?: bigint;
afk_channel_id?: string;
}
export const GuildGetSchema = {
+6
View File
@@ -1,4 +1,7 @@
export const InviteCreateSchema = {
$target_user_id: String,
$target_type: String,
$validate: String, //? wtf is this
$max_age: Number,
$max_uses: Number,
$temporary: Boolean,
@@ -7,6 +10,9 @@ export const InviteCreateSchema = {
$target_user_type: Number,
};
export interface InviteCreateSchema {
target_user_id?: String;
target_type?: String;
validate?: String; //? wtf is this
max_age?: Number;
max_uses?: Number;
temporary?: Boolean;
+6 -6
View File
@@ -44,9 +44,9 @@ export const MessageCreateSchema = {
},
$allowed_mentions: [],
$message_reference: {
message_id: BigInt,
channel_id: BigInt,
$guild_id: BigInt,
message_id: String,
channel_id: String,
$guild_id: String,
$fail_if_not_exists: Boolean,
},
$payload_json: String,
@@ -60,9 +60,9 @@ export interface MessageCreateSchema {
embed?: Embed & { timestamp?: string };
allowed_mentions?: [];
message_reference?: {
message_id: bigint;
channel_id: bigint;
guild_id?: bigint;
message_id: string;
channel_id: string;
guild_id?: string;
fail_if_not_exists: boolean;
};
payload_json?: string;
+2 -3
View File
@@ -1,13 +1,12 @@
import mongoose, { Schema, Types } from "mongoose";
import { Long as MongoTypeLong } from "mongodb";
require("mongoose-long")(mongoose);
const userSchema = new Schema({
id: MongoTypeLong,
id: String,
});
const messageSchema = new Schema({
id: MongoTypeLong,
id: String,
content: String,
});
const message = mongoose.model("message", messageSchema, "messages");
+1 -1
View File
@@ -1,7 +1,7 @@
import { getPermission } from "fosscord-server-util";
async function main() {
const t = await getPermission(811642917432066048n, 812327318532915201n);
const t = await getPermission("811642917432066048", "812327318532915201");
console.log(t);
}
+1 -1
View File
@@ -23,7 +23,7 @@ export interface RateLimit {
export interface DefaultOptions {
general: {
instance_id: bigint;
instance_id: string;
};
permissions: {
user: {
+1
View File
@@ -5,6 +5,7 @@ export async function emitEvent(payload: Omit<Event, "created_at">) {
created_at: new Date(), // in seconds
...payload,
};
// TODO: bigint isn't working
return await new EventModel(obj).save();
}
+3 -4
View File
@@ -25,12 +25,10 @@ export const PublicMemberProjection = {
premium_since: true,
};
export async function addMember(user_id: bigint, guild_id: bigint, cache?: { guild?: Guild }) {
export async function addMember(user_id: string, guild_id: string, cache?: { guild?: Guild }) {
const user = await getPublicUser(user_id, { guilds: true });
const guildSize = user.guilds.length;
// @ts-ignore
user.guilds = undefined;
const { maxGuilds } = Config.get().limits.user;
if (guildSize >= maxGuilds) {
@@ -77,6 +75,7 @@ export async function addMember(user_id: bigint, guild_id: bigint, cache?: { gui
},
guild_id: guild_id,
} as GuildMemberAddEvent),
emitEvent({
event: "GUILD_CREATE",
data: guild,
@@ -85,7 +84,7 @@ export async function addMember(user_id: bigint, guild_id: bigint, cache?: { gui
]);
}
export async function removeMember(user_id: bigint, guild_id: bigint) {
export async function removeMember(user_id: string, guild_id: string) {
const user = await getPublicUser(user_id);
const guild = await GuildModel.findOne({ id: guild_id }, { owner_id: true }).exec();
+3 -3
View File
@@ -1,4 +1,4 @@
import { UserModel } from "fosscord-server-util";
import { toObject, UserModel } from "fosscord-server-util";
import { HTTPError } from "lambert-server";
export const PublicUserProjection = {
@@ -9,7 +9,7 @@ export const PublicUserProjection = {
avatar: true,
};
export async function getPublicUser(user_id: bigint, additional_fields?: any) {
export async function getPublicUser(user_id: string, additional_fields?: any) {
const user = await UserModel.findOne(
{ id: user_id },
{
@@ -18,5 +18,5 @@ export async function getPublicUser(user_id: bigint, additional_fields?: any) {
}
).exec();
if (!user) throw new HTTPError("User not found", 404);
return user;
return toObject(user);
}
+22 -8
View File
@@ -114,8 +114,6 @@ export function instanceOf(
}
if (typeof type === "object") {
if (typeof value !== "object") throw new FieldError("BASE_TYPE_OBJECT", req.t("common:field.BASE_TYPE_OBJECT"));
if (Array.isArray(type)) {
if (!Array.isArray(value)) throw new FieldError("BASE_TYPE_ARRAY", req.t("common:field.BASE_TYPE_ARRAY"));
if (!type.length) return true; // type array didn't specify any type
@@ -123,7 +121,8 @@ export function instanceOf(
return (
value.every((val, i) => {
errors[i] = {};
return (
if (
instanceOf(type[0], val, {
path: `${path}[${i}]`,
optional,
@@ -131,7 +130,12 @@ export function instanceOf(
req,
ref: { key: i, obj: value },
}) === true
);
) {
delete errors[i];
return true;
}
return false;
}) || errors
);
} else if (type?.constructor?.name != "Object") {
@@ -150,10 +154,15 @@ export function instanceOf(
})
);
}
if (value instanceof type) return true;
throw new FieldError("BASE_TYPE_CLASS", req.t("common:field.BASE_TYPE_CLASS", { type }));
try {
if (value instanceof type) return true;
} catch (error) {
throw new FieldError("BASE_TYPE_CLASS", req.t("common:field.BASE_TYPE_CLASS", { type }));
}
}
if (typeof value !== "object") throw new FieldError("BASE_TYPE_OBJECT", req.t("common:field.BASE_TYPE_OBJECT"));
const diff = Object.keys(value).missing(
Object.keys(type).map((x) => (x.startsWith(OPTIONAL_PREFIX) ? x.slice(OPTIONAL_PREFIX.length) : x))
);
@@ -167,7 +176,7 @@ export function instanceOf(
if (OPTIONAL) newKey = newKey.slice(OPTIONAL_PREFIX.length);
errors[newKey] = {};
return (
if (
instanceOf(type[key], value[newKey], {
path: `${path}.${newKey}`,
optional: OPTIONAL,
@@ -175,7 +184,12 @@ export function instanceOf(
req,
ref: { key: newKey, obj: value },
}) === true
);
) {
delete errors[newKey];
return true;
}
return false;
}) || errors
);
} else if (typeof type === "number" || typeof type === "string" || typeof type === "boolean") {