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
}
|