From f228561f4c2059d3568d4cf7dd8fc98dd0260c2a Mon Sep 17 00:00:00 2001
From: Madeline <46743919+MaddyUnderStars@users.noreply.github.com>
Date: Mon, 13 Mar 2023 19:02:52 +1100
Subject: Initial identify rewrite
---
src/gateway/opcodes/Identify.ts | 490 ++++++++++++++++++++++++----------------
src/util/dtos/ReadyGuildDTO.ts | 56 ++++-
src/util/entities/Channel.ts | 7 +
src/util/entities/User.ts | 9 +
src/util/interfaces/Event.ts | 31 +--
5 files changed, 382 insertions(+), 211 deletions(-)
(limited to 'src')
diff --git a/src/gateway/opcodes/Identify.ts b/src/gateway/opcodes/Identify.ts
index 1a632b84..51a6e2e4 100644
--- a/src/gateway/opcodes/Identify.ts
+++ b/src/gateway/opcodes/Identify.ts
@@ -16,7 +16,7 @@
along with this program. If not, see .
*/
-import { WebSocket, Payload } from "@fosscord/gateway";
+import { WebSocket, Payload, setupListener } from "@fosscord/gateway";
import {
checkToken,
Intents,
@@ -26,7 +26,6 @@ import {
Session,
EVENTEnum,
Config,
- PublicMember,
PublicUser,
PrivateUserProjection,
ReadState,
@@ -36,19 +35,19 @@ import {
PrivateSessionProjection,
MemberPrivateProjection,
PresenceUpdateEvent,
- UserSettings,
IdentifySchema,
DefaultUserGuildSettings,
- UserGuildSettings,
ReadyGuildDTO,
Guild,
- UserTokenData,
+ PublicUserProjection,
+ ReadyUserGuildSettingsEntries,
+ UserSettings,
+ Permissions,
+ DMChannel,
+ GuildOrUnavailable,
} from "@fosscord/util";
import { Send } from "../util/Send";
import { CLOSECODES, OPCODES } from "../util/Constants";
-import { setupListener } from "../listener/listener";
-// import experiments from "./experiments.json";
-const experiments: unknown[] = [];
import { check } from "./instanceOf";
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 if already identified
-// TODO: Refactor identify ( and lazyrequest, tbh )
+const getUserFromToken = async (token: string): Promise => {
+ 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) {
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;
- let decoded: UserTokenData["decoded"];
- try {
- const { jwtSecret } = Config.get().security;
- decoded = (await checkToken(identify.token, jwtSecret)).decoded; // will throw an error if invalid
- } catch (error) {
- console.error("invalid token", error);
- return this.close(CLOSECODES.Authentication_failed);
+ // Check auth
+ // TODO: the checkToken call will fetch user, and then we have to refetch with different select
+ // checkToken should be able to select what we want
+ const user_id = await getUserFromToken(identify.token);
+ if (!user_id) return this.close(CLOSECODES.Authentication_failed);
+ this.user_id = user_id;
+
+ // 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);
+ }
}
- this.user_id = decoded.id;
- const session_id = this.session_id;
- const [user, read_states, members, recipients, session, application] =
+ // 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([
+ session.save(),
+
+ // TODO: Refactor checkToken to allow us to skip this additional query
User.findOneOrFail({
where: { id: this.user_id },
relations: ["relationships", "relationships.to", "settings"],
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({
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: [
"guild",
"guild.channels",
"guild.emojis",
"guild.roles",
"guild.stickers",
- "user",
"roles",
+
+ // For these entities, `user` is always just the logged in user we fetched above
+ // "user",
],
}),
+
Recipient.find({
where: { user_id: this.user_id, closed: false },
relations: [
@@ -106,220 +188,240 @@ export async function onIdentify(this: WebSocket, data: Payload) {
"channel.recipients",
"channel.recipients.user",
],
- // TODO: public user selection
- }),
- // save the session and delete it when the websocket is closed
- Session.create({
- user_id: this.user_id,
- session_id: session_id,
- // TODO: check if status is only one of: online, dnd, offline, idle
- status: identify.presence?.status || "offline", //does the session always start as online?
- client_info: {
- //TODO read from identity
- client: "desktop",
- os: identify.properties?.os,
- version: 0,
+ select: {
+ channel: {
+ id: true,
+ flags: true,
+ // is_spam: true, // TODO
+ last_message_id: true,
+ last_pin_timestamp: true,
+ type: true,
+ icon: true,
+ name: true,
+ owner_id: true,
+ recipients: {
+ // 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) {
user.settings = new UserSettings();
await user.settings.save();
}
- if (!identify.intents) identify.intents = BigInt("0x6ffffffff");
- this.intents = new Intents(identify.intents);
- 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) => {
+ // Generate merged_members
+ const merged_members = members.map((x) => {
return [
{
...x,
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,
- guild: undefined,
},
];
- }) as PublicMember[][];
- // TODO: This type is bad.
- let guilds: Partial[] = 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 };
+ // Populated with guilds 'unavailable' currently
+ // Just for bots
+ const pending_guilds: Guild[] = [];
+
+ // Generate guilds list ( make them unavailable if user is bot )
+ 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;
+ }
});
- // 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,
- );
+ // 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 x.channel;
- });
- for (const relation of user.relationships) {
- const related_user = relation.to;
- const public_related_user = {
- 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,
+ return {
+ ...member.guild.toJSON(),
+ joined_at: member.joined_at,
};
- users.push(public_related_user);
- }
+ });
+
+ // 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],
+ })),
+ }));
- setImmediate(async () => {
- // run in seperate "promise context" because ready payload is not dependent on those events
+ // Popultaed with users from private channels, relationships.
+ // Uses a set to dedupe for us.
+ const users: Set = 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({
event: "SESSIONS_REPLACE",
user_id: this.user_id,
- data: await Session.find({
- where: { user_id: this.user_id },
- select: PrivateSessionProjection,
- }),
- } as SessionsReplace);
+ data: allSessions,
+ } as SessionsReplace),
emitEvent({
event: "PRESENCE_UPDATE",
user_id: this.user_id,
data: {
- user: await User.getPublicUser(this.user_id),
+ user: user.toPublicUser(),
activities: session.activities,
- client_status: session?.client_info,
+ client_status: session.client_info,
status: session.status,
},
- } as PresenceUpdateEvent);
- });
+ } as PresenceUpdateEvent),
+ ]);
- read_states.forEach((s: Partial) => {
- s.id = s.channel_id;
- delete s.user_id;
- delete s.channel_id;
- });
+ // Build READY
- const privateUser = {
- avatar: user.avatar,
- 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,
- };
+ read_states.forEach((x) => {
+ x.id = x.channel_id;
+ });
const d: ReadyEventData = {
v: 9,
- application: {
- id: application?.id ?? "",
- flags: application?.flags ?? 0,
- }, //TODO: check this code!
- user: privateUser,
+ application: application
+ ? { id: application.id, flags: application.flags }
+ : undefined,
+ user: user.toPrivateUser(),
user_settings: user.settings,
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
- // @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
+ guilds: guilds.map((x) => new ReadyGuildDTO(x).toJSON()),
relationships: user.relationships.map((x) => x.toPublicRelationship()),
read_state: {
entries: read_states,
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,
},
user_guild_settings: {
entries: user_guild_settings_entries,
- partial: false, // TODO partial
- version: 642,
+ partial: false,
+ version: 642, // TODO: see above
},
private_channels: channels,
- session_id: session_id,
- analytics_token: "", // TODO
- connected_accounts: [], // TODO
+ session_id: this.session_id,
+ country_code: user.settings.locale, // TODO: do ip analysis instead
+ users: Array.from(users),
+ merged_members: merged_members,
+ sessions: allSessions,
+
consents: {
personalization: {
consented: false, // TODO
},
},
- country_code: user.settings.locale,
- friend_suggestion_count: 0, // TODO
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
- // @ts-ignore
- experiments: experiments, // TODO
- guild_join_requests: [], // TODO what is this?
- users: users.filter((x) => x).unique(),
- merged_members: merged_members,
- // shard // TODO: only for user sharding
- sessions: [], // TODO:
+ experiments: [],
+ guild_join_requests: [],
+ connected_accounts: [],
+ guild_experiments: [],
+ geo_ordered_rtc_regions: [],
+ api_code_version: 1,
+ friend_suggestion_count: 0,
+ analytics_token: "",
+ tutorial: null,
+ resume_gateway_url:
+ Config.get().gateway.endpointClient ||
+ Config.get().gateway.endpointPublic ||
+ "ws://127.0.0.1:3001",
+ session_type: "normal", // TODO
// lol hack whatever
required_action:
@@ -328,7 +430,7 @@ export async function onIdentify(this: WebSocket, data: Payload) {
: undefined,
};
- // TODO: send real proper data structure
+ // Send READY
await Send(this, {
op: OPCODES.Dispatch,
t: EVENTEnum.Ready,
@@ -336,23 +438,23 @@ export async function onIdentify(this: WebSocket, data: Payload) {
d,
});
+ // If we're a bot user, send GUILD_CREATE for each unavailable guild
await Promise.all(
- pending_guilds.map((guild) =>
+ pending_guilds.map((x) =>
Send(this, {
op: OPCODES.Dispatch,
t: EVENTEnum.GuildCreate,
s: this.sequence++,
- d: guild,
- })?.catch(console.error),
+ d: x,
+ })?.catch((e) =>
+ console.error(`[Gateway] error when sending bot guilds`, e),
+ ),
),
);
//TODO send READY_SUPPLEMENTAL
//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
await setupListener.call(this);
-
- // console.log(`${this.ipAddress} identified as ${d.user.id}`);
}
diff --git a/src/util/dtos/ReadyGuildDTO.ts b/src/util/dtos/ReadyGuildDTO.ts
index 97e6931f..e91248d2 100644
--- a/src/util/dtos/ReadyGuildDTO.ts
+++ b/src/util/dtos/ReadyGuildDTO.ts
@@ -16,7 +16,46 @@
along with this program. If not, see .
*/
-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 {
application_command_counts?: { 1: number; 2: number; 3: number }; // ????????????
@@ -64,6 +103,8 @@ export interface IReadyGuildDTO {
stickers: Sticker[];
threads: unknown[];
version: string;
+ guild_hashes: unknown;
+ unavailable: boolean;
}
export class ReadyGuildDTO implements IReadyGuildDTO {
@@ -112,8 +153,17 @@ export class ReadyGuildDTO implements IReadyGuildDTO {
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,
@@ -163,6 +213,8 @@ export class ReadyGuildDTO implements IReadyGuildDTO {
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 1f128713..7c0828eb 100644
--- a/src/util/entities/Channel.ts
+++ b/src/util/entities/Channel.ts
@@ -482,3 +482,10 @@ export enum ChannelPermissionOverwriteType {
member = 1,
group = 2,
}
+
+export interface DMChannel extends Omit {
+ type: ChannelType.DM | ChannelType.GROUP_DM;
+ recipients: Recipient[];
+
+ // TODO: probably more props
+}
diff --git a/src/util/entities/User.ts b/src/util/entities/User.ts
index f99a85e7..0ed88c15 100644
--- a/src/util/entities/User.ts
+++ b/src/util/entities/User.ts
@@ -280,6 +280,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) {
return await User.findOneOrFail({
where: { id: user_id },
diff --git a/src/util/interfaces/Event.ts b/src/util/interfaces/Event.ts
index c3bfbf9b..492821f1 100644
--- a/src/util/interfaces/Event.ts
+++ b/src/util/interfaces/Event.ts
@@ -40,6 +40,9 @@ import {
UserSettings,
IReadyGuildDTO,
ReadState,
+ UserPrivate,
+ ReadyUserGuildSettingsEntries,
+ ReadyPrivateChannel,
} from "@fosscord/util";
export interface Event {
@@ -68,20 +71,8 @@ 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[];
analytics_token?: string;
@@ -115,7 +106,7 @@ export interface ReadyEventData {
version: number;
};
user_guild_settings?: {
- entries: UserGuildSettings[];
+ entries: ReadyUserGuildSettingsEntries[];
version: number;
partial: boolean;
};
@@ -127,6 +118,16 @@ 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;
+ 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 {
--
cgit 1.4.1