diff --git a/api/assets/openapi.json b/api/assets/openapi.json index 15e55bf9..1af0600d 100644 --- a/api/assets/openapi.json +++ b/api/assets/openapi.json @@ -2821,6 +2821,10 @@ "type": "string", "format": "date-time" }, + "premium_since": { + "type": "string", + "format": "date-time" + }, "verified": { "type": "boolean" }, @@ -3800,7 +3804,8 @@ "format": "date-time" }, "premium_since": { - "type": "integer" + "type": "string", + "format": "date-time" }, "deaf": { "type": "boolean" diff --git a/api/package-lock.json b/api/package-lock.json index a7c29a79..e3e29800 100644 Binary files a/api/package-lock.json and b/api/package-lock.json differ diff --git a/api/src/routes/guilds/#guild_id/bans.ts b/api/src/routes/guilds/#guild_id/bans.ts index 1e09a38d..7ccf34d7 100644 --- a/api/src/routes/guilds/#guild_id/bans.ts +++ b/api/src/routes/guilds/#guild_id/bans.ts @@ -1,5 +1,5 @@ import { Request, Response, Router } from "express"; -import { emitEvent, getPermission, GuildBanAddEvent, GuildBanRemoveEvent, Guild, Ban, User, Member } from "@fosscord/util"; +import { DiscordApiErrors, emitEvent, getPermission, GuildBanAddEvent, GuildBanRemoveEvent, Guild, Ban, User, Member } from "@fosscord/util"; import { HTTPError } from "lambert-server"; import { getIpAdress, route } from "@fosscord/api"; @@ -17,6 +17,14 @@ export interface BanRegistrySchema { reason?: string | undefined; }; +export interface BanModeratorSchema { + id: string; + user_id: string; + guild_id: string; + executor_id: string; + reason?: string | undefined; +}; + const router: Router = Router(); /* TODO: Deleting the secrets is just a temporary go-around. Views should be implemented for both safety and better handling. */ @@ -27,11 +35,14 @@ router.get("/", route({ permission: "BAN_MEMBERS" }), async (req: Request, res: let bans = await Ban.find({ guild_id: guild_id }); /* Filter secret from database registry.*/ + + bans.filter(ban => ban.user_id !== ban.executor_id); + // pretend self-bans don't exist to prevent victim chasing bans.forEach((registry: BanRegistrySchema) => { delete registry.ip; }); - + return res.json(bans); }); @@ -41,7 +52,12 @@ router.get("/:user", route({ permission: "BAN_MEMBERS" }), async (req: Request, let ban = await Ban.findOneOrFail({ guild_id: guild_id, user_id: user_id }) as BanRegistrySchema; + if (ban.user_id === ban.executor_id) throw DiscordApiErrors.UNKNOWN_BAN; + // pretend self-bans don't exist to prevent victim chasing + /* Filter secret from registry. */ + + ban = ban as BanModeratorSchema; delete ban.ip @@ -52,10 +68,12 @@ router.put("/:user_id", route({ body: "BanCreateSchema", permission: "BAN_MEMBER const { guild_id } = req.params; const banned_user_id = req.params.user_id; - const banned_user = await User.getPublicUser(banned_user_id); - - if (req.user_id === banned_user_id) throw new HTTPError("You can't ban yourself", 400); + if ( (req.user_id === banned_user_id) && (banned_user_id === req.permission!.cache.guild?.owner_id)) + throw new HTTPError("You are the guild owner, hence can't ban yourself", 403); + if (req.permission!.cache.guild?.owner_id === banned_user_id) throw new HTTPError("You can't ban the owner", 400); + + const banned_user = await User.getPublicUser(banned_user_id); const ban = new Ban({ user_id: banned_user_id, @@ -81,11 +99,48 @@ router.put("/:user_id", route({ body: "BanCreateSchema", permission: "BAN_MEMBER return res.json(ban); }); +router.put("/@me", route({ body: "BanCreateSchema"}), async (req: Request, res: Response) => { + const { guild_id } = req.params; + + const banned_user = await User.getPublicUser(req.params.user_id); + + if (req.permission!.cache.guild?.owner_id === req.params.user_id) + throw new HTTPError("You are the guild owner, hence can't ban yourself", 403); + + const ban = new Ban({ + user_id: req.params.user_id, + guild_id: guild_id, + ip: getIpAdress(req), + executor_id: req.params.user_id, + reason: req.body.reason // || otherwise empty + }); + + await Promise.all([ + Member.removeFromGuild(req.user_id, guild_id), + ban.save(), + emitEvent({ + event: "GUILD_BAN_ADD", + data: { + guild_id: guild_id, + user: banned_user + }, + guild_id: guild_id + } as GuildBanAddEvent) + ]); + + return res.json(ban); +}); + router.delete("/:user_id", route({ permission: "BAN_MEMBERS" }), async (req: Request, res: Response) => { const { guild_id, user_id } = req.params; + let ban = await Ban.findOneOrFail({ guild_id: guild_id, user_id: user_id }); + + if (ban.user_id === ban.executor_id) throw DiscordApiErrors.UNKNOWN_BAN; + // make self-bans irreversible and hide them from view to avoid victim chasing + const banned_user = await User.getPublicUser(user_id); - + await Promise.all([ Ban.delete({ user_id: user_id, diff --git a/api/src/routes/users/#id/profile.ts b/api/src/routes/users/#id/profile.ts index 9481451d..4dbb84cf 100644 --- a/api/src/routes/users/#id/profile.ts +++ b/api/src/routes/users/#id/profile.ts @@ -16,22 +16,30 @@ router.get("/", route({ test: { response: { body: "UserProfileResponse" } } }), 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) { + 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}) } } } - res.json({ connected_accounts: user.connected_accounts, - premium_guild_since: null, // TODO - premium_since: null, // TODO + 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, diff --git a/api/src/routes/users/@me/index.ts b/api/src/routes/users/@me/index.ts index 93d2cb01..bf62e7fc 100644 --- a/api/src/routes/users/@me/index.ts +++ b/api/src/routes/users/@me/index.ts @@ -65,8 +65,8 @@ router.patch("/", route({ body: "UserModifySchema" }), async (req: Request, res: } var check_username = body?.username?.replace(/\s/g, ''); - //claiming an account does not provide username so check if username in body before throw - if (!check_username && body.username) { + + if(!check_username && !body?.avatar && !body?.banner) { throw FieldErrors({ username: { code: "BASE_TYPE_REQUIRED", message: req.t("common:field.BASE_TYPE_REQUIRED") } }); diff --git a/bundle/package-lock.json b/bundle/package-lock.json index 485f3bf4..a9129c24 100644 Binary files a/bundle/package-lock.json and b/bundle/package-lock.json differ diff --git a/bundle/package.json b/bundle/package.json index 8915665d..0b3fc817 100644 --- a/bundle/package.json +++ b/bundle/package.json @@ -105,6 +105,7 @@ "typeorm": "^0.2.37", "typescript": "^4.1.2", "typescript-json-schema": "^0.50.1", - "ws": "^7.4.2" + "ws": "^7.4.2", + "nan": "^2.15.0" } } diff --git a/gateway/package-lock.json b/gateway/package-lock.json index 878799d2..9b3841af 100644 Binary files a/gateway/package-lock.json and b/gateway/package-lock.json differ diff --git a/gateway/src/opcodes/Identify.ts b/gateway/src/opcodes/Identify.ts index f39ac808..904aa963 100644 --- a/gateway/src/opcodes/Identify.ts +++ b/gateway/src/opcodes/Identify.ts @@ -173,6 +173,7 @@ export async function onIdentify(this: WebSocket, data: Payload) { avatar: related_user.avatar, bot: related_user.bot, bio: related_user.bio, + premium_since: user.premium_since }; users.push(public_related_user); } @@ -225,6 +226,7 @@ export async function onIdentify(this: WebSocket, data: Payload) { accent_color: user.accent_color || 0, banner: user.banner, bio: user.bio, + premium_since: user.premium_since }; const d: ReadyEventData = { diff --git a/util/package-lock.json b/util/package-lock.json index c5e96742..82e90b36 100644 Binary files a/util/package-lock.json and b/util/package-lock.json differ diff --git a/util/src/entities/Member.ts b/util/src/entities/Member.ts index 0f7be2a7..3c5f9db0 100644 --- a/util/src/entities/Member.ts +++ b/util/src/entities/Member.ts @@ -86,7 +86,7 @@ export class Member extends BaseClassWithoutId { joined_at: Date; @Column({ nullable: true }) - premium_since?: number; + premium_since?: Date; @Column() deaf: boolean; @@ -245,7 +245,7 @@ export class Member extends BaseClassWithoutId { nick: undefined, roles: [guild_id], // @everyone role joined_at: new Date(), - premium_since: undefined, + premium_since: new Date(), deaf: false, mute: false, pending: false, diff --git a/util/src/entities/ReadState.ts b/util/src/entities/ReadState.ts index ebef89be..e6d73105 100644 --- a/util/src/entities/ReadState.ts +++ b/util/src/entities/ReadState.ts @@ -31,8 +31,17 @@ export class ReadState extends BaseClass { }) user: User; + // fully read marker @Column({ nullable: true }) - last_message_id: string; + last_message_id: string; + + // public read receipt + @Column({ nullable: true }) + public_ack: string; + + // notification cursor / private read receipt + @Column({ nullable: true }) + notifications_cursor: string; @Column({ nullable: true }) last_pin_timestamp?: Date; diff --git a/util/src/entities/User.ts b/util/src/entities/User.ts index f157ac39..1d18c838 100644 --- a/util/src/entities/User.ts +++ b/util/src/entities/User.ts @@ -16,6 +16,7 @@ export enum PublicUserEnum { banner, bio, bot, + premium_since, } export type PublicUserKeys = keyof typeof PublicUserEnum; @@ -110,6 +111,9 @@ export class User extends BaseClass { @Column() created_at: Date; // registration date + @Column({ nullable: true }) + premium_since: Date; // premium date + @Column({ select: false }) verified: boolean; // if the user is offically verified @@ -246,6 +250,7 @@ export class User extends BaseClass { id: Snowflake.generate(), bot: false, system: false, + premium_since: new Date(), desktop: false, mobile: false, premium: true,