diff --git a/api/src/middlewares/RateLimit.ts b/api/src/middlewares/RateLimit.ts
index 13f1602c..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,12 +44,6 @@ 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
- if (req.user_id) {
- 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;
@@ -60,12 +53,12 @@ export default function rateLimit(opts: {
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;
@@ -77,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 (
@@ -163,7 +151,7 @@ export async function initRateLimits(app: Router) {
app.use("/auth/register", rateLimit({ onlyIp: true, success: true, ...routes.auth.register }));
}
-async function hitRoute(opts: { executor_id: string; bucket_id: string; max_hits: number; window: number; }) {
+async function hitRoute(opts: { executor_id: string; bucket_id: string; max_hits: number; window: number }) {
const id = opts.executor_id + opts.bucket_id;
var limit = Cache.get(id);
if (!limit) {
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/#message_id/ack.ts b/api/src/routes/channels/#channel_id/messages/#message_id/ack.ts
index 885c5eca..1e3564d8 100644
--- a/api/src/routes/channels/#channel_id/messages/#message_id/ack.ts
+++ b/api/src/routes/channels/#channel_id/messages/#message_id/ack.ts
@@ -35,7 +35,7 @@ router.post("/", route({ body: "MessageAcknowledgeSchema" }), async (req: Reques
}
} as MessageAckEvent);
- res.sendStatus(204);
+ res.json({ token: null });
});
export default router;
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 c285abb3..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
@@ -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);
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
index 2ad01682..16b5a59f 100644
--- a/api/src/routes/guilds/#guild_id/roles/#role_id/index.ts
+++ b/api/src/routes/guilds/#guild_id/roles/#role_id/index.ts
@@ -41,7 +41,8 @@ router.patch("/", route({ body: "RoleModifySchema", permission: "MANAGE_ROLES" }
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);
+ 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,
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/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/#id/profile.ts b/api/src/routes/users/#id/profile.ts
index 4dbb84cf..a77fbdb5 100644
--- a/api/src/routes/users/#id/profile.ts
+++ b/api/src/routes/users/#id/profile.ts
@@ -1,5 +1,5 @@
import { Router, Request, Response } from "express";
-import { PublicConnectedAccount, PublicUser, User, UserPublic, Member } from "@fosscord/util";
+import { PublicConnectedAccount, PublicUser, User, UserPublic, Member, Guild } from "@fosscord/util";
import { route } from "@fosscord/api";
const router: Router = Router();
@@ -13,45 +13,78 @@ export interface UserProfileResponse {
router.get("/", route({ test: { response: { body: "UserProfileResponse" } } }), async (req: Request, res: Response) => {
if (req.params.id === "@me") req.params.id = req.user_id;
+
+ const { guild_id, with_mutual_guilds } = req.query;
+
const user = await User.getPublicUser(req.params.id, { relations: ["connected_accounts"] });
var mutual_guilds: object[] = [];
var premium_guild_since;
- const requested_member = await Member.find( { id: req.params.id, })
- const self_member = await Member.find( { id: req.user_id, })
- for(const rmem of requested_member) {
- if(rmem.premium_since) {
- if(premium_guild_since){
- if(premium_guild_since > rmem.premium_since) {
+ if (with_mutual_guilds == "true") {
+ const requested_member = await Member.find({ id: req.params.id, });
+ const self_member = await Member.find({ id: req.user_id, });
+
+ for (const rmem of requested_member) {
+ if (rmem.premium_since) {
+ if (premium_guild_since) {
+ if (premium_guild_since > rmem.premium_since) {
+ premium_guild_since = rmem.premium_since;
+ }
+ } else {
premium_guild_since = rmem.premium_since;
}
- } else {
- premium_guild_since = rmem.premium_since;
}
- }
- for(const smem of self_member) {
- if (smem.guild_id === rmem.guild_id) {
- mutual_guilds.push({id: rmem.guild_id, nick: rmem.nick})
+ for (const smem of self_member) {
+ if (smem.guild_id === rmem.guild_id) {
+ mutual_guilds.push({ id: rmem.guild_id, nick: rmem.nick });
+ }
}
}
}
+
+ const guild_member = guild_id && typeof guild_id == "string"
+ ? await Member.findOneOrFail({ id: req.params.id, guild_id: guild_id }, { relations: ["roles"] })
+ : undefined;
+
+ // TODO: make proper DTO's in util?
+
+ const userDto = {
+ 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
+ };
+
+ const guildMemberDto = guild_member ? {
+ avatar: user.avatar, // TODO
+ banner: user.banner, // TODO
+ bio: req.user_bot ? null : user.bio, // TODO
+ communication_disabled_until: null, // TODO
+ deaf: guild_member.deaf,
+ flags: user.flags,
+ is_pending: guild_member.pending,
+ pending: guild_member.pending, // why is this here twice, discord?
+ joined_at: guild_member.joined_at,
+ mute: guild_member.mute,
+ nick: guild_member.nick,
+ premium_since: guild_member.premium_since,
+ roles: guild_member.roles.map(x => x.id).filter(id => id != guild_id),
+ user: userDto
+ } : undefined;
+
res.json({
connected_accounts: user.connected_accounts,
premium_guild_since: premium_guild_since, // TODO
premium_since: user.premium_since, // TODO
mutual_guilds: mutual_guilds, // TODO {id: "", nick: null} when ?with_mutual_guilds=true
- 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
- }
+ user: userDto,
+ guild_member: guildMemberDto,
});
});
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();
|