diff options
Diffstat (limited to 'src/gateway/opcodes/Identify.ts')
-rw-r--r-- | src/gateway/opcodes/Identify.ts | 298 |
1 files changed, 298 insertions, 0 deletions
diff --git a/src/gateway/opcodes/Identify.ts b/src/gateway/opcodes/Identify.ts new file mode 100644 index 00000000..44db598c --- /dev/null +++ b/src/gateway/opcodes/Identify.ts @@ -0,0 +1,298 @@ +import { WebSocket, Payload } from "@fosscord/gateway"; +import { + checkToken, + Intents, + Member, + ReadyEventData, + User, + Session, + EVENTEnum, + Config, + PublicMember, + PublicUser, + PrivateUserProjection, + ReadState, + Application, + emitEvent, + SessionsReplace, + PrivateSessionProjection, + MemberPrivateProjection, + PresenceUpdateEvent, + UserSettings, + IdentifySchema, +} from "@fosscord/util"; +import { Send } from "../util/Send"; +import { CLOSECODES, OPCODES } from "../util/Constants"; +import { genSessionId } from "../util/SessionUtils"; +import { setupListener } from "../listener/listener"; +// import experiments from "./experiments.json"; +const experiments: any = []; +import { check } from "./instanceOf"; +import { Recipient } from "@fosscord/util"; +import { OrmUtils } from "@fosscord/util"; + +// TODO: user sharding +// TODO: check privileged intents, if defined in the config +// TODO: check if already identified + +export async function onIdentify(this: WebSocket, data: Payload) { + clearTimeout(this.readyTimeout); + check.call(this, IdentifySchema, data.d); + + const identify: IdentifySchema = data.d; + + try { + const { jwtSecret } = Config.get().security; + var { decoded } = await checkToken(identify.token, jwtSecret); // will throw an error if invalid + } catch (error) { + console.error("invalid token", error); + return this.close(CLOSECODES.Authentication_failed); + } + this.user_id = decoded.id; + + const session_id = genSessionId(); + this.session_id = session_id; //Set the session of the WebSocket object + + const [user, read_states, members, recipients, session, application] = + await Promise.all([ + User.findOneOrFail({ + where: { id: this.user_id }, + relations: ["relationships", "relationships.to", "settings"], + select: [...PrivateUserProjection, "relationships"], + }), + ReadState.find({ where: { user_id: this.user_id } }), + Member.find({ + where: { id: this.user_id }, + select: MemberPrivateProjection, + relations: [ + "guild", + "guild.channels", + "guild.emojis", + "guild.emojis.user", + "guild.roles", + "guild.stickers", + "user", + "roles", + ], + }), + Recipient.find({ + where: { user_id: this.user_id, closed: false }, + relations: [ + "channel", + "channel.recipients", + "channel.recipients.user", + ], + // TODO: public user selection + }), + // save the session and delete it when the websocket is closed + await OrmUtils.mergeDeep(new Session(), { + 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, + }, + activities: [], + }).save(), + Application.findOne({ where: { id: this.user_id } }), + ]); + + if (!user) return this.close(CLOSECODES.Authentication_failed); + if (!user.settings) { //settings may not exist after updating... + user.settings = new UserSettings(); + user.settings.id = user.id; + //await (user.settings as UserSettings).save(); + } + + if (!identify.intents) identify.intents = "30064771071"; + 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) => { + return [ + { + ...x, + roles: x.roles.map((x) => x.id), + settings: undefined, + guild: undefined, + }, + ]; + }) as PublicMember[][]; + let guilds = members.map((x) => ({ ...x.guild, joined_at: x.joined_at })); + + // @ts-ignore + guilds = guilds.map((guild) => { + if (user.bot) { + setTimeout(() => { + Send(this, { + op: OPCODES.Dispatch, + t: EVENTEnum.GuildCreate, + s: this.sequence++, + d: guild, + }); + }, 500); + return { id: guild.id, unavailable: true }; + } + + return guild; + }); + + const user_guild_settings_entries = members.map((x) => x.settings); + + const channels = recipients.map((x) => { + // @ts-ignore + x.channel.recipients = x.channel.recipients?.map((x) => x.user); + //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 (let 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 + }; + users.push(public_related_user); + } + + setImmediate(async () => { + // run in seperate "promise context" because ready payload is not dependent on those events + emitEvent({ + event: "SESSIONS_REPLACE", + user_id: this.user_id, + data: await Session.find({ + where: { user_id: this.user_id }, + select: PrivateSessionProjection, + }), + } as SessionsReplace); + emitEvent({ + event: "PRESENCE_UPDATE", + user_id: this.user_id, + data: { + user: await User.getPublicUser(this.user_id), + activities: session.activities, + client_status: session?.client_info, + status: session.status, + }, + } as PresenceUpdateEvent); + }); + + read_states.forEach((s: any) => { + s.id = s.channel_id; + delete s.user_id; + delete s.channel_id; + }); + + 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, + username: user.username, + verified: user.verified, + bot: user.bot, + accent_color: user.accent_color || 0, + banner: user.banner, + bio: user.bio, + premium_since: user.premium_since + }; + + const d: ReadyEventData = { + v: 8, + application: {id: application?.id??'', flags: application?.flags??0}, //TODO: check this code! + user: privateUser, + user_settings: user.settings, + // @ts-ignore + guilds: guilds.map((x) => { + // @ts-ignore + x.guild_hashes = {}; // @ts-ignore + x.guild_scheduled_events = []; // @ts-ignore + x.threads = []; + return x; + }), + guild_experiments: [], // TODO + geo_ordered_rtc_regions: [], // TODO + relationships: user.relationships.map((x) => x.toPublicRelationship()), + read_state: { + entries: read_states, + partial: false, + version: 304128, + }, + user_guild_settings: { + entries: user_guild_settings_entries, + partial: false, // TODO partial + version: 642, + }, + private_channels: channels, + session_id: session_id, + analytics_token: "", // TODO + connected_accounts: [], // TODO + consents: { + personalization: { + consented: false, // TODO + }, + }, + country_code: user.settings.locale, + friend_suggestion_count: 0, // TODO + // @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 + }; + + // TODO: send real proper data structure + await Send(this, { + op: OPCODES.Dispatch, + t: EVENTEnum.Ready, + s: this.sequence++, + d, + }); + + //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); +} |