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);
|