Merge pull request #966 from spacebarchat/feat/refactorIdentify
Rewrite identify handler
This commit is contained in:
commit
549979372b
@ -2390,12 +2390,16 @@
|
|||||||
},
|
},
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
},
|
},
|
||||||
|
"flags": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
"id": {
|
"id": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
"color",
|
"color",
|
||||||
|
"flags",
|
||||||
"guild",
|
"guild",
|
||||||
"guild_id",
|
"guild_id",
|
||||||
"hoist",
|
"hoist",
|
||||||
@ -3632,47 +3636,45 @@
|
|||||||
"type": "object",
|
"type": "object",
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
},
|
},
|
||||||
"id": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"roles": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/components/schemas/Role"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"name": {
|
"name": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"banner": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"unavailable": {
|
|
||||||
"type": "boolean"
|
|
||||||
},
|
|
||||||
"channels": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/components/schemas/Channel"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"region": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"icon": {
|
"icon": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"system_channel_id": {
|
"parent": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"rules_channel_id": {
|
"owner_id": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"afk_timeout": {
|
"nsfw": {
|
||||||
"type": "integer"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
"explicit_content_filter": {
|
"invites": {
|
||||||
"type": "integer"
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/Invite"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"voice_states": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/VoiceState"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"webhooks": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/Webhook"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"_do_validate": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false
|
||||||
},
|
},
|
||||||
"assign": {
|
"assign": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
@ -3707,6 +3709,39 @@
|
|||||||
"type": "object",
|
"type": "object",
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
},
|
},
|
||||||
|
"roles": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/Role"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"banner": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"unavailable": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"channels": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/Channel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"region": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"system_channel_id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"rules_channel_id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"afk_timeout": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"explicit_content_filter": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
"afk_channel_id": {
|
"afk_channel_id": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
@ -3773,30 +3808,9 @@
|
|||||||
"$ref": "#/components/schemas/Sticker"
|
"$ref": "#/components/schemas/Sticker"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"invites": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/components/schemas/Invite"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"voice_states": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/components/schemas/VoiceState"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"webhooks": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/components/schemas/Webhook"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"mfa_level": {
|
"mfa_level": {
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
"owner_id": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"preferred_locale": {
|
"preferred_locale": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
@ -3830,21 +3844,11 @@
|
|||||||
"nsfw_level": {
|
"nsfw_level": {
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
"nsfw": {
|
|
||||||
"type": "boolean"
|
|
||||||
},
|
|
||||||
"parent": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"permissions": {
|
"permissions": {
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
"premium_progress_bar_enabled": {
|
"premium_progress_bar_enabled": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
|
||||||
"_do_validate": {
|
|
||||||
"type": "object",
|
|
||||||
"additionalProperties": false
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
@ -4173,7 +4177,9 @@
|
|||||||
"channel": {
|
"channel": {
|
||||||
"$ref": "#/components/schemas/RateLimitOptions"
|
"$ref": "#/components/schemas/RateLimitOptions"
|
||||||
},
|
},
|
||||||
"auth": {}
|
"auth": {
|
||||||
|
"$ref": "#/components/schemas/AuthRateLimit"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
"auth",
|
"auth",
|
||||||
@ -4182,6 +4188,21 @@
|
|||||||
"webhook"
|
"webhook"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"AuthRateLimit": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"login": {
|
||||||
|
"$ref": "#/components/schemas/RateLimitOptions"
|
||||||
|
},
|
||||||
|
"register": {
|
||||||
|
"$ref": "#/components/schemas/RateLimitOptions"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"login",
|
||||||
|
"register"
|
||||||
|
]
|
||||||
|
},
|
||||||
"GlobalRateLimits": {
|
"GlobalRateLimits": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@ -4664,13 +4685,13 @@
|
|||||||
"discovery_splash": {
|
"discovery_splash": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"region": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"icon": {
|
"icon": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"nullable": true
|
"nullable": true
|
||||||
},
|
},
|
||||||
|
"region": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"guild_template_code": {
|
"guild_template_code": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
@ -5406,6 +5427,12 @@
|
|||||||
},
|
},
|
||||||
"promotional_email_opt_in": {
|
"promotional_email_opt_in": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"unique_username_registration": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"global_name": {
|
||||||
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
@ -5684,13 +5711,6 @@
|
|||||||
"version": {
|
"version": {
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
"guild_id": {
|
|
||||||
"type": "string",
|
|
||||||
"nullable": true
|
|
||||||
},
|
|
||||||
"flags": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"message_notifications": {
|
"message_notifications": {
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
@ -5716,6 +5736,13 @@
|
|||||||
"suppress_roles": {
|
"suppress_roles": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
|
"guild_id": {
|
||||||
|
"type": "string",
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
|
"flags": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
"mute_scheduled_events": {
|
"mute_scheduled_events": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
@ -6934,6 +6961,9 @@
|
|||||||
"APIPrivateUser": {
|
"APIPrivateUser": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"flags": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"id": {
|
"id": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
@ -6980,9 +7010,6 @@
|
|||||||
"pronouns": {
|
"pronouns": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"flags": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"mfa_enabled": {
|
"mfa_enabled": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
@ -7051,6 +7078,9 @@
|
|||||||
"newToken": {
|
"newToken": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"flags": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"id": {
|
"id": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
@ -7097,9 +7127,6 @@
|
|||||||
"pronouns": {
|
"pronouns": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"flags": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"mfa_enabled": {
|
"mfa_enabled": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
@ -7264,10 +7291,10 @@
|
|||||||
"APIPublicMember": {
|
"APIPublicMember": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"id": {
|
"guild_id": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"guild_id": {
|
"id": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"nick": {
|
"nick": {
|
||||||
@ -7696,10 +7723,10 @@
|
|||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"id": {
|
"guild_id": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"guild_id": {
|
"id": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"nick": {
|
"nick": {
|
||||||
|
18812
assets/schemas.json
18812
assets/schemas.json
File diff suppressed because it is too large
Load Diff
@ -1,82 +0,0 @@
|
|||||||
/*
|
|
||||||
Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
|
|
||||||
Copyright (C) 2023 Spacebar and Spacebar Contributors
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU Affero General Public License as published
|
|
||||||
by the Free Software Foundation, either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU Affero General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU Affero General Public License
|
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
require("dotenv").config();
|
|
||||||
const cluster = require("cluster");
|
|
||||||
const WebSocket = require("ws");
|
|
||||||
const endpoint = process.env.GATEWAY || "ws://localhost:3001";
|
|
||||||
const connections = Number(process.env.CONNECTIONS) || 50;
|
|
||||||
const token = process.env.TOKEN;
|
|
||||||
var cores = 1;
|
|
||||||
try {
|
|
||||||
cores = Number(process.env.THREADS) || os.cpus().length;
|
|
||||||
} catch {
|
|
||||||
console.log("[Bundle] Failed to get thread count! Using 1...");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!token) {
|
|
||||||
console.error("TOKEN env var missing");
|
|
||||||
process.exit();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cluster.isMaster) {
|
|
||||||
for (let i = 0; i < cores; i++) {
|
|
||||||
cluster.fork();
|
|
||||||
}
|
|
||||||
|
|
||||||
cluster.on("exit", (worker, code, signal) => {
|
|
||||||
console.log(`worker ${worker.process.pid} died`);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
for (let i = 0; i < connections; i++) {
|
|
||||||
connect();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function connect() {
|
|
||||||
const client = new WebSocket(endpoint);
|
|
||||||
client.on("message", (data) => {
|
|
||||||
data = JSON.parse(data);
|
|
||||||
|
|
||||||
switch (data.op) {
|
|
||||||
case 10:
|
|
||||||
client.interval = setInterval(() => {
|
|
||||||
client.send(JSON.stringify({ op: 1 }));
|
|
||||||
}, data.d.heartbeat_interval);
|
|
||||||
|
|
||||||
client.send(
|
|
||||||
JSON.stringify({
|
|
||||||
op: 2,
|
|
||||||
d: {
|
|
||||||
token,
|
|
||||||
properties: {},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
client.once("close", (code, reason) => {
|
|
||||||
clearInterval(client.interval);
|
|
||||||
connect();
|
|
||||||
});
|
|
||||||
client.on("error", (err) => {
|
|
||||||
// console.log(err);
|
|
||||||
});
|
|
||||||
}
|
|
53
scripts/stress/identify.js
Normal file
53
scripts/stress/identify.js
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
/* eslint-env node */
|
||||||
|
|
||||||
|
require("dotenv").config();
|
||||||
|
const { OPCODES } = require("../../dist/gateway/util/Constants.js");
|
||||||
|
const WebSocket = require("ws");
|
||||||
|
const ENDPOINT = `ws://localhost:3002?v=9&encoding=json`;
|
||||||
|
const TOKEN =
|
||||||
|
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjEwOTMxMTgwMjgzNjA1MzYxMDYiLCJpYXQiOjE2ODA2OTE5MDB9.9ByCqDvC4mIutW8nM7WhVCtGuKW08UimPnmBeNw-K0E";
|
||||||
|
const TOTAL_ITERATIONS = 500;
|
||||||
|
|
||||||
|
const doTimedIdentify = () =>
|
||||||
|
new Promise((resolve) => {
|
||||||
|
let start;
|
||||||
|
const ws = new WebSocket(ENDPOINT);
|
||||||
|
ws.on("message", (data) => {
|
||||||
|
const parsed = JSON.parse(data);
|
||||||
|
|
||||||
|
switch (parsed.op) {
|
||||||
|
case OPCODES.Hello:
|
||||||
|
// send identify
|
||||||
|
start = performance.now();
|
||||||
|
ws.send(
|
||||||
|
JSON.stringify({
|
||||||
|
op: OPCODES.Identify,
|
||||||
|
d: {
|
||||||
|
token: TOKEN,
|
||||||
|
properties: {},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case OPCODES.Dispatch:
|
||||||
|
if (parsed.t == "READY") {
|
||||||
|
ws.close();
|
||||||
|
return resolve(performance.now() - start);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
const perfs = [];
|
||||||
|
while (perfs.length < TOTAL_ITERATIONS) {
|
||||||
|
const ret = await doTimedIdentify();
|
||||||
|
perfs.push(ret);
|
||||||
|
// console.log(`${perfs.length}/${TOTAL_ITERATIONS} - this: ${Math.floor(ret)}ms`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const avg = perfs.reduce((prev, curr) => prev + curr) / (perfs.length - 1);
|
||||||
|
console.log(`Average identify time: ${Math.floor(avg * 100) / 100}ms`);
|
||||||
|
})();
|
@ -92,12 +92,7 @@ export async function Authentication(
|
|||||||
Sentry.setUser({ id: req.user_id });
|
Sentry.setUser({ id: req.user_id });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { jwtSecret } = Config.get().security;
|
const { decoded, user } = await checkToken(req.headers.authorization);
|
||||||
|
|
||||||
const { decoded, user } = await checkToken(
|
|
||||||
req.headers.authorization,
|
|
||||||
jwtSecret,
|
|
||||||
);
|
|
||||||
|
|
||||||
req.token = decoded;
|
req.token = decoded;
|
||||||
req.user_id = decoded.id;
|
req.user_id = decoded.id;
|
||||||
|
@ -48,11 +48,9 @@ router.post(
|
|||||||
async (req: Request, res: Response) => {
|
async (req: Request, res: Response) => {
|
||||||
const { password, token } = req.body as PasswordResetSchema;
|
const { password, token } = req.body as PasswordResetSchema;
|
||||||
|
|
||||||
const { jwtSecret } = Config.get().security;
|
|
||||||
|
|
||||||
let user;
|
let user;
|
||||||
try {
|
try {
|
||||||
const userTokenData = await checkToken(token, jwtSecret, true);
|
const userTokenData = await checkToken(token);
|
||||||
user = userTokenData.user;
|
user = userTokenData.user;
|
||||||
} catch {
|
} catch {
|
||||||
throw FieldErrors({
|
throw FieldErrors({
|
||||||
|
@ -78,11 +78,10 @@ router.post(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const { jwtSecret } = Config.get().security;
|
|
||||||
let user;
|
let user;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const userTokenData = await checkToken(token, jwtSecret, true);
|
const userTokenData = await checkToken(token);
|
||||||
user = userTokenData.user;
|
user = userTokenData.user;
|
||||||
} catch {
|
} catch {
|
||||||
throw FieldErrors({
|
throw FieldErrors({
|
||||||
|
@ -144,8 +144,9 @@ router.get(
|
|||||||
|
|
||||||
const endpoint = Config.get().cdn.endpointPublic;
|
const endpoint = Config.get().cdn.endpointPublic;
|
||||||
|
|
||||||
return res.json(
|
const ret = messages.map((x: Message) => {
|
||||||
messages.map((x: Partial<Message>) => {
|
x = x.toJSON();
|
||||||
|
|
||||||
(x.reactions || []).forEach((y: Partial<Reaction>) => {
|
(x.reactions || []).forEach((y: Partial<Reaction>) => {
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
@ -180,8 +181,9 @@ router.get(
|
|||||||
// }
|
// }
|
||||||
|
|
||||||
return x;
|
return x;
|
||||||
}),
|
});
|
||||||
);
|
|
||||||
|
return res.json(ret);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -307,9 +309,11 @@ router.post(
|
|||||||
embeds,
|
embeds,
|
||||||
channel_id,
|
channel_id,
|
||||||
attachments,
|
attachments,
|
||||||
edited_timestamp: undefined,
|
|
||||||
timestamp: new Date(),
|
timestamp: new Date(),
|
||||||
});
|
});
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
//@ts-ignore dont care2
|
||||||
|
message.edited_timestamp = null;
|
||||||
|
|
||||||
channel.last_message_id = message.id;
|
channel.last_message_id = message.id;
|
||||||
|
|
||||||
|
@ -161,7 +161,7 @@ router.patch(
|
|||||||
const data = guild.toJSON();
|
const data = guild.toJSON();
|
||||||
// TODO: guild hashes
|
// TODO: guild hashes
|
||||||
// TODO: fix vanity_url_code, template_id
|
// TODO: fix vanity_url_code, template_id
|
||||||
delete data.vanity_url_code;
|
// delete data.vanity_url_code;
|
||||||
delete data.template_id;
|
delete data.template_id;
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
|
@ -25,5 +25,5 @@ export async function onHeartbeat(this: WebSocket) {
|
|||||||
|
|
||||||
setHeartbeat(this);
|
setHeartbeat(this);
|
||||||
|
|
||||||
await Send(this, { op: 11 });
|
await Send(this, { op: 11, d: {} });
|
||||||
}
|
}
|
||||||
|
@ -16,17 +16,23 @@
|
|||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { WebSocket, Payload } from "@spacebar/gateway";
|
import {
|
||||||
|
WebSocket,
|
||||||
|
Payload,
|
||||||
|
setupListener,
|
||||||
|
Capabilities,
|
||||||
|
CLOSECODES,
|
||||||
|
OPCODES,
|
||||||
|
Send,
|
||||||
|
} from "@spacebar/gateway";
|
||||||
import {
|
import {
|
||||||
checkToken,
|
checkToken,
|
||||||
Intents,
|
Intents,
|
||||||
Member,
|
Member,
|
||||||
ReadyEventData,
|
ReadyEventData,
|
||||||
User,
|
|
||||||
Session,
|
Session,
|
||||||
EVENTEnum,
|
EVENTEnum,
|
||||||
Config,
|
Config,
|
||||||
PublicMember,
|
|
||||||
PublicUser,
|
PublicUser,
|
||||||
PrivateUserProjection,
|
PrivateUserProjection,
|
||||||
ReadState,
|
ReadState,
|
||||||
@ -36,77 +42,138 @@ import {
|
|||||||
PrivateSessionProjection,
|
PrivateSessionProjection,
|
||||||
MemberPrivateProjection,
|
MemberPrivateProjection,
|
||||||
PresenceUpdateEvent,
|
PresenceUpdateEvent,
|
||||||
UserSettings,
|
|
||||||
IdentifySchema,
|
IdentifySchema,
|
||||||
DefaultUserGuildSettings,
|
DefaultUserGuildSettings,
|
||||||
UserGuildSettings,
|
|
||||||
ReadyGuildDTO,
|
ReadyGuildDTO,
|
||||||
Guild,
|
Guild,
|
||||||
UserTokenData,
|
PublicUserProjection,
|
||||||
ConnectedAccount,
|
ReadyUserGuildSettingsEntries,
|
||||||
|
UserSettings,
|
||||||
|
Permissions,
|
||||||
|
DMChannel,
|
||||||
|
GuildOrUnavailable,
|
||||||
|
Recipient,
|
||||||
|
OPCodes,
|
||||||
} from "@spacebar/util";
|
} from "@spacebar/util";
|
||||||
import { Send } from "../util/Send";
|
|
||||||
import { CLOSECODES, OPCODES } from "../util/Constants";
|
|
||||||
import { setupListener } from "../listener/listener";
|
|
||||||
// import experiments from "./experiments.json";
|
|
||||||
const experiments: unknown[] = [];
|
|
||||||
import { check } from "./instanceOf";
|
import { check } from "./instanceOf";
|
||||||
import { Recipient } from "@spacebar/util";
|
|
||||||
|
|
||||||
// TODO: user sharding
|
// TODO: user sharding
|
||||||
// TODO: check privileged intents, if defined in the config
|
// TODO: check privileged intents, if defined in the config
|
||||||
// TODO: check if already identified
|
|
||||||
|
|
||||||
// TODO: Refactor identify ( and lazyrequest, tbh )
|
|
||||||
|
|
||||||
export async function onIdentify(this: WebSocket, data: Payload) {
|
export async function onIdentify(this: WebSocket, data: Payload) {
|
||||||
clearTimeout(this.readyTimeout);
|
if (this.user_id) {
|
||||||
// TODO: is this needed now that we use `json-bigint`?
|
// we've already identified
|
||||||
if (typeof data.d?.client_state?.highest_last_message_id === "number")
|
return this.close(CLOSECODES.Already_authenticated);
|
||||||
data.d.client_state.highest_last_message_id += "";
|
}
|
||||||
check.call(this, IdentifySchema, data.d);
|
|
||||||
|
|
||||||
|
clearTimeout(this.readyTimeout);
|
||||||
|
|
||||||
|
// Check payload matches schema
|
||||||
|
check.call(this, IdentifySchema, data.d);
|
||||||
const identify: IdentifySchema = data.d;
|
const identify: IdentifySchema = data.d;
|
||||||
|
|
||||||
let decoded: UserTokenData["decoded"];
|
this.capabilities = new Capabilities(identify.capabilities || 0);
|
||||||
try {
|
|
||||||
const { jwtSecret } = Config.get().security;
|
|
||||||
decoded = (await checkToken(identify.token, jwtSecret)).decoded; // will throw an error if invalid
|
|
||||||
} catch (error) {
|
|
||||||
console.error("invalid token", error);
|
|
||||||
return this.close(CLOSECODES.Authentication_failed);
|
|
||||||
}
|
|
||||||
this.user_id = decoded.id;
|
|
||||||
const session_id = this.session_id;
|
|
||||||
|
|
||||||
const [
|
const { user } = await checkToken(identify.token, {
|
||||||
user,
|
|
||||||
read_states,
|
|
||||||
members,
|
|
||||||
recipients,
|
|
||||||
session,
|
|
||||||
application,
|
|
||||||
connected_accounts,
|
|
||||||
] = await Promise.all([
|
|
||||||
User.findOneOrFail({
|
|
||||||
where: { id: this.user_id },
|
|
||||||
relations: ["relationships", "relationships.to", "settings"],
|
relations: ["relationships", "relationships.to", "settings"],
|
||||||
select: [...PrivateUserProjection, "relationships"],
|
select: [...PrivateUserProjection, "relationships"],
|
||||||
|
});
|
||||||
|
if (!user) return this.close(CLOSECODES.Authentication_failed);
|
||||||
|
this.user_id = user.id;
|
||||||
|
|
||||||
|
// Check intents
|
||||||
|
if (!identify.intents) identify.intents = 30064771071n; // TODO: what is this number?
|
||||||
|
this.intents = new Intents(identify.intents);
|
||||||
|
|
||||||
|
// TODO: actually do intent things.
|
||||||
|
|
||||||
|
// Validate sharding
|
||||||
|
if (identify.shard) {
|
||||||
|
this.shard_id = identify.shard[0];
|
||||||
|
this.shard_count = identify.shard[1];
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.shard_count == null ||
|
||||||
|
this.shard_id == null ||
|
||||||
|
this.shard_id > this.shard_count ||
|
||||||
|
this.shard_id < 0 ||
|
||||||
|
this.shard_count <= 0
|
||||||
|
) {
|
||||||
|
// TODO: why do we even care about this right now?
|
||||||
|
console.log(
|
||||||
|
`[Gateway] Invalid sharding from ${user.id}: ${identify.shard}`,
|
||||||
|
);
|
||||||
|
return this.close(CLOSECODES.Invalid_shard);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate a new gateway session ( id is already made, just save it in db )
|
||||||
|
const session = Session.create({
|
||||||
|
user_id: this.user_id,
|
||||||
|
session_id: this.session_id,
|
||||||
|
status: identify.presence?.status || "online",
|
||||||
|
client_info: {
|
||||||
|
client: identify.properties?.$device,
|
||||||
|
os: identify.properties?.os,
|
||||||
|
version: 0,
|
||||||
|
},
|
||||||
|
activities: identify.presence?.activities, // TODO: validation
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get from database:
|
||||||
|
// * the users read states
|
||||||
|
// * guild members for this user
|
||||||
|
// * recipients ( dm channels )
|
||||||
|
// * the bot application, if it exists
|
||||||
|
const [, application, read_states, members, recipients] = await Promise.all(
|
||||||
|
[
|
||||||
|
session.save(),
|
||||||
|
|
||||||
|
Application.findOne({
|
||||||
|
where: { id: this.user_id },
|
||||||
|
select: ["id", "flags"],
|
||||||
}),
|
}),
|
||||||
ReadState.find({ where: { user_id: this.user_id } }),
|
|
||||||
|
ReadState.find({
|
||||||
|
where: { user_id: this.user_id },
|
||||||
|
select: [
|
||||||
|
"id",
|
||||||
|
"channel_id",
|
||||||
|
"last_message_id",
|
||||||
|
"last_pin_timestamp",
|
||||||
|
"mention_count",
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
|
||||||
Member.find({
|
Member.find({
|
||||||
where: { id: this.user_id },
|
where: { id: this.user_id },
|
||||||
select: MemberPrivateProjection,
|
select: {
|
||||||
|
// We only want some member props
|
||||||
|
...Object.fromEntries(
|
||||||
|
MemberPrivateProjection.map((x) => [x, true]),
|
||||||
|
),
|
||||||
|
settings: true, // guild settings
|
||||||
|
roles: { id: true }, // the full role is fetched from the `guild` relation
|
||||||
|
|
||||||
|
// TODO: we don't really need every property of
|
||||||
|
// guild channels, emoji, roles, stickers
|
||||||
|
// but we do want almost everything from guild.
|
||||||
|
// How do you do that without just enumerating the guild props?
|
||||||
|
guild: true,
|
||||||
|
},
|
||||||
relations: [
|
relations: [
|
||||||
"guild",
|
"guild",
|
||||||
"guild.channels",
|
"guild.channels",
|
||||||
"guild.emojis",
|
"guild.emojis",
|
||||||
"guild.roles",
|
"guild.roles",
|
||||||
"guild.stickers",
|
"guild.stickers",
|
||||||
"user",
|
|
||||||
"roles",
|
"roles",
|
||||||
|
|
||||||
|
// For these entities, `user` is always just the logged in user we fetched above
|
||||||
|
// "user",
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
|
|
||||||
Recipient.find({
|
Recipient.find({
|
||||||
where: { user_id: this.user_id, closed: false },
|
where: { user_id: this.user_id, closed: false },
|
||||||
relations: [
|
relations: [
|
||||||
@ -114,232 +181,246 @@ export async function onIdentify(this: WebSocket, data: Payload) {
|
|||||||
"channel.recipients",
|
"channel.recipients",
|
||||||
"channel.recipients.user",
|
"channel.recipients.user",
|
||||||
],
|
],
|
||||||
// TODO: public user selection
|
select: {
|
||||||
}),
|
channel: {
|
||||||
// save the session and delete it when the websocket is closed
|
id: true,
|
||||||
Session.create({
|
flags: true,
|
||||||
user_id: this.user_id,
|
// is_spam: true, // TODO
|
||||||
session_id: session_id,
|
last_message_id: true,
|
||||||
// TODO: check if status is only one of: online, dnd, offline, idle
|
last_pin_timestamp: true,
|
||||||
status: identify.presence?.status || "offline", //does the session always start as online?
|
type: true,
|
||||||
client_info: {
|
icon: true,
|
||||||
//TODO read from identity
|
name: true,
|
||||||
client: "desktop",
|
owner_id: true,
|
||||||
os: identify.properties?.os,
|
recipients: {
|
||||||
version: 0,
|
// we don't actually need this ID or any other information about the recipient info,
|
||||||
|
// but typeorm does not select anything from the users relation of recipients unless we select
|
||||||
|
// at least one column.
|
||||||
|
id: true,
|
||||||
|
// We only want public user data for each dm channel
|
||||||
|
user: Object.fromEntries(
|
||||||
|
PublicUserProjection.map((x) => [x, true]),
|
||||||
|
),
|
||||||
},
|
},
|
||||||
activities: [],
|
},
|
||||||
}).save(),
|
},
|
||||||
Application.findOne({ where: { id: this.user_id } }),
|
}),
|
||||||
ConnectedAccount.find({ where: { user_id: this.user_id } }),
|
],
|
||||||
]);
|
);
|
||||||
|
|
||||||
if (!user) return this.close(CLOSECODES.Authentication_failed);
|
// We forgot to migrate user settings from the JSON column of `users`
|
||||||
|
// to the `user_settings` table theyre in now,
|
||||||
|
// so for instances that migrated, users may not have a `user_settings` row.
|
||||||
if (!user.settings) {
|
if (!user.settings) {
|
||||||
user.settings = new UserSettings();
|
user.settings = new UserSettings();
|
||||||
await user.settings.save();
|
await user.settings.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!identify.intents) identify.intents = BigInt("0x6ffffffff");
|
// Generate merged_members
|
||||||
this.intents = new Intents(identify.intents);
|
const merged_members = members.map((x) => {
|
||||||
if (identify.shard) {
|
|
||||||
this.shard_id = identify.shard[0];
|
|
||||||
this.shard_count = identify.shard[1];
|
|
||||||
if (
|
|
||||||
this.shard_count == null ||
|
|
||||||
this.shard_id == null ||
|
|
||||||
this.shard_id >= this.shard_count ||
|
|
||||||
this.shard_id < 0 ||
|
|
||||||
this.shard_count <= 0
|
|
||||||
) {
|
|
||||||
console.log(identify.shard);
|
|
||||||
return this.close(CLOSECODES.Invalid_shard);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let users: PublicUser[] = [];
|
|
||||||
|
|
||||||
const merged_members = members.map((x: Member) => {
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
...x,
|
...x,
|
||||||
roles: x.roles.map((x) => x.id),
|
roles: x.roles.map((x) => x.id),
|
||||||
|
|
||||||
|
// add back user, which we don't fetch from db
|
||||||
|
// TODO: For guild profiles, this may need to be changed.
|
||||||
|
// TODO: The only field required in the user prop is `id`,
|
||||||
|
// but our types are annoying so I didn't bother.
|
||||||
|
user: user.toPublicUser(),
|
||||||
|
|
||||||
|
guild: {
|
||||||
|
id: x.guild.id,
|
||||||
|
},
|
||||||
settings: undefined,
|
settings: undefined,
|
||||||
guild: undefined,
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}) as PublicMember[][];
|
|
||||||
// TODO: This type is bad.
|
|
||||||
let guilds: Partial<Guild>[] = members.map((x) => ({
|
|
||||||
...x.guild,
|
|
||||||
joined_at: x.joined_at,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const pending_guilds: typeof guilds = [];
|
|
||||||
if (user.bot)
|
|
||||||
guilds = guilds.map((guild) => {
|
|
||||||
pending_guilds.push(guild);
|
|
||||||
return { id: guild.id, unavailable: true };
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: Rewrite this. Perhaps a DTO?
|
// Populated with guilds 'unavailable' currently
|
||||||
const user_guild_settings_entries = members.map((x) => ({
|
// Just for bots
|
||||||
|
const pending_guilds: Guild[] = [];
|
||||||
|
|
||||||
|
// Generate guilds list ( make them unavailable if user is bot )
|
||||||
|
const guilds: GuildOrUnavailable[] = members.map((member) => {
|
||||||
|
// filter guild channels we don't have permission to view
|
||||||
|
// TODO: check if this causes issues when the user is granted other roles?
|
||||||
|
member.guild.channels = member.guild.channels.filter((channel) => {
|
||||||
|
const perms = Permissions.finalPermission({
|
||||||
|
user: {
|
||||||
|
id: member.id,
|
||||||
|
roles: member.roles.map((x) => x.id),
|
||||||
|
},
|
||||||
|
guild: member.guild,
|
||||||
|
channel,
|
||||||
|
});
|
||||||
|
|
||||||
|
return perms.has("VIEW_CHANNEL");
|
||||||
|
});
|
||||||
|
|
||||||
|
if (user.bot) {
|
||||||
|
pending_guilds.push(member.guild);
|
||||||
|
return { id: member.guild.id, unavailable: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...member.guild.toJSON(),
|
||||||
|
joined_at: member.joined_at,
|
||||||
|
|
||||||
|
threads: [],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Generate user_guild_settings
|
||||||
|
const user_guild_settings_entries: ReadyUserGuildSettingsEntries[] =
|
||||||
|
members.map((x) => ({
|
||||||
...DefaultUserGuildSettings,
|
...DefaultUserGuildSettings,
|
||||||
...x.settings,
|
...x.settings,
|
||||||
guild_id: x.guild.id,
|
guild_id: x.guild_id,
|
||||||
channel_overrides: Object.entries(
|
channel_overrides: Object.entries(
|
||||||
x.settings.channel_overrides ?? {},
|
x.settings.channel_overrides ?? {},
|
||||||
).map((y) => ({
|
).map((y) => ({
|
||||||
...y[1],
|
...y[1],
|
||||||
channel_id: y[0],
|
channel_id: y[0],
|
||||||
})),
|
})),
|
||||||
})) as unknown as UserGuildSettings[];
|
}));
|
||||||
|
|
||||||
const channels = recipients.map((x) => {
|
// Popultaed with users from private channels, relationships.
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// Uses a set to dedupe for us.
|
||||||
//@ts-ignore
|
const users: Set<PublicUser> = new Set();
|
||||||
x.channel.recipients = x.channel.recipients.map((x) =>
|
|
||||||
x.user.toPublicUser(),
|
// Generate dm channels from recipients list. Append recipients to `users` list
|
||||||
|
const channels = recipients
|
||||||
|
.filter(({ channel }) => channel.isDm())
|
||||||
|
.map((r) => {
|
||||||
|
// TODO: fix the types of Recipient
|
||||||
|
// Their channels are only ever private (I think) and thus are always DM channels
|
||||||
|
const channel = r.channel as DMChannel;
|
||||||
|
|
||||||
|
// Remove ourself from the list of other users in dm channel
|
||||||
|
channel.recipients = channel.recipients.filter(
|
||||||
|
(recipient) => recipient.user.id !== this.user_id,
|
||||||
);
|
);
|
||||||
//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[]);
|
const channelUsers = channel.recipients?.map((recipient) =>
|
||||||
if (x.channel.isDm()) {
|
recipient.user.toPublicUser(),
|
||||||
x.channel.recipients = x.channel.recipients?.filter(
|
|
||||||
(x) => x.id !== this.user_id,
|
|
||||||
);
|
);
|
||||||
}
|
|
||||||
return x.channel;
|
if (channelUsers && channelUsers.length > 0)
|
||||||
|
channelUsers.forEach((user) => users.add(user));
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: channel.id,
|
||||||
|
flags: channel.flags,
|
||||||
|
last_message_id: channel.last_message_id,
|
||||||
|
type: channel.type,
|
||||||
|
recipients: channelUsers || [],
|
||||||
|
is_spam: false, // TODO
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const relation of user.relationships) {
|
// From user relationships ( friends ), also append to `users` list
|
||||||
const related_user = relation.to;
|
user.relationships.forEach((x) => users.add(x.to.toPublicUser()));
|
||||||
const public_related_user = {
|
|
||||||
username: related_user.username,
|
|
||||||
discriminator: related_user.discriminator,
|
|
||||||
id: related_user.id,
|
|
||||||
public_flags: related_user.public_flags,
|
|
||||||
avatar: related_user.avatar,
|
|
||||||
bot: related_user.bot,
|
|
||||||
bio: related_user.bio,
|
|
||||||
premium_since: user.premium_since,
|
|
||||||
premium_type: user.premium_type,
|
|
||||||
accent_color: related_user.accent_color,
|
|
||||||
};
|
|
||||||
users.push(public_related_user);
|
|
||||||
}
|
|
||||||
|
|
||||||
setImmediate(async () => {
|
// Send SESSIONS_REPLACE and PRESENCE_UPDATE
|
||||||
// run in seperate "promise context" because ready payload is not dependent on those events
|
const allSessions = (
|
||||||
|
await Session.find({
|
||||||
|
where: { user_id: this.user_id },
|
||||||
|
select: PrivateSessionProjection,
|
||||||
|
})
|
||||||
|
).map((x) => ({
|
||||||
|
// TODO how is active determined?
|
||||||
|
// in our lazy request impl, we just pick the 'most relevant' session
|
||||||
|
active: x.session_id == session.session_id,
|
||||||
|
activities: x.activities,
|
||||||
|
client_info: x.client_info,
|
||||||
|
// TODO: what does all mean?
|
||||||
|
session_id: x.session_id == session.session_id ? "all" : x.session_id,
|
||||||
|
status: x.status,
|
||||||
|
}));
|
||||||
|
|
||||||
|
Promise.all([
|
||||||
emitEvent({
|
emitEvent({
|
||||||
event: "SESSIONS_REPLACE",
|
event: "SESSIONS_REPLACE",
|
||||||
user_id: this.user_id,
|
user_id: this.user_id,
|
||||||
data: await Session.find({
|
data: allSessions,
|
||||||
where: { user_id: this.user_id },
|
} as SessionsReplace),
|
||||||
select: PrivateSessionProjection,
|
|
||||||
}),
|
|
||||||
} as SessionsReplace);
|
|
||||||
emitEvent({
|
emitEvent({
|
||||||
event: "PRESENCE_UPDATE",
|
event: "PRESENCE_UPDATE",
|
||||||
user_id: this.user_id,
|
user_id: this.user_id,
|
||||||
data: {
|
data: {
|
||||||
user: await User.getPublicUser(this.user_id),
|
user: user.toPublicUser(),
|
||||||
activities: session.activities,
|
activities: session.activities,
|
||||||
client_status: session?.client_info,
|
client_status: session.client_info,
|
||||||
status: session.status,
|
status: session.status,
|
||||||
},
|
},
|
||||||
} as PresenceUpdateEvent);
|
} as PresenceUpdateEvent),
|
||||||
});
|
]);
|
||||||
|
|
||||||
read_states.forEach((s: Partial<ReadState>) => {
|
// Build READY
|
||||||
s.id = s.channel_id;
|
|
||||||
delete s.user_id;
|
|
||||||
delete s.channel_id;
|
|
||||||
});
|
|
||||||
|
|
||||||
const privateUser = {
|
read_states.forEach((x) => {
|
||||||
avatar: user.avatar,
|
x.id = x.channel_id;
|
||||||
mobile: user.mobile,
|
});
|
||||||
desktop: user.desktop,
|
|
||||||
discriminator: user.discriminator,
|
|
||||||
email: user.email,
|
|
||||||
flags: user.flags,
|
|
||||||
id: user.id,
|
|
||||||
mfa_enabled: user.mfa_enabled,
|
|
||||||
nsfw_allowed: user.nsfw_allowed,
|
|
||||||
phone: user.phone,
|
|
||||||
premium: user.premium,
|
|
||||||
premium_type: user.premium_type,
|
|
||||||
public_flags: user.public_flags,
|
|
||||||
premium_usage_flags: user.premium_usage_flags,
|
|
||||||
purchased_flags: user.purchased_flags,
|
|
||||||
username: user.username,
|
|
||||||
verified: user.verified,
|
|
||||||
bot: user.bot,
|
|
||||||
accent_color: user.accent_color,
|
|
||||||
banner: user.banner,
|
|
||||||
bio: user.bio,
|
|
||||||
premium_since: user.premium_since,
|
|
||||||
};
|
|
||||||
|
|
||||||
const d: ReadyEventData = {
|
const d: ReadyEventData = {
|
||||||
v: 9,
|
v: 9,
|
||||||
application: {
|
application: application
|
||||||
id: application?.id ?? "",
|
? { id: application.id, flags: application.flags }
|
||||||
flags: application?.flags ?? 0,
|
: undefined,
|
||||||
}, //TODO: check this code!
|
user: user.toPrivateUser(),
|
||||||
user: privateUser,
|
|
||||||
user_settings: user.settings,
|
user_settings: user.settings,
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
guilds: this.capabilities.has(Capabilities.FLAGS.CLIENT_STATE_V2)
|
||||||
// @ts-ignore
|
? guilds.map((x) => new ReadyGuildDTO(x).toJSON())
|
||||||
guilds: guilds.map((x: Guild & { joined_at: Date }) => {
|
: guilds,
|
||||||
return {
|
|
||||||
...new ReadyGuildDTO(x).toJSON(),
|
|
||||||
guild_hashes: {},
|
|
||||||
joined_at: x.joined_at,
|
|
||||||
name: x.name,
|
|
||||||
icon: x.icon,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
guild_experiments: [], // TODO
|
|
||||||
geo_ordered_rtc_regions: [], // TODO
|
|
||||||
relationships: user.relationships.map((x) => x.toPublicRelationship()),
|
relationships: user.relationships.map((x) => x.toPublicRelationship()),
|
||||||
read_state: {
|
read_state: {
|
||||||
entries: read_states,
|
entries: read_states,
|
||||||
partial: false,
|
partial: false,
|
||||||
version: 304128,
|
version: 0, // TODO
|
||||||
},
|
},
|
||||||
user_guild_settings: {
|
user_guild_settings: {
|
||||||
entries: user_guild_settings_entries,
|
entries: user_guild_settings_entries,
|
||||||
partial: false, // TODO partial
|
partial: false,
|
||||||
version: 642,
|
version: 0, // TODO
|
||||||
},
|
},
|
||||||
private_channels: channels,
|
private_channels: channels,
|
||||||
session_id: session_id,
|
session_id: this.session_id,
|
||||||
analytics_token: "", // TODO
|
country_code: user.settings.locale, // TODO: do ip analysis instead
|
||||||
connected_accounts,
|
users: Array.from(users),
|
||||||
consents: {
|
|
||||||
personalization: {
|
|
||||||
consented: false, // TODO
|
|
||||||
},
|
|
||||||
},
|
|
||||||
country_code: user.settings.locale,
|
|
||||||
friend_suggestion_count: 0, // TODO
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
experiments: experiments, // TODO
|
|
||||||
guild_join_requests: [], // TODO what is this?
|
|
||||||
users: users.filter((x) => x).unique(),
|
|
||||||
merged_members: merged_members,
|
merged_members: merged_members,
|
||||||
// shard // TODO: only for user sharding
|
sessions: allSessions,
|
||||||
sessions: [], // TODO:
|
|
||||||
|
resume_gateway_url:
|
||||||
|
Config.get().gateway.endpointClient ||
|
||||||
|
Config.get().gateway.endpointPublic ||
|
||||||
|
"ws://127.0.0.1:3001",
|
||||||
|
|
||||||
// lol hack whatever
|
// lol hack whatever
|
||||||
required_action:
|
required_action:
|
||||||
Config.get().login.requireVerification && !user.verified
|
Config.get().login.requireVerification && !user.verified
|
||||||
? "REQUIRE_VERIFIED_EMAIL"
|
? "REQUIRE_VERIFIED_EMAIL"
|
||||||
: undefined,
|
: undefined,
|
||||||
|
|
||||||
|
consents: {
|
||||||
|
personalization: {
|
||||||
|
consented: false, // TODO
|
||||||
|
},
|
||||||
|
},
|
||||||
|
experiments: [],
|
||||||
|
guild_join_requests: [],
|
||||||
|
connected_accounts: [],
|
||||||
|
guild_experiments: [],
|
||||||
|
geo_ordered_rtc_regions: [],
|
||||||
|
api_code_version: 1,
|
||||||
|
friend_suggestion_count: 0,
|
||||||
|
analytics_token: "",
|
||||||
|
tutorial: null,
|
||||||
|
session_type: "normal", // TODO
|
||||||
|
auth_session_id_hash: "", // TODO
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: send real proper data structure
|
// Send READY
|
||||||
await Send(this, {
|
await Send(this, {
|
||||||
op: OPCODES.Dispatch,
|
op: OPCODES.Dispatch,
|
||||||
t: EVENTEnum.Ready,
|
t: EVENTEnum.Ready,
|
||||||
@ -347,23 +428,41 @@ export async function onIdentify(this: WebSocket, data: Payload) {
|
|||||||
d,
|
d,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// If we're a bot user, send GUILD_CREATE for each unavailable guild
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
pending_guilds.map((guild) =>
|
pending_guilds.map((x) =>
|
||||||
Send(this, {
|
Send(this, {
|
||||||
op: OPCODES.Dispatch,
|
op: OPCODES.Dispatch,
|
||||||
t: EVENTEnum.GuildCreate,
|
t: EVENTEnum.GuildCreate,
|
||||||
s: this.sequence++,
|
s: this.sequence++,
|
||||||
d: guild,
|
d: x,
|
||||||
})?.catch(console.error),
|
})?.catch((e) =>
|
||||||
|
console.error(`[Gateway] error when sending bot guilds`, e),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
//TODO send READY_SUPPLEMENTAL
|
// TODO: ready supplemental
|
||||||
|
await Send(this, {
|
||||||
|
op: OPCodes.DISPATCH,
|
||||||
|
t: EVENTEnum.ReadySupplemental,
|
||||||
|
s: this.sequence++,
|
||||||
|
d: {
|
||||||
|
merged_presences: {
|
||||||
|
guilds: [],
|
||||||
|
friends: [],
|
||||||
|
},
|
||||||
|
// these merged members seem to be all users currently in vc in your guilds
|
||||||
|
merged_members: [],
|
||||||
|
lazy_private_channels: [],
|
||||||
|
guilds: [], // { voice_states: [], id: string, embedded_activities: [] }
|
||||||
|
// embedded_activities are users currently in an activity?
|
||||||
|
disclose: [], // Config.get().general.uniqueUsernames ? ["pomelo"] : []
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
//TODO send GUILD_MEMBER_LIST_UPDATE
|
//TODO send GUILD_MEMBER_LIST_UPDATE
|
||||||
//TODO send SESSIONS_REPLACE
|
|
||||||
//TODO send VOICE_STATE_UPDATE to let the client know if another device is already connected to a voice channel
|
//TODO send VOICE_STATE_UPDATE to let the client know if another device is already connected to a voice channel
|
||||||
|
|
||||||
await setupListener.call(this);
|
await setupListener.call(this);
|
||||||
|
|
||||||
// console.log(`${this.ipAddress} identified as ${d.user.id}`);
|
|
||||||
}
|
}
|
||||||
|
@ -95,7 +95,7 @@ async function getMembers(guild_id: string, range: [number, number]) {
|
|||||||
console.error(`LazyRequest`, e);
|
console.error(`LazyRequest`, e);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!members) {
|
if (!members || !members.length) {
|
||||||
return {
|
return {
|
||||||
items: [],
|
items: [],
|
||||||
groups: [],
|
groups: [],
|
||||||
|
26
src/gateway/util/Capabilities.ts
Normal file
26
src/gateway/util/Capabilities.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { BitField, BitFieldResolvable, BitFlag } from "@spacebar/util";
|
||||||
|
|
||||||
|
export type CapabilityResolvable = BitFieldResolvable | CapabilityString;
|
||||||
|
type CapabilityString = keyof typeof Capabilities.FLAGS;
|
||||||
|
|
||||||
|
export class Capabilities extends BitField {
|
||||||
|
static FLAGS = {
|
||||||
|
// Thanks, Opencord!
|
||||||
|
// https://github.com/MateriiApps/OpenCord/blob/master/app/src/main/java/com/xinto/opencord/gateway/io/Capabilities.kt
|
||||||
|
LAZY_USER_NOTES: BitFlag(0),
|
||||||
|
NO_AFFINE_USER_IDS: BitFlag(1),
|
||||||
|
VERSIONED_READ_STATES: BitFlag(2),
|
||||||
|
VERSIONED_USER_GUILD_SETTINGS: BitFlag(3),
|
||||||
|
DEDUPLICATE_USER_OBJECTS: BitFlag(4),
|
||||||
|
PRIORITIZED_READY_PAYLOAD: BitFlag(5),
|
||||||
|
MULTIPLE_GUILD_EXPERIMENT_POPULATIONS: BitFlag(6),
|
||||||
|
NON_CHANNEL_READ_STATES: BitFlag(7),
|
||||||
|
AUTH_TOKEN_REFRESH: BitFlag(8),
|
||||||
|
USER_SETTINGS_PROTO: BitFlag(9),
|
||||||
|
CLIENT_STATE_V2: BitFlag(10),
|
||||||
|
PASSIVE_GUILD_UPDATE: BitFlag(11),
|
||||||
|
};
|
||||||
|
|
||||||
|
any = (capability: CapabilityResolvable) => super.any(capability);
|
||||||
|
has = (capability: CapabilityResolvable) => super.has(capability);
|
||||||
|
}
|
@ -19,6 +19,7 @@
|
|||||||
import { Intents, ListenEventOpts, Permissions } from "@spacebar/util";
|
import { Intents, ListenEventOpts, Permissions } from "@spacebar/util";
|
||||||
import WS from "ws";
|
import WS from "ws";
|
||||||
import { Deflate, Inflate } from "fast-zlib";
|
import { Deflate, Inflate } from "fast-zlib";
|
||||||
|
import { Capabilities } from "./Capabilities";
|
||||||
// import { Client } from "@spacebar/webrtc";
|
// import { Client } from "@spacebar/webrtc";
|
||||||
|
|
||||||
export interface WebSocket extends WS {
|
export interface WebSocket extends WS {
|
||||||
@ -40,5 +41,6 @@ export interface WebSocket extends WS {
|
|||||||
events: Record<string, undefined | (() => unknown)>;
|
events: Record<string, undefined | (() => unknown)>;
|
||||||
member_events: Record<string, () => unknown>;
|
member_events: Record<string, () => unknown>;
|
||||||
listen_options: ListenEventOpts;
|
listen_options: ListenEventOpts;
|
||||||
|
capabilities?: Capabilities;
|
||||||
// client?: Client;
|
// client?: Client;
|
||||||
}
|
}
|
||||||
|
@ -21,3 +21,4 @@ export * from "./Send";
|
|||||||
export * from "./SessionUtils";
|
export * from "./SessionUtils";
|
||||||
export * from "./Heartbeat";
|
export * from "./Heartbeat";
|
||||||
export * from "./WebSocket";
|
export * from "./WebSocket";
|
||||||
|
export * from "./Capabilities";
|
||||||
|
@ -18,13 +18,45 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
Channel,
|
Channel,
|
||||||
|
ChannelOverride,
|
||||||
|
ChannelType,
|
||||||
Emoji,
|
Emoji,
|
||||||
Guild,
|
Guild,
|
||||||
PublicMember,
|
PublicUser,
|
||||||
Role,
|
Role,
|
||||||
Sticker,
|
Sticker,
|
||||||
|
UserGuildSettings,
|
||||||
|
PublicMember,
|
||||||
} from "../entities";
|
} from "../entities";
|
||||||
|
|
||||||
|
// TODO: this is not the best place for this type
|
||||||
|
export type ReadyUserGuildSettingsEntries = Omit<
|
||||||
|
UserGuildSettings,
|
||||||
|
"channel_overrides"
|
||||||
|
> & {
|
||||||
|
channel_overrides: (ChannelOverride & { channel_id: string })[];
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: probably should move somewhere else
|
||||||
|
export interface ReadyPrivateChannel {
|
||||||
|
id: string;
|
||||||
|
flags: number;
|
||||||
|
is_spam: boolean;
|
||||||
|
last_message_id?: string;
|
||||||
|
recipients: PublicUser[];
|
||||||
|
type: ChannelType.DM | ChannelType.GROUP_DM;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GuildOrUnavailable =
|
||||||
|
| { id: string; unavailable: boolean }
|
||||||
|
| (Guild & { joined_at?: Date; unavailable: undefined });
|
||||||
|
|
||||||
|
const guildIsAvailable = (
|
||||||
|
guild: GuildOrUnavailable,
|
||||||
|
): guild is Guild & { joined_at: Date; unavailable: false } => {
|
||||||
|
return guild.unavailable != true;
|
||||||
|
};
|
||||||
|
|
||||||
export interface IReadyGuildDTO {
|
export interface IReadyGuildDTO {
|
||||||
application_command_counts?: { 1: number; 2: number; 3: number }; // ????????????
|
application_command_counts?: { 1: number; 2: number; 3: number }; // ????????????
|
||||||
channels: Channel[];
|
channels: Channel[];
|
||||||
@ -65,12 +97,21 @@ export interface IReadyGuildDTO {
|
|||||||
max_members: number | undefined;
|
max_members: number | undefined;
|
||||||
nsfw_level: number | undefined;
|
nsfw_level: number | undefined;
|
||||||
hub_type?: unknown | null; // ????
|
hub_type?: unknown | null; // ????
|
||||||
|
|
||||||
|
home_header: null; // TODO
|
||||||
|
latest_onboarding_question_id: null; // TODO
|
||||||
|
safety_alerts_channel_id: null; // TODO
|
||||||
|
max_stage_video_channel_users: 50; // TODO
|
||||||
|
nsfw: boolean;
|
||||||
|
id: string;
|
||||||
};
|
};
|
||||||
roles: Role[];
|
roles: Role[];
|
||||||
stage_instances: unknown[];
|
stage_instances: unknown[];
|
||||||
stickers: Sticker[];
|
stickers: Sticker[];
|
||||||
threads: unknown[];
|
threads: unknown[];
|
||||||
version: string;
|
version: string;
|
||||||
|
guild_hashes: unknown;
|
||||||
|
unavailable: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ReadyGuildDTO implements IReadyGuildDTO {
|
export class ReadyGuildDTO implements IReadyGuildDTO {
|
||||||
@ -113,14 +154,30 @@ export class ReadyGuildDTO implements IReadyGuildDTO {
|
|||||||
max_members: number | undefined;
|
max_members: number | undefined;
|
||||||
nsfw_level: number | undefined;
|
nsfw_level: number | undefined;
|
||||||
hub_type?: unknown | null; // ????
|
hub_type?: unknown | null; // ????
|
||||||
|
|
||||||
|
home_header: null; // TODO
|
||||||
|
latest_onboarding_question_id: null; // TODO
|
||||||
|
safety_alerts_channel_id: null; // TODO
|
||||||
|
max_stage_video_channel_users: 50; // TODO
|
||||||
|
nsfw: boolean;
|
||||||
|
id: string;
|
||||||
};
|
};
|
||||||
roles: Role[];
|
roles: Role[];
|
||||||
stage_instances: unknown[];
|
stage_instances: unknown[];
|
||||||
stickers: Sticker[];
|
stickers: Sticker[];
|
||||||
threads: unknown[];
|
threads: unknown[];
|
||||||
version: string;
|
version: string;
|
||||||
|
guild_hashes: unknown;
|
||||||
|
unavailable: boolean;
|
||||||
|
joined_at: Date;
|
||||||
|
|
||||||
|
constructor(guild: GuildOrUnavailable) {
|
||||||
|
if (!guildIsAvailable(guild)) {
|
||||||
|
this.id = guild.id;
|
||||||
|
this.unavailable = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
constructor(guild: Guild) {
|
|
||||||
this.application_command_counts = {
|
this.application_command_counts = {
|
||||||
1: 5,
|
1: 5,
|
||||||
2: 2,
|
2: 2,
|
||||||
@ -164,12 +221,21 @@ export class ReadyGuildDTO implements IReadyGuildDTO {
|
|||||||
max_members: guild.max_members,
|
max_members: guild.max_members,
|
||||||
nsfw_level: guild.nsfw_level,
|
nsfw_level: guild.nsfw_level,
|
||||||
hub_type: null,
|
hub_type: null,
|
||||||
|
|
||||||
|
home_header: null,
|
||||||
|
id: guild.id,
|
||||||
|
latest_onboarding_question_id: null,
|
||||||
|
max_stage_video_channel_users: 50, // TODO
|
||||||
|
nsfw: guild.nsfw,
|
||||||
|
safety_alerts_channel_id: null,
|
||||||
};
|
};
|
||||||
this.roles = guild.roles;
|
this.roles = guild.roles;
|
||||||
this.stage_instances = [];
|
this.stage_instances = [];
|
||||||
this.stickers = guild.stickers;
|
this.stickers = guild.stickers;
|
||||||
this.threads = [];
|
this.threads = [];
|
||||||
this.version = "1"; // ??????
|
this.version = "1"; // ??????
|
||||||
|
this.guild_hashes = {};
|
||||||
|
this.joined_at = guild.joined_at;
|
||||||
}
|
}
|
||||||
|
|
||||||
toJSON() {
|
toJSON() {
|
||||||
|
@ -468,6 +468,18 @@ export class Channel extends BaseClass {
|
|||||||
];
|
];
|
||||||
return disallowedChannelTypes.indexOf(this.type) == -1;
|
return disallowedChannelTypes.indexOf(this.type) == -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toJSON() {
|
||||||
|
return {
|
||||||
|
...this,
|
||||||
|
|
||||||
|
// these fields are not returned depending on the type of channel
|
||||||
|
bitrate: this.bitrate || undefined,
|
||||||
|
user_limit: this.user_limit || undefined,
|
||||||
|
rate_limit_per_user: this.rate_limit_per_user || undefined,
|
||||||
|
owner_id: this.owner_id || undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ChannelPermissionOverwrite {
|
export interface ChannelPermissionOverwrite {
|
||||||
@ -483,6 +495,12 @@ export enum ChannelPermissionOverwriteType {
|
|||||||
group = 2,
|
group = 2,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DMChannel extends Omit<Channel, "type" | "recipients"> {
|
||||||
|
type: ChannelType.DM | ChannelType.GROUP_DM;
|
||||||
|
recipients: Recipient[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: probably more props
|
||||||
export function isTextChannel(type: ChannelType): boolean {
|
export function isTextChannel(type: ChannelType): boolean {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case ChannelType.GUILD_STORE:
|
case ChannelType.GUILD_STORE:
|
||||||
|
@ -353,6 +353,7 @@ export class Guild extends BaseClass {
|
|||||||
position: 0,
|
position: 0,
|
||||||
icon: undefined,
|
icon: undefined,
|
||||||
unicode_emoji: undefined,
|
unicode_emoji: undefined,
|
||||||
|
flags: 0, // TODO?
|
||||||
}).save();
|
}).save();
|
||||||
|
|
||||||
if (!body.channels || !body.channels.length)
|
if (!body.channels || !body.channels.length)
|
||||||
@ -389,4 +390,11 @@ export class Guild extends BaseClass {
|
|||||||
|
|
||||||
return guild;
|
return guild;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toJSON() {
|
||||||
|
return {
|
||||||
|
...this,
|
||||||
|
unavailable: this.unavailable == false ? undefined : true,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -193,7 +193,7 @@ export class Message extends BaseClass {
|
|||||||
};
|
};
|
||||||
|
|
||||||
@Column({ nullable: true })
|
@Column({ nullable: true })
|
||||||
flags?: string;
|
flags?: number;
|
||||||
|
|
||||||
@Column({ type: "simple-json", nullable: true })
|
@Column({ type: "simple-json", nullable: true })
|
||||||
message_reference?: {
|
message_reference?: {
|
||||||
@ -217,6 +217,30 @@ export class Message extends BaseClass {
|
|||||||
|
|
||||||
@Column({ type: "simple-json", nullable: true })
|
@Column({ type: "simple-json", nullable: true })
|
||||||
components?: MessageComponent[];
|
components?: MessageComponent[];
|
||||||
|
|
||||||
|
toJSON(): Message {
|
||||||
|
return {
|
||||||
|
...this,
|
||||||
|
author_id: undefined,
|
||||||
|
member_id: undefined,
|
||||||
|
guild_id: undefined,
|
||||||
|
webhook_id: undefined,
|
||||||
|
application_id: undefined,
|
||||||
|
nonce: undefined,
|
||||||
|
|
||||||
|
tts: this.tts ?? false,
|
||||||
|
guild: this.guild ?? undefined,
|
||||||
|
webhook: this.webhook ?? undefined,
|
||||||
|
interaction: this.interaction ?? undefined,
|
||||||
|
reactions: this.reactions ?? undefined,
|
||||||
|
sticker_items: this.sticker_items ?? undefined,
|
||||||
|
message_reference: this.message_reference ?? undefined,
|
||||||
|
author: this.author?.toPublicUser() ?? undefined,
|
||||||
|
activity: this.activity ?? undefined,
|
||||||
|
application: this.application ?? undefined,
|
||||||
|
components: this.components ?? undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MessageComponent {
|
export interface MessageComponent {
|
||||||
|
@ -66,4 +66,7 @@ export class Role extends BaseClass {
|
|||||||
integration_id?: string;
|
integration_id?: string;
|
||||||
premium_subscriber?: boolean;
|
premium_subscriber?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
flags: number;
|
||||||
}
|
}
|
||||||
|
@ -175,7 +175,7 @@ export class User extends BaseClass {
|
|||||||
email?: string; // email of the user
|
email?: string; // email of the user
|
||||||
|
|
||||||
@Column()
|
@Column()
|
||||||
flags: string = "0"; // UserFlags // TODO: generate
|
flags: number = 0; // UserFlags // TODO: generate
|
||||||
|
|
||||||
@Column()
|
@Column()
|
||||||
public_flags: number = 0;
|
public_flags: number = 0;
|
||||||
@ -281,6 +281,15 @@ export class User extends BaseClass {
|
|||||||
return user as PublicUser;
|
return user as PublicUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toPrivateUser() {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const user: any = {};
|
||||||
|
PrivateUserProjection.forEach((x) => {
|
||||||
|
user[x] = this[x];
|
||||||
|
});
|
||||||
|
return user as UserPrivate;
|
||||||
|
}
|
||||||
|
|
||||||
static async getPublicUser(user_id: string, opts?: FindOneOptions<User>) {
|
static async getPublicUser(user_id: string, opts?: FindOneOptions<User>) {
|
||||||
return await User.findOneOrFail({
|
return await User.findOneOrFail({
|
||||||
where: { id: user_id },
|
where: { id: user_id },
|
||||||
|
@ -28,7 +28,6 @@ import {
|
|||||||
Role,
|
Role,
|
||||||
Emoji,
|
Emoji,
|
||||||
PublicMember,
|
PublicMember,
|
||||||
UserGuildSettings,
|
|
||||||
Guild,
|
Guild,
|
||||||
Channel,
|
Channel,
|
||||||
PublicUser,
|
PublicUser,
|
||||||
@ -40,6 +39,10 @@ import {
|
|||||||
UserSettings,
|
UserSettings,
|
||||||
IReadyGuildDTO,
|
IReadyGuildDTO,
|
||||||
ReadState,
|
ReadState,
|
||||||
|
UserPrivate,
|
||||||
|
ReadyUserGuildSettingsEntries,
|
||||||
|
ReadyPrivateChannel,
|
||||||
|
GuildOrUnavailable,
|
||||||
} from "@spacebar/util";
|
} from "@spacebar/util";
|
||||||
|
|
||||||
export interface Event {
|
export interface Event {
|
||||||
@ -68,22 +71,10 @@ export interface PublicRelationship {
|
|||||||
|
|
||||||
export interface ReadyEventData {
|
export interface ReadyEventData {
|
||||||
v: number;
|
v: number;
|
||||||
user: PublicUser & {
|
user: UserPrivate;
|
||||||
mobile: boolean;
|
private_channels: ReadyPrivateChannel[]; // this will be empty for bots
|
||||||
desktop: boolean;
|
|
||||||
email: string | undefined;
|
|
||||||
flags: string;
|
|
||||||
mfa_enabled: boolean;
|
|
||||||
nsfw_allowed: boolean;
|
|
||||||
phone: string | undefined;
|
|
||||||
premium: boolean;
|
|
||||||
premium_type: number;
|
|
||||||
verified: boolean;
|
|
||||||
bot: boolean;
|
|
||||||
};
|
|
||||||
private_channels: Channel[]; // this will be empty for bots
|
|
||||||
session_id: string; // resuming
|
session_id: string; // resuming
|
||||||
guilds: IReadyGuildDTO[];
|
guilds: IReadyGuildDTO[] | GuildOrUnavailable[]; // depends on capability
|
||||||
analytics_token?: string;
|
analytics_token?: string;
|
||||||
connected_accounts?: ConnectedAccount[];
|
connected_accounts?: ConnectedAccount[];
|
||||||
consents?: {
|
consents?: {
|
||||||
@ -115,7 +106,7 @@ export interface ReadyEventData {
|
|||||||
version: number;
|
version: number;
|
||||||
};
|
};
|
||||||
user_guild_settings?: {
|
user_guild_settings?: {
|
||||||
entries: UserGuildSettings[];
|
entries: ReadyUserGuildSettingsEntries[];
|
||||||
version: number;
|
version: number;
|
||||||
partial: boolean;
|
partial: boolean;
|
||||||
};
|
};
|
||||||
@ -127,6 +118,17 @@ export interface ReadyEventData {
|
|||||||
// probably all users who the user is in contact with
|
// probably all users who the user is in contact with
|
||||||
users?: PublicUser[];
|
users?: PublicUser[];
|
||||||
sessions: unknown[];
|
sessions: unknown[];
|
||||||
|
api_code_version: number;
|
||||||
|
tutorial: number | null;
|
||||||
|
resume_gateway_url: string;
|
||||||
|
session_type: string;
|
||||||
|
auth_session_id_hash: string;
|
||||||
|
required_action?:
|
||||||
|
| "REQUIRE_VERIFIED_EMAIL"
|
||||||
|
| "REQUIRE_VERIFIED_PHONE"
|
||||||
|
| "REQUIRE_CAPTCHA" // TODO: allow these to be triggered
|
||||||
|
| "TOS_UPDATE_ACKNOWLEDGMENT"
|
||||||
|
| "AGREEMENTS";
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ReadyEvent extends Event {
|
export interface ReadyEvent extends Event {
|
||||||
@ -581,6 +583,7 @@ export type EventData =
|
|||||||
|
|
||||||
export enum EVENTEnum {
|
export enum EVENTEnum {
|
||||||
Ready = "READY",
|
Ready = "READY",
|
||||||
|
ReadySupplemental = "READY_SUPPLEMENTAL",
|
||||||
ChannelCreate = "CHANNEL_CREATE",
|
ChannelCreate = "CHANNEL_CREATE",
|
||||||
ChannelUpdate = "CHANNEL_UPDATE",
|
ChannelUpdate = "CHANNEL_UPDATE",
|
||||||
ChannelDelete = "CHANNEL_DELETE",
|
ChannelDelete = "CHANNEL_DELETE",
|
||||||
|
@ -29,7 +29,7 @@ export interface MessageCreateSchema {
|
|||||||
nonce?: string;
|
nonce?: string;
|
||||||
channel_id?: string;
|
channel_id?: string;
|
||||||
tts?: boolean;
|
tts?: boolean;
|
||||||
flags?: string;
|
flags?: number;
|
||||||
embeds?: Embed[];
|
embeds?: Embed[];
|
||||||
embed?: Embed;
|
embed?: Embed;
|
||||||
// TODO: ^ embed is deprecated in favor of embeds (https://discord.com/developers/docs/resources/channel#message-object)
|
// TODO: ^ embed is deprecated in favor of embeds (https://discord.com/developers/docs/resources/channel#message-object)
|
||||||
|
@ -42,4 +42,8 @@ export interface RegisterSchema {
|
|||||||
captcha_key?: string;
|
captcha_key?: string;
|
||||||
|
|
||||||
promotional_email_opt_in?: boolean;
|
promotional_email_opt_in?: boolean;
|
||||||
|
|
||||||
|
// part of pomelo
|
||||||
|
unique_username_registration?: boolean;
|
||||||
|
global_name?: string;
|
||||||
}
|
}
|
||||||
|
@ -27,6 +27,16 @@ const JSONReplacer = function (
|
|||||||
return (this[key] as Date).toISOString().replace("Z", "+00:00");
|
return (this[key] as Date).toISOString().replace("Z", "+00:00");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// erlpack encoding doesn't call json.stringify,
|
||||||
|
// so our toJSON functions don't get called.
|
||||||
|
// manually call it here
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
//@ts-ignore
|
||||||
|
if (this?.[key]?.toJSON)
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
//@ts-ignore
|
||||||
|
this[key] = this[key].toJSON();
|
||||||
|
|
||||||
return value;
|
return value;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -19,94 +19,66 @@
|
|||||||
import jwt, { VerifyOptions } from "jsonwebtoken";
|
import jwt, { VerifyOptions } from "jsonwebtoken";
|
||||||
import { Config } from "./Config";
|
import { Config } from "./Config";
|
||||||
import { User } from "../entities";
|
import { User } from "../entities";
|
||||||
|
// TODO: dont use deprecated APIs lol
|
||||||
|
import {
|
||||||
|
FindOptionsRelationByString,
|
||||||
|
FindOptionsSelectByString,
|
||||||
|
} from "typeorm";
|
||||||
|
|
||||||
export const JWTOptions: VerifyOptions = { algorithms: ["HS256"] };
|
export const JWTOptions: VerifyOptions = { algorithms: ["HS256"] };
|
||||||
|
|
||||||
export type UserTokenData = {
|
export type UserTokenData = {
|
||||||
user: User;
|
user: User;
|
||||||
decoded: { id: string; iat: number };
|
decoded: { id: string; iat: number; email?: string };
|
||||||
};
|
};
|
||||||
|
|
||||||
async function checkEmailToken(
|
export const checkToken = (
|
||||||
decoded: jwt.JwtPayload,
|
token: string,
|
||||||
): Promise<UserTokenData> {
|
opts?: {
|
||||||
// eslint-disable-next-line no-async-promise-executor
|
select?: FindOptionsSelectByString<User>;
|
||||||
return new Promise(async (res, rej) => {
|
relations?: FindOptionsRelationByString;
|
||||||
if (!decoded.iat) return rej("Invalid Token"); // will never happen, just for typings.
|
},
|
||||||
|
): Promise<UserTokenData> =>
|
||||||
|
new Promise((resolve, reject) => {
|
||||||
|
jwt.verify(
|
||||||
|
token,
|
||||||
|
Config.get().security.jwtSecret,
|
||||||
|
JWTOptions,
|
||||||
|
async (err, out) => {
|
||||||
|
const decoded = out as UserTokenData["decoded"];
|
||||||
|
if (err || !decoded) return reject("Invalid Token");
|
||||||
|
|
||||||
const user = await User.findOne({
|
const user = await User.findOne({
|
||||||
where: {
|
where: decoded.email
|
||||||
email: decoded.email,
|
? { email: decoded.email }
|
||||||
},
|
: { id: decoded.id },
|
||||||
select: [
|
select: [
|
||||||
"email",
|
...(opts?.select || []),
|
||||||
"id",
|
"bot",
|
||||||
"verified",
|
|
||||||
"deleted",
|
|
||||||
"disabled",
|
"disabled",
|
||||||
"username",
|
"deleted",
|
||||||
|
"rights",
|
||||||
"data",
|
"data",
|
||||||
],
|
],
|
||||||
|
relations: opts?.relations,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!user) return rej("Invalid Token");
|
if (!user) return reject("User not found");
|
||||||
|
|
||||||
if (new Date().getTime() > decoded.iat * 1000 + 86400 * 1000)
|
|
||||||
return rej("Invalid Token");
|
|
||||||
|
|
||||||
// Using as here because we assert `id` and `iat` are in decoded.
|
|
||||||
// TS just doesn't want to assume its there, though.
|
|
||||||
return res({ decoded, user } as UserTokenData);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function checkToken(
|
|
||||||
token: string,
|
|
||||||
jwtSecret: string,
|
|
||||||
isEmailVerification = false,
|
|
||||||
): Promise<UserTokenData> {
|
|
||||||
return new Promise((res, rej) => {
|
|
||||||
token = token.replace("Bot ", "");
|
|
||||||
token = token.replace("Bearer ", "");
|
|
||||||
/**
|
|
||||||
in spacebar, even with instances that have bot distinction; we won't enforce "Bot" prefix,
|
|
||||||
as we don't really have separate pathways for bots
|
|
||||||
**/
|
|
||||||
|
|
||||||
jwt.verify(token, jwtSecret, JWTOptions, async (err, decoded) => {
|
|
||||||
if (err || !decoded) return rej("Invalid Token");
|
|
||||||
if (
|
|
||||||
typeof decoded == "string" ||
|
|
||||||
!("id" in decoded) ||
|
|
||||||
!decoded.iat
|
|
||||||
)
|
|
||||||
return rej("Invalid Token"); // will never happen, just for typings.
|
|
||||||
|
|
||||||
if (isEmailVerification) return res(checkEmailToken(decoded));
|
|
||||||
|
|
||||||
const user = await User.findOne({
|
|
||||||
where: { id: decoded.id },
|
|
||||||
select: ["data", "bot", "disabled", "deleted", "rights"],
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!user) return rej("Invalid Token");
|
|
||||||
|
|
||||||
// we need to round it to seconds as it saved as seconds in jwt iat and valid_tokens_since is stored in milliseconds
|
// we need to round it to seconds as it saved as seconds in jwt iat and valid_tokens_since is stored in milliseconds
|
||||||
if (
|
if (
|
||||||
decoded.iat * 1000 <
|
decoded.iat * 1000 <
|
||||||
new Date(user.data.valid_tokens_since).setSeconds(0, 0)
|
new Date(user.data.valid_tokens_since).setSeconds(0, 0)
|
||||||
)
|
)
|
||||||
return rej("Invalid Token");
|
return reject("Invalid Token");
|
||||||
|
|
||||||
if (user.disabled) return rej("User disabled");
|
if (user.disabled) return reject("User disabled");
|
||||||
if (user.deleted) return rej("User not found");
|
if (user.deleted) return reject("User not found");
|
||||||
|
|
||||||
// Using as here because we assert `id` and `iat` are in decoded.
|
return resolve({ decoded, user });
|
||||||
// TS just doesn't want to assume its there, though.
|
},
|
||||||
return res({ decoded, user } as UserTokenData);
|
);
|
||||||
});
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function generateToken(id: string, email?: string) {
|
export async function generateToken(id: string, email?: string) {
|
||||||
const iat = Math.floor(Date.now() / 1000);
|
const iat = Math.floor(Date.now() / 1000);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user