summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--api/package-lock.json14
-rw-r--r--api/package.json2
-rw-r--r--api/src/routes/users/@me/settings.ts5
-rw-r--r--api/src/util/Instance.ts5
-rw-r--r--api/tests/routes.test.ts2
-rw-r--r--bundle/package-lock.json14
-rw-r--r--bundle/package.json20
-rw-r--r--bundle/scripts/benchmark/connections.js1
-rw-r--r--bundle/scripts/benchmark/messages.js1
-rw-r--r--bundle/scripts/benchmark/users.js25
-rw-r--r--cdn/package-lock.json14
-rw-r--r--cdn/package.json2
-rw-r--r--gateway/package-lock.json14
-rw-r--r--gateway/package.json2
-rw-r--r--gateway/src/events/Close.ts41
-rw-r--r--gateway/src/events/Connection.ts2
-rw-r--r--gateway/src/listener/listener.ts115
-rw-r--r--gateway/src/opcodes/Identify.ts116
-rw-r--r--gateway/src/opcodes/LazyRequest.ts132
-rw-r--r--gateway/src/opcodes/PresenceUpdate.ts24
-rw-r--r--gateway/src/schema/Activity.ts39
-rw-r--r--gateway/src/schema/Emoji.ts11
-rw-r--r--gateway/src/schema/LazyRequest.ts2
-rw-r--r--gateway/src/util/WebSocket.ts2
-rw-r--r--gateway/tsconfig.json2
-rw-r--r--util/package-lock.json14
-rw-r--r--util/package.json2
-rw-r--r--util/src/entities/Member.ts18
-rw-r--r--util/src/entities/Session.ts20
-rw-r--r--util/src/entities/User.ts17
-rw-r--r--util/src/interfaces/Activity.ts37
-rw-r--r--util/src/interfaces/Event.ts37
-rw-r--r--util/src/interfaces/Presence.ts4
33 files changed, 500 insertions, 256 deletions
diff --git a/api/package-lock.json b/api/package-lock.json

index d33c54af..c78d3248 100644 --- a/api/package-lock.json +++ b/api/package-lock.json
@@ -29,7 +29,7 @@ "image-size": "^1.0.0", "jsonwebtoken": "^8.5.1", "lambert-server": "^1.2.12", - "missing-native-js-functions": "^1.2.17", + "missing-native-js-functions": "^1.2.18", "morgan": "^1.10.0", "multer": "^1.4.2", "node-fetch": "^2.6.1", @@ -13177,9 +13177,9 @@ } }, "node_modules/missing-native-js-functions": { - "version": "1.2.17", - "resolved": "https://registry.npmjs.org/missing-native-js-functions/-/missing-native-js-functions-1.2.17.tgz", - "integrity": "sha512-Ev48VaLqp/7e7zmQ78oMCeMeZEUDeRRQGXITmiHtS62qJEThBLuKFExQjwu0Yzj9UO4MhN7TvljDsITCTu3fqg==" + "version": "1.2.18", + "resolved": "https://registry.npmjs.org/missing-native-js-functions/-/missing-native-js-functions-1.2.18.tgz", + "integrity": "sha512-TZr1muzDE4kfu0LHDzg63O7m2qW3Gpyc875ki8+YlSRj+4ibZRv0ySQ0cSB06GoBL9ejeehLmkQnybLpp9jYcg==" }, "node_modules/mkdirp": { "version": "0.5.5", @@ -25914,9 +25914,9 @@ } }, "missing-native-js-functions": { - "version": "1.2.17", - "resolved": "https://registry.npmjs.org/missing-native-js-functions/-/missing-native-js-functions-1.2.17.tgz", - "integrity": "sha512-Ev48VaLqp/7e7zmQ78oMCeMeZEUDeRRQGXITmiHtS62qJEThBLuKFExQjwu0Yzj9UO4MhN7TvljDsITCTu3fqg==" + "version": "1.2.18", + "resolved": "https://registry.npmjs.org/missing-native-js-functions/-/missing-native-js-functions-1.2.18.tgz", + "integrity": "sha512-TZr1muzDE4kfu0LHDzg63O7m2qW3Gpyc875ki8+YlSRj+4ibZRv0ySQ0cSB06GoBL9ejeehLmkQnybLpp9jYcg==" }, "mkdirp": { "version": "0.5.5", diff --git a/api/package.json b/api/package.json
index af587cb6..1d1386d2 100644 --- a/api/package.json +++ b/api/package.json
@@ -82,7 +82,7 @@ "image-size": "^1.0.0", "jsonwebtoken": "^8.5.1", "lambert-server": "^1.2.12", - "missing-native-js-functions": "^1.2.17", + "missing-native-js-functions": "^1.2.18", "morgan": "^1.10.0", "multer": "^1.4.2", "node-fetch": "^2.6.1", diff --git a/api/src/routes/users/@me/settings.ts b/api/src/routes/users/@me/settings.ts
index 4e014126..b22b72fb 100644 --- a/api/src/routes/users/@me/settings.ts +++ b/api/src/routes/users/@me/settings.ts
@@ -10,8 +10,9 @@ router.patch("/", route({ body: "UserSettingsSchema" }), async (req: Request, re const body = req.body as UserSettings; if (body.locale === "en") body.locale = "en-US"; // fix discord client crash on unkown locale - // only users can update user settings - await User.update({ id: req.user_id, bot: false }, { settings: body }); + const user = await User.findOneOrFail({ id: req.user_id, bot: false }); + user.settings = { ...user.settings, ...body }; + await user.save(); res.sendStatus(204); }); diff --git a/api/src/util/Instance.ts b/api/src/util/Instance.ts
index 7dcd126e..6bddfa98 100644 --- a/api/src/util/Instance.ts +++ b/api/src/util/Instance.ts
@@ -1,4 +1,4 @@ -import { Config, Guild } from "@fosscord/util"; +import { Config, Guild, Session } from "@fosscord/util"; export async function initInstance() { // TODO: clean up database and delete tombstone data @@ -15,4 +15,7 @@ export async function initInstance() { await Config.set({ guild: { autoJoin: { guilds: [guild.id] } } }); } } + + // TODO: do no clear sessions for instance cluster + await Session.delete({}); } diff --git a/api/tests/routes.test.ts b/api/tests/routes.test.ts
index 2c265ee3..35d74a94 100644 --- a/api/tests/routes.test.ts +++ b/api/tests/routes.test.ts
@@ -56,9 +56,7 @@ beforeAll(async (done) => { const response = await request("/auth/register", { body: { fingerprint: "805826570869932034.wR8vi8lGlFBJerErO9LG5NViJFw", - email: "test@example.com", username: "tester", - password: "wtp9gep9gw", invite: null, consent: true, date_of_birth: "2000-01-01", diff --git a/bundle/package-lock.json b/bundle/package-lock.json
index 506b2bf3..3461ec95 100644 --- a/bundle/package-lock.json +++ b/bundle/package-lock.json
@@ -37,7 +37,7 @@ "jsonwebtoken": "^8.5.1", "lambert-db": "^1.2.3", "lambert-server": "^1.2.11", - "missing-native-js-functions": "^1.2.17", + "missing-native-js-functions": "^1.2.18", "morgan": "^1.10.0", "multer": "^1.4.2", "nanocolors": "^0.2.12", @@ -7564,9 +7564,9 @@ } }, "node_modules/missing-native-js-functions": { - "version": "1.2.17", - "resolved": "https://registry.npmjs.org/missing-native-js-functions/-/missing-native-js-functions-1.2.17.tgz", - "integrity": "sha512-Ev48VaLqp/7e7zmQ78oMCeMeZEUDeRRQGXITmiHtS62qJEThBLuKFExQjwu0Yzj9UO4MhN7TvljDsITCTu3fqg==" + "version": "1.2.18", + "resolved": "https://registry.npmjs.org/missing-native-js-functions/-/missing-native-js-functions-1.2.18.tgz", + "integrity": "sha512-TZr1muzDE4kfu0LHDzg63O7m2qW3Gpyc875ki8+YlSRj+4ibZRv0ySQ0cSB06GoBL9ejeehLmkQnybLpp9jYcg==" }, "node_modules/mkdirp": { "version": "0.5.5", @@ -16752,9 +16752,9 @@ } }, "missing-native-js-functions": { - "version": "1.2.17", - "resolved": "https://registry.npmjs.org/missing-native-js-functions/-/missing-native-js-functions-1.2.17.tgz", - "integrity": "sha512-Ev48VaLqp/7e7zmQ78oMCeMeZEUDeRRQGXITmiHtS62qJEThBLuKFExQjwu0Yzj9UO4MhN7TvljDsITCTu3fqg==" + "version": "1.2.18", + "resolved": "https://registry.npmjs.org/missing-native-js-functions/-/missing-native-js-functions-1.2.18.tgz", + "integrity": "sha512-TZr1muzDE4kfu0LHDzg63O7m2qW3Gpyc875ki8+YlSRj+4ibZRv0ySQ0cSB06GoBL9ejeehLmkQnybLpp9jYcg==" }, "mkdirp": { "version": "0.5.5", diff --git a/bundle/package.json b/bundle/package.json
index b3e5c39a..c24902fb 100644 --- a/bundle/package.json +++ b/bundle/package.json
@@ -37,6 +37,7 @@ "@types/jest": "^27.0.1", "@types/jest-expect-message": "^1.0.3", "@types/jsonwebtoken": "^8.5.0", + "@types/morgan": "^1.9.3", "@types/multer": "^1.4.7", "@types/node": "^14.17.9", "@types/node-fetch": "^2.5.12", @@ -51,10 +52,13 @@ "ts-node-dev": "^1.1.6", "ts-patch": "^1.4.4", "typescript": "^4.2.3", - "typescript-json-schema": "0.50.1", - "@types/morgan": "^1.9.3" + "typescript-json-schema": "0.50.1" }, "dependencies": { + "@aws-sdk/client-s3": "^3.36.1", + "@aws-sdk/node-http-handler": "^3.36.0", + "@babel/preset-env": "^7.15.8", + "@babel/preset-typescript": "^7.15.0", "ajv": "8.6.2", "ajv-formats": "^2.1.1", "amqplib": "^0.8.0", @@ -63,6 +67,7 @@ "bcrypt": "^5.0.1", "body-parser": "^1.19.0", "btoa": "^1.2.1", + "cheerio": "^1.0.0-rc.10", "dotenv": "^8.2.0", "exif-be-gone": "^1.2.0", "express": "^4.17.1", @@ -78,7 +83,7 @@ "jsonwebtoken": "^8.5.1", "lambert-db": "^1.2.3", "lambert-server": "^1.2.11", - "missing-native-js-functions": "^1.2.17", + "missing-native-js-functions": "^1.2.18", "morgan": "^1.10.0", "multer": "^1.4.2", "nanocolors": "^0.2.12", @@ -92,11 +97,6 @@ "typeorm": "^0.2.37", "typescript": "^4.1.2", "typescript-json-schema": "^0.50.1", - "ws": "^7.4.2", - "cheerio": "^1.0.0-rc.10", - "@aws-sdk/client-s3": "^3.36.1", - "@aws-sdk/node-http-handler": "^3.36.0", - "@babel/preset-env": "^7.15.8", - "@babel/preset-typescript": "^7.15.0" + "ws": "^7.4.2" } -} \ No newline at end of file +} diff --git a/bundle/scripts/benchmark/connections.js b/bundle/scripts/benchmark/connections.js
index efc1bcb6..2a4125b4 100644 --- a/bundle/scripts/benchmark/connections.js +++ b/bundle/scripts/benchmark/connections.js
@@ -1,3 +1,4 @@ +require("dotenv").config(); const cluster = require("cluster"); const WebSocket = require("ws"); const endpoint = process.env.GATEWAY || "ws://localhost:3001"; diff --git a/bundle/scripts/benchmark/messages.js b/bundle/scripts/benchmark/messages.js deleted file mode 100644
index 70b786d1..00000000 --- a/bundle/scripts/benchmark/messages.js +++ /dev/null
@@ -1 +0,0 @@ -// TODO diff --git a/bundle/scripts/benchmark/users.js b/bundle/scripts/benchmark/users.js new file mode 100644
index 00000000..bce67bf4 --- /dev/null +++ b/bundle/scripts/benchmark/users.js
@@ -0,0 +1,25 @@ +require("dotenv").config(); +const fetch = require("node-fetch"); +const count = Number(process.env.COUNT) || 50; +const endpoint = process.env.API || "http://localhost:3001"; + +async function main() { + for (let i = 0; i < count; i++) { + fetch(`${endpoint}/api/auth/register`, { + method: "POST", + body: JSON.stringify({ + fingerprint: `${i}.wR8vi8lGlFBJerErO9LG5NViJFw`, + username: `test${i}`, + invite: null, + consent: true, + date_of_birth: "2000-01-01", + gift_code_sku_id: null, + captcha_key: null, + }), + headers: { "content-type": "application/json" }, + }); + console.log(i); + } +} + +main(); diff --git a/cdn/package-lock.json b/cdn/package-lock.json
index 32a1e366..74e1ce52 100644 --- a/cdn/package-lock.json +++ b/cdn/package-lock.json
@@ -25,7 +25,7 @@ "jest": "^27.0.6", "lambert-db": "^1.2.3", "lambert-server": "^1.2.11", - "missing-native-js-functions": "^1.2.17", + "missing-native-js-functions": "^1.2.18", "multer": "^1.4.2", "nanocolors": "^0.2.12", "node-fetch": "^2.6.1", @@ -11954,9 +11954,9 @@ "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" }, "node_modules/missing-native-js-functions": { - "version": "1.2.17", - "resolved": "https://registry.npmjs.org/missing-native-js-functions/-/missing-native-js-functions-1.2.17.tgz", - "integrity": "sha512-Ev48VaLqp/7e7zmQ78oMCeMeZEUDeRRQGXITmiHtS62qJEThBLuKFExQjwu0Yzj9UO4MhN7TvljDsITCTu3fqg==" + "version": "1.2.18", + "resolved": "https://registry.npmjs.org/missing-native-js-functions/-/missing-native-js-functions-1.2.18.tgz", + "integrity": "sha512-TZr1muzDE4kfu0LHDzg63O7m2qW3Gpyc875ki8+YlSRj+4ibZRv0ySQ0cSB06GoBL9ejeehLmkQnybLpp9jYcg==" }, "node_modules/mkdirp": { "version": "0.5.5", @@ -22859,9 +22859,9 @@ "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" }, "missing-native-js-functions": { - "version": "1.2.17", - "resolved": "https://registry.npmjs.org/missing-native-js-functions/-/missing-native-js-functions-1.2.17.tgz", - "integrity": "sha512-Ev48VaLqp/7e7zmQ78oMCeMeZEUDeRRQGXITmiHtS62qJEThBLuKFExQjwu0Yzj9UO4MhN7TvljDsITCTu3fqg==" + "version": "1.2.18", + "resolved": "https://registry.npmjs.org/missing-native-js-functions/-/missing-native-js-functions-1.2.18.tgz", + "integrity": "sha512-TZr1muzDE4kfu0LHDzg63O7m2qW3Gpyc875ki8+YlSRj+4ibZRv0ySQ0cSB06GoBL9ejeehLmkQnybLpp9jYcg==" }, "mkdirp": { "version": "0.5.5", diff --git a/cdn/package.json b/cdn/package.json
index 0d4d4619..852bfcdc 100644 --- a/cdn/package.json +++ b/cdn/package.json
@@ -51,7 +51,7 @@ "jest": "^27.0.6", "lambert-db": "^1.2.3", "lambert-server": "^1.2.11", - "missing-native-js-functions": "^1.2.17", + "missing-native-js-functions": "^1.2.18", "multer": "^1.4.2", "nanocolors": "^0.2.12", "node-fetch": "^2.6.1", diff --git a/gateway/package-lock.json b/gateway/package-lock.json
index df673398..4dbfbc49 100644 --- a/gateway/package-lock.json +++ b/gateway/package-lock.json
@@ -15,7 +15,7 @@ "dotenv": "^8.2.0", "jsonwebtoken": "^8.5.1", "lambert-server": "^1.2.11", - "missing-native-js-functions": "^1.2.17", + "missing-native-js-functions": "^1.2.18", "node-fetch": "^2.6.1", "typeorm": "^0.2.37", "ws": "^7.4.2" @@ -7766,9 +7766,9 @@ } }, "node_modules/missing-native-js-functions": { - "version": "1.2.17", - "resolved": "https://registry.npmjs.org/missing-native-js-functions/-/missing-native-js-functions-1.2.17.tgz", - "integrity": "sha512-Ev48VaLqp/7e7zmQ78oMCeMeZEUDeRRQGXITmiHtS62qJEThBLuKFExQjwu0Yzj9UO4MhN7TvljDsITCTu3fqg==" + "version": "1.2.18", + "resolved": "https://registry.npmjs.org/missing-native-js-functions/-/missing-native-js-functions-1.2.18.tgz", + "integrity": "sha512-TZr1muzDE4kfu0LHDzg63O7m2qW3Gpyc875ki8+YlSRj+4ibZRv0ySQ0cSB06GoBL9ejeehLmkQnybLpp9jYcg==" }, "node_modules/mkdirp": { "version": "1.0.4", @@ -14773,9 +14773,9 @@ } }, "missing-native-js-functions": { - "version": "1.2.17", - "resolved": "https://registry.npmjs.org/missing-native-js-functions/-/missing-native-js-functions-1.2.17.tgz", - "integrity": "sha512-Ev48VaLqp/7e7zmQ78oMCeMeZEUDeRRQGXITmiHtS62qJEThBLuKFExQjwu0Yzj9UO4MhN7TvljDsITCTu3fqg==" + "version": "1.2.18", + "resolved": "https://registry.npmjs.org/missing-native-js-functions/-/missing-native-js-functions-1.2.18.tgz", + "integrity": "sha512-TZr1muzDE4kfu0LHDzg63O7m2qW3Gpyc875ki8+YlSRj+4ibZRv0ySQ0cSB06GoBL9ejeehLmkQnybLpp9jYcg==" }, "mkdirp": { "version": "1.0.4", diff --git a/gateway/package.json b/gateway/package.json
index d630c56b..c7db2160 100644 --- a/gateway/package.json +++ b/gateway/package.json
@@ -31,7 +31,7 @@ "dotenv": "^8.2.0", "jsonwebtoken": "^8.5.1", "lambert-server": "^1.2.11", - "missing-native-js-functions": "^1.2.17", + "missing-native-js-functions": "^1.2.18", "node-fetch": "^2.6.1", "typeorm": "^0.2.37", "ws": "^7.4.2" diff --git a/gateway/src/events/Close.ts b/gateway/src/events/Close.ts
index 5c1bd292..5b7c512c 100644 --- a/gateway/src/events/Close.ts +++ b/gateway/src/events/Close.ts
@@ -1,13 +1,46 @@ import { WebSocket } from "@fosscord/gateway"; -import { Session } from "@fosscord/util"; +import { + emitEvent, + PresenceUpdateEvent, + PrivateSessionProjection, + Session, + SessionsReplace, + User, +} from "@fosscord/util"; export async function Close(this: WebSocket, code: number, reason: string) { console.log("[WebSocket] closed", code, reason); - if (this.session_id) await Session.delete({ session_id: this.session_id }); if (this.heartbeatTimeout) clearTimeout(this.heartbeatTimeout); if (this.readyTimeout) clearTimeout(this.readyTimeout); - this.deflate?.close(); - this.removeAllListeners(); + + if (this.session_id) { + await Session.delete({ session_id: this.session_id }); + const sessions = await Session.find({ + where: { user_id: this.user_id }, + select: PrivateSessionProjection, + }); + await emitEvent({ + event: "SESSIONS_REPLACE", + user_id: this.user_id, + data: sessions, + } as SessionsReplace); + const session = sessions.first() || { + activities: [], + client_info: {}, + status: "offline", + }; + + await 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); + } } diff --git a/gateway/src/events/Connection.ts b/gateway/src/events/Connection.ts
index 9bb034f0..4954cd08 100644 --- a/gateway/src/events/Connection.ts +++ b/gateway/src/events/Connection.ts
@@ -8,7 +8,6 @@ import { Close } from "./Close"; import { Message } from "./Message"; import { createDeflate } from "zlib"; import { URL } from "url"; -import { Session } from "@fosscord/util"; var erlpack: any; try { erlpack = require("@yukikaze-bot/erlpack"); @@ -57,6 +56,7 @@ export async function Connection( } socket.events = {}; + socket.member_events = {}; socket.permissions = {}; socket.sequence = 0; diff --git a/gateway/src/listener/listener.ts b/gateway/src/listener/listener.ts
index c5b1a576..79659a1f 100644 --- a/gateway/src/listener/listener.ts +++ b/gateway/src/listener/listener.ts
@@ -6,6 +6,9 @@ import { EventOpts, ListenEventOpts, Member, + EVENTEnum, + Relationship, + RelationshipType, } from "@fosscord/util"; import { OPCODES } from "../util/Constants"; import { Send } from "../util/Send"; @@ -21,22 +24,45 @@ import { Recipient } from "@fosscord/util"; // Sharding: calculate if the current shard id matches the formula: shard_id = (guild_id >> 22) % num_shards // https://discord.com/developers/docs/topics/gateway#sharding +export function handlePresenceUpdate( + this: WebSocket, + { event, acknowledge, data }: EventOpts +) { + acknowledge?.(); + if (event === EVENTEnum.PresenceUpdate) { + return Send(this, { + op: OPCODES.Dispatch, + t: event, + d: data, + s: this.sequence++, + }); + } +} + // TODO: use already queried guilds/channels of Identify and don't fetch them again export async function setupListener(this: WebSocket) { - const members = await Member.find({ - where: { id: this.user_id }, - relations: ["guild", "guild.channels"], - }); + const [members, recipients, relationships] = await Promise.all([ + Member.find({ + where: { id: this.user_id }, + relations: ["guild", "guild.channels"], + }), + Recipient.find({ + where: { user_id: this.user_id, closed: false }, + relations: ["channel"], + }), + Relationship.find({ + from_id: this.user_id, + type: RelationshipType.friends, + }), + ]); + const guilds = members.map((x) => x.guild); - const recipients = await Recipient.find({ - where: { user_id: this.user_id, closed: false }, - relations: ["channel"], - }); const dm_channels = recipients.map((x) => x.channel); const opts: { acknowledge: boolean; channel?: AMQChannel } = { acknowledge: true, }; + this.listen_options = opts; const consumer = consume.bind(this); if (RabbitMQ.connection) { @@ -47,45 +73,44 @@ export async function setupListener(this: WebSocket) { this.events[this.user_id] = await listenEvent(this.user_id, consumer, opts); - for (const channel of dm_channels) { + relationships.forEach(async (relationship) => { + this.events[relationship.to_id] = await listenEvent( + relationship.to_id, + handlePresenceUpdate.bind(this), + opts + ); + }); + + dm_channels.forEach(async (channel) => { this.events[channel.id] = await listenEvent(channel.id, consumer, opts); - } + }); - for (const guild of guilds) { - // contains guild and dm channels + guilds.forEach(async (guild) => { + const permission = await getPermission(this.user_id, guild.id); + this.permissions[guild.id] = permission; + this.events[guild.id] = await listenEvent(guild.id, consumer, opts); - getPermission(this.user_id, guild.id) - .then(async (x) => { - this.permissions[guild.id] = x; - this.listeners; - this.events[guild.id] = await listenEvent( - guild.id, + guild.channels.forEach(async (channel) => { + if ( + permission + .overwriteChannel(channel.permission_overwrites!) + .has("VIEW_CHANNEL") + ) { + this.events[channel.id] = await listenEvent( + channel.id, consumer, opts ); - - for (const channel of guild.channels) { - if ( - x - .overwriteChannel(channel.permission_overwrites!) - .has("VIEW_CHANNEL") - ) { - this.events[channel.id] = await listenEvent( - channel.id, - consumer, - opts - ); - } - } - }) - .catch((e) => - console.log("couldn't get permission for guild " + guild, e) - ); - } + } + }); + }); this.once("close", () => { if (opts.channel) opts.channel.close(); - else Object.values(this.events).forEach((x) => x()); + else { + Object.values(this.events).forEach((x) => x()); + Object.values(this.member_events).forEach((x) => x()); + } }); } @@ -97,10 +122,23 @@ async function consume(this: WebSocket, opts: EventOpts) { const consumer = consume.bind(this); const listenOpts = opts as ListenEventOpts; + opts.acknowledge?.(); // console.log("event", event); // subscription managment switch (event) { + case "GUILD_MEMBER_REMOVE": + this.member_events[data.user.id]?.(); + delete this.member_events[data.user.id]; + case "GUILD_MEMBER_ADD": + if (this.member_events[data.user.id]) break; // already subscribed + this.member_events[data.user.id] = await listenEvent( + data.user.id, + handlePresenceUpdate.bind(this), + this.listen_options + ); + break; + case "RELATIONSHIP_REMOVE": case "CHANNEL_DELETE": case "GUILD_DELETE": delete this.events[id]; @@ -196,5 +234,4 @@ async function consume(this: WebSocket, opts: EventOpts) { d: data, s: this.sequence++, }); - opts.acknowledge?.(); } diff --git a/gateway/src/opcodes/Identify.ts b/gateway/src/opcodes/Identify.ts
index 006dc83c..bd7fc894 100644 --- a/gateway/src/opcodes/Identify.ts +++ b/gateway/src/opcodes/Identify.ts
@@ -13,6 +13,11 @@ import { PrivateUserProjection, ReadState, Application, + emitEvent, + SessionsReplace, + PrivateSessionProjection, + MemberPrivateProjection, + PresenceUpdateEvent, } from "@fosscord/util"; import { Send } from "../util/Send"; import { CLOSECODES, OPCODES } from "../util/Constants"; @@ -43,11 +48,56 @@ export async function onIdentify(this: WebSocket, data: Payload) { } this.user_id = decoded.id; - const user = await User.findOneOrFail({ - where: { id: this.user_id }, - relations: ["relationships", "relationships.to"], - select: [...PrivateUserProjection, "relationships"], - }); + 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"], + select: [...PrivateUserProjection, "relationships"], + }), + ReadState.find({ 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 + 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 || "online", //does the session always start as online? + client_info: { + //TODO read from identity + client: "desktop", + os: identify.properties?.os, + version: 0, + }, + }).save(), + Application.findOne({ id: this.user_id }), + ]); + if (!user) return this.close(CLOSECODES.Authentication_failed); if (!identify.intents) identify.intents = BigInt("0b11111111111111"); @@ -68,19 +118,6 @@ export async function onIdentify(this: WebSocket, data: Payload) { } var users: PublicUser[] = []; - const members = await Member.find({ - where: { id: this.user_id }, - relations: [ - "guild", - "guild.channels", - "guild.emojis", - "guild.emojis.user", - "guild.roles", - "guild.stickers", - "user", - "roles", - ], - }); const merged_members = members.map((x: Member) => { return [ { @@ -112,11 +149,6 @@ export async function onIdentify(this: WebSocket, data: Payload) { const user_guild_settings_entries = members.map((x) => x.settings); - const recipients = await Recipient.find({ - where: { user_id: this.user_id, closed: false }, - relations: ["channel", "channel.recipients", "channel.recipients.user"], - // TODO: public user selection - }); const channels = recipients.map((x) => { // @ts-ignore x.channel.recipients = x.channel.recipients?.map((x) => x.user); @@ -144,24 +176,28 @@ export async function onIdentify(this: WebSocket, data: Payload) { users.push(public_related_user); } - const session_id = genSessionId(); - this.session_id = session_id; //Set the session of the WebSocket object - const session = new Session({ - user_id: this.user_id, - session_id: session_id, - status: "online", //does the session always start as online? - client_info: { - //TODO read from identity - client: "desktop", - os: "linux", - version: 0, - }, + 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); }); - //We save the session and we delete it when the websocket is closed - await session.save(); - - const read_states = await ReadState.find({ user_id: this.user_id }); read_states.forEach((s: any) => { s.id = s.channel_id; delete s.user_id; @@ -192,7 +228,7 @@ export async function onIdentify(this: WebSocket, data: Payload) { const d: ReadyEventData = { v: 8, - application: await Application.findOne({ id: this.user_id }), + application, user: privateUser, user_settings: user.settings, // @ts-ignore diff --git a/gateway/src/opcodes/LazyRequest.ts b/gateway/src/opcodes/LazyRequest.ts
index f5fd561a..c304dfe7 100644 --- a/gateway/src/opcodes/LazyRequest.ts +++ b/gateway/src/opcodes/LazyRequest.ts
@@ -1,46 +1,55 @@ import { + EVENTEnum, + EventOpts, getPermission, + listenEvent, Member, - PublicMemberProjection, Role, } from "@fosscord/util"; import { LazyRequest } from "../schema/LazyRequest"; import { Send } from "../util/Send"; import { OPCODES } from "../util/Constants"; -import { WebSocket, Payload } from "@fosscord/gateway"; +import { WebSocket, Payload, handlePresenceUpdate } from "@fosscord/gateway"; import { check } from "./instanceOf"; import "missing-native-js-functions"; +import { getRepository } from "typeorm"; +import "missing-native-js-functions"; -// TODO: check permission and only show roles/members that have access to this channel +// TODO: only show roles/members that have access to this channel // TODO: config: to list all members (even those who are offline) sorted by role, or just those who are online // TODO: rewrite typeorm -export async function onLazyRequest(this: WebSocket, { d }: Payload) { - // TODO: check data - check.call(this, LazyRequest, d); - const { guild_id, typing, channels, activities } = d as LazyRequest; - - const permissions = await getPermission(this.user_id, guild_id); - permissions.hasThrow("VIEW_CHANNEL"); - - var members = await Member.find({ - where: { guild_id: guild_id }, - relations: ["roles", "user"], - select: PublicMemberProjection, - }); +async function getMembers(guild_id: string, range: [number, number]) { + if (!Array.isArray(range) || range.length !== 2) { + throw new Error("range is not a valid array"); + } + // TODO: wait for typeorm to implement ordering for .find queries https://github.com/typeorm/typeorm/issues/2620 - const roles = await Role.find({ - where: { guild_id: guild_id }, - order: { - position: "DESC", - }, - }); + let members = await getRepository(Member) + .createQueryBuilder("member") + .where("member.guild_id = :guild_id", { guild_id }) + .leftJoinAndSelect("member.roles", "role") + .leftJoinAndSelect("member.user", "user") + .leftJoinAndSelect("user.sessions", "session") + .addSelect( + "CASE WHEN session.status = 'offline' THEN 0 ELSE 1 END", + "_status" + ) + .orderBy("role.position", "DESC") + .addOrderBy("_status", "DESC") + .addOrderBy("user.username", "ASC") + .offset(Number(range[0]) || 0) + .limit(Number(range[1]) || 100) + .getMany(); const groups = [] as any[]; - var member_count = 0; const items = []; + const member_roles = members + .map((m) => m.roles) + .flat() + .unique((r) => r.id); - for (const role of roles) { + for (const role of member_roles) { // @ts-ignore const [role_members, other_members] = partition(members, (m: Member) => m.roles.find((r) => r.id === role.id) @@ -54,35 +63,86 @@ export async function onLazyRequest(this: WebSocket, { d }: Payload) { groups.push(group); for (const member of role_members) { - member.roles = member.roles.filter((x: Role) => x.id !== guild_id); + const roles = member.roles + .filter((x: Role) => x.id !== guild_id) + .map((x: Role) => x.id); + + const session = member.user.sessions.first(); + + // TODO: properly mock/hide offline/invisible status items.push({ member: { ...member, - roles: member.roles.map((x: Role) => x.id), + roles, + user: { ...member.user, sessions: undefined }, + presence: { + ...session, + activities: session?.activities || [], + user: { id: member.user.id }, + }, }, }); } members = other_members; - member_count += role_members.length; } + return { + items, + groups, + range, + members: items.map((x) => x.member).filter((x) => x), + }; +} + +export async function onLazyRequest(this: WebSocket, { d }: Payload) { + // TODO: check data + check.call(this, LazyRequest, d); + const { guild_id, typing, channels, activities } = d as LazyRequest; + + const channel_id = Object.keys(channels || {}).first(); + if (!channel_id) return; + + const permissions = await getPermission(this.user_id, guild_id, channel_id); + permissions.hasThrow("VIEW_CHANNEL"); + + const ranges = channels![channel_id]; + if (!Array.isArray(ranges)) throw new Error("Not a valid Array"); + + const member_count = await Member.count({ guild_id }); + const ops = await Promise.all(ranges.map((x) => getMembers(guild_id, x))); + + // TODO: unsubscribe member_events that are not in op.members + + ops.forEach((op) => { + op.members.forEach(async (member) => { + if (this.events[member.user.id]) return; // already subscribed as friend + if (this.member_events[member.user.id]) return; // already subscribed in member list + this.member_events[member.user.id] = await listenEvent( + member.user.id, + handlePresenceUpdate.bind(this), + this.listen_options + ); + }); + }); + return Send(this, { op: OPCODES.Dispatch, s: this.sequence++, t: "GUILD_MEMBER_LIST_UPDATE", d: { - ops: [ - { - range: [0, 99], - op: "SYNC", - items, - }, - ], - online_count: member_count, // TODO count online count + ops: ops.map((x) => ({ + items: x.items, + op: "SYNC", + range: x.range, + })), + online_count: member_count, member_count, id: "everyone", guild_id, - groups, + groups: ops + .map((x) => x.groups) + .flat() + .unique(), }, }); } diff --git a/gateway/src/opcodes/PresenceUpdate.ts b/gateway/src/opcodes/PresenceUpdate.ts
index 53d7b9d2..415df6ee 100644 --- a/gateway/src/opcodes/PresenceUpdate.ts +++ b/gateway/src/opcodes/PresenceUpdate.ts
@@ -1,5 +1,25 @@ import { WebSocket, Payload } from "@fosscord/gateway"; +import { emitEvent, PresenceUpdateEvent, Session, User } from "@fosscord/util"; +import { ActivitySchema } from "../schema/Activity"; +import { check } from "./instanceOf"; -export function onPresenceUpdate(this: WebSocket, data: Payload) { - // return this.close(CLOSECODES.Unknown_error); +export async function onPresenceUpdate(this: WebSocket, { d }: Payload) { + check.call(this, ActivitySchema, d); + const presence = d as ActivitySchema; + + await Session.update( + { session_id: this.session_id }, + { status: presence.status, activities: presence.activities } + ); + + await emitEvent({ + event: "PRESENCE_UPDATE", + user_id: this.user_id, + data: { + user: await User.getPublicUser(this.user_id), + activities: presence.activities, + client_status: {}, // TODO: + status: presence.status, + }, + } as PresenceUpdateEvent); } diff --git a/gateway/src/schema/Activity.ts b/gateway/src/schema/Activity.ts
index f1665efd..e8763046 100644 --- a/gateway/src/schema/Activity.ts +++ b/gateway/src/schema/Activity.ts
@@ -1,4 +1,4 @@ -import { EmojiSchema } from "./Emoji"; +import { Activity, Status } from "@fosscord/util"; export const ActivitySchema = { afk: Boolean, @@ -47,40 +47,7 @@ export const ActivitySchema = { export interface ActivitySchema { afk: boolean; - status: string; - activities?: [ - { - name: string; // the activity's name - type: number; // activity type // TODO: check if its between range 0-5 - url?: string; // stream url, is validated when type is 1 - created_at?: number; // unix timestamp of when the activity was added to the user's session - timestamps?: { - // unix timestamps for start and/or end of the game - start: number; - end: number; - }; - application_id?: string; // application id for the game - details?: string; - state?: string; - emoji?: EmojiSchema; - party?: { - id?: string; - size?: [number]; // used to show the party's current and maximum size // TODO: array length 2 - }; - assets?: { - large_image?: string; // the id for a large asset of the activity, usually a snowflake - large_text?: string; // text displayed when hovering over the large image of the activity - small_image?: string; // the id for a small asset of the activity, usually a snowflake - small_text?: string; // text displayed when hovering over the small image of the activity - }; - secrets?: { - join?: string; // the secret for joining a party - spectate?: string; // the secret for spectating a game - match?: string; // the secret for a specific instanced match - }; - instance?: boolean; - flags: string; // activity flags OR d together, describes what the payload includes - } - ]; + status: Status; + activities?: Activity[]; since?: number; // unix time (in milliseconds) of when the client went idle, or null if the client is not idle } diff --git a/gateway/src/schema/Emoji.ts b/gateway/src/schema/Emoji.ts deleted file mode 100644
index 413b8359..00000000 --- a/gateway/src/schema/Emoji.ts +++ /dev/null
@@ -1,11 +0,0 @@ -export const EmojiSchema = { - name: String, // the name of the emoji - $id: String, // the id of the emoji - animated: Boolean, // whether this emoji is animated -}; - -export interface EmojiSchema { - name: string; - id?: string; - animated: Boolean; -} diff --git a/gateway/src/schema/LazyRequest.ts b/gateway/src/schema/LazyRequest.ts
index 7c828ac6..1fe658bb 100644 --- a/gateway/src/schema/LazyRequest.ts +++ b/gateway/src/schema/LazyRequest.ts
@@ -1,6 +1,6 @@ export interface LazyRequest { guild_id: string; - channels?: Record<string, [number, number]>; + channels?: Record<string, [number, number][]>; activities?: boolean; threads?: boolean; typing?: true; diff --git a/gateway/src/util/WebSocket.ts b/gateway/src/util/WebSocket.ts
index 49626b2a..e3313f40 100644 --- a/gateway/src/util/WebSocket.ts +++ b/gateway/src/util/WebSocket.ts
@@ -17,4 +17,6 @@ export interface WebSocket extends WS { sequence: number; permissions: Record<string, Permissions>; events: Record<string, Function>; + member_events: Record<string, Function>; + listen_options: any; } diff --git a/gateway/tsconfig.json b/gateway/tsconfig.json
index 2ad38f93..b6ae9455 100644 --- a/gateway/tsconfig.json +++ b/gateway/tsconfig.json
@@ -27,7 +27,7 @@ // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ /* Strict Type-Checking Options */ - "strict": false /* Enable all strict type-checking options. */, + "strict": true /* Enable all strict type-checking options. */, "noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */, "strictNullChecks": true /* Enable strict null checks. */, // "strictFunctionTypes": true, /* Enable strict checking of function types. */ diff --git a/util/package-lock.json b/util/package-lock.json
index b5683f14..a5d88518 100644 --- a/util/package-lock.json +++ b/util/package-lock.json
@@ -13,7 +13,7 @@ "amqplib": "^0.8.0", "jsonwebtoken": "^8.5.1", "lambert-server": "^1.2.12", - "missing-native-js-functions": "^1.2.17", + "missing-native-js-functions": "^1.2.18", "multer": "^1.4.3", "nanocolors": "^0.2.12", "node-fetch": "^2.6.1", @@ -4336,9 +4336,9 @@ } }, "node_modules/missing-native-js-functions": { - "version": "1.2.17", - "resolved": "https://registry.npmjs.org/missing-native-js-functions/-/missing-native-js-functions-1.2.17.tgz", - "integrity": "sha512-Ev48VaLqp/7e7zmQ78oMCeMeZEUDeRRQGXITmiHtS62qJEThBLuKFExQjwu0Yzj9UO4MhN7TvljDsITCTu3fqg==" + "version": "1.2.18", + "resolved": "https://registry.npmjs.org/missing-native-js-functions/-/missing-native-js-functions-1.2.18.tgz", + "integrity": "sha512-TZr1muzDE4kfu0LHDzg63O7m2qW3Gpyc875ki8+YlSRj+4ibZRv0ySQ0cSB06GoBL9ejeehLmkQnybLpp9jYcg==" }, "node_modules/mkdirp": { "version": "0.5.5", @@ -10101,9 +10101,9 @@ } }, "missing-native-js-functions": { - "version": "1.2.17", - "resolved": "https://registry.npmjs.org/missing-native-js-functions/-/missing-native-js-functions-1.2.17.tgz", - "integrity": "sha512-Ev48VaLqp/7e7zmQ78oMCeMeZEUDeRRQGXITmiHtS62qJEThBLuKFExQjwu0Yzj9UO4MhN7TvljDsITCTu3fqg==" + "version": "1.2.18", + "resolved": "https://registry.npmjs.org/missing-native-js-functions/-/missing-native-js-functions-1.2.18.tgz", + "integrity": "sha512-TZr1muzDE4kfu0LHDzg63O7m2qW3Gpyc875ki8+YlSRj+4ibZRv0ySQ0cSB06GoBL9ejeehLmkQnybLpp9jYcg==" }, "mkdirp": { "version": "0.5.5", diff --git a/util/package.json b/util/package.json
index dba9f8e0..4e261abe 100644 --- a/util/package.json +++ b/util/package.json
@@ -41,7 +41,7 @@ "amqplib": "^0.8.0", "jsonwebtoken": "^8.5.1", "lambert-server": "^1.2.12", - "missing-native-js-functions": "^1.2.17", + "missing-native-js-functions": "^1.2.18", "multer": "^1.4.3", "nanocolors": "^0.2.12", "node-fetch": "^2.6.1", diff --git a/util/src/entities/Member.ts b/util/src/entities/Member.ts
index 12b0b49a..0f7be2a7 100644 --- a/util/src/entities/Member.ts +++ b/util/src/entities/Member.ts
@@ -26,6 +26,22 @@ import { BaseClassWithoutId } from "./BaseClass"; import { Ban, PublicGuildRelations } from "."; import { DiscordApiErrors } from "../util/Constants"; +export const MemberPrivateProjection: (keyof Member)[] = [ + "id", + "guild", + "guild_id", + "deaf", + "joined_at", + "last_message_id", + "mute", + "nick", + "pending", + "premium_since", + "roles", + "settings", + "user", +]; + @Entity("members") @Index(["id", "guild_id"], { unique: true }) export class Member extends BaseClassWithoutId { @@ -81,7 +97,7 @@ export class Member extends BaseClassWithoutId { @Column() pending: boolean; - @Column({ type: "simple-json" }) + @Column({ type: "simple-json", select: false }) settings: UserGuildSettings; @Column({ nullable: true }) diff --git a/util/src/entities/Session.ts b/util/src/entities/Session.ts
index 7cc325f5..ac5313f1 100644 --- a/util/src/entities/Session.ts +++ b/util/src/entities/Session.ts
@@ -1,6 +1,8 @@ import { User } from "./User"; import { BaseClass } from "./BaseClass"; import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm"; +import { Status } from "../interfaces/Status"; +import { Activity } from "../interfaces/Activity"; //TODO we need to remove all sessions on server start because if the server crashes without closing websockets it won't delete them @@ -17,11 +19,13 @@ export class Session extends BaseClass { user: User; //TODO check, should be 32 char long hex string - @Column({ nullable: false }) + @Column({ nullable: false, select: false }) session_id: string; - activities: []; //TODO + @Column({ type: "simple-json", nullable: true }) + activities: Activity[] = []; + // TODO client_status @Column({ type: "simple-json", select: false }) client_info: { client: string; @@ -29,6 +33,14 @@ export class Session extends BaseClass { version: number; }; - @Column({ nullable: false }) - status: string; //TODO enum + @Column({ nullable: false, type: "varchar" }) + status: Status; //TODO enum } + +export const PrivateSessionProjection: (keyof Session)[] = [ + "user_id", + "session_id", + "activities", + "client_info", + "status", +]; diff --git a/util/src/entities/User.ts b/util/src/entities/User.ts
index 04f1e9cb..bc852616 100644 --- a/util/src/entities/User.ts +++ b/util/src/entities/User.ts
@@ -4,7 +4,7 @@ import { BitField } from "../util/BitField"; import { Relationship } from "./Relationship"; import { ConnectedAccount } from "./ConnectedAccount"; import { Config, FieldErrors, Snowflake, trimSpecial } from ".."; -import { Member } from "."; +import { Member, Session } from "."; export enum PublicUserEnum { username, @@ -131,6 +131,9 @@ export class User extends BaseClass { @Column() rights: string; // Rights + @OneToMany(() => Session, (session: Session) => session.user) + sessions: Session[]; + @JoinColumn({ name: "relationship_ids" }) @OneToMany(() => Relationship, (relationship: Relationship) => relationship.from, { cascade: true, @@ -250,11 +253,13 @@ export class User extends BaseClass { await user.save(); - if (Config.get().guild.autoJoin.enabled) { - for (const guild of Config.get().guild.autoJoin.guilds || []) { - await Member.addToGuild(user.id, guild); + setImmediate(async () => { + if (Config.get().guild.autoJoin.enabled) { + for (const guild of Config.get().guild.autoJoin.guilds || []) { + await Member.addToGuild(user.id, guild).catch((e) => {}); + } } - } + }); return user; } @@ -293,7 +298,7 @@ export const defaultSettings: UserSettings = { render_reactions: true, restricted_guilds: [], show_current_game: true, - status: "offline", + status: "online", stream_notifications_enabled: true, theme: "dark", timezone_offset: 0, diff --git a/util/src/interfaces/Activity.ts b/util/src/interfaces/Activity.ts
index f5a3c270..43984afd 100644 --- a/util/src/interfaces/Activity.ts +++ b/util/src/interfaces/Activity.ts
@@ -1,37 +1,38 @@ export interface Activity { - name: string; - type: ActivityType; - url?: string; - created_at?: Date; + name: string; // the activity's name + type: ActivityType; // activity type // TODO: check if its between range 0-5 + url?: string; // stream url, is validated when type is 1 + created_at?: number; // unix timestamp of when the activity was added to the user's session timestamps?: { - start?: number; - end?: number; - }[]; - application_id?: string; + // unix timestamps for start and/or end of the game + start: number; + end: number; + }; + application_id?: string; // application id for the game details?: string; state?: string; emoji?: { name: string; id?: string; - amimated?: boolean; + animated: boolean; }; party?: { id?: string; - size?: [number, number]; + size?: [number]; // used to show the party's current and maximum size // TODO: array length 2 }; assets?: { - large_image?: string; - large_text?: string; - small_image?: string; - small_text?: string; + large_image?: string; // the id for a large asset of the activity, usually a snowflake + large_text?: string; // text displayed when hovering over the large image of the activity + small_image?: string; // the id for a small asset of the activity, usually a snowflake + small_text?: string; // text displayed when hovering over the small image of the activity }; secrets?: { - join?: string; - spectate?: string; - match?: string; + join?: string; // the secret for joining a party + spectate?: string; // the secret for spectating a game + match?: string; // the secret for a specific instanced match }; instance?: boolean; - flags?: bigint; + flags: string; // activity flags OR d together, describes what the payload includes } export enum ActivityType { diff --git a/util/src/interfaces/Event.ts b/util/src/interfaces/Event.ts
index 13fd4b8b..a5253c09 100644 --- a/util/src/interfaces/Event.ts +++ b/util/src/interfaces/Event.ts
@@ -13,6 +13,7 @@ import { ConnectedAccount } from "../entities/ConnectedAccount"; import { Relationship, RelationshipType } from "../entities/Relationship"; import { Presence } from "./Presence"; import { Sticker } from ".."; +import { Activity, Status } from "."; export interface Event { guild_id?: string; @@ -454,6 +455,37 @@ export interface RelationshipRemoveEvent extends Event { data: Omit<PublicRelationship, "nickname">; } +export interface SessionsReplace extends Event { + event: "SESSIONS_REPLACE"; + data: { + activities: Activity[]; + client_info: { + version: number; + os: string; + client: string; + }; + status: Status; + }[]; +} + +export interface GuildMemberListUpdate extends Event { + event: "GUILD_MEMBER_LIST_UPDATE"; + data: { + groups: { id: string; count: number }[]; + guild_id: string; + id: string; + member_count: number; + online_count: number; + ops: { + index: number; + item: { + member?: PublicMember & { presence: Presence }; + group?: { id: string; count: number }[]; + }; + }[]; + }; +} + export type EventData = | InvalidatedEvent | ReadyEvent @@ -474,6 +506,7 @@ export type EventData = | GuildMemberRemoveEvent | GuildMemberUpdateEvent | GuildMembersChunkEvent + | GuildMemberListUpdate | GuildRoleCreateEvent | GuildRoleUpdateEvent | GuildRoleDeleteEvent @@ -523,6 +556,7 @@ export enum EVENTEnum { GuildMemberUpdate = "GUILD_MEMBER_UPDATE", GuildMemberSpeaking = "GUILD_MEMBER_SPEAKING", GuildMembersChunk = "GUILD_MEMBERS_CHUNK", + GuildMemberListUpdate = "GUILD_MEMBER_LIST_UPDATE", GuildRoleCreate = "GUILD_ROLE_CREATE", GuildRoleDelete = "GUILD_ROLE_DELETE", GuildRoleUpdate = "GUILD_ROLE_UPDATE", @@ -546,6 +580,7 @@ export enum EVENTEnum { ApplicationCommandCreate = "APPLICATION_COMMAND_CREATE", ApplicationCommandUpdate = "APPLICATION_COMMAND_UPDATE", ApplicationCommandDelete = "APPLICATION_COMMAND_DELETE", + SessionsReplace = "SESSIONS_REPLACE", } export type EVENT = @@ -569,6 +604,7 @@ export type EVENT = | "GUILD_MEMBER_UPDATE" | "GUILD_MEMBER_SPEAKING" | "GUILD_MEMBERS_CHUNK" + | "GUILD_MEMBER_LIST_UPDATE" | "GUILD_ROLE_CREATE" | "GUILD_ROLE_DELETE" | "GUILD_ROLE_UPDATE" @@ -597,6 +633,7 @@ export type EVENT = | "MESSAGE_ACK" | "RELATIONSHIP_ADD" | "RELATIONSHIP_REMOVE" + | "SESSIONS_REPLACE" | CUSTOMEVENTS; export type CUSTOMEVENTS = "INVALIDATED" | "RATELIMIT"; diff --git a/util/src/interfaces/Presence.ts b/util/src/interfaces/Presence.ts
index 4a1ff038..7663891a 100644 --- a/util/src/interfaces/Presence.ts +++ b/util/src/interfaces/Presence.ts
@@ -1,10 +1,12 @@ import { ClientStatus, Status } from "./Status"; import { Activity } from "./Activity"; +import { PublicUser } from "../entities/User"; export interface Presence { - user_id: string; + user: PublicUser; guild_id?: string; status: Status; activities: Activity[]; client_status: ClientStatus; + // TODO: game }