summary refs log tree commit diff
path: root/src/util
diff options
context:
space:
mode:
Diffstat (limited to 'src/util')
-rw-r--r--src/util/config/types/SecurityConfiguration.ts2
-rw-r--r--src/util/dtos/ReadyGuildDTO.ts70
-rw-r--r--src/util/entities/Channel.ts18
-rw-r--r--src/util/entities/Guild.ts8
-rw-r--r--src/util/entities/Member.ts10
-rw-r--r--src/util/entities/Message.ts26
-rw-r--r--src/util/entities/Role.ts3
-rw-r--r--src/util/entities/User.ts11
-rw-r--r--src/util/interfaces/Event.ts37
-rw-r--r--src/util/schemas/MessageCreateSchema.ts2
-rw-r--r--src/util/schemas/RegisterSchema.ts4
-rw-r--r--src/util/schemas/UserProfileResponse.ts26
-rw-r--r--src/util/schemas/responses/TypedResponses.ts2
-rw-r--r--src/util/schemas/responses/UserProfileResponse.ts31
-rw-r--r--src/util/util/JSON.ts10
-rw-r--r--src/util/util/Token.ts130
16 files changed, 256 insertions, 134 deletions
diff --git a/src/util/config/types/SecurityConfiguration.ts b/src/util/config/types/SecurityConfiguration.ts

index 5e971cfe..35776642 100644 --- a/src/util/config/types/SecurityConfiguration.ts +++ b/src/util/config/types/SecurityConfiguration.ts
@@ -28,7 +28,7 @@ export class SecurityConfiguration { // header to get the real user ip address // X-Forwarded-For for nginx/reverse proxies // CF-Connecting-IP for cloudflare - forwadedFor: string | null = null; + forwardedFor: string | null = null; ipdataApiKey: string | null = "eca677b284b3bac29eb72f5e496aa9047f26543605efe99ff2ce35c9"; mfaBackupCodeCount: number = 10; diff --git a/src/util/dtos/ReadyGuildDTO.ts b/src/util/dtos/ReadyGuildDTO.ts
index b21afe74..905ede74 100644 --- a/src/util/dtos/ReadyGuildDTO.ts +++ b/src/util/dtos/ReadyGuildDTO.ts
@@ -18,13 +18,45 @@ import { Channel, + ChannelOverride, + ChannelType, Emoji, Guild, - PublicMember, + PublicUser, Role, Sticker, + UserGuildSettings, + PublicMember, } from "../entities"; +// TODO: this is not the best place for this type +export type ReadyUserGuildSettingsEntries = Omit< + UserGuildSettings, + "channel_overrides" +> & { + channel_overrides: (ChannelOverride & { channel_id: string })[]; +}; + +// TODO: probably should move somewhere else +export interface ReadyPrivateChannel { + id: string; + flags: number; + is_spam: boolean; + last_message_id?: string; + recipients: PublicUser[]; + type: ChannelType.DM | ChannelType.GROUP_DM; +} + +export type GuildOrUnavailable = + | { id: string; unavailable: boolean } + | (Guild & { joined_at?: Date; unavailable: undefined }); + +const guildIsAvailable = ( + guild: GuildOrUnavailable, +): guild is Guild & { joined_at: Date; unavailable: false } => { + return guild.unavailable != true; +}; + export interface IReadyGuildDTO { application_command_counts?: { 1: number; 2: number; 3: number }; // ???????????? channels: Channel[]; @@ -65,12 +97,21 @@ export interface IReadyGuildDTO { max_members: number | undefined; nsfw_level: number | undefined; hub_type?: unknown | null; // ???? + + home_header: null; // TODO + latest_onboarding_question_id: null; // TODO + safety_alerts_channel_id: null; // TODO + max_stage_video_channel_users: 50; // TODO + nsfw: boolean; + id: string; }; roles: Role[]; stage_instances: unknown[]; stickers: Sticker[]; threads: unknown[]; version: string; + guild_hashes: unknown; + unavailable: boolean; } export class ReadyGuildDTO implements IReadyGuildDTO { @@ -113,14 +154,30 @@ export class ReadyGuildDTO implements IReadyGuildDTO { max_members: number | undefined; nsfw_level: number | undefined; hub_type?: unknown | null; // ???? + + home_header: null; // TODO + latest_onboarding_question_id: null; // TODO + safety_alerts_channel_id: null; // TODO + max_stage_video_channel_users: 50; // TODO + nsfw: boolean; + id: string; }; roles: Role[]; stage_instances: unknown[]; stickers: Sticker[]; threads: unknown[]; version: string; + guild_hashes: unknown; + unavailable: boolean; + joined_at: Date; + + constructor(guild: GuildOrUnavailable) { + if (!guildIsAvailable(guild)) { + this.id = guild.id; + this.unavailable = true; + return; + } - constructor(guild: Guild) { this.application_command_counts = { 1: 5, 2: 2, @@ -164,12 +221,21 @@ export class ReadyGuildDTO implements IReadyGuildDTO { max_members: guild.max_members, nsfw_level: guild.nsfw_level, hub_type: null, + + home_header: null, + id: guild.id, + latest_onboarding_question_id: null, + max_stage_video_channel_users: 50, // TODO + nsfw: guild.nsfw, + safety_alerts_channel_id: null, }; this.roles = guild.roles; this.stage_instances = []; this.stickers = guild.stickers; this.threads = []; this.version = "1"; // ?????? + this.guild_hashes = {}; + this.joined_at = guild.joined_at; } toJSON() { diff --git a/src/util/entities/Channel.ts b/src/util/entities/Channel.ts
index e23d93db..38627c39 100644 --- a/src/util/entities/Channel.ts +++ b/src/util/entities/Channel.ts
@@ -468,6 +468,18 @@ export class Channel extends BaseClass { ]; return disallowedChannelTypes.indexOf(this.type) == -1; } + + toJSON() { + return { + ...this, + + // these fields are not returned depending on the type of channel + bitrate: this.bitrate || undefined, + user_limit: this.user_limit || undefined, + rate_limit_per_user: this.rate_limit_per_user || undefined, + owner_id: this.owner_id || undefined, + }; + } } export interface ChannelPermissionOverwrite { @@ -483,6 +495,12 @@ export enum ChannelPermissionOverwriteType { group = 2, } +export interface DMChannel extends Omit<Channel, "type" | "recipients"> { + type: ChannelType.DM | ChannelType.GROUP_DM; + recipients: Recipient[]; +} + +// TODO: probably more props export function isTextChannel(type: ChannelType): boolean { switch (type) { case ChannelType.GUILD_STORE: diff --git a/src/util/entities/Guild.ts b/src/util/entities/Guild.ts
index e2b3e1bd..e364ed98 100644 --- a/src/util/entities/Guild.ts +++ b/src/util/entities/Guild.ts
@@ -353,6 +353,7 @@ export class Guild extends BaseClass { position: 0, icon: undefined, unicode_emoji: undefined, + flags: 0, // TODO? }).save(); if (!body.channels || !body.channels.length) @@ -389,4 +390,11 @@ export class Guild extends BaseClass { return guild; } + + toJSON() { + return { + ...this, + unavailable: this.unavailable == false ? undefined : true, + }; + } } diff --git a/src/util/entities/Member.ts b/src/util/entities/Member.ts
index 8c208202..8be6eae1 100644 --- a/src/util/entities/Member.ts +++ b/src/util/entities/Member.ts
@@ -344,11 +344,7 @@ export class Member extends BaseClassWithoutId { relations: ["user", "roles"], take: 10, }) - ).map((member) => ({ - ...member.toPublicMember(), - user: member.user.toPublicUser(), - roles: member.roles.map((x) => x.id), - })); + ).map((member) => member.toPublicMember()); if ( await Member.count({ @@ -455,6 +451,10 @@ export class Member extends BaseClassWithoutId { PublicMemberProjection.forEach((x) => { member[x] = this[x]; }); + + if (member.roles) member.roles = member.roles.map((x: Role) => x.id); + if (member.user) member.user = member.user.toPublicUser(); + return member as PublicMember; } } diff --git a/src/util/entities/Message.ts b/src/util/entities/Message.ts
index 519c431e..e5390300 100644 --- a/src/util/entities/Message.ts +++ b/src/util/entities/Message.ts
@@ -193,7 +193,7 @@ export class Message extends BaseClass { }; @Column({ nullable: true }) - flags?: string; + flags?: number; @Column({ type: "simple-json", nullable: true }) message_reference?: { @@ -217,6 +217,30 @@ export class Message extends BaseClass { @Column({ type: "simple-json", nullable: true }) components?: MessageComponent[]; + + toJSON(): Message { + return { + ...this, + author_id: undefined, + member_id: undefined, + guild_id: undefined, + webhook_id: undefined, + application_id: undefined, + nonce: undefined, + + tts: this.tts ?? false, + guild: this.guild ?? undefined, + webhook: this.webhook ?? undefined, + interaction: this.interaction ?? undefined, + reactions: this.reactions ?? undefined, + sticker_items: this.sticker_items ?? undefined, + message_reference: this.message_reference ?? undefined, + author: this.author?.toPublicUser() ?? undefined, + activity: this.activity ?? undefined, + application: this.application ?? undefined, + components: this.components ?? undefined, + }; + } } export interface MessageComponent { diff --git a/src/util/entities/Role.ts b/src/util/entities/Role.ts
index 85877c12..3ae5efc1 100644 --- a/src/util/entities/Role.ts +++ b/src/util/entities/Role.ts
@@ -66,4 +66,7 @@ export class Role extends BaseClass { integration_id?: string; premium_subscriber?: boolean; }; + + @Column() + flags: number; } diff --git a/src/util/entities/User.ts b/src/util/entities/User.ts
index 3e72c3c9..3f1bda05 100644 --- a/src/util/entities/User.ts +++ b/src/util/entities/User.ts
@@ -175,7 +175,7 @@ export class User extends BaseClass { email?: string; // email of the user @Column() - flags: string = "0"; // UserFlags // TODO: generate + flags: number = 0; // UserFlags // TODO: generate @Column() public_flags: number = 0; @@ -281,6 +281,15 @@ export class User extends BaseClass { return user as PublicUser; } + toPrivateUser() { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const user: any = {}; + PrivateUserProjection.forEach((x) => { + user[x] = this[x]; + }); + return user as UserPrivate; + } + static async getPublicUser(user_id: string, opts?: FindOneOptions<User>) { return await User.findOneOrFail({ where: { id: user_id }, diff --git a/src/util/interfaces/Event.ts b/src/util/interfaces/Event.ts
index 76a5f8d0..deb54428 100644 --- a/src/util/interfaces/Event.ts +++ b/src/util/interfaces/Event.ts
@@ -28,7 +28,6 @@ import { Role, Emoji, PublicMember, - UserGuildSettings, Guild, Channel, PublicUser, @@ -40,6 +39,10 @@ import { UserSettings, IReadyGuildDTO, ReadState, + UserPrivate, + ReadyUserGuildSettingsEntries, + ReadyPrivateChannel, + GuildOrUnavailable, } from "@spacebar/util"; export interface Event { @@ -68,22 +71,10 @@ export interface PublicRelationship { export interface ReadyEventData { v: number; - user: PublicUser & { - mobile: boolean; - desktop: boolean; - email: string | undefined; - flags: string; - mfa_enabled: boolean; - nsfw_allowed: boolean; - phone: string | undefined; - premium: boolean; - premium_type: number; - verified: boolean; - bot: boolean; - }; - private_channels: Channel[]; // this will be empty for bots + user: UserPrivate; + private_channels: ReadyPrivateChannel[]; // this will be empty for bots session_id: string; // resuming - guilds: IReadyGuildDTO[]; + guilds: IReadyGuildDTO[] | GuildOrUnavailable[]; // depends on capability analytics_token?: string; connected_accounts?: ConnectedAccount[]; consents?: { @@ -115,7 +106,7 @@ export interface ReadyEventData { version: number; }; user_guild_settings?: { - entries: UserGuildSettings[]; + entries: ReadyUserGuildSettingsEntries[]; version: number; partial: boolean; }; @@ -127,6 +118,17 @@ export interface ReadyEventData { // probably all users who the user is in contact with users?: PublicUser[]; sessions: unknown[]; + api_code_version: number; + tutorial: number | null; + resume_gateway_url: string; + session_type: string; + auth_session_id_hash: string; + required_action?: + | "REQUIRE_VERIFIED_EMAIL" + | "REQUIRE_VERIFIED_PHONE" + | "REQUIRE_CAPTCHA" // TODO: allow these to be triggered + | "TOS_UPDATE_ACKNOWLEDGMENT" + | "AGREEMENTS"; } export interface ReadyEvent extends Event { @@ -581,6 +583,7 @@ export type EventData = export enum EVENTEnum { Ready = "READY", + ReadySupplemental = "READY_SUPPLEMENTAL", ChannelCreate = "CHANNEL_CREATE", ChannelUpdate = "CHANNEL_UPDATE", ChannelDelete = "CHANNEL_DELETE", diff --git a/src/util/schemas/MessageCreateSchema.ts b/src/util/schemas/MessageCreateSchema.ts
index 45cd735e..7e130751 100644 --- a/src/util/schemas/MessageCreateSchema.ts +++ b/src/util/schemas/MessageCreateSchema.ts
@@ -29,7 +29,7 @@ export interface MessageCreateSchema { nonce?: string; channel_id?: string; tts?: boolean; - flags?: string; + flags?: number; embeds?: Embed[]; embed?: Embed; // TODO: ^ embed is deprecated in favor of embeds (https://discord.com/developers/docs/resources/channel#message-object) diff --git a/src/util/schemas/RegisterSchema.ts b/src/util/schemas/RegisterSchema.ts
index f6c99b18..7b7de9c7 100644 --- a/src/util/schemas/RegisterSchema.ts +++ b/src/util/schemas/RegisterSchema.ts
@@ -42,4 +42,8 @@ export interface RegisterSchema { captcha_key?: string; promotional_email_opt_in?: boolean; + + // part of pomelo + unique_username_registration?: boolean; + global_name?: string; } diff --git a/src/util/schemas/UserProfileResponse.ts b/src/util/schemas/UserProfileResponse.ts deleted file mode 100644
index 10bbcdbf..00000000 --- a/src/util/schemas/UserProfileResponse.ts +++ /dev/null
@@ -1,26 +0,0 @@ -/* - Spacebar: A FOSS re-implementation and extension of the Discord.com backend. - Copyright (C) 2023 Spacebar and Spacebar Contributors - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see <https://www.gnu.org/licenses/>. -*/ - -import { PublicConnectedAccount, PublicUser } from ".."; - -export interface UserProfileResponse { - user: PublicUser; - connected_accounts: PublicConnectedAccount; - premium_guild_since?: Date; - premium_since?: Date; -} diff --git a/src/util/schemas/responses/TypedResponses.ts b/src/util/schemas/responses/TypedResponses.ts
index 099efba3..4349b93c 100644 --- a/src/util/schemas/responses/TypedResponses.ts +++ b/src/util/schemas/responses/TypedResponses.ts
@@ -11,6 +11,7 @@ import { Member, Message, PrivateUser, + PublicMember, PublicUser, Role, Sticker, @@ -68,6 +69,7 @@ export type APIChannelArray = Channel[]; export type APIEmojiArray = Emoji[]; export type APIMemberArray = Member[]; +export type APIPublicMember = PublicMember; export interface APIGuildWithJoinedAt extends Guild { joined_at: string; diff --git a/src/util/schemas/responses/UserProfileResponse.ts b/src/util/schemas/responses/UserProfileResponse.ts
index bd1f46dd..eba7cbcc 100644 --- a/src/util/schemas/responses/UserProfileResponse.ts +++ b/src/util/schemas/responses/UserProfileResponse.ts
@@ -1,8 +1,37 @@ -import { PublicConnectedAccount, PublicUser } from "../../entities"; +import { + Member, + PublicConnectedAccount, + PublicMember, + PublicUser, + User, +} from "@spacebar/util"; + +export type MutualGuild = { + id: string; + nick?: string; +}; + +export type PublicMemberProfile = Pick< + Member, + "banner" | "bio" | "guild_id" +> & { + accent_color: null; // TODO +}; + +export type UserProfile = Pick< + User, + "bio" | "accent_color" | "banner" | "pronouns" | "theme_colors" +>; export interface UserProfileResponse { user: PublicUser; connected_accounts: PublicConnectedAccount; premium_guild_since?: Date; premium_since?: Date; + mutual_guilds: MutualGuild[]; + premium_type: number; + profile_themes_experiment_bucket: number; + user_profile: UserProfile; + guild_member?: PublicMember; + guild_member_profile?: PublicMemberProfile; } diff --git a/src/util/util/JSON.ts b/src/util/util/JSON.ts
index 1c39b66e..c7dcf47e 100644 --- a/src/util/util/JSON.ts +++ b/src/util/util/JSON.ts
@@ -27,6 +27,16 @@ const JSONReplacer = function ( return (this[key] as Date).toISOString().replace("Z", "+00:00"); } + // erlpack encoding doesn't call json.stringify, + // so our toJSON functions don't get called. + // manually call it here + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore + if (this?.[key]?.toJSON) + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore + this[key] = this[key].toJSON(); + return value; }; diff --git a/src/util/util/Token.ts b/src/util/util/Token.ts
index 90310176..eec72522 100644 --- a/src/util/util/Token.ts +++ b/src/util/util/Token.ts
@@ -19,94 +19,66 @@ import jwt, { VerifyOptions } from "jsonwebtoken"; import { Config } from "./Config"; import { User } from "../entities"; +// TODO: dont use deprecated APIs lol +import { + FindOptionsRelationByString, + FindOptionsSelectByString, +} from "typeorm"; export const JWTOptions: VerifyOptions = { algorithms: ["HS256"] }; export type UserTokenData = { user: User; - decoded: { id: string; iat: number }; + decoded: { id: string; iat: number; email?: string }; }; -async function checkEmailToken( - decoded: jwt.JwtPayload, -): Promise<UserTokenData> { - // eslint-disable-next-line no-async-promise-executor - return new Promise(async (res, rej) => { - if (!decoded.iat) return rej("Invalid Token"); // will never happen, just for typings. - - const user = await User.findOne({ - where: { - email: decoded.email, - }, - select: [ - "email", - "id", - "verified", - "deleted", - "disabled", - "username", - "data", - ], - }); - - if (!user) return rej("Invalid Token"); - - if (new Date().getTime() > decoded.iat * 1000 + 86400 * 1000) - return rej("Invalid Token"); - - // Using as here because we assert `id` and `iat` are in decoded. - // TS just doesn't want to assume its there, though. - return res({ decoded, user } as UserTokenData); - }); -} - -export function checkToken( +export const checkToken = ( token: string, - jwtSecret: string, - isEmailVerification = false, -): Promise<UserTokenData> { - return new Promise((res, rej) => { - token = token.replace("Bot ", ""); - token = token.replace("Bearer ", ""); - /** - in spacebar, even with instances that have bot distinction; we won't enforce "Bot" prefix, - as we don't really have separate pathways for bots - **/ - - jwt.verify(token, jwtSecret, JWTOptions, async (err, decoded) => { - if (err || !decoded) return rej("Invalid Token"); - if ( - typeof decoded == "string" || - !("id" in decoded) || - !decoded.iat - ) - return rej("Invalid Token"); // will never happen, just for typings. - - if (isEmailVerification) return res(checkEmailToken(decoded)); - - const user = await User.findOne({ - where: { id: decoded.id }, - select: ["data", "bot", "disabled", "deleted", "rights"], - }); - - if (!user) return rej("Invalid Token"); - - // we need to round it to seconds as it saved as seconds in jwt iat and valid_tokens_since is stored in milliseconds - if ( - decoded.iat * 1000 < - new Date(user.data.valid_tokens_since).setSeconds(0, 0) - ) - return rej("Invalid Token"); - - if (user.disabled) return rej("User disabled"); - if (user.deleted) return rej("User not found"); - - // Using as here because we assert `id` and `iat` are in decoded. - // TS just doesn't want to assume its there, though. - return res({ decoded, user } as UserTokenData); - }); + opts?: { + select?: FindOptionsSelectByString<User>; + relations?: FindOptionsRelationByString; + }, +): Promise<UserTokenData> => + new Promise((resolve, reject) => { + jwt.verify( + token, + Config.get().security.jwtSecret, + JWTOptions, + async (err, out) => { + const decoded = out as UserTokenData["decoded"]; + if (err || !decoded) return reject("Invalid Token"); + + const user = await User.findOne({ + where: decoded.email + ? { email: decoded.email } + : { id: decoded.id }, + select: [ + ...(opts?.select || []), + "bot", + "disabled", + "deleted", + "rights", + "data", + ], + relations: opts?.relations, + }); + + if (!user) return reject("User not found"); + + // we need to round it to seconds as it saved as seconds in jwt iat and valid_tokens_since is stored in milliseconds + if ( + decoded.iat * 1000 < + new Date(user.data.valid_tokens_since).setSeconds(0, 0) + ) + return reject("Invalid Token"); + + if (user.disabled) return reject("User disabled"); + if (user.deleted) return reject("User not found"); + + return resolve({ decoded, user }); + }, + ); }); -} export async function generateToken(id: string, email?: string) { const iat = Math.floor(Date.now() / 1000);