diff --git a/api/src/middlewares/RateLimit.ts b/api/src/middlewares/RateLimit.ts
index ca6de98f..1a38cfcf 100644
--- a/api/src/middlewares/RateLimit.ts
+++ b/api/src/middlewares/RateLimit.ts
@@ -1,4 +1,4 @@
-import { Config, getRights, listenEvent, Rights } from "@fosscord/util";
+import { Config, listenEvent } from "@fosscord/util";
import { NextFunction, Request, Response, Router } from "express";
import { getIpAdress } from "@fosscord/api";
import { API_PREFIX_TRAILING_SLASH } from "./Authentication";
@@ -9,7 +9,6 @@ import { API_PREFIX_TRAILING_SLASH } from "./Authentication";
/*
? bucket limit? Max actions/sec per bucket?
-(ANSWER: a small fosscord instance might not need a complex rate limiting system)
TODO: delay database requests to include multiple queries
TODO: different for methods (GET/POST)
@@ -45,25 +44,21 @@ export default function rateLimit(opts: {
onlyIp?: boolean;
}): any {
return async (req: Request, res: Response, next: NextFunction): Promise<any> => {
- // exempt user? if so, immediately short circuit
- const rights = await getRights(req.user_id);
- if (rights.has("BYPASS_RATE_LIMITS")) return;
-
const bucket_id = opts.bucket || req.originalUrl.replace(API_PREFIX_TRAILING_SLASH, "");
var executor_id = getIpAdress(req);
- if (!opts.onlyIp && req.user_id) executor_id = req.user_id;
+ if (!opts.onlyIp && req.user_id) executor_id = req.user_id;
var max_hits = opts.count;
if (opts.bot && req.user_bot) max_hits = opts.bot;
if (opts.GET && ["GET", "OPTIONS", "HEAD"].includes(req.method)) max_hits = opts.GET;
else if (opts.MODIFY && ["POST", "DELETE", "PATCH", "PUT"].includes(req.method)) max_hits = opts.MODIFY;
- let offender = Cache.get(executor_id + bucket_id);
+ const offender = Cache.get(executor_id + bucket_id);
if (offender) {
- let reset = offender.expires_at.getTime();
- let resetAfterMs = reset - Date.now();
- let resetAfterSec = Math.ceil(resetAfterMs / 1000);
+ const reset = offender.expires_at.getTime();
+ const resetAfterMs = reset - Date.now();
+ const resetAfterSec = resetAfterMs / 1000;
if (resetAfterMs <= 0) {
offender.hits = 0;
@@ -75,11 +70,6 @@ export default function rateLimit(opts: {
if (offender.blocked) {
const global = bucket_id === "global";
- // each block violation pushes the expiry one full window further
- reset += opts.window * 1000;
- offender.expires_at = new Date(offender.expires_at.getTime() + opts.window * 1000);
- resetAfterMs = reset - Date.now();
- resetAfterSec = Math.ceil(resetAfterMs / 1000);
console.log("blocked bucket: " + bucket_id, { resetAfterMs });
return (
diff --git a/api/src/routes/auth/register.ts b/api/src/routes/auth/register.ts
index 94dd6502..126f3dbc 100644
--- a/api/src/routes/auth/register.ts
+++ b/api/src/routes/auth/register.ts
@@ -31,6 +31,8 @@ export interface RegisterSchema {
date_of_birth?: Date; // "2000-04-03"
gift_code_sku_id?: string;
captcha_key?: string;
+
+ promotional_email_opt_in?: boolean;
}
router.post("/", route({ body: "RegisterSchema" }), async (req: Request, res: Response) => {
diff --git a/api/src/routes/channels/#channel_id/messages/index.ts b/api/src/routes/channels/#channel_id/messages/index.ts
index 2d6a2977..54e6edcc 100644
--- a/api/src/routes/channels/#channel_id/messages/index.ts
+++ b/api/src/routes/channels/#channel_id/messages/index.ts
@@ -50,8 +50,10 @@ export function isTextChannel(type: ChannelType): boolean {
}
export interface MessageCreateSchema {
+ type?: number;
content?: string;
nonce?: string;
+ channel_id?: string;
tts?: boolean;
flags?: string;
embeds?: Embed[];
@@ -161,7 +163,7 @@ const messageUpload = multer({
limits: {
fileSize: 1024 * 1024 * 100,
fields: 10,
- files: 1
+ // files: 1
},
storage: multer.memoryStorage()
}); // max upload 50 mb
@@ -176,7 +178,7 @@ const messageUpload = multer({
// Send message
router.post(
"/",
- messageUpload.single("file"),
+ messageUpload.any(),
async (req, res, next) => {
if (req.body.payload_json) {
req.body = JSON.parse(req.body.payload_json);
@@ -190,18 +192,21 @@ router.post(
var body = req.body as MessageCreateSchema;
const attachments: Attachment[] = [];
- if (req.file) {
+ const channel = await Channel.findOneOrFail({ where: { id: channel_id }, relations: ["recipients", "recipients.user"] });
+ if (!channel.isWritable()) {
+ throw new HTTPError(`Cannot send messages to channel of type ${channel.type}`, 400)
+ }
+
+ const files = req.files as Express.Multer.File[] ?? [];
+ for (var currFile of files) {
try {
- const file = await uploadFile(`/attachments/${req.params.channel_id}`, req.file);
+ const file = await uploadFile(`/attachments/${channel.id}`, currFile);
attachments.push({ ...file, proxy_url: file.url });
- } catch (error) {
+ }
+ catch (error) {
return res.status(400).json(error);
}
}
- const channel = await Channel.findOneOrFail({ where: { id: channel_id }, relations: ["recipients", "recipients.user"] });
- if (!channel.isWritable()) {
- throw new HTTPError(`Cannot send messages to channel of type ${channel.type}`, 400)
- }
const embeds = body.embeds || [];
if (body.embed) embeds.push(body.embed);
diff --git a/api/src/routes/guilds/#guild_id/index.ts b/api/src/routes/guilds/#guild_id/index.ts
index 4ec3df72..45e30a74 100644
--- a/api/src/routes/guilds/#guild_id/index.ts
+++ b/api/src/routes/guilds/#guild_id/index.ts
@@ -20,6 +20,7 @@ export interface GuildUpdateSchema extends Omit<GuildCreateSchema, "channels"> {
afk_timeout?: number;
afk_channel_id?: string;
preferred_locale?: string;
+ premium_progress_bar_enabled?: boolean;
}
router.get("/", route({}), async (req: Request, res: Response) => {
diff --git a/api/src/routes/guilds/#guild_id/member-verification.ts b/api/src/routes/guilds/#guild_id/member-verification.ts
new file mode 100644
index 00000000..265a1b35
--- /dev/null
+++ b/api/src/routes/guilds/#guild_id/member-verification.ts
@@ -0,0 +1,14 @@
+import { Router, Request, Response } from "express";
+import { route } from "@fosscord/api";
+const router = Router();
+
+router.get("/",route({}), async (req: Request, res: Response) => {
+ // TODO: member verification
+
+ res.status(404).json({
+ message: "Unknown Guild Member Verification Form",
+ code: 10068
+ });
+});
+
+export default router;
diff --git a/api/src/routes/guilds/#guild_id/members/#member_id/index.ts b/api/src/routes/guilds/#guild_id/members/#member_id/index.ts
index 34836292..2ff89eae 100644
--- a/api/src/routes/guilds/#guild_id/members/#member_id/index.ts
+++ b/api/src/routes/guilds/#guild_id/members/#member_id/index.ts
@@ -1,5 +1,5 @@
import { Request, Response, Router } from "express";
-import { Member, getPermission, Role, GuildMemberUpdateEvent, emitEvent, Sticker, Emoji, Guild } from "@fosscord/util";
+import { Member, getPermission, getRights, Role, GuildMemberUpdateEvent, emitEvent, Sticker, Emoji, Rights, Guild } from "@fosscord/util";
import { HTTPError } from "lambert-server";
import { route } from "@fosscord/api";
@@ -7,6 +7,7 @@ const router = Router();
export interface MemberChangeSchema {
roles?: string[];
+ nick?: string;
}
router.get("/", route({}), async (req: Request, res: Response) => {
@@ -34,6 +35,8 @@ router.patch("/", route({ body: "MemberChangeSchema" }), async (req: Request, re
member.roles = body.roles.map((x) => new Role({ id: x })); // foreign key constraint will fail if role doesn't exist
}
+ if (body.nick) member.nick = body.nick;
+
await member.save();
member.roles = member.roles.filter((x) => x.id !== everyone.id);
@@ -52,27 +55,47 @@ router.put("/", route({}), async (req: Request, res: Response) => {
// TODO: Lurker mode
+ const rights = await getRights(req.user_id);
+
let { guild_id, member_id } = req.params;
- if (member_id === "@me") member_id = req.user_id;
+ if (member_id === "@me") {
+ member_id = req.user_id;
+ rights.hasThrow("JOIN_GUILDS");
+ } else {
+ // TODO: join others by controller
+ }
var guild = await Guild.findOneOrFail({
- where: { id: guild_id } });
+ where: { id: guild_id }
+ });
var emoji = await Emoji.find({
- where: { guild_id: guild_id } });
+ where: { guild_id: guild_id }
+ });
var roles = await Role.find({
- where: { guild_id: guild_id } });
+ where: { guild_id: guild_id }
+ });
var stickers = await Sticker.find({
- where: { guild_id: guild_id } });
-
+ where: { guild_id: guild_id }
+ });
+
await Member.addToGuild(member_id, guild_id);
- res.send({...guild, emojis: emoji, roles: roles, stickers: stickers});
+ res.send({ ...guild, emojis: emoji, roles: roles, stickers: stickers });
});
-router.delete("/", route({ permission: "KICK_MEMBERS" }), async (req: Request, res: Response) => {
+router.delete("/", route({}), async (req: Request, res: Response) => {
+ const permission = await getPermission(req.user_id);
+ const rights = await getRights(req.user_id);
const { guild_id, member_id } = req.params;
+ if (member_id !== "@me" || member_id === req.user_id) {
+ // TODO: unless force-joined
+ rights.hasThrow("SELF_LEAVE_GROUPS");
+ } else {
+ rights.hasThrow("KICK_BAN_MEMBERS");
+ permission.hasThrow("KICK_MEMBERS");
+ }
await Member.removeFromGuild(member_id, guild_id);
res.sendStatus(204);
diff --git a/api/src/routes/guilds/#guild_id/roles/#role_id/index.ts b/api/src/routes/guilds/#guild_id/roles/#role_id/index.ts
new file mode 100644
index 00000000..16b5a59f
--- /dev/null
+++ b/api/src/routes/guilds/#guild_id/roles/#role_id/index.ts
@@ -0,0 +1,69 @@
+import { Router, Request, Response } from "express";
+import { Role, Member, GuildRoleUpdateEvent, GuildRoleDeleteEvent, emitEvent, handleFile } from "@fosscord/util";
+import { route } from "@fosscord/api";
+import { HTTPError } from "lambert-server";
+import { RoleModifySchema } from "../";
+
+const router = Router();
+
+router.get("/", route({}), async (req: Request, res: Response) => {
+ const { guild_id, role_id } = req.params;
+ await Member.IsInGuildOrFail(req.user_id, guild_id);
+ const role = await Role.findOneOrFail({ guild_id, id: role_id });
+ return res.json(role);
+});
+
+router.delete("/", route({ permission: "MANAGE_ROLES" }), async (req: Request, res: Response) => {
+ const { guild_id, role_id } = req.params;
+ if (role_id === guild_id) throw new HTTPError("You can't delete the @everyone role");
+
+ await Promise.all([
+ Role.delete({
+ id: role_id,
+ guild_id: guild_id
+ }),
+ emitEvent({
+ event: "GUILD_ROLE_DELETE",
+ guild_id,
+ data: {
+ guild_id,
+ role_id
+ }
+ } as GuildRoleDeleteEvent)
+ ]);
+
+ res.sendStatus(204);
+});
+
+// TODO: check role hierarchy
+
+router.patch("/", route({ body: "RoleModifySchema", permission: "MANAGE_ROLES" }), async (req: Request, res: Response) => {
+ const { role_id, guild_id } = req.params;
+ const body = req.body as RoleModifySchema;
+
+ if (body.icon && body.icon.length) body.icon = await handleFile(`/role-icons/${role_id}`, body.icon as string);
+ else body.icon = undefined;
+
+ const role = new Role({
+ ...body,
+ id: role_id,
+ guild_id,
+ permissions: String(req.permission!.bitfield & BigInt(body.permissions || "0"))
+ });
+
+ await Promise.all([
+ role.save(),
+ emitEvent({
+ event: "GUILD_ROLE_UPDATE",
+ guild_id,
+ data: {
+ guild_id,
+ role
+ }
+ } as GuildRoleUpdateEvent)
+ ]);
+
+ res.json(role);
+});
+
+export default router;
diff --git a/api/src/routes/guilds/#guild_id/roles.ts b/api/src/routes/guilds/#guild_id/roles/index.ts
index b6894e3f..53465105 100644
--- a/api/src/routes/guilds/#guild_id/roles.ts
+++ b/api/src/routes/guilds/#guild_id/roles/index.ts
@@ -81,59 +81,6 @@ router.post("/", route({ body: "RoleModifySchema", permission: "MANAGE_ROLES" })
res.json(role);
});
-router.delete("/:role_id", route({ permission: "MANAGE_ROLES" }), async (req: Request, res: Response) => {
- const guild_id = req.params.guild_id;
- const { role_id } = req.params;
- if (role_id === guild_id) throw new HTTPError("You can't delete the @everyone role");
-
- await Promise.all([
- Role.delete({
- id: role_id,
- guild_id: guild_id
- }),
- 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", route({ body: "RoleModifySchema", permission: "MANAGE_ROLES" }), async (req: Request, res: Response) => {
- const { role_id, guild_id } = req.params;
- const body = req.body as RoleModifySchema;
-
- if (body.icon) body.icon = await handleFile(`/role-icons/${role_id}`, body.icon as string);
-
- const role = new Role({
- ...body,
- id: role_id,
- guild_id,
- permissions: String(req.permission!.bitfield & BigInt(body.permissions || "0"))
- });
-
- await Promise.all([
- role.save(),
- emitEvent({
- event: "GUILD_ROLE_UPDATE",
- guild_id,
- data: {
- guild_id,
- role
- }
- } as GuildRoleUpdateEvent)
- ]);
-
- res.json(role);
-});
-
router.patch("/", route({ body: "RolePositionUpdateSchema" }), async (req: Request, res: Response) => {
const { guild_id } = req.params;
const body = req.body as RolePositionUpdateSchema;
diff --git a/api/src/routes/guilds/#guild_id/welcome_screen.ts b/api/src/routes/guilds/#guild_id/welcome-screen.ts
index 7141f17e..5c7a9daa 100644
--- a/api/src/routes/guilds/#guild_id/welcome_screen.ts
+++ b/api/src/routes/guilds/#guild_id/welcome-screen.ts
@@ -10,7 +10,7 @@ export interface GuildUpdateWelcomeScreenSchema {
channel_id: string;
description: string;
emoji_id?: string;
- emoji_name: string;
+ emoji_name?: string;
}[];
enabled?: boolean;
description?: string;
@@ -36,6 +36,8 @@ router.patch("/", route({ body: "GuildUpdateWelcomeScreenSchema", permission: "M
if (body.description) guild.welcome_screen.description = body.description;
if (body.enabled != null) guild.welcome_screen.enabled = body.enabled;
+ await guild.save();
+
res.sendStatus(204);
});
diff --git a/api/src/routes/guilds/index.ts b/api/src/routes/guilds/index.ts
index 10721413..489dea49 100644
--- a/api/src/routes/guilds/index.ts
+++ b/api/src/routes/guilds/index.ts
@@ -9,7 +9,7 @@ export interface GuildCreateSchema {
/**
* @maxLength 100
*/
- name: string;
+ name?: string;
region?: string;
icon?: string | null;
channels?: ChannelModifySchema[];
diff --git a/api/src/routes/invites/index.ts b/api/src/routes/invites/index.ts
index 21da2d18..eeafb22a 100644
--- a/api/src/routes/invites/index.ts
+++ b/api/src/routes/invites/index.ts
@@ -13,7 +13,7 @@ router.get("/:code", route({}), async (req: Request, res: Response) => {
res.status(200).send(invite);
});
-router.post("/:code", route({right: "JOIN_GUILDS"}), async (req: Request, res: Response) => {
+router.post("/:code", route({right: "USE_MASS_INVITES"}), async (req: Request, res: Response) => {
const { code } = req.params;
const { guild_id } = await Invite.findOneOrFail({ code })
const { features } = await Guild.findOneOrFail({ id: guild_id});
diff --git a/api/src/routes/ping.ts b/api/src/routes/ping.ts
index 5cdea705..3c1da2c3 100644
--- a/api/src/routes/ping.ts
+++ b/api/src/routes/ping.ts
@@ -1,10 +1,26 @@
import { Router, Response, Request } from "express";
import { route } from "@fosscord/api";
+import { Config } from "@fosscord/util";
const router = Router();
router.get("/", route({}), (req: Request, res: Response) => {
- res.send("pong");
+ const { general } = Config.get();
+ res.send({
+ ping: "pong!",
+ instance: {
+ id: general.instanceId,
+ name: general.instanceName,
+ description: general.instanceDescription,
+ image: general.image,
+
+ correspondenceEmail: general.correspondenceEmail,
+ correspondenceUserID: general.correspondenceUserID,
+
+ frontPage: general.frontPage,
+ tosPage: general.tosPage,
+ },
+ });
});
export default router;
diff --git a/api/src/routes/store/published-listings/skus/#sku_id/subscription-plans.ts b/api/src/routes/store/published-listings/skus/#sku_id/subscription-plans.ts
index 723a5160..03162ec8 100644
--- a/api/src/routes/store/published-listings/skus/#sku_id/subscription-plans.ts
+++ b/api/src/routes/store/published-listings/skus/#sku_id/subscription-plans.ts
@@ -5,6 +5,22 @@ const router: Router = Router();
const skus = new Map([
[
+ "978380684370378762",
+ [
+ {
+ id: "978380692553465866",
+ name: "Nitro Lite Monthly",
+ interval: 1,
+ interval_count: 1,
+ tag_inclusive: true,
+ sku_id: "978380684370378762",
+ currency: "usd",
+ price: 0,
+ price_tier: null,
+ }
+ ]
+ ],
+ [
"521842865731534868",
[
{
diff --git a/api/src/routes/users/@me/index.ts b/api/src/routes/users/@me/index.ts
index 1af413c4..dc0d1cb1 100644
--- a/api/src/routes/users/@me/index.ts
+++ b/api/src/routes/users/@me/index.ts
@@ -1,7 +1,8 @@
import { Router, Request, Response } from "express";
-import { User, PrivateUserProjection, emitEvent, UserUpdateEvent, handleFile, FieldErrors } from "@fosscord/util";
+import { User, PrivateUserProjection, emitEvent, UserUpdateEvent, handleFile, FieldErrors, adjustEmail, Config } from "@fosscord/util";
import { route } from "@fosscord/api";
import bcrypt from "bcrypt";
+import { HTTPError } from "lambert-server";
const router: Router = Router();
@@ -21,6 +22,8 @@ export interface UserModifySchema {
password?: string;
new_password?: string;
code?: string;
+ email?: string;
+ discriminator?: string;
}
router.get("/", route({}), async (req: Request, res: Response) => {
@@ -30,11 +33,13 @@ router.get("/", route({}), async (req: Request, res: Response) => {
router.patch("/", route({ body: "UserModifySchema" }), async (req: Request, res: Response) => {
const body = req.body as UserModifySchema;
+ const user = await User.findOneOrFail({ where: { id: req.user_id }, select: [...PrivateUserProjection, "data"] });
+
+ if (user.email == "demo@maddy.k.vu") throw new HTTPError("Demo user, sorry", 400);
+
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 User.findOneOrFail({ where: { id: req.user_id }, select: [...PrivateUserProjection, "data"] });
-
if (body.password) {
if (user.data?.hash) {
const same_password = await bcrypt.compare(body.password, user.data.hash || "");
@@ -46,6 +51,14 @@ router.patch("/", route({ body: "UserModifySchema" }), async (req: Request, res:
}
}
+ if (body.email) {
+ body.email = adjustEmail(body.email);
+ if (!body.email && Config.get().register.email.required)
+ throw FieldErrors({ email: { message: req.t("auth:register.EMAIL_INVALID"), code: "EMAIL_INVALID" } });
+ if (!body.password)
+ throw FieldErrors({ password: { message: req.t("auth:register.INVALID_PASSWORD"), code: "INVALID_PASSWORD" } });
+ }
+
if (body.new_password) {
if (!body.password && !user.email) {
throw FieldErrors({
@@ -55,14 +68,14 @@ router.patch("/", route({ body: "UserModifySchema" }), async (req: Request, res:
user.data.hash = await bcrypt.hash(body.new_password, 12);
}
- if(body.username){
- var check_username = body?.username?.replace(/\s/g, '');
- if(!check_username) {
- throw FieldErrors({
- username: { code: "BASE_TYPE_REQUIRED", message: req.t("common:field.BASE_TYPE_REQUIRED") }
- });
- }
- }
+ if (body.username) {
+ var check_username = body?.username?.replace(/\s/g, '');
+ if (!check_username) {
+ throw FieldErrors({
+ username: { code: "BASE_TYPE_REQUIRED", message: req.t("common:field.BASE_TYPE_REQUIRED") }
+ });
+ }
+ }
user.assign(body);
await user.save();
diff --git a/api/src/util/handlers/Message.ts b/api/src/util/handlers/Message.ts
index e9f0ac55..48f87dfe 100644
--- a/api/src/util/handlers/Message.ts
+++ b/api/src/util/handlers/Message.ts
@@ -38,7 +38,7 @@ const DEFAULT_FETCH_OPTIONS: any = {
headers: {
"user-agent": "Mozilla/5.0 (compatible; Fosscord/1.0; +https://github.com/fosscord/fosscord)"
},
- size: 1024 * 1024 * 1,
+ // size: 1024 * 1024 * 5, // grabbed from config later
compress: true,
method: "GET"
};
@@ -154,7 +154,10 @@ export async function postHandleMessage(message: Message) {
for (const link of links) {
try {
- const request = await fetch(link, DEFAULT_FETCH_OPTIONS);
+ const request = await fetch(link, {
+ ...DEFAULT_FETCH_OPTIONS,
+ size: Config.get().limits.message.maxEmbedDownloadSize,
+ });
const text = await request.text();
const $ = cheerio.load(text);
@@ -191,7 +194,7 @@ export async function postHandleMessage(message: Message) {
channel_id: message.channel_id,
data
} as MessageUpdateEvent),
- Message.update({ id: message.id, channel_id: message.channel_id }, data)
+ Message.update({ id: message.id, channel_id: message.channel_id }, { embeds: data.embeds })
]);
}
|