Initial identify rewrite

This commit is contained in:
Madeline 2023-03-13 19:02:52 +11:00
parent bd5f750024
commit f228561f4c
No known key found for this signature in database
GPG Key ID: 1958E017C36F2E47
5 changed files with 387 additions and 216 deletions

View File

@ -16,7 +16,7 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { WebSocket, Payload } from "@fosscord/gateway"; import { WebSocket, Payload, setupListener } from "@fosscord/gateway";
import { import {
checkToken, checkToken,
Intents, Intents,
@ -26,7 +26,6 @@ import {
Session, Session,
EVENTEnum, EVENTEnum,
Config, Config,
PublicMember,
PublicUser, PublicUser,
PrivateUserProjection, PrivateUserProjection,
ReadState, ReadState,
@ -36,19 +35,19 @@ import {
PrivateSessionProjection, PrivateSessionProjection,
MemberPrivateProjection, MemberPrivateProjection,
PresenceUpdateEvent, PresenceUpdateEvent,
UserSettings,
IdentifySchema, IdentifySchema,
DefaultUserGuildSettings, DefaultUserGuildSettings,
UserGuildSettings,
ReadyGuildDTO, ReadyGuildDTO,
Guild, Guild,
UserTokenData, PublicUserProjection,
ReadyUserGuildSettingsEntries,
UserSettings,
Permissions,
DMChannel,
GuildOrUnavailable,
} from "@fosscord/util"; } from "@fosscord/util";
import { Send } from "../util/Send"; import { Send } from "../util/Send";
import { CLOSECODES, OPCODES } from "../util/Constants"; import { CLOSECODES, OPCODES } from "../util/Constants";
import { setupListener } from "../listener/listener";
// import experiments from "./experiments.json";
const experiments: unknown[] = [];
import { check } from "./instanceOf"; import { check } from "./instanceOf";
import { Recipient } from "@fosscord/util"; import { Recipient } from "@fosscord/util";
@ -56,49 +55,132 @@ import { Recipient } from "@fosscord/util";
// TODO: check privileged intents, if defined in the config // TODO: check privileged intents, if defined in the config
// TODO: check if already identified // TODO: check if already identified
// TODO: Refactor identify ( and lazyrequest, tbh ) const getUserFromToken = async (token: string): Promise<string | null> => {
try {
const { jwtSecret } = Config.get().security;
const { decoded } = await checkToken(token, jwtSecret);
return decoded.id;
} catch (e) {
console.error(`[Gateway] Invalid token`, e);
return null;
}
};
export async function onIdentify(this: WebSocket, data: Payload) { export async function onIdentify(this: WebSocket, data: Payload) {
clearTimeout(this.readyTimeout); clearTimeout(this.readyTimeout);
// TODO: is this needed now that we use `json-bigint`?
if (typeof data.d?.client_state?.highest_last_message_id === "number")
data.d.client_state.highest_last_message_id += "";
check.call(this, IdentifySchema, data.d);
// Check payload matches schema
check.call(this, IdentifySchema, data.d);
const identify: IdentifySchema = data.d; const identify: IdentifySchema = data.d;
let decoded: UserTokenData["decoded"]; // Check auth
try { // TODO: the checkToken call will fetch user, and then we have to refetch with different select
const { jwtSecret } = Config.get().security; // checkToken should be able to select what we want
decoded = (await checkToken(identify.token, jwtSecret)).decoded; // will throw an error if invalid const user_id = await getUserFromToken(identify.token);
} catch (error) { if (!user_id) return this.close(CLOSECODES.Authentication_failed);
console.error("invalid token", error); this.user_id = user_id;
return this.close(CLOSECODES.Authentication_failed);
}
this.user_id = decoded.id;
const session_id = this.session_id;
const [user, read_states, members, recipients, session, application] = // Check intents
if (!identify.intents) identify.intents = 30064771071n; // TODO: what is this number?
this.intents = new Intents(identify.intents);
// TODO: actually do intent things.
// Validate sharding
if (identify.shard) {
this.shard_id = identify.shard[0];
this.shard_count = identify.shard[1];
if (
this.shard_count == null ||
this.shard_id == null ||
this.shard_id > this.shard_count ||
this.shard_id < 0 ||
this.shard_count <= 0
) {
// TODO: why do we even care about this?
console.log(
`[Gateway] Invalid sharding from ${user_id}: ${identify.shard}`,
);
return this.close(CLOSECODES.Invalid_shard);
}
}
// Generate a new gateway session ( id is already made, just save it in db )
const session = Session.create({
user_id: this.user_id,
session_id: this.session_id,
status: identify.presence?.status || "online",
client_info: {
client: identify.properties?.$device,
os: identify.properties?.os,
version: 0,
},
activities: identify.presence?.activities, // TODO: validation
});
// Get from database:
// * the current user,
// * the users read states
// * guild members for this user
// * recipients ( dm channels )
// * the bot application, if it exists
const [, user, application, read_states, members, recipients] =
await Promise.all([ await Promise.all([
session.save(),
// TODO: Refactor checkToken to allow us to skip this additional query
User.findOneOrFail({ User.findOneOrFail({
where: { id: this.user_id }, where: { id: this.user_id },
relations: ["relationships", "relationships.to", "settings"], relations: ["relationships", "relationships.to", "settings"],
select: [...PrivateUserProjection, "relationships"], select: [...PrivateUserProjection, "relationships"],
}), }),
ReadState.find({ where: { user_id: this.user_id } }),
Application.findOne({
where: { id: this.user_id },
select: ["id", "flags"],
}),
ReadState.find({
where: { user_id: this.user_id },
select: [
"id",
"channel_id",
"last_message_id",
"last_pin_timestamp",
"mention_count",
],
}),
Member.find({ Member.find({
where: { id: this.user_id }, where: { id: this.user_id },
select: MemberPrivateProjection, select: {
// We only want some member props
...Object.fromEntries(
MemberPrivateProjection.map((x) => [x, true]),
),
settings: true, // guild settings
roles: { id: true }, // the full role is fetched from the `guild` relation
// TODO: we don't really need every property of
// guild channels, emoji, roles, stickers
// but we do want almost everything from guild.
// How do you do that without just enumerating the guild props?
guild: true,
},
relations: [ relations: [
"guild", "guild",
"guild.channels", "guild.channels",
"guild.emojis", "guild.emojis",
"guild.roles", "guild.roles",
"guild.stickers", "guild.stickers",
"user",
"roles", "roles",
// For these entities, `user` is always just the logged in user we fetched above
// "user",
], ],
}), }),
Recipient.find({ Recipient.find({
where: { user_id: this.user_id, closed: false }, where: { user_id: this.user_id, closed: false },
relations: [ relations: [
@ -106,220 +188,240 @@ export async function onIdentify(this: WebSocket, data: Payload) {
"channel.recipients", "channel.recipients",
"channel.recipients.user", "channel.recipients.user",
], ],
// TODO: public user selection select: {
}), channel: {
// save the session and delete it when the websocket is closed id: true,
Session.create({ flags: true,
user_id: this.user_id, // is_spam: true, // TODO
session_id: session_id, last_message_id: true,
// TODO: check if status is only one of: online, dnd, offline, idle last_pin_timestamp: true,
status: identify.presence?.status || "offline", //does the session always start as online? type: true,
client_info: { icon: true,
//TODO read from identity name: true,
client: "desktop", owner_id: true,
os: identify.properties?.os, recipients: {
version: 0, // we don't actually need this ID or any other information about the recipient info,
// but typeorm does not select anything from the users relation of recipients unless we select
// at least one column.
id: true,
// We only want public user data for each dm channel
user: Object.fromEntries(
PublicUserProjection.map((x) => [x, true]),
),
},
},
}, },
activities: [], }),
}).save(),
Application.findOne({ where: { id: this.user_id } }),
]); ]);
if (!user) return this.close(CLOSECODES.Authentication_failed); // We forgot to migrate user settings from the JSON column of `users`
// to the `user_settings` table theyre in now,
// so for instances that migrated, users may not have a `user_settings` row.
if (!user.settings) { if (!user.settings) {
user.settings = new UserSettings(); user.settings = new UserSettings();
await user.settings.save(); await user.settings.save();
} }
if (!identify.intents) identify.intents = BigInt("0x6ffffffff"); // Generate merged_members
this.intents = new Intents(identify.intents); const merged_members = members.map((x) => {
if (identify.shard) {
this.shard_id = identify.shard[0];
this.shard_count = identify.shard[1];
if (
this.shard_count == null ||
this.shard_id == null ||
this.shard_id >= this.shard_count ||
this.shard_id < 0 ||
this.shard_count <= 0
) {
console.log(identify.shard);
return this.close(CLOSECODES.Invalid_shard);
}
}
let users: PublicUser[] = [];
const merged_members = members.map((x: Member) => {
return [ return [
{ {
...x, ...x,
roles: x.roles.map((x) => x.id), roles: x.roles.map((x) => x.id),
// add back user, which we don't fetch from db
// TODO: For guild profiles, this may need to be changed.
// TODO: The only field required in the user prop is `id`,
// but our types are annoying so I didn't bother.
user: user.toPublicUser(),
guild: {
id: x.guild.id,
},
settings: undefined, settings: undefined,
guild: undefined,
}, },
]; ];
}) as PublicMember[][];
// TODO: This type is bad.
let guilds: Partial<Guild>[] = members.map((x) => ({
...x.guild,
joined_at: x.joined_at,
}));
const pending_guilds: typeof guilds = [];
if (user.bot)
guilds = guilds.map((guild) => {
pending_guilds.push(guild);
return { id: guild.id, unavailable: true };
});
// TODO: Rewrite this. Perhaps a DTO?
const user_guild_settings_entries = members.map((x) => ({
...DefaultUserGuildSettings,
...x.settings,
guild_id: x.guild.id,
channel_overrides: Object.entries(
x.settings.channel_overrides ?? {},
).map((y) => ({
...y[1],
channel_id: y[0],
})),
})) as unknown as UserGuildSettings[];
const channels = recipients.map((x) => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
x.channel.recipients = x.channel.recipients.map((x) =>
x.user.toPublicUser(),
);
//TODO is this needed? check if users in group dm that are not friends are sent in the READY event
users = users.concat(x.channel.recipients as unknown as User[]);
if (x.channel.isDm()) {
x.channel.recipients = x.channel.recipients?.filter(
(x) => x.id !== this.user_id,
);
}
return x.channel;
}); });
for (const relation of user.relationships) { // Populated with guilds 'unavailable' currently
const related_user = relation.to; // Just for bots
const public_related_user = { const pending_guilds: Guild[] = [];
username: related_user.username,
discriminator: related_user.discriminator,
id: related_user.id,
public_flags: related_user.public_flags,
avatar: related_user.avatar,
bot: related_user.bot,
bio: related_user.bio,
premium_since: user.premium_since,
premium_type: user.premium_type,
accent_color: related_user.accent_color,
};
users.push(public_related_user);
}
setImmediate(async () => { // Generate guilds list ( make them unavailable if user is bot )
// run in seperate "promise context" because ready payload is not dependent on those events const guilds: GuildOrUnavailable[] = members.map((member) => {
// Some Discord libraries do `'blah' in object` instead of
// checking if the type is correct
member.guild.roles.forEach((role) => {
for (const key in role) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
if (!role[key]) role[key] = undefined;
}
});
// filter guild channels we don't have permission to view
// TODO: check if this causes issues when the user is granted other roles?
member.guild.channels = member.guild.channels.filter((channel) => {
const perms = Permissions.finalPermission({
user: { id: member.id, roles: member.roles.map((x) => x.id) },
guild: member.guild,
channel,
});
return perms.has("VIEW_CHANNEL");
});
if (user.bot) {
pending_guilds.push(member.guild);
return { id: member.guild.id, unavailable: true };
}
return {
...member.guild.toJSON(),
joined_at: member.joined_at,
};
});
// Generate user_guild_settings
const user_guild_settings_entries: ReadyUserGuildSettingsEntries[] =
members.map((x) => ({
...DefaultUserGuildSettings,
...x.settings,
guild_id: x.guild_id,
channel_overrides: Object.entries(
x.settings.channel_overrides ?? {},
).map((y) => ({
...y[1],
channel_id: y[0],
})),
}));
// Popultaed with users from private channels, relationships.
// Uses a set to dedupe for us.
const users: Set<PublicUser> = new Set();
// Generate dm channels from recipients list. Append recipients to `users` list
const channels = recipients
.filter(({ channel }) => channel.isDm())
.map((r) => {
// TODO: fix the types of Recipient
// Their channels are only ever private (I think) and thus are always DM channels
const channel = r.channel as DMChannel;
// Remove ourself from the list of other users in dm channel
channel.recipients = channel.recipients.filter(
(recipient) => recipient.user.id !== this.user_id,
);
const channelUsers = channel.recipients?.map((recipient) =>
recipient.user.toPublicUser(),
);
if (channelUsers && channelUsers.length > 0)
channelUsers.forEach((user) => users.add(user));
return {
id: channel.id,
flags: channel.flags,
last_message_id: channel.last_message_id,
type: channel.type,
recipients: channelUsers || [],
is_spam: false, // TODO
};
});
// From user relationships ( friends ), also append to `users` list
user.relationships.forEach((x) => users.add(x.to.toPublicUser()));
// Send SESSIONS_REPLACE and PRESENCE_UPDATE
const allSessions = (
await Session.find({
where: { user_id: this.user_id },
select: PrivateSessionProjection,
})
).map((x) => ({
// TODO how is active determined?
// in our lazy request impl, we just pick the 'most relevant' session
active: x.session_id == session.session_id,
activities: x.activities,
client_info: x.client_info,
// TODO: what does all mean?
session_id: x.session_id == session.session_id ? "all" : x.session_id,
status: x.status,
}));
Promise.all([
emitEvent({ emitEvent({
event: "SESSIONS_REPLACE", event: "SESSIONS_REPLACE",
user_id: this.user_id, user_id: this.user_id,
data: await Session.find({ data: allSessions,
where: { user_id: this.user_id }, } as SessionsReplace),
select: PrivateSessionProjection,
}),
} as SessionsReplace);
emitEvent({ emitEvent({
event: "PRESENCE_UPDATE", event: "PRESENCE_UPDATE",
user_id: this.user_id, user_id: this.user_id,
data: { data: {
user: await User.getPublicUser(this.user_id), user: user.toPublicUser(),
activities: session.activities, activities: session.activities,
client_status: session?.client_info, client_status: session.client_info,
status: session.status, status: session.status,
}, },
} as PresenceUpdateEvent); } as PresenceUpdateEvent),
}); ]);
read_states.forEach((s: Partial<ReadState>) => { // Build READY
s.id = s.channel_id;
delete s.user_id;
delete s.channel_id;
});
const privateUser = { read_states.forEach((x) => {
avatar: user.avatar, x.id = x.channel_id;
mobile: user.mobile, });
desktop: user.desktop,
discriminator: user.discriminator,
email: user.email,
flags: user.flags,
id: user.id,
mfa_enabled: user.mfa_enabled,
nsfw_allowed: user.nsfw_allowed,
phone: user.phone,
premium: user.premium,
premium_type: user.premium_type,
public_flags: user.public_flags,
premium_usage_flags: user.premium_usage_flags,
purchased_flags: user.purchased_flags,
username: user.username,
verified: user.verified,
bot: user.bot,
accent_color: user.accent_color,
banner: user.banner,
bio: user.bio,
premium_since: user.premium_since,
};
const d: ReadyEventData = { const d: ReadyEventData = {
v: 9, v: 9,
application: { application: application
id: application?.id ?? "", ? { id: application.id, flags: application.flags }
flags: application?.flags ?? 0, : undefined,
}, //TODO: check this code! user: user.toPrivateUser(),
user: privateUser,
user_settings: user.settings, user_settings: user.settings,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment guilds: guilds.map((x) => new ReadyGuildDTO(x).toJSON()),
// @ts-ignore
guilds: guilds.map((x: Guild & { joined_at: Date }) => {
return {
...new ReadyGuildDTO(x).toJSON(),
guild_hashes: {},
joined_at: x.joined_at,
};
}),
guild_experiments: [], // TODO
geo_ordered_rtc_regions: [], // TODO
relationships: user.relationships.map((x) => x.toPublicRelationship()), relationships: user.relationships.map((x) => x.toPublicRelationship()),
read_state: { read_state: {
entries: read_states, entries: read_states,
partial: false, partial: false,
// TODO: what is this magic number?
// Isn't `version` referring to the number of changes since this obj was created?
// Why do we send this specific version?
version: 304128, version: 304128,
}, },
user_guild_settings: { user_guild_settings: {
entries: user_guild_settings_entries, entries: user_guild_settings_entries,
partial: false, // TODO partial partial: false,
version: 642, version: 642, // TODO: see above
}, },
private_channels: channels, private_channels: channels,
session_id: session_id, session_id: this.session_id,
analytics_token: "", // TODO country_code: user.settings.locale, // TODO: do ip analysis instead
connected_accounts: [], // TODO users: Array.from(users),
merged_members: merged_members,
sessions: allSessions,
consents: { consents: {
personalization: { personalization: {
consented: false, // TODO consented: false, // TODO
}, },
}, },
country_code: user.settings.locale, experiments: [],
friend_suggestion_count: 0, // TODO guild_join_requests: [],
// eslint-disable-next-line @typescript-eslint/ban-ts-comment connected_accounts: [],
// @ts-ignore guild_experiments: [],
experiments: experiments, // TODO geo_ordered_rtc_regions: [],
guild_join_requests: [], // TODO what is this? api_code_version: 1,
users: users.filter((x) => x).unique(), friend_suggestion_count: 0,
merged_members: merged_members, analytics_token: "",
// shard // TODO: only for user sharding tutorial: null,
sessions: [], // TODO: resume_gateway_url:
Config.get().gateway.endpointClient ||
Config.get().gateway.endpointPublic ||
"ws://127.0.0.1:3001",
session_type: "normal", // TODO
// lol hack whatever // lol hack whatever
required_action: required_action:
@ -328,7 +430,7 @@ export async function onIdentify(this: WebSocket, data: Payload) {
: undefined, : undefined,
}; };
// TODO: send real proper data structure // Send READY
await Send(this, { await Send(this, {
op: OPCODES.Dispatch, op: OPCODES.Dispatch,
t: EVENTEnum.Ready, t: EVENTEnum.Ready,
@ -336,23 +438,23 @@ export async function onIdentify(this: WebSocket, data: Payload) {
d, d,
}); });
// If we're a bot user, send GUILD_CREATE for each unavailable guild
await Promise.all( await Promise.all(
pending_guilds.map((guild) => pending_guilds.map((x) =>
Send(this, { Send(this, {
op: OPCODES.Dispatch, op: OPCODES.Dispatch,
t: EVENTEnum.GuildCreate, t: EVENTEnum.GuildCreate,
s: this.sequence++, s: this.sequence++,
d: guild, d: x,
})?.catch(console.error), })?.catch((e) =>
console.error(`[Gateway] error when sending bot guilds`, e),
),
), ),
); );
//TODO send READY_SUPPLEMENTAL //TODO send READY_SUPPLEMENTAL
//TODO send GUILD_MEMBER_LIST_UPDATE //TODO send GUILD_MEMBER_LIST_UPDATE
//TODO send SESSIONS_REPLACE
//TODO send VOICE_STATE_UPDATE to let the client know if another device is already connected to a voice channel //TODO send VOICE_STATE_UPDATE to let the client know if another device is already connected to a voice channel
await setupListener.call(this); await setupListener.call(this);
// console.log(`${this.ipAddress} identified as ${d.user.id}`);
} }

View File

@ -16,7 +16,46 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { Channel, Emoji, Guild, Member, Role, Sticker } from "../entities"; import {
Channel,
ChannelOverride,
ChannelType,
Emoji,
Guild,
Member,
PublicUser,
Role,
Sticker,
UserGuildSettings,
} 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: boolean });
const guildIsAvailable = (
guild: GuildOrUnavailable,
): guild is Guild & { joined_at: Date; unavailable: false } => {
return guild.unavailable == false;
};
export interface IReadyGuildDTO { export interface IReadyGuildDTO {
application_command_counts?: { 1: number; 2: number; 3: number }; // ???????????? application_command_counts?: { 1: number; 2: number; 3: number }; // ????????????
@ -64,6 +103,8 @@ export interface IReadyGuildDTO {
stickers: Sticker[]; stickers: Sticker[];
threads: unknown[]; threads: unknown[];
version: string; version: string;
guild_hashes: unknown;
unavailable: boolean;
} }
export class ReadyGuildDTO implements IReadyGuildDTO { export class ReadyGuildDTO implements IReadyGuildDTO {
@ -112,8 +153,17 @@ export class ReadyGuildDTO implements IReadyGuildDTO {
stickers: Sticker[]; stickers: Sticker[];
threads: unknown[]; threads: unknown[];
version: string; 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 = { this.application_command_counts = {
1: 5, 1: 5,
2: 2, 2: 2,
@ -163,6 +213,8 @@ export class ReadyGuildDTO implements IReadyGuildDTO {
this.stickers = guild.stickers; this.stickers = guild.stickers;
this.threads = []; this.threads = [];
this.version = "1"; // ?????? this.version = "1"; // ??????
this.guild_hashes = {};
this.joined_at = guild.joined_at;
} }
toJSON() { toJSON() {

View File

@ -482,3 +482,10 @@ export enum ChannelPermissionOverwriteType {
member = 1, member = 1,
group = 2, group = 2,
} }
export interface DMChannel extends Omit<Channel, "type" | "recipients"> {
type: ChannelType.DM | ChannelType.GROUP_DM;
recipients: Recipient[];
// TODO: probably more props
}

View File

@ -280,6 +280,15 @@ export class User extends BaseClass {
return user as PublicUser; 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>) { static async getPublicUser(user_id: string, opts?: FindOneOptions<User>) {
return await User.findOneOrFail({ return await User.findOneOrFail({
where: { id: user_id }, where: { id: user_id },

View File

@ -40,6 +40,9 @@ import {
UserSettings, UserSettings,
IReadyGuildDTO, IReadyGuildDTO,
ReadState, ReadState,
UserPrivate,
ReadyUserGuildSettingsEntries,
ReadyPrivateChannel,
} from "@fosscord/util"; } from "@fosscord/util";
export interface Event { export interface Event {
@ -68,20 +71,8 @@ export interface PublicRelationship {
export interface ReadyEventData { export interface ReadyEventData {
v: number; v: number;
user: PublicUser & { user: UserPrivate;
mobile: boolean; private_channels: ReadyPrivateChannel[]; // this will be empty for bots
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
session_id: string; // resuming session_id: string; // resuming
guilds: IReadyGuildDTO[]; guilds: IReadyGuildDTO[];
analytics_token?: string; analytics_token?: string;
@ -115,7 +106,7 @@ export interface ReadyEventData {
version: number; version: number;
}; };
user_guild_settings?: { user_guild_settings?: {
entries: UserGuildSettings[]; entries: ReadyUserGuildSettingsEntries[];
version: number; version: number;
partial: boolean; partial: boolean;
}; };
@ -127,6 +118,16 @@ export interface ReadyEventData {
// probably all users who the user is in contact with // probably all users who the user is in contact with
users?: PublicUser[]; users?: PublicUser[];
sessions: unknown[]; sessions: unknown[];
api_code_version: number;
tutorial: number | null;
resume_gateway_url: string;
session_type: 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 { export interface ReadyEvent extends Event {