This commit is contained in:
Flam3rboy committed 2021-08-12 20:09:35 +02:00
1 parent a1c85f0b16
commit 08e837bf55
233 files changed

No files matched your search

+113
View File
@@ -0,0 +1,113 @@
import { Request, Response, Router } from "express";
import { check, FieldErrors, Length } from "../../util/instanceOf";
import bcrypt from "bcrypt";
import jwt from "jsonwebtoken";
import { Config, UserModel } from "@fosscord/server-util";
import { adjustEmail } from "./register";
import RateLimit from "../../middlewares/RateLimit";
const router: Router = Router();
export default router;
// TODO: check if user is deleted --> prohibit login
router.post(
"/",
check({
login: new Length(String, 2, 100), // email or telephone
password: new Length(String, 8, 72),
$undelete: Boolean,
$captcha_key: String,
$login_source: String,
$gift_code_sku_id: String
}),
async (req: Request, res: Response) => {
const { login, password, captcha_key, undelete } = req.body;
const email = adjustEmail(login);
const query: any[] = [{ phone: login }];
if (email) query.push({ email });
// TODO: Rewrite this to have the proper config syntax on the new method
const config = Config.get();
if (config.login.requireCaptcha && config.security.captcha.enabled) {
if (!captcha_key) {
const { sitekey, service } = config.security.captcha;
return res.status(400).json({
captcha_key: ["captcha-required"],
captcha_sitekey: sitekey,
captcha_service: service
});
}
// TODO: check captcha
}
const user = await UserModel.findOne(
{ $or: query },
{ user_data: { hash: true }, id: true, disabled: true, deleted: true, user_settings: { locale: true, theme: true } }
)
.exec()
.catch((e) => {
throw FieldErrors({ login: { message: req.t("auth:login.INVALID_LOGIN"), code: "INVALID_LOGIN" } });
});
if (undelete) {
// undelete refers to un'disable' here
if (user.disabled) await UserModel.updateOne({ id: user.id }, { disabled: false }).exec();
if (user.deleted) await UserModel.updateOne({ id: user.id }, { deleted: false }).exec();
} else {
if (user.deleted) return res.status(400).json({ message: "This account is scheduled for deletion.", code: 20011 });
if (user.disabled) return res.status(400).json({ message: req.t("auth:login.ACCOUNT_DISABLED"), code: 20013 });
}
// the salt is saved in the password refer to bcrypt docs
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" } });
}
const token = await generateToken(user.id);
// Notice this will have a different token structure, than discord
// Discord header is just the user id as string, which is not possible with npm-jsonwebtoken package
// https://user-images.githubusercontent.com/6506416/81051916-dd8c9900-8ec2-11ea-8794-daf12d6f31f0.png
res.json({ token, user_settings: user.user_settings });
}
);
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 },
Config.get().security.jwtSecret,
{
algorithm
},
(err, token) => {
if (err) return rej(err);
return res(token);
}
);
});
}
/**
* 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"}
* Captcha required:
* @returns {"captcha_key": ["captcha-required"], "captcha_sitekey": null, "captcha_service": "recaptcha"}
* Sucess:
* @returns {"token": "USERTOKEN", "user_settings": {"locale": "en", "theme": "dark"}}
*/
+309
View File
@@ -0,0 +1,309 @@
import { Request, Response, Router } from "express";
import { trimSpecial, User, Snowflake, UserModel, Config } from "@fosscord/server-util";
import bcrypt from "bcrypt";
import { check, Email, EMAIL_REGEX, FieldErrors, Length } from "../../util/instanceOf";
import "missing-native-js-functions";
import { generateToken } from "./login";
import { getIpAdress, IPAnalysis, isProxy } from "../../util/ipAddress";
import { HTTPError } from "lambert-server";
import RateLimit from "../../middlewares/RateLimit";
const router: Router = Router();
router.post(
"/",
check({
username: new Length(String, 2, 32),
// TODO: check min password length in config
// prevent Denial of Service with max length of 72 chars
password: new Length(String, 8, 72),
consent: Boolean,
$email: new Length(Email, 5, 100),
$fingerprint: String,
$invite: String,
$date_of_birth: Date, // "2000-04-03"
$gift_code_sku_id: String,
$captcha_key: String
}),
async (req: Request, res: Response) => {
const {
email,
username,
password,
consent,
fingerprint,
invite,
date_of_birth,
gift_code_sku_id, // ? what is this
captcha_key
} = req.body;
// get register Config
const { register, security } = Config.get();
const ip = getIpAdress(req);
if (register.blockProxies) {
if (isProxy(await IPAnalysis(ip))) {
console.log(`proxy ${ip} blocked from registration`);
throw new HTTPError("Your IP is blocked from registration");
}
}
console.log("register", req.body.email, req.body.username, ip);
// TODO: automatically join invite
// TODO: gift_code_sku_id?
// TODO: check password strength
// adjusted_email will be slightly modified version of the user supplied email -> e.g. protection against GMail Trick
let adjusted_email: string | null = adjustEmail(email);
// adjusted_password will be the hash of the password
let adjusted_password: string = "";
// trim special uf8 control characters -> Backspace, Newline, ...
let adjusted_username: string = trimSpecial(username);
// discriminator will be randomly generated
let discriminator = "";
// check if registration is allowed
if (!register.allowNewRegistration) {
throw FieldErrors({
email: { code: "REGISTRATION_DISABLED", message: req.t("auth:register.REGISTRATION_DISABLED") }
});
}
// check if the user agreed to the Terms of Service
if (!consent) {
throw FieldErrors({
consent: { code: "CONSENT_REQUIRED", message: req.t("auth:register.CONSENT_REQUIRED") }
});
}
// require invite to register -> e.g. for organizations to send invites to their employees
if (register.requireInvite && !invite) {
throw FieldErrors({
email: { code: "INVITE_ONLY", message: req.t("auth:register.INVITE_ONLY") }
});
}
if (email) {
// replace all dots and chars after +, if its a gmail.com email
if (!adjusted_email) throw FieldErrors({ email: { code: "INVALID_EMAIL", message: req.t("auth:register.INVALID_EMAIL") } });
// check if there is already an account with this email
const exists = await UserModel.findOne({ email: adjusted_email })
.exec()
.catch((e) => {});
if (exists) {
throw FieldErrors({
email: {
code: "EMAIL_ALREADY_REGISTERED",
message: req.t("auth:register.EMAIL_ALREADY_REGISTERED")
}
});
}
} else if (register.email.necessary) {
throw FieldErrors({
email: { code: "BASE_TYPE_REQUIRED", message: req.t("common:field.BASE_TYPE_REQUIRED") }
});
}
if (register.dateOfBirth.necessary && !date_of_birth) {
throw FieldErrors({
date_of_birth: { code: "BASE_TYPE_REQUIRED", message: req.t("common:field.BASE_TYPE_REQUIRED") }
});
} else if (register.dateOfBirth.minimum) {
const minimum = new Date();
minimum.setFullYear(minimum.getFullYear() - register.dateOfBirth.minimum);
// higher is younger
if (date_of_birth > minimum) {
throw FieldErrors({
date_of_birth: {
code: "DATE_OF_BIRTH_UNDERAGE",
message: req.t("auth:register.DATE_OF_BIRTH_UNDERAGE", { years: register.dateOfBirth.minimum })
}
});
}
}
if (!register.allowMultipleAccounts) {
// TODO: check if fingerprint was eligible generated
const exists = await UserModel.findOne({ fingerprints: fingerprint })
.exec()
.catch((e) => {});
if (exists) {
throw FieldErrors({
email: {
code: "EMAIL_ALREADY_REGISTERED",
message: req.t("auth:register.EMAIL_ALREADY_REGISTERED")
}
});
}
}
if (register.requireCaptcha && security.captcha.enabled) {
if (!captcha_key) {
const { sitekey, service } = security.captcha;
return res.status(400).json({
captcha_key: ["captcha-required"],
captcha_sitekey: sitekey,
captcha_service: service
});
}
// TODO: check captcha
}
// the salt is saved in the password refer to bcrypt docs
adjusted_password = await bcrypt.hash(password, 12);
let exists;
// randomly generates a discriminator between 1 and 9999 and checks max five times if it already exists
// if it all five times already exists, abort with USERNAME_TOO_MANY_USERS error
// else just continue
// TODO: is there any better way to generate a random discriminator only once, without checking if it already exists in the mongodb database?
for (let tries = 0; tries < 5; tries++) {
discriminator = Math.randomIntBetween(1, 9999).toString().padStart(4, "0");
try {
exists = await UserModel.findOne({ discriminator, username: adjusted_username }, "id").exec();
} catch (error) {
// doesn't exist -> break
break;
}
}
if (exists) {
throw FieldErrors({
username: {
code: "USERNAME_TOO_MANY_USERS",
message: req.t("auth:register.USERNAME_TOO_MANY_USERS")
}
});
}
// 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(),
username: adjusted_username,
discriminator,
avatar: null,
accent_color: null,
banner: null,
bot: false,
system: false,
desktop: false,
mobile: false,
premium: true,
premium_type: 2,
phone: null,
bio: "",
mfa_enabled: false,
verified: false,
disabled: false,
deleted: 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
guilds: [],
user_data: {
hash: adjusted_password,
valid_tokens_since: new Date(),
relationships: [],
connected_accounts: [],
fingerprints: []
},
user_settings: {
afk_timeout: 300,
allow_accessibility_detection: true,
animate_emoji: true,
animate_stickers: 0,
contact_sync_enabled: false,
convert_emoticons: false,
custom_status: {
emoji_id: null,
emoji_name: null,
expires_at: null,
text: null
},
default_guilds_restricted: false,
detect_platform_accounts: true,
developer_mode: false,
disable_games_tab: false,
enable_tts_command: true,
explicit_content_filter: 0,
friend_source_flags: { all: true },
gateway_connected: false,
gif_auto_play: true,
guild_folders: [],
guild_positions: [],
inline_attachment_media: true,
inline_embed_media: true,
locale: req.language,
message_display_compact: false,
native_phone_integration_enabled: true,
render_embeds: true,
render_reactions: true,
restricted_guilds: [],
show_current_game: true,
status: "offline",
stream_notifications_enabled: true,
theme: "dark",
timezone_offset: 0
// timezone_offset: // TODO: timezone from request
}
};
// insert user into database
await new UserModel(user).save();
return res.json({ token: await generateToken(user.id) });
}
);
export function adjustEmail(email: string): string | null {
// body parser already checked if it is a valid email
const parts = <RegExpMatchArray>email.match(EMAIL_REGEX);
// @ts-ignore
if (!parts || parts.length < 5) return undefined;
const domain = parts[5];
const user = parts[1];
// TODO: check accounts with uncommon email domains
if (domain === "gmail.com" || domain === "googlemail.com") {
// replace .dots and +alternatives -> Gmail Dot Trick https://support.google.com/mail/answer/7436150 and https://generator.email/blog/gmail-generator
return user.replace(/[.]|(\+.*)/g, "") + "@gmail.com";
}
return email;
}
export default router;
/**
* POST /auth/register
* @argument { "fingerprint":"805826570869932034.wR8vi8lGlFBJerErO9LG5NViJFw", "email":"qo8etzvaf@gmail.com", "username":"qp39gr98", "password":"wtp9gep9gw", "invite":null, "consent":true, "date_of_birth":"2000-04-04", "gift_code_sku_id":null, "captcha_key":null}
*
* Field Error
* @returns { "code": 50035, "errors": { "consent": { "_errors": [{ "code": "CONSENT_REQUIRED", "message": "You must agree to Discord's Terms of Service and Privacy Policy." }]}}, "message": "Invalid Form Body"}
*
* Success 201:
* @returns {token: "OMITTED"}
*/
@@ -0,0 +1,14 @@
import { Router, Response, Request } from "express";
const router: Router = Router();
// TODO:
export default router;
/**
*
* @param {"webhook_channel_id":"754001514330062952"}
*
* Creates a WebHook in the channel and returns the id of it
*
* @returns {"channel_id": "816382962056560690", "webhook_id": "834910735095037962"}
*/
@@ -0,0 +1,60 @@
import { ChannelDeleteEvent, ChannelModel, ChannelUpdateEvent, getPermission, GuildUpdateEvent, toObject } from "@fosscord/server-util";
import { Router, Response, Request } from "express";
import { HTTPError } from "lambert-server";
import { ChannelModifySchema } from "../../../schema/Channel";
import { emitEvent } from "../../../util/Event";
import { check } from "../../../util/instanceOf";
const router: Router = Router();
// TODO: delete channel
// TODO: Get channel
router.get("/", async (req: Request, res: Response) => {
const { channel_id } = req.params;
const channel = await ChannelModel.findOne({ id: channel_id }).exec();
const permission = await getPermission(req.user_id, channel.guild_id, channel_id);
permission.hasThrow("VIEW_CHANNEL");
return res.send(toObject(channel));
});
router.delete("/", async (req: Request, res: Response) => {
const { channel_id } = req.params;
const channel = await ChannelModel.findOne({ id: channel_id }).exec();
const permission = await getPermission(req.user_id, channel?.guild_id, channel_id, { channel });
permission.hasThrow("MANAGE_CHANNELS");
// TODO: Dm channel "close" not delete
const data = toObject(channel);
await emitEvent({ event: "CHANNEL_DELETE", data, channel_id } as ChannelDeleteEvent);
await ChannelModel.deleteOne({ id: channel_id });
res.send(data);
});
router.patch("/", check(ChannelModifySchema), async (req: Request, res: Response) => {
var payload = req.body as ChannelModifySchema;
const { channel_id } = req.params;
const permission = await getPermission(req.user_id, undefined, channel_id);
permission.hasThrow("MANAGE_CHANNELS");
const channel = await ChannelModel.findOneAndUpdate({ id: channel_id }, payload).exec();
const data = toObject(channel);
await emitEvent({
event: "CHANNEL_UPDATE",
data,
channel_id
} as ChannelUpdateEvent);
res.send(data);
});
export default router;
@@ -0,0 +1,65 @@
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.guild_id) {
throw new HTTPError("This channel doesn't exist", 404);
}
const { guild_id } = channel;
const permission = await getPermission(user_id, guild_id);
permission.hasThrow("CREATE_INSTANT_INVITE");
const expires_at = new Date(req.body.max_age * 1000 + Date.now());
const invite = {
code: random(),
temporary: req.body.temporary,
uses: 0,
max_uses: req.body.max_uses,
max_age: req.body.max_age,
expires_at,
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.guild_id) {
throw new HTTPError("This channel doesn't exist", 404);
}
const { guild_id } = channel;
const permission = await getPermission(user_id, guild_id);
permission.hasThrow("MANAGE_CHANNELS");
const invites = await InviteModel.find({ guild_id }).exec();
res.status(200).send(toObject(invites));
});
export default router;
@@ -0,0 +1,35 @@
import { getPermission, MessageAckEvent, ReadStateModel } from "@fosscord/server-util";
import { Request, Response, Router } from "express";
import { emitEvent } from "../../../../../util/Event";
import { check } from "../../../../../util/instanceOf";
const router = Router();
// TODO: check if message exists
// TODO: send read state event to all channel members
router.post("/", check({ $manual: Boolean, $mention_count: Number }), async (req: Request, res: Response) => {
const { channel_id, message_id } = req.params;
const permission = await getPermission(req.user_id, undefined, channel_id);
permission.hasThrow("VIEW_CHANNEL");
await ReadStateModel.updateOne(
{ user_id: req.user_id, channel_id, message_id },
{ user_id: req.user_id, channel_id, message_id }
).exec();
await emitEvent({
event: "MESSAGE_ACK",
user_id: req.user_id,
data: {
channel_id,
message_id,
version: 496
}
} as MessageAckEvent);
res.sendStatus(204);
});
export default router;
@@ -0,0 +1,8 @@
import { Router, Response, Request } from "express";
const router = Router();
// TODO:
// router.post("/", (req: Request, res: Response) => {});
export default router;
@@ -0,0 +1,72 @@
import { ChannelModel, getPermission, MessageDeleteEvent, MessageModel, MessageUpdateEvent, toObject } from "@fosscord/server-util";
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, postHandleMessage } from "../../../../../util/Message";
const router = Router();
router.patch("/", check(MessageCreateSchema), async (req: Request, res: Response) => {
const { message_id, channel_id } = req.params;
var body = req.body as MessageCreateSchema;
var message = await MessageModel.findOne({ id: message_id, channel_id }, { author_id: true }).exec();
const permissions = await getPermission(req.user_id, undefined, channel_id);
if (req.user_id !== message.author_id) {
permissions.hasThrow("MANAGE_MESSAGES");
body = { flags: body.flags };
}
const opts = await handleMessage({
...body,
author_id: message.author_id,
channel_id,
id: message_id,
edited_timestamp: new Date()
});
// @ts-ignore
message = await MessageModel.findOneAndUpdate({ id: message_id }, opts).populate("author").exec();
await emitEvent({
event: "MESSAGE_UPDATE",
channel_id,
data: { ...toObject(message), nonce: undefined }
} as MessageUpdateEvent);
postHandleMessage(message);
return res.json(toObject(message));
});
// 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 });
const message = await MessageModel.findOne({ id: message_id }, { author_id: true }).exec();
const permission = await getPermission(req.user_id, channel.guild_id, channel_id);
if (message.author_id !== req.user_id) permission.hasThrow("MANAGE_MESSAGES");
await MessageModel.deleteOne({ id: message_id }).exec();
await emitEvent({
event: "MESSAGE_DELETE",
channel_id,
data: {
id: message_id,
channel_id,
guild_id: channel.guild_id
}
} as MessageDeleteEvent);
res.sendStatus(204);
});
export default router;
@@ -0,0 +1,191 @@
import {
ChannelModel,
EmojiModel,
getPermission,
MemberModel,
MessageModel,
MessageReactionAddEvent,
MessageReactionRemoveAllEvent,
MessageReactionRemoveEmojiEvent,
MessageReactionRemoveEvent,
PartialEmoji,
PublicUserProjection,
toObject,
UserModel
} from "@fosscord/server-util";
import { Router, Response, Request } from "express";
import { HTTPError } from "lambert-server";
import { emitEvent } from "../../../../../util/Event";
const router = Router();
// TODO: check if emoji is really an unicode emoji or a prperly encoded external emoji
function getEmoji(emoji: string): PartialEmoji {
emoji = decodeURIComponent(emoji);
const parts = emoji.includes(":") && emoji.split(":");
if (parts)
return {
name: parts[0],
id: parts[1]
};
return {
id: undefined,
name: emoji
};
}
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();
const permissions = await getPermission(req.user_id, undefined, channel_id);
permissions.hasThrow("MANAGE_MESSAGES");
await MessageModel.findOneAndUpdate({ id: message_id, channel_id }, { reactions: [] }).exec();
await emitEvent({
event: "MESSAGE_REACTION_REMOVE_ALL",
channel_id,
data: {
channel_id,
message_id,
guild_id: channel.guild_id
}
} as MessageReactionRemoveAllEvent);
res.sendStatus(204);
});
router.delete("/:emoji", async (req: Request, res: Response) => {
const { message_id, channel_id } = req.params;
const emoji = getEmoji(req.params.emoji);
const channel = await ChannelModel.findOne({ id: channel_id }, { guild_id: true }).exec();
const permissions = await getPermission(req.user_id, undefined, channel_id);
permissions.hasThrow("MANAGE_MESSAGES");
const message = await MessageModel.findOne({ id: message_id, channel_id }).exec();
const already_added = message.reactions.find((x) => (x.emoji.id === emoji.id && emoji.id) || x.emoji.name === emoji.name);
if (!already_added) throw new HTTPError("Reaction not found", 404);
message.reactions.remove(already_added);
await MessageModel.updateOne({ id: message_id, channel_id }, message).exec();
await emitEvent({
event: "MESSAGE_REACTION_REMOVE_EMOJI",
channel_id,
data: {
channel_id,
message_id,
guild_id: channel.guild_id,
emoji
}
} as MessageReactionRemoveEmojiEvent);
res.sendStatus(204);
});
router.get("/:emoji", async (req: Request, res: Response) => {
const { message_id, channel_id } = req.params;
const emoji = getEmoji(req.params.emoji);
const message = await MessageModel.findOne({ id: message_id, channel_id }).exec();
if (!message) throw new HTTPError("Message not found", 404);
const reaction = message.reactions.find((x) => (x.emoji.id === emoji.id && emoji.id) || x.emoji.name === emoji.name);
if (!reaction) throw new HTTPError("Reaction not found", 404);
const permissions = await getPermission(req.user_id, undefined, channel_id);
permissions.hasThrow("VIEW_CHANNEL");
const users = await UserModel.find({ id: { $in: reaction.user_ids } }, PublicUserProjection).exec();
res.json(toObject(users));
});
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);
const channel = await ChannelModel.findOne({ id: channel_id }, { guild_id: true }).exec();
const message = await MessageModel.findOne({ id: message_id, channel_id }).exec();
const already_added = message.reactions.find((x) => (x.emoji.id === emoji.id && emoji.id) || x.emoji.name === emoji.name);
const permissions = await getPermission(req.user_id, undefined, channel_id);
permissions.hasThrow("READ_MESSAGE_HISTORY");
if (!already_added) permissions.hasThrow("ADD_REACTIONS");
if (emoji.id) {
const external_emoji = await EmojiModel.findOne({ id: emoji.id }).exec();
if (!already_added) permissions.hasThrow("USE_EXTERNAL_EMOJIS");
emoji.animated = external_emoji.animated;
emoji.name = external_emoji.name;
}
if (already_added) {
if (already_added.user_ids.includes(req.user_id)) return res.sendStatus(204); // Do not throw an error ¯\_(ツ)_/¯ as discord also doesn't throw any error
already_added.count++;
} else message.reactions.push({ count: 1, emoji, user_ids: [req.user_id] });
await MessageModel.updateOne({ id: message_id, channel_id }, message).exec();
const member = channel.guild_id && (await MemberModel.findOne({ id: req.user_id }).exec());
await emitEvent({
event: "MESSAGE_REACTION_ADD",
channel_id,
data: {
user_id: req.user_id,
channel_id,
message_id,
guild_id: channel.guild_id,
emoji,
member
}
} as MessageReactionAddEvent);
res.sendStatus(204);
});
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);
const channel = await ChannelModel.findOne({ id: channel_id }, { guild_id: true }).exec();
const message = await MessageModel.findOne({ id: message_id, channel_id }).exec();
const permissions = await getPermission(req.user_id, undefined, channel_id);
if (user_id === "@me") user_id = req.user_id;
else permissions.hasThrow("MANAGE_MESSAGES");
const already_added = message.reactions.find((x) => (x.emoji.id === emoji.id && emoji.id) || x.emoji.name === emoji.name);
if (!already_added || !already_added.user_ids.includes(user_id)) throw new HTTPError("Reaction not found", 404);
already_added.count--;
if (already_added.count <= 0) message.reactions.remove(already_added);
await MessageModel.updateOne({ id: message_id, channel_id }, message).exec();
await emitEvent({
event: "MESSAGE_REACTION_REMOVE",
channel_id,
data: {
user_id: req.user_id,
channel_id,
message_id,
guild_id: channel.guild_id,
emoji
}
} as MessageReactionRemoveEvent);
res.sendStatus(204);
});
export default router;
@@ -0,0 +1,37 @@
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";
import { check } from "../../../../util/instanceOf";
const router: Router = Router();
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: 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);
const permission = await getPermission(req.user_id, channel?.guild_id, channel_id, { channel });
permission.hasThrow("MANAGE_MESSAGES");
const { maxBulkDelete } = Config.get().limits.message;
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`);
await MessageModel.deleteMany({ id: { $in: messages } }).exec();
await emitEvent({
event: "MESSAGE_DELETE_BULK",
channel_id,
data: { ids: messages, channel_id, guild_id: channel.guild_id }
} as MessageDeleteBulkEvent);
res.sendStatus(204);
});
@@ -0,0 +1,146 @@
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 multer from "multer";
import { Query } from "mongoose";
import { sendMessage } from "../../../../util/Message";
import { uploadFile } from "../../../../util/cdn";
const router: Router = Router();
export default router;
export function isTextChannel(type: ChannelType): boolean {
switch (type) {
case ChannelType.GUILD_VOICE:
case ChannelType.GUILD_CATEGORY:
throw new HTTPError("not a text channel", 400);
case ChannelType.DM:
case ChannelType.GROUP_DM:
case ChannelType.GUILD_NEWS:
case ChannelType.GUILD_STORE:
case ChannelType.GUILD_TEXT:
return true;
}
}
// https://discord.com/developers/docs/resources/channel#create-message
// get messages
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, recipient_ids: true, owner_id: true }
)
.lean() // lean is needed, because we don't want to populate .recipients that also auto deletes .recipient_ids
.exec();
if (!channel) throw new HTTPError("Channel not found", 404);
isTextChannel(channel.type);
try {
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?: string; after?: string; before?: string; limit?: number } = req.query;
if (!limit) limit = 50;
var halfLimit = Math.floor(limit / 2);
// @ts-ignore
const permissions = await getPermission(req.user_id, channel.guild_id, channel_id, { channel });
permissions.hasThrow("VIEW_CHANNEL");
if (!permissions.has("READ_MESSAGE_HISTORY")) return res.json([]);
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: (BigInt(around) - BigInt(halfLimit)).toString(), $lt: (BigInt(around) + BigInt(halfLimit)).toString() }
});
else {
query = MessageModel.find({ channel_id });
}
query = query.sort({ id: -1 });
const messages = await query.limit(limit).exec();
return res.json(
toObject(messages).map((x) => {
(x.reactions || []).forEach((x) => {
// @ts-ignore
if ((x.user_ids || []).includes(req.user_id)) x.me = true;
// @ts-ignore
delete x.user_ids;
});
// @ts-ignore
if (!x.author) x.author = { discriminator: "0000", username: "Deleted User", public_flags: 0n, avatar: null };
return x;
})
);
});
// TODO: config max upload size
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
// https://discord.com/developers/docs/resources/channel#create-message
// TODO: text channel slowdown
// TODO: trim and replace message content and every embed field
// TODO: check allowed_mentions
// Send message
router.post("/", messageUpload.single("file"), async (req: Request, res: Response) => {
const { channel_id } = req.params;
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,
attachments,
edited_timestamp: null
});
return res.send(data);
});
@@ -0,0 +1,72 @@
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: 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.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", 501);
// @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;
// @ts-ignore
channel = await ChannelModel.findOneAndUpdate({ id: channel_id }, channel).exec();
await emitEvent({
event: "CHANNEL_UPDATE",
channel_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.guild_id) throw new HTTPError("Channel not found", 404);
await emitEvent({
event: "CHANNEL_UPDATE",
channel_id,
data: channel
} as ChannelUpdateEvent);
return res.sendStatus(204);
});
export default router;
@@ -0,0 +1,93 @@
import {
ChannelModel,
ChannelPinsUpdateEvent,
Config,
getPermission,
MessageModel,
MessageUpdateEvent,
toObject
} from "@fosscord/server-util";
import { Router, Request, Response } from "express";
import { HTTPError } from "lambert-server";
import { emitEvent } from "../../../util/Event";
const router: Router = Router();
router.put("/:message_id", async (req: Request, res: Response) => {
const { channel_id, message_id } = req.params;
const channel = await ChannelModel.findOne({ id: channel_id }).exec();
const permission = await getPermission(req.user_id, channel.guild_id, channel_id);
permission.hasThrow("VIEW_CHANNEL");
// * in dm channels anyone can pin messages -> only check for guilds
if (channel.guild_id) permission.hasThrow("MANAGE_MESSAGES");
const pinned_count = await MessageModel.count({ channel_id, pinned: true }).exec();
const { maxPins } = Config.get().limits.channel;
if (pinned_count >= maxPins) throw new HTTPError("Max pin count reached: " + maxPins);
await MessageModel.updateOne({ id: message_id }, { pinned: true }).exec();
const message = toObject(await MessageModel.findOne({ id: message_id }).exec());
await emitEvent({
event: "MESSAGE_UPDATE",
channel_id,
data: message
} as MessageUpdateEvent);
await emitEvent({
event: "CHANNEL_PINS_UPDATE",
channel_id,
data: {
channel_id,
guild_id: channel.guild_id,
last_pin_timestamp: undefined
}
} as ChannelPinsUpdateEvent);
res.sendStatus(204);
});
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();
const permission = await getPermission(req.user_id, channel.guild_id, channel_id);
permission.hasThrow("VIEW_CHANNEL");
if (channel.guild_id) permission.hasThrow("MANAGE_MESSAGES");
const message = toObject(await MessageModel.findOneAndUpdate({ id: message_id }, { pinned: false }).exec());
await emitEvent({
event: "MESSAGE_UPDATE",
channel_id,
data: message
} as MessageUpdateEvent);
await emitEvent({
event: "CHANNEL_PINS_UPDATE",
channel_id,
data: {
channel_id,
guild_id: channel.guild_id,
last_pin_timestamp: undefined
}
} as ChannelPinsUpdateEvent);
res.sendStatus(204);
});
router.get("/", async (req: Request, res: Response) => {
const { channel_id } = req.params;
const channel = await ChannelModel.findOne({ id: channel_id }).exec();
const permission = await getPermission(req.user_id, channel.guild_id, channel_id);
permission.hasThrow("VIEW_CHANNEL");
let pins = await MessageModel.find({ channel_id: channel_id, pinned: true }).exec();
res.send(toObject(pins));
});
export default router;
@@ -0,0 +1,5 @@
import { Router, Response, Request } from "express";
const router: Router = Router();
// TODO:
export default router;
@@ -0,0 +1,31 @@
import { ChannelModel, MemberModel, toObject, TypingStartEvent } from "@fosscord/server-util";
import { Router, Request, Response } from "express";
import { HTTPError } from "lambert-server";
import { emitEvent } from "../../../util/Event";
const router: Router = Router();
router.post("/", async (req: Request, res: Response) => {
const { channel_id } = req.params;
const user_id = req.user_id;
const timestamp = Date.now();
const channel = await ChannelModel.findOne({ id: channel_id });
const member = await MemberModel.findOne({ id: user_id }).exec();
await emitEvent({
event: "TYPING_START",
channel_id: channel_id,
data: {
// this is the paylod
member: toObject(member),
channel_id,
timestamp,
user_id,
guild_id: channel.guild_id
}
} as TypingStartEvent);
res.sendStatus(204);
});
export default router;
@@ -0,0 +1,26 @@
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";
import { isTextChannel } from "./messages/index";
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: Request, res: Response) => {
const channel_id = req.params.channel_id;
const channel = await ChannelModel.findOne({ id: channel_id }, { guild_id: true, type: true }).exec();
isTextChannel(channel.type);
if (!channel.guild_id) throw new HTTPError("Not a guild channel", 400);
const permission = await getPermission(req.user_id, channel.guild_id);
permission.hasThrow("MANAGE_WEBHOOKS");
var { avatar, name } = req.body as { name: string; avatar?: string };
name = trimSpecial(name);
if (name === "clyde") throw new HTTPError("Invalid name", 400);
});
export default router;
+10
View File
@@ -0,0 +1,10 @@
import { Router, Response, Request } from "express";
const router = Router();
router.get("/", (req: Request, res: Response) => {
// TODO:
res.send({ fingerprint: "", assignments: [] });
});
export default router;
+11
View File
@@ -0,0 +1,11 @@
import { Config } from "@fosscord/server-util";
import { Router, Response, Request } from "express";
const router = Router();
router.get("/", (req: Request, res: Response) => {
const { endpoint } = Config.get().gateway;
res.json({ url: endpoint || process.env.GATEWAY || "ws://localhost:3002" });
});
export default router;
+90
View File
@@ -0,0 +1,90 @@
import { Request, Response, Router } from "express";
import { BanModel, getPermission, GuildBanAddEvent, GuildBanRemoveEvent, GuildModel, toObject } from "@fosscord/server-util";
import { HTTPError } from "lambert-server";
import { getIpAdress } from "../../../util/ipAddress";
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 } = 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 }, { user_id: true, reason: true }).exec();
return res.json(toObject(bans));
});
router.get("/:user", async (req: Request, res: Response) => {
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();
return res.json(ban);
});
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);
const perms = await getPermission(req.user_id, guild_id);
perms.hasThrow("BAN_MEMBERS");
if (req.user_id === banned_user_id) throw new HTTPError("You can't ban yourself", 400);
await removeMember(banned_user_id, guild_id);
const ban = await new BanModel({
user_id: banned_user_id,
guild_id: guild_id,
ip: getIpAdress(req),
executor_id: req.user_id,
reason: req.body.reason // || otherwise empty
}).save();
await emitEvent({
event: "GUILD_BAN_ADD",
data: {
guild_id: guild_id,
user: banned_user
},
guild_id: guild_id
} as GuildBanAddEvent);
return res.json(toObject(ban));
});
router.delete("/:user_id", async (req: Request, res: Response) => {
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.exists({ id: guild_id });
if (!guild) throw new HTTPError("Guild not found", 404);
const perms = await getPermission(req.user_id, guild_id);
perms.hasThrow("BAN_MEMBERS");
await BanModel.deleteOne({
user_id: banned_user_id,
guild_id
}).exec();
await emitEvent({
event: "GUILD_BAN_REMOVE",
data: {
guild_id,
user: banned_user
},
guild_id
} as GuildBanRemoveEvent);
return res.status(204).send();
});
export default router;
@@ -0,0 +1,73 @@
import { Router, Response, Request } from "express";
import {
ChannelCreateEvent,
ChannelModel,
ChannelType,
GuildModel,
Snowflake,
toObject,
ChannelUpdateEvent,
AnyChannel,
getPermission
} from "@fosscord/server-util";
import { HTTPError } from "lambert-server";
import { ChannelModifySchema } from "../../../schema/Channel";
import { emitEvent } from "../../../util/Event";
import { check } from "../../../util/instanceOf";
import { createChannel } from "../../../util/Channel";
const router = Router();
router.get("/", async (req: Request, res: Response) => {
const { guild_id } = req.params;
const channels = await ChannelModel.find({ guild_id }).exec();
res.json(toObject(channels));
});
// TODO: check if channel type is permitted
// TODO: check if parent_id exists
router.post("/", check(ChannelModifySchema), async (req: Request, res: Response) => {
// creates a new guild channel https://discord.com/developers/docs/resources/guild#create-guild-channel
const { guild_id } = req.params;
const body = req.body as ChannelModifySchema;
const channel = await createChannel({ ...body, guild_id }, req.user_id);
res.json(toObject(channel));
});
// TODO: check if parent_id exists
router.patch(
"/",
check([{ id: String, $position: Number, $lock_permissions: Boolean, $parent_id: String }]),
async (req: Request, res: Response) => {
// changes guild channel position
const { guild_id } = req.params;
const body = req.body as { id: string; position?: number; lock_permissions?: boolean; parent_id?: string };
body.position = Math.floor(body.position || 0);
if (!body.position && !body.parent_id) throw new HTTPError(`You need to at least specify position or parent_id`, 400);
const permission = await getPermission(req.user_id, guild_id);
permission.hasThrow("MANAGE_CHANNELS");
const opts: any = {};
if (body.position) opts.position = body.position;
if (body.parent_id) {
opts.parent_id = body.parent_id;
const parent_channel = await ChannelModel.findOne({ id: body.parent_id, guild_id }, { permission_overwrites: true }).exec();
if (body.lock_permissions) {
opts.permission_overwrites = parent_channel.permission_overwrites;
}
}
const channel = await ChannelModel.findOneAndUpdate({ id: req.body, guild_id }, opts).exec();
await emitEvent({ event: "CHANNEL_UPDATE", data: channel, channel_id: body.id, guild_id } as ChannelUpdateEvent);
res.json(toObject(channel));
}
);
export default router;
+48
View File
@@ -0,0 +1,48 @@
import {
ChannelModel,
EmojiModel,
GuildDeleteEvent,
GuildModel,
InviteModel,
MemberModel,
MessageModel,
RoleModel,
UserModel
} from "@fosscord/server-util";
import { Router, Request, Response } from "express";
import { HTTPError } from "lambert-server";
import { emitEvent } from "../../../util/Event";
const router = Router();
// 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("/", async (req: Request, res: Response) => {
var { guild_id } = req.params;
const guild = await GuildModel.findOne({ id: guild_id }, "owner_id").exec();
if (guild.owner_id !== req.user_id) throw new HTTPError("You are not the owner of this guild", 401);
await emitEvent({
event: "GUILD_DELETE",
data: {
id: guild_id
},
guild_id: guild_id
} as GuildDeleteEvent);
await Promise.all([
GuildModel.deleteOne({ id: guild_id }).exec(),
UserModel.updateMany({ guilds: guild_id }, { $pull: { guilds: guild_id } }).exec(),
RoleModel.deleteMany({ guild_id }).exec(),
ChannelModel.deleteMany({ guild_id }).exec(),
EmojiModel.deleteMany({ guild_id }).exec(),
InviteModel.deleteMany({ guild_id }).exec(),
MessageModel.deleteMany({ guild_id }).exec(),
MemberModel.deleteMany({ guild_id }).exec()
]);
return res.sendStatus(204);
});
export default router;
+61
View File
@@ -0,0 +1,61 @@
import { Request, Response, Router } from "express";
import {
ChannelModel,
EmojiModel,
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 { handleFile } from "../../../util/cdn";
import "missing-native-js-functions";
const router = Router();
router.get("/", async (req: Request, res: Response) => {
const { guild_id } = req.params;
const guild = await GuildModel.findOne({ id: guild_id })
.populate({ path: "joined_at", match: { id: req.user_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);
});
router.patch("/", check(GuildUpdateSchema), async (req: Request, res: Response) => {
const body = req.body as GuildUpdateSchema;
const { guild_id } = req.params;
// TODO: guild update check image
const perms = await getPermission(req.user_id, guild_id);
perms.hasThrow("MANAGE_GUILD");
if (body.icon) body.icon = await handleFile(`/icons/${guild_id}`, body.icon);
if (body.banner) body.banner = await handleFile(`/banners/${guild_id}`, body.banner);
if (body.splash) body.splash = await handleFile(`/splashes/${guild_id}`, body.splash);
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.json(data);
});
export default router;
@@ -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;
@@ -0,0 +1,69 @@
import { Request, Response, Router } from "express";
import {
GuildModel,
MemberModel,
UserModel,
toObject,
GuildMemberAddEvent,
getPermission,
PermissionResolvable,
RoleModel,
GuildMemberUpdateEvent
} from "@fosscord/server-util";
import { HTTPError } from "lambert-server";
import { addMember, isMember, removeMember } from "../../../../../util/Member";
import { check } from "../../../../../util/instanceOf";
import { MemberChangeSchema } from "../../../../../schema/Member";
import { emitEvent } from "../../../../../util/Event";
const router = Router();
router.get("/", async (req: Request, res: Response) => {
const { guild_id, member_id } = req.params;
await isMember(req.user_id, guild_id);
const member = await MemberModel.findOne({ id: member_id, guild_id }).exec();
return res.json(toObject(member));
});
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) {
const roles = await RoleModel.find({ id: { $in: body.roles } }).exec();
if (body.roles.length !== roles.length) throw new HTTPError("Roles not found", 404);
// TODO: check if user has permission to add role
}
const member = await MemberModel.findOneAndUpdate({ id: member_id, guild_id }, body).exec();
await emitEvent({
event: "GUILD_MEMBER_UPDATE",
guild_id,
data: toObject(member)
} as GuildMemberUpdateEvent);
res.json(toObject(member));
});
router.put("/", async (req: Request, res: Response) => {
const { guild_id, member_id } = req.params;
throw new HTTPError("Maintenance: Currently you can't add a member", 403);
// TODO: only for oauth2 applications
await addMember(member_id, guild_id);
res.sendStatus(204);
});
router.delete("/", async (req: Request, res: Response) => {
const { guild_id, member_id } = req.params;
const perms = await getPermission(req.user_id, guild_id);
perms.hasThrow("KICK_MEMBERS");
await removeMember(member_id, guild_id);
res.sendStatus(204);
});
export default router;
@@ -0,0 +1,24 @@
import { getPermission, PermissionResolvable } from "@fosscord/server-util";
import { Request, Response, Router } from "express";
import { check } from "lambert-server";
import { MemberNickChangeSchema } from "../../../../../schema/Member";
import { changeNickname } from "../../../../../util/Member";
const router = Router();
router.patch("/", check(MemberNickChangeSchema), async (req: Request, res: Response) => {
var { guild_id, member_id } = req.params;
var permissionString: PermissionResolvable = "MANAGE_NICKNAMES";
if (member_id === "@me") {
member_id = req.user_id;
permissionString = "CHANGE_NICKNAME";
}
const perms = await getPermission(req.user_id, guild_id);
perms.hasThrow(permissionString);
await changeNickname(member_id, guild_id, req.body.nickname);
res.status(204);
});
export default router;
@@ -0,0 +1,27 @@
import { getPermission } from "@fosscord/server-util";
import { Request, Response, Router } from "express";
import { addRole, removeRole } from "../../../../../../../util/Member";
const router = Router();
router.delete("/:member_id/roles/:role_id", async (req: Request, res: Response) => {
const { guild_id, role_id, member_id } = req.params;
const perms = await getPermission(req.user_id, guild_id);
perms.hasThrow("MANAGE_ROLES");
await removeRole(member_id, guild_id, role_id);
res.sendStatus(204);
});
router.put("/:member_id/roles/:role_id", async (req: Request, res: Response) => {
const { guild_id, role_id, member_id } = req.params;
const perms = await getPermission(req.user_id, guild_id);
perms.hasThrow("MANAGE_ROLES");
await addRole(member_id, guild_id, role_id);
res.sendStatus(204);
});
export default router;
@@ -0,0 +1,38 @@
import { Request, Response, Router } from "express";
import { GuildModel, MemberModel, toObject } from "@fosscord/server-util";
import { HTTPError } from "lambert-server";
import { instanceOf, Length } from "../../../../util/instanceOf";
import { PublicMemberProjection, isMember } from "../../../../util/Member";
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 } = req.params;
const guild = await GuildModel.findOne({ id: guild_id }).exec();
await isMember(req.user_id, guild_id);
try {
instanceOf({ $limit: new Length(Number, 1, 1000), $after: String }, req.query, {
path: "query",
req,
ref: { obj: null, key: "" }
});
} catch (error) {
return res.status(400).json({ code: 50035, message: "Invalid Query", success: false, errors: error });
}
// @ts-ignore
if (!req.query.limit) req.query.limit = 1;
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)
.exec();
return res.json(toObject(members));
});
export default router;
@@ -0,0 +1,10 @@
import { Config } from "@fosscord/server-util";
import { Request, Response, Router } from "express";
const router = Router();
router.get("/", async (req: Request, res: Response) => {
return res.json(Config.get().regions.available);
});
export default router;
+128
View File
@@ -0,0 +1,128 @@
import { Request, Response, Router } from "express";
import {
RoleModel,
GuildModel,
getPermission,
toObject,
UserModel,
Snowflake,
MemberModel,
GuildRoleCreateEvent,
GuildRoleUpdateEvent,
GuildRoleDeleteEvent
} from "@fosscord/server-util";
import { HTTPError } from "lambert-server";
import { emitEvent } from "../../../util/Event";
import { check } from "../../../util/instanceOf";
import { RoleModifySchema } from "../../../schema/Roles";
import { getPublicUser } from "../../../util/User";
import { isMember } from "../../../util/Member";
const router: Router = Router();
router.get("/", async (req: Request, res: Response) => {
const guild_id = req.params.guild_id;
await isMember(req.user_id, guild_id);
const roles = await RoleModel.find({ guild_id: guild_id }).exec();
return res.json(toObject(roles));
});
router.post("/", check(RoleModifySchema), async (req: Request, res: Response) => {
const guild_id = req.params.guild_id;
const body = req.body as RoleModifySchema;
const guild = await GuildModel.findOne({ id: guild_id }, { id: true }).exec();
const user = await UserModel.findOne({ id: req.user_id }).exec();
const perms = await getPermission(req.user_id, guild_id);
perms.hasThrow("MANAGE_ROLES");
if (!body.name) throw new HTTPError("You need to specify a name");
const role = await new RoleModel({
...body,
id: Snowflake.generate(),
guild_id: guild_id,
managed: false,
position: 0,
tags: null,
permissions: body.permissions || 0n
}).save();
await emitEvent({
event: "GUILD_ROLE_CREATE",
guild_id,
data: {
guild_id,
role: toObject(role)
}
} as GuildRoleCreateEvent);
res.json(toObject(role));
});
router.delete("/:role_id", async (req: Request, res: Response) => {
const guild_id = req.params.guild_id;
const { role_id } = req.params;
const guild = await GuildModel.findOne({ id: guild_id }, { id: true }).exec();
const user = await UserModel.findOne({ id: req.user_id }).exec();
const perms = await getPermission(req.user_id, guild_id);
if (!perms.has("MANAGE_ROLES")) throw new HTTPError("You missing the MANAGE_ROLES permission", 401);
await RoleModel.findOneAndDelete({
id: role_id,
guild_id: guild_id
}).exec();
await emitEvent({
event: "GUILD_ROLE_DELETE",
guild_id,
data: {
guild_id,
role_id
}
} as GuildRoleDeleteEvent);
res.sendStatus(204);
});
// TODO: check role hierarchy
router.patch("/:role_id", check(RoleModifySchema), async (req: Request, res: Response) => {
const guild_id = req.params.guild_id;
const { role_id } = req.params;
const body = req.body as RoleModifySchema;
const guild = await GuildModel.findOne({ id: guild_id }, { id: true }).exec();
const user = await UserModel.findOne({ id: req.user_id }).exec();
const perms = await getPermission(req.user_id, guild_id);
perms.hasThrow("MANAGE_ROLES");
const role = await RoleModel.findOneAndUpdate(
{
id: role_id,
guild_id: guild_id
},
// @ts-ignore
body
).exec();
await emitEvent({
event: "GUILD_ROLE_UPDATE",
guild_id,
data: {
guild_id,
role
}
} as GuildRoleUpdateEvent);
res.json(toObject(role));
});
export default router;
@@ -0,0 +1,99 @@
import { Request, Response, Router } from "express";
import { TemplateModel, GuildModel, getPermission, toObject, UserModel, Snowflake } from "@fosscord/server-util";
import { HTTPError } from "lambert-server";
import { TemplateCreateSchema, TemplateModifySchema } from "../../../schema/Template";
import { check } from "../../../util/instanceOf";
import { generateCode } from "../../../util/String";
const router: Router = Router();
const TemplateGuildProjection = {
name: true,
description: true,
region: true,
verification_level: true,
default_message_notifications: true,
explicit_content_filter: true,
preferred_locale: true,
afk_timeout: true,
roles: true,
channels: true,
afk_channel_id: true,
system_channel_id: true,
system_channel_flags: true,
icon_hash: true
};
router.get("/", async (req: Request, res: Response) => {
const { guild_id } = req.params;
var templates = await TemplateModel.find({ source_guild_id: guild_id }).exec();
return res.json(toObject(templates));
});
router.post("/", check(TemplateCreateSchema), async (req: Request, res: Response) => {
const { guild_id } = req.params;
const guild = await GuildModel.findOne({ id: guild_id }, TemplateGuildProjection).exec();
const perms = await getPermission(req.user_id, guild_id);
perms.hasThrow("MANAGE_GUILD");
const exists = await TemplateModel.findOne({ id: guild_id })
.exec()
.catch((e) => {});
if (exists) throw new HTTPError("Template already exists", 400);
const template = await new TemplateModel({
...req.body,
code: generateCode(),
creator_id: req.user_id,
created_at: new Date(),
updated_at: new Date(),
source_guild_id: guild_id,
serialized_source_guild: guild
}).save();
res.json(toObject(template)).send();
});
router.delete("/:code", async (req: Request, res: Response) => {
const guild_id = req.params.guild_id;
const { code } = req.params;
const perms = await getPermission(req.user_id, guild_id);
perms.hasThrow("MANAGE_GUILD");
const template = await TemplateModel.findOneAndDelete({
code
}).exec();
res.send(toObject(template));
});
router.put("/:code", async (req: Request, res: Response) => {
const guild_id = req.params.guild_id;
const { code } = req.params;
const guild = await GuildModel.findOne({ id: guild_id }, TemplateGuildProjection).exec();
const perms = await getPermission(req.user_id, guild_id);
perms.hasThrow("MANAGE_GUILD");
const template = await TemplateModel.findOneAndUpdate({ code }, { serialized_source_guild: guild }).exec();
res.json(toObject(template)).send();
});
router.patch("/:code", check(TemplateModifySchema), async (req: Request, res: Response) => {
const { guild_id } = req.params;
const { code } = req.params;
const perms = await getPermission(req.user_id, guild_id);
perms.hasThrow("MANAGE_GUILD");
const template = await TemplateModel.findOneAndUpdate({ code }, { name: req.body.name, description: req.body.description }).exec();
res.json(toObject(template)).send();
});
export default router;
@@ -0,0 +1,45 @@
import { getPermission, GuildModel, InviteModel, trimSpecial } from "@fosscord/server-util";
import { Router, Request, Response } from "express";
import { HTTPError } from "lambert-server";
import { check, Length } from "../../../util/instanceOf";
import { isMember } from "../../../util/Member";
const router = Router();
const InviteRegex = /\W/g;
router.get("/", async (req: Request, res: Response) => {
const { guild_id } = req.params;
await isMember(req.user_id, guild_id);
const guild = await GuildModel.findOne({ id: guild_id }).exec();
if (!guild.vanity_url) throw new HTTPError("This guild has no vanity url", 204);
return res.json({ code: guild.vanity_url.code });
});
// TODO: check if guild is elgible for vanity url
router.patch("/", check({ code: new Length(String, 0, 20) }), async (req: Request, res: Response) => {
const { guild_id } = req.params;
var code = req.body.code.replace(InviteRegex);
if (!code) code = null;
const permission = await getPermission(req.user_id, guild_id);
permission.hasThrow("MANAGE_GUILD");
const alreadyExists = await Promise.all([
GuildModel.findOne({ "vanity_url.code": code })
.exec()
.catch(() => null),
InviteModel.findOne({ code: code })
.exec()
.catch(() => null)
]);
if (alreadyExists.some((x) => x)) throw new HTTPError("Vanity url already exists", 400);
await GuildModel.updateOne({ id: guild_id }, { "vanity_url.code": code }).exec();
return res.json({ code: code });
});
export default router;
@@ -0,0 +1,49 @@
import { Request, Response, Router } from "express";
import { GuildModel, getPermission, toObject, Snowflake } from "@fosscord/server-util";
import { HTTPError } from "lambert-server";
import { emitEvent } from "../../../util/Event";
import { check } from "../../../util/instanceOf";
import { isMember } from "../../../util/Member";
import { GuildAddChannelToWelcomeScreenSchema } from "../../../schema/Guild";
import { getPublicUser } from "../../../util/User";
const router: Router = Router();
router.get("/", async (req: Request, res: Response) => {
const guild_id = req.params.guild_id;
const guild = await GuildModel.findOne({ id: guild_id });
await isMember(req.user_id, guild_id);
res.json(toObject(guild.welcome_screen));
});
router.post("/", check(GuildAddChannelToWelcomeScreenSchema), async (req: Request, res: Response) => {
const guild_id = req.params.guild_id;
const body = req.body as GuildAddChannelToWelcomeScreenSchema;
const guild = await GuildModel.findOne({ id: guild_id }).exec();
var channelObject = {
...body
};
const perms = await getPermission(req.user_id, guild_id);
perms.hasThrow("MANAGE_GUILD");
if (!guild.welcome_screen.enabled) throw new HTTPError("Welcome screen disabled", 400);
if (guild.welcome_screen.welcome_channels.some((channel) => channel.channel_id === body.channel_id))
throw new Error("Welcome Channel exists");
await GuildModel.findOneAndUpdate(
{
id: guild_id
},
{ $push: { "welcome_screen.welcome_channels": channelObject } }
).exec();
res.sendStatus(204);
});
export default router;
@@ -0,0 +1,139 @@
import { Request, Response, Router } from "express";
import { Config, Permissions, GuildModel, InviteModel, ChannelModel, MemberModel } from "@fosscord/server-util";
import { HTTPError } from "lambert-server";
import { random } from "../../../util/RandomInviteID";
const router: Router = Router();
// Undocumented API notes:
// An invite is created for the widget_channel_id on request (only if an existing one created by the widget doesn't already exist)
// This invite created doesn't include an inviter object like user created ones and has a default expiry of 24 hours
// Missing user object information is intentional (https://github.com/discord/discord-api-docs/issues/1287)
// channels returns voice channel objects where @everyone has the CONNECT permission
// members (max 100 returned) is a sample of all members, and bots par invisible status, there exists some alphabetical distribution pattern between the members returned
// https://discord.com/developers/docs/resources/guild#get-guild-widget
// TODO: Cache the response for a guild for 5 minutes regardless of response
router.get("/", async (req: Request, res: Response) => {
const { guild_id } = req.params;
const guild = await GuildModel.findOne({ id: guild_id }).exec();
if (!guild.widget_enabled) throw new HTTPError("Widget Disabled", 404);
// Fetch existing widget invite for widget channel
var invite = await InviteModel.findOne({ channel_id: guild.widget_channel_id, inviter_id: { $type: 10 } }).exec();
if (guild.widget_channel_id && !invite) {
// Create invite for channel if none exists
// TODO: Refactor invite create code to a shared function
const max_age = 86400; // 24 hours
const expires_at = new Date(max_age * 1000 + Date.now());
const body = {
code: random(),
temporary: false,
uses: 0,
max_uses: 0,
max_age: max_age,
expires_at,
created_at: new Date(),
guild_id,
channel_id: guild.widget_channel_id,
inviter_id: null
};
invite = await new InviteModel(body).save();
}
// Fetch voice channels, and the @everyone permissions object
let channels: any[] = [];
await ChannelModel.find({ guild_id: guild_id, type: 2 }, { permission_overwrites: { $elemMatch: { id: guild_id } } })
.lean()
.select("id name position permission_overwrites")
.sort({ position: 1 })
.cursor()
.eachAsync((doc) => {
// Only return channels where @everyone has the CONNECT permission
if (
doc.permission_overwrites === undefined ||
Permissions.channelPermission(doc.permission_overwrites, Permissions.FLAGS.CONNECT) === Permissions.FLAGS.CONNECT
) {
channels.push({
id: doc.id,
name: doc.name,
position: doc.position
});
}
});
// Fetch members
// TODO: Understand how Discord's max 100 random member sample works, and apply to here (see top of this file)
let members: any[] = [];
await MemberModel.find({ guild_id: guild_id })
.lean()
.populate({ path: "user", select: { _id: 0, username: 1, avatar: 1, presence: 1 } })
.select("id user nick deaf mute")
.cursor()
.eachAsync((doc) => {
const status = doc.user?.presence?.status || "offline";
if (status == "offline") return;
let item = {};
item = {
...item,
id: null, // this is updated during the sort outside of the query
username: doc.nick || doc.user?.username,
discriminator: "0000", // intended (https://github.com/discord/discord-api-docs/issues/1287)
avatar: null, // intended, avatar_url below will return a unique guild + user url to the avatar
status: status
};
const activity = doc.user?.presence?.activities?.[0];
if (activity) {
item = {
...item,
game: { name: activity.name }
};
}
// TODO: If the member is in a voice channel, return extra widget details
// Extra fields returned include deaf, mute, self_deaf, self_mute, supress, and channel_id (voice channel connected to)
// Get this from VoiceState
// TODO: Implement a widget-avatar endpoint on the CDN, and implement logic here to request it
// Get unique avatar url for guild user, cdn to serve the actual avatar image on this url
/*
const avatar = doc.user?.avatar;
if (avatar) {
const CDN_HOST = Config.get().cdn.endpoint || "http://localhost:3003";
const avatar_url = "/widget-avatars/" + ;
item = {
...item,
avatar_url: avatar_url
}
}
*/
members.push(item);
});
// Sort members, and update ids (Unable to do under the mongoose query due to https://mongoosejs.com/docs/faq.html#populate_sort_order)
members = members.sort((first, second) => 0 - (first.username > second.username ? -1 : 1));
members.forEach((x, i) => {
x.id = i;
});
// Construct object to respond with
const data = {
id: guild_id,
name: guild.name,
instant_invite: invite?.code,
channels: channels,
members: members,
presence_count: guild.presence_count
};
res.set("Cache-Control", "public, max-age=300");
return res.json(data);
});
export default router;
@@ -0,0 +1,110 @@
import { Request, Response, Router } from "express";
import { GuildModel } from "@fosscord/server-util";
import { HTTPError } from "lambert-server";
import fs from "fs";
import path from "path";
const router: Router = Router();
// TODO: use svg templates instead of node-canvas for improved performance and to change it easily
// https://discord.com/developers/docs/resources/guild#get-guild-widget-image
// TODO: Cache the response
router.get("/", async (req: Request, res: Response) => {
const { guild_id } = req.params;
const guild = await GuildModel.findOne({ id: guild_id }).exec();
if (!guild.widget_enabled) throw new HTTPError("Unknown Guild", 404);
// Fetch guild information
const icon = guild.icon;
const name = guild.name;
const presence = guild.presence_count + " ONLINE";
// Fetch parameter
const style = req.query.style?.toString() || "shield";
if (!["shield", "banner1", "banner2", "banner3", "banner4"].includes(style)) {
throw new HTTPError("Value must be one of ('shield', 'banner1', 'banner2', 'banner3', 'banner4').", 400);
}
// Setup canvas
const { createCanvas } = require("canvas");
const { loadImage } = require("canvas");
const sizeOf = require("image-size");
// TODO: Widget style templates need Fosscord branding
const source = path.join(__dirname, "..", "..", "..", "..", "assets", "widget", `${style}.png`);
if (!fs.existsSync(source)) {
throw new HTTPError("Widget template does not exist.", 400);
}
// Create base template image for parameter
const { width, height } = await sizeOf(source);
const canvas = createCanvas(width, height);
const ctx = canvas.getContext("2d");
const template = await loadImage(source);
ctx.drawImage(template, 0, 0);
// Add the guild specific information to the template asset image
switch (style) {
case "shield":
ctx.textAlign = "center";
await drawText(ctx, 73, 13, "#FFFFFF", "thin 10px Verdana", presence);
break;
case "banner1":
if (icon) await drawIcon(ctx, 20, 27, 50, icon);
await drawText(ctx, 83, 51, "#FFFFFF", "12px Verdana", name, 22);
await drawText(ctx, 83, 66, "#C9D2F0FF", "thin 11px Verdana", presence);
break;
case "banner2":
if (icon) await drawIcon(ctx, 13, 19, 36, icon);
await drawText(ctx, 62, 34, "#FFFFFF", "12px Verdana", name, 15);
await drawText(ctx, 62, 49, "#C9D2F0FF", "thin 11px Verdana", presence);
break;
case "banner3":
if (icon) await drawIcon(ctx, 20, 20, 50, icon);
await drawText(ctx, 83, 44, "#FFFFFF", "12px Verdana", name, 27);
await drawText(ctx, 83, 58, "#C9D2F0FF", "thin 11px Verdana", presence);
break;
case "banner4":
if (icon) await drawIcon(ctx, 21, 136, 50, icon);
await drawText(ctx, 84, 156, "#FFFFFF", "13px Verdana", name, 27);
await drawText(ctx, 84, 171, "#C9D2F0FF", "thin 12px Verdana", presence);
break;
default:
throw new HTTPError("Value must be one of ('shield', 'banner1', 'banner2', 'banner3', 'banner4').", 400);
}
// Return final image
const buffer = canvas.toBuffer("image/png");
res.set("Content-Type", "image/png");
res.set("Cache-Control", "public, max-age=3600");
return res.send(buffer);
});
async function drawIcon(canvas: any, x: number, y: number, scale: number, icon: string) {
// @ts-ignore
const img = new require("canvas").Image();
img.src = icon;
// Do some canvas clipping magic!
canvas.save();
canvas.beginPath();
const r = scale / 2; // use scale to determine radius
canvas.arc(x + r, y + r, r, 0, 2 * Math.PI, false); // start circle at x, and y coords + radius to find center
canvas.clip();
canvas.drawImage(img, x, y, scale, scale);
canvas.restore();
}
async function drawText(canvas: any, x: number, y: number, color: string, font: string, text: string, maxcharacters?: number) {
canvas.fillStyle = color;
canvas.font = font;
if (text.length > (maxcharacters || 0) && maxcharacters) text = text.slice(0, maxcharacters) + "...";
canvas.fillText(text, x, y);
}
export default router;
+35
View File
@@ -0,0 +1,35 @@
import { Request, Response, Router } from "express";
import { getPermission, GuildModel } from "@fosscord/server-util";
import { HTTPError } from "lambert-server";
import { check } from "../../../util/instanceOf";
import { WidgetModifySchema } from "../../../schema/Widget";
const router: Router = Router();
// https://discord.com/developers/docs/resources/guild#get-guild-widget-settings
router.get("/", async (req: Request, res: Response) => {
const { guild_id } = req.params;
const perms = await getPermission(req.user_id, guild_id);
perms.hasThrow("MANAGE_GUILD");
const guild = await GuildModel.findOne({ id: guild_id }).exec();
return res.json({ enabled: guild.widget_enabled || false, channel_id: guild.widget_channel_id || null });
});
// https://discord.com/developers/docs/resources/guild#modify-guild-widget
router.patch("/", check(WidgetModifySchema), async (req: Request, res: Response) => {
const body = req.body as WidgetModifySchema;
const { guild_id } = req.params;
const perms = await getPermission(req.user_id, guild_id);
perms.hasThrow("MANAGE_GUILD");
await GuildModel.updateOne({ id: guild_id }, { widget_enabled: body.enabled, widget_channel_id: body.channel_id }).exec();
// Widget invite for the widget_channel_id gets created as part of the /guilds/{guild.id}/widget.json request
return res.json(body);
});
export default router;
+89
View File
@@ -0,0 +1,89 @@
import { Router, Request, Response } from "express";
import { RoleModel, GuildModel, Snowflake, Guild, RoleDocument, Config } from "@fosscord/server-util";
import { HTTPError } from "lambert-server";
import { check } from "./../../util/instanceOf";
import { GuildCreateSchema } from "../../schema/Guild";
import { getPublicUser } from "../../util/User";
import { addMember } from "../../util/Member";
import { createChannel } from "../../util/Channel";
const router: Router = Router();
//TODO: create default channel
router.post("/", check(GuildCreateSchema), async (req: Request, res: Response) => {
const body = req.body as GuildCreateSchema;
const { maxGuilds } = Config.get().limits.user;
const user = await getPublicUser(req.user_id, { guilds: true });
if (user.guilds.length >= maxGuilds) {
throw new HTTPError(`Maximum number of guilds reached ${maxGuilds}`, 403);
}
const guild_id = Snowflake.generate();
const guild: Guild = {
name: body.name,
region: Config.get().regions.default,
owner_id: req.user_id,
icon: undefined,
afk_channel_id: undefined,
afk_timeout: 300,
application_id: undefined,
banner: undefined,
default_message_notifications: 0,
description: undefined,
splash: undefined,
discovery_splash: undefined,
explicit_content_filter: 0,
features: [],
id: guild_id,
large: undefined,
max_members: 250000,
max_presences: 250000,
max_video_channel_users: 25,
presence_count: 0,
member_count: 0, // will automatically be increased by addMember()
mfa_level: 0,
preferred_locale: "en-US",
premium_subscription_count: 0,
premium_tier: 0,
public_updates_channel_id: undefined,
rules_channel_id: undefined,
system_channel_flags: 0,
system_channel_id: undefined,
unavailable: false,
vanity_url: undefined,
verification_level: 0,
welcome_screen: {
enabled: false,
description: "No description",
welcome_channels: []
},
widget_channel_id: undefined,
widget_enabled: false
};
const [guild_doc, role] = await Promise.all([
new GuildModel(guild).save(),
new RoleModel({
id: guild_id,
guild_id: guild_id,
color: 0,
hoist: false,
managed: false,
mentionable: false,
name: "@everyone",
permissions: 2251804225n,
position: 0,
tags: null
}).save()
]);
await createChannel({ name: "general", type: 0, guild_id, position: 0, permission_overwrites: [] }, req.user_id);
await addMember(req.user_id, guild_id);
res.status(201).json({ id: guild.id });
});
export default router;
+61
View File
@@ -0,0 +1,61 @@
import { Request, Response, Router } from "express";
const router: Router = Router();
import { TemplateModel, GuildModel, toObject, UserModel, RoleModel, Snowflake, Guild, Config } from "@fosscord/server-util";
import { HTTPError } from "lambert-server";
import { GuildTemplateCreateSchema } from "../../../schema/Guild";
import { getPublicUser } from "../../../util/User";
import { check } from "../../../util/instanceOf";
import { addMember } from "../../../util/Member";
router.get("/:code", async (req: Request, res: Response) => {
const { code } = req.params;
const template = await TemplateModel.findOne({ code: code }).exec();
res.json(toObject(template)).send();
});
router.post("/:code", check(GuildTemplateCreateSchema), async (req: Request, res: Response) => {
const { code } = req.params;
const body = req.body as GuildTemplateCreateSchema;
const { maxGuilds } = Config.get().limits.user;
const user = await getPublicUser(req.user_id, { guilds: true });
if (user.guilds.length >= maxGuilds) {
throw new HTTPError(`Maximum number of guilds reached ${maxGuilds}`, 403);
}
const template = await TemplateModel.findOne({ code: code }).exec();
const guild_id = Snowflake.generate();
const guild: Guild = {
...body,
...template.serialized_source_guild,
id: guild_id,
owner_id: req.user_id
};
const [guild_doc, role] = await Promise.all([
new GuildModel(guild).save(),
new RoleModel({
id: guild_id,
guild_id: guild_id,
color: 0,
hoist: false,
managed: true,
mentionable: true,
name: "@everyone",
permissions: 2251804225n,
position: 0,
tags: null
}).save()
]);
await addMember(req.user_id, guild_id, { guild: guild_doc });
res.status(201).json({ id: guild.id });
});
export default router;
+44
View File
@@ -0,0 +1,44 @@
import { Router, Request, Response } from "express";
import { getPermission, InviteModel, toObject } from "@fosscord/server-util";
import { HTTPError } from "lambert-server";
import { addMember } from "../../util/Member";
const router: Router = Router();
router.get("/:code", async (req: Request, res: Response) => {
const { code } = req.params;
const invite = await InviteModel.findOne({ code }).exec();
if (!invite) throw new HTTPError("Unknown Invite", 404);
res.status(200).send(toObject(invite));
});
router.post("/:code", async (req: Request, res: Response) => {
const { code } = req.params;
const invite = await InviteModel.findOneAndUpdate({ code }, { $inc: { uses: 1 } }).exec();
if (!invite) throw new HTTPError("Unknown Invite", 404);
await addMember(req.user_id, invite.guild_id);
res.status(200).send(toObject(invite));
});
router.delete("/:code", async (req: Request, res: Response) => {
const { 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 missing the MANAGE_GUILD or MANAGE_CHANNELS permission", 401);
await InviteModel.deleteOne({ code }).exec();
res.status(200).send({ invite: toObject(invite) });
});
export default router;
+9
View File
@@ -0,0 +1,9 @@
import { Router, Response, Request } from "express";
const router = Router();
router.get("/", (req: Request, res: Response) => {
res.send("pong");
});
export default router;
+10
View File
@@ -0,0 +1,10 @@
import { Router, Response, Request } from "express";
const router = Router();
router.post("/", (req: Request, res: Response) => {
// TODO:
res.sendStatus(204);
});
export default router;
+13
View File
@@ -0,0 +1,13 @@
import { Router, Request, Response } from "express";
import { getPublicUser } from "../../../util/User";
import { HTTPError } from "lambert-server";
const router: Router = Router();
router.get("/", async (req: Request, res: Response) => {
const { id } = req.params;
res.json(await getPublicUser(id));
});
export default router;
+27
View File
@@ -0,0 +1,27 @@
import { Router, Request, Response } from "express";
import { getPublicUser } from "../../../util/User";
const router: Router = Router();
router.get("/", async (req: Request, res: Response) => {
const user = await getPublicUser(req.params.id, { user_data: true })
res.json({
connected_accounts: user.user_data.connected_accounts,
premium_guild_since: null, // TODO
premium_since: null, // TODO
user: {
username: user.username,
discriminator: user.discriminator,
id: user.id,
public_flags: user.public_flags,
avatar: user.avatar,
accent_color: user.accent_color,
banner: user.banner,
bio: req.user_bot ? null : user.bio,
bot: user.bot,
}
});
});
export default router;
@@ -0,0 +1,10 @@
import { Router, Response, Request } from "express";
const router = Router();
router.get("/", (req: Request, res: Response) => {
// TODO:
res.status(200).send({ guild_affinities: [] });
});
export default router;
@@ -0,0 +1,10 @@
import { Router, Response, Request } from "express";
const router = Router();
router.get("/", (req: Request, res: Response) => {
// TODO:
res.status(200).send({ user_affinities: [], inverse_user_affinities: [] });
});
export default router;
+53
View File
@@ -0,0 +1,53 @@
import { Router, Request, Response } from "express";
import {
ChannelModel,
ChannelCreateEvent,
toObject,
ChannelType,
Snowflake,
trimSpecial,
Channel,
DMChannel,
UserModel
} from "@fosscord/server-util";
import { HTTPError } from "lambert-server";
import { emitEvent } from "../../../util/Event";
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({ recipient_ids: req.user_id }).exec();
res.json(toObject(channels));
});
router.post("/", check(DmChannelCreateSchema), async (req: Request, res: Response) => {
const body = req.body as DmChannelCreateSchema;
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 = await new ChannelModel({
name,
type,
owner_id: req.user_id,
id: Snowflake.generate(),
created_at: new Date(),
last_message_id: null,
recipient_ids: [...body.recipients, req.user_id]
}).save();
await emitEvent({ event: "CHANNEL_CREATE", data: toObject(channel), user_id: req.user_id } as ChannelCreateEvent);
res.json(toObject(channel));
});
export default router;
+22
View File
@@ -0,0 +1,22 @@
import { Router, Request, Response } from "express";
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
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(401);
}
});
export default router;
+20
View File
@@ -0,0 +1,20 @@
import { UserModel } from "@fosscord/server-util";
import { Router, Response, Request } from "express";
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
let correctpass = await bcrypt.compare(req.body.password, user!.user_data.hash); //Not sure if user typed right password :/
if (correctpass) {
await UserModel.updateOne({ id: req.user_id }, { disabled: true }).exec();
res.sendStatus(204);
} else {
res.status(400).json({ message: "Password does not match", code: 50018 });
}
});
export default router;
+55
View File
@@ -0,0 +1,55 @@
import { Router, Request, Response } from "express";
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";
const router: Router = Router();
router.get("/", async (req: Request, res: Response) => {
const user = await UserModel.findOne({ id: req.user_id }, { guilds: true }).exec();
if (!user) throw new HTTPError("User not found", 404);
var guildIDs = user.guilds || [];
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 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 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),
]);
const user = await getPublicUser(req.user_id);
await emitEvent({
event: "GUILD_MEMBER_REMOVE",
data: {
guild_id: guild_id,
user: user,
},
guild_id: guild_id,
} as GuildMemberRemoveEvent);
return res.sendStatus(204);
});
export default router;
+48
View File
@@ -0,0 +1,48 @@
import { Router, Request, Response } from "express";
import { UserModel, toObject, PublicUserProjection } from "@fosscord/server-util";
import { getPublicUser } from "../../../util/User";
import { UserModifySchema } from "../../../schema/User";
import { check } from "../../../util/instanceOf";
import { handleFile } from "../../../util/cdn";
const router: Router = Router();
router.get("/", async (req: Request, res: Response) => {
res.json(await getPublicUser(req.user_id));
});
const UserUpdateProjection = {
accent_color: true,
avatar: true,
banner: true,
bio: true,
bot: true,
discriminator: true,
email: true,
flags: true,
id: true,
locale: true,
mfa_enabled: true,
nsfw_alllowed: true,
phone: true,
public_flags: true,
purchased_flags: true,
// token: true, // this isn't saved in the db and needs to be set manually
username: true,
verified: true
};
router.patch("/", check(UserModifySchema), async (req: Request, res: Response) => {
const body = req.body as UserModifySchema;
if (body.avatar) body.avatar = await handleFile(`/avatars/${req.user_id}`, body.avatar as string);
if (body.banner) body.banner = await handleFile(`/banners/${req.user_id}`, body.banner as string);
const user = await UserModel.findOneAndUpdate({ id: req.user_id }, body, { projection: UserUpdateProjection }).exec();
// TODO: dispatch user update event
res.json(toObject(user));
});
export default router;
// {"message": "Invalid two-factor code", "code": 60008}
+10
View File
@@ -0,0 +1,10 @@
import { Router, Response, Request } from "express";
const router = Router();
router.get("/", (req: Request, res: Response) => {
// TODO:
res.status(200).send([]);
});
export default router;
+27
View File
@@ -0,0 +1,27 @@
import { Router, Request, Response } from "express";
import { getPublicUser } from "../../../util/User";
const router: Router = Router();
router.get("/", async (req: Request, res: Response) => {
const user = await getPublicUser(req.user_id, { user_data: true })
res.json({
connected_accounts: user.user_data.connected_accounts,
premium_guild_since: null, // TODO
premium_since: null, // TODO
user: {
username: user.username,
discriminator: user.discriminator,
id: user.id,
public_flags: user.public_flags,
avatar: user.avatar,
accent_color: user.accent_color,
banner: user.banner,
bio: user.bio,
bot: user.bot,
}
});
});
export default router;
+176
View File
@@ -0,0 +1,176 @@
import {
RelationshipAddEvent,
UserModel,
PublicUserProjection,
toObject,
RelationshipType,
RelationshipRemoveEvent,
UserDocument
} from "@fosscord/server-util";
import { Router, Response, Request } from "express";
import { HTTPError } from "lambert-server";
import { emitEvent } from "../../../util/Event";
import { check, Length } from "../../../util/instanceOf";
const router = Router();
const userProjection = { "user_data.relationships": true, ...PublicUserProjection };
router.get("/", async (req: Request, res: Response) => {
const user = await UserModel.findOne({ id: req.user_id }, { user_data: { relationships: true } })
.populate({ path: "user_data.relationships.id", model: UserModel })
.exec();
return res.json(toObject(user.user_data.relationships));
});
async function addRelationship(req: Request, res: Response, friend: UserDocument, type: RelationshipType) {
const id = friend.id;
if (id === req.user_id) throw new HTTPError("You can't add yourself as a friend");
const user = await UserModel.findOne({ id: req.user_id }, userProjection).exec();
const newUserRelationships = [...user.user_data.relationships];
const newFriendRelationships = [...friend.user_data.relationships];
var relationship = newUserRelationships.find((x) => x.id === id);
const friendRequest = newFriendRelationships.find((x) => x.id === req.user_id);
if (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 };
newUserRelationships.push(relationship);
}
if (friendRequest && friendRequest.type !== RelationshipType.blocked) {
newFriendRelationships.remove(friendRequest);
await Promise.all([
UserModel.updateOne({ id: friend.id }, { "user_data.relationships": newFriendRelationships }).exec(),
emitEvent({
event: "RELATIONSHIP_REMOVE",
data: friendRequest,
user_id: id
} as RelationshipRemoveEvent)
]);
}
await Promise.all([
UserModel.updateOne({ id: req.user_id }, { "user_data.relationships": newUserRelationships }).exec(),
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 newFriendRelationships.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 newUserRelationships.push(outgoing_relationship);
await Promise.all([
UserModel.updateOne({ id: req.user_id }, { "user_data.relationships": newUserRelationships }).exec(),
UserModel.updateOne({ id: friend.id }, { "user_data.relationships": newFriendRelationships }).exec(),
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.put("/:id", check({ $type: new Length(Number, 1, 4) }), async (req: Request, res: Response) => {
return await addRelationship(req, res, await UserModel.findOne({ id: req.params.id }), req.body.type);
});
router.post("/", check({ discriminator: String, username: String }), async (req: Request, res: Response) => {
return await addRelationship(
req,
res,
await UserModel.findOne(req.body as { discriminator: string; username: string }).exec(),
req.body.type
);
});
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;
+10
View File
@@ -0,0 +1,10 @@
import { Router, Response, Request } from "express";
const router = Router();
router.patch("/", (req: Request, res: Response) => {
// TODO:
res.sendStatus(204);
});
export default router;