Merge branch 'spacebarchat:master' into fix/gh-webhook-events-more

This commit is contained in:
Cyber authored and GitHub committed 2025-12-15 08:29:43 +01:00
commit a583d85d44
195 files changed
+3917 -788

No files matched your search

+15 -1
View File
@@ -70,6 +70,7 @@ declare global {
user_bot: boolean;
token: { id: string; iat: number };
rights: Rights;
fingerprint?: string;
}
}
}
@@ -77,6 +78,15 @@ declare global {
export async function Authentication(req: Request, res: Response, next: NextFunction) {
if (req.method === "OPTIONS") return res.sendStatus(204);
const url = req.url.replace(API_PREFIX, "");
if (req.headers.cookie?.split("; ").find((x) => x.startsWith("__sb_sessid=")))
req.fingerprint = req.headers.cookie
.split("; ")
.find((x) => x.startsWith("__sb_sessid="))!
.split("=")[1];
// for some reason we need to require here, else the openapi generator fails with "route is not a function"
else res.setHeader("Set-Cookie", `__sb_sessid=${req.fingerprint = (await require("../util")).randomString(32)}; Secure; HttpOnly; SameSite=None; Path=/`);
if (
NO_AUTHORIZATION_ROUTES.some((x) => {
if (typeof x !== "string") {
@@ -102,10 +112,14 @@ export async function Authentication(req: Request, res: Response, next: NextFunc
})
)
return next();
if (!req.headers.authorization) return next(new HTTPError("Missing Authorization Header", 401));
try {
const { decoded, user } = await checkToken(req.headers.authorization);
const { decoded, user } = await checkToken(req.headers.authorization, {
ipAddress: req.ip,
fingerprint: req.fingerprint,
});
req.token = decoded;
req.user_id = decoded.id;
+5 -9
View File
@@ -21,20 +21,16 @@ import { NextFunction, Request, Response } from "express";
// TODO: config settings
export function CORS(req: Request, res: Response, next: NextFunction) {
res.set("Access-Control-Allow-Origin", "*");
res.set("Access-Control-Allow-Credentials", "true");
res.set("Access-Control-Allow-Headers", req.header("Access-Control-Request-Headers") || "*");
res.set("Access-Control-Allow-Methods", req.header("Access-Control-Request-Method") || "*");
res.set("Access-Control-Allow-Origin", req.header("Origin") ?? "*");
res.set("Access-Control-Max-Age", "5"); // dont make it too long so we can change it dynamically
// TODO: use better CSP
res.set(
"Content-security-policy",
"default-src * data: blob: filesystem: about: ws: wss: 'unsafe-inline' 'unsafe-eval'; script-src * data: blob: 'unsafe-inline' 'unsafe-eval'; connect-src * data: blob: 'unsafe-inline'; img-src * data: blob: 'unsafe-inline'; frame-src * data: blob: ; style-src * data: blob: 'unsafe-inline'; font-src * data: blob: 'unsafe-inline';",
);
res.set(
"Access-Control-Allow-Headers",
req.header("Access-Control-Request-Headers") || "*",
);
res.set(
"Access-Control-Allow-Methods",
req.header("Access-Control-Request-Methods") || "*",
);
if (req.method === "OPTIONS") {
res.status(204).end();
+10 -38
View File
@@ -16,7 +16,6 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { getIpAdress } from "@spacebar/api";
import { Config, getRights, listenEvent } from "@spacebar/util";
import { NextFunction, Request, Response, Router } from "express";
import { API_PREFIX_TRAILING_SLASH } from "./Authentication";
@@ -65,21 +64,14 @@ export default function rateLimit(opts: {
if (rights.has("BYPASS_RATE_LIMITS")) return next();
}
const bucket_id =
opts.bucket ||
req.originalUrl.replace(API_PREFIX_TRAILING_SLASH, "");
let executor_id = getIpAdress(req);
const bucket_id = opts.bucket || req.originalUrl.replace(API_PREFIX_TRAILING_SLASH, "");
let executor_id = req.ip || "127.0.0.1";
if (!opts.onlyIp && req.user_id) executor_id = req.user_id;
let max_hits = opts.count;
if (opts.bot && req.user_bot) max_hits = opts.bot;
if (opts.GET && ["GET", "OPTIONS", "HEAD"].includes(req.method))
max_hits = opts.GET;
else if (
opts.MODIFY &&
["POST", "DELETE", "PATCH", "PUT"].includes(req.method)
)
max_hits = opts.MODIFY;
if (opts.GET && ["GET", "OPTIONS", "HEAD"].includes(req.method)) max_hits = opts.GET;
else if (opts.MODIFY && ["POST", "DELETE", "PATCH", "PUT"].includes(req.method)) max_hits = opts.MODIFY;
const offender = Cache.get(executor_id + bucket_id);
@@ -104,18 +96,13 @@ export default function rateLimit(opts: {
}
res.set("X-RateLimit-Reset", `${reset}`);
res.set(
"X-RateLimit-Reset-After",
`${Math.max(0, Math.ceil(resetAfterSec))}`,
);
res.set("X-RateLimit-Reset-After", `${Math.max(0, Math.ceil(resetAfterSec))}`);
if (offender.blocked) {
const global = bucket_id === "global";
// each block violation pushes the expiry one full window further
reset += opts.window * 1000;
offender.expires_at = new Date(
offender.expires_at.getTime() + opts.window * 1000,
);
offender.expires_at = new Date(offender.expires_at.getTime() + opts.window * 1000);
resetAfterMs = reset - Date.now();
resetAfterSec = Math.ceil(resetAfterMs / 1000);
@@ -129,10 +116,7 @@ export default function rateLimit(opts: {
res
.status(429)
.set("X-RateLimit-Remaining", "0")
.set(
"Retry-After",
`${Math.max(0, Math.ceil(resetAfterSec))}`,
)
.set("Retry-After", `${Math.max(0, Math.ceil(resetAfterSec))}`)
// TODO: error rate limit message translation
.send({
message: "You are being rate limited.",
@@ -156,11 +140,7 @@ export default function rateLimit(opts: {
// check if error and increment error rate limit
if (res.statusCode >= 400 && opts.error) {
return hitRoute(hitRouteOpts);
} else if (
res.statusCode >= 200 &&
res.statusCode < 300 &&
opts.success
) {
} else if (res.statusCode >= 200 && res.statusCode < 300 && opts.success) {
return hitRoute(hitRouteOpts);
}
});
@@ -213,18 +193,10 @@ export async function initRateLimits(app: Router) {
app.use("/webhooks/:webhook_id", rateLimit(routes.webhook));
app.use("/channels/:channel_id", rateLimit(routes.channel));
app.use("/auth/login", rateLimit(routes.auth.login));
app.use(
"/auth/register",
rateLimit({ onlyIp: true, success: true, ...routes.auth.register }),
);
app.use("/auth/register", rateLimit({ onlyIp: true, success: true, ...routes.auth.register }));
}
async function hitRoute(opts: {
executor_id: string;
bucket_id: string;
max_hits: number;
window: number;
}) {
async function hitRoute(opts: { executor_id: string; bucket_id: string; max_hits: number; window: number }) {
const id = opts.executor_id + opts.bucket_id;
let limit = Cache.get(id);
if (!limit) {
+5 -10
View File
@@ -16,10 +16,10 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { getIpAdress, route, verifyCaptcha } from "@spacebar/api";
import { route, verifyCaptcha } from "@spacebar/api";
import { Config, Email, User } from "@spacebar/util";
import { Request, Response, Router } from "express";
import { ForgotPasswordSchema } from "@spacebar/schemas"
import { ForgotPasswordSchema } from "@spacebar/schemas";
const router = Router({ mergeParams: true });
router.post(
@@ -38,10 +38,7 @@ router.post(
const config = Config.get();
if (
config.passwordReset.requireCaptcha &&
config.security.captcha.enabled
) {
if (config.passwordReset.requireCaptcha && config.security.captcha.enabled) {
const { sitekey, service } = config.security.captcha;
if (!captcha_key) {
return res.status(400).json({
@@ -51,7 +48,7 @@ router.post(
});
}
const ip = getIpAdress(req);
const ip = req.ip;
const verify = await verifyCaptcha(captcha_key, ip);
if (!verify.success) {
return res.status(400).json({
@@ -71,9 +68,7 @@ router.post(
if (user && user.email) {
Email.sendResetPassword(user, user.email).catch((e) => {
console.error(
`Failed to send password reset email to ${user.username}#${user.discriminator} (${user.id}): ${e}`,
);
console.error(`Failed to send password reset email to ${user.username}#${user.discriminator} (${user.id}): ${e}`);
});
}
},
+2 -2
View File
@@ -16,7 +16,7 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { getIpAdress, route } from "@spacebar/api";
import { route } from "@spacebar/api";
import { IpDataClient } from "@spacebar/util";
import { Request, Response, Router } from "express";
const router = Router({ mergeParams: true });
@@ -33,7 +33,7 @@ router.get(
async (req: Request, res: Response) => {
//TODO
//Note: It's most likely related to legal. At the moment Discord hasn't finished this too
const country_code = (await IpDataClient.getIpInfo(getIpAdress(req)))?.country_code;
const country_code = (await IpDataClient.getIpInfo(req.ip!))?.country_code;
res.json({
consent_required: false,
country_code: country_code,
+10 -35
View File
@@ -16,19 +16,12 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { getIpAdress, route, verifyCaptcha } from "@spacebar/api";
import {
Config,
FieldErrors,
User,
WebAuthn,
generateToken,
generateWebAuthnTicket,
} from "@spacebar/util";
import { route, verifyCaptcha } from "@spacebar/api";
import { Config, FieldErrors, User, WebAuthn, generateToken, generateWebAuthnTicket } from "@spacebar/util";
import bcrypt from "bcrypt";
import crypto from "crypto";
import { Request, Response, Router } from "express";
import { LoginSchema } from "@spacebar/schemas"
import { LoginSchema } from "@spacebar/schemas";
const router: Router = Router({ mergeParams: true });
export default router;
@@ -61,7 +54,7 @@ router.post(
});
}
const ip = getIpAdress(req);
const ip = req.ip;
const verify = await verifyCaptcha(captcha_key, ip);
if (!verify.success) {
return res.status(400).json({
@@ -74,17 +67,7 @@ router.post(
const user = await User.findOneOrFail({
where: [{ phone: login }, { email: login }],
select: [
"data",
"id",
"disabled",
"deleted",
"totp_secret",
"mfa_enabled",
"webauthn_enabled",
"security_keys",
"verified",
],
select: ["data", "id", "disabled", "deleted", "totp_secret", "mfa_enabled", "webauthn_enabled", "security_keys", "verified"],
relations: ["security_keys", "settings"],
}).catch(() => {
throw FieldErrors({
@@ -100,10 +83,7 @@ router.post(
});
// the salt is saved in the password refer to bcrypt docs
const same_password = await bcrypt.compare(
password,
user.data.hash || "",
);
const same_password = await bcrypt.compare(password, user.data.hash || "");
if (!same_password) {
throw FieldErrors({
login: {
@@ -122,8 +102,7 @@ router.post(
throw FieldErrors({
login: {
code: "ACCOUNT_LOGIN_VERIFICATION_EMAIL",
message:
"Email verification is required, please check your email.",
message: "Email verification is required, please check your email.",
},
});
}
@@ -152,9 +131,7 @@ router.post(
const challenge = JSON.stringify({
publicKey: {
...options,
challenge: Buffer.from(options.challenge).toString(
"base64",
),
challenge: Buffer.from(options.challenge).toString("base64"),
allowCredentials: user.security_keys.map((x) => ({
id: x.key_id,
type: "public-key",
@@ -178,10 +155,8 @@ router.post(
if (undelete) {
// undelete refers to un'disable' here
if (user.disabled)
await User.update({ id: user.id }, { disabled: false });
if (user.deleted)
await User.update({ id: user.id }, { deleted: false });
if (user.disabled) await User.update({ id: user.id }, { disabled: false });
if (user.deleted) await User.update({ id: user.id }, { deleted: false });
} else {
if (user.deleted)
return res.status(400).json({
+4 -4
View File
@@ -16,7 +16,7 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { getIpAdress, route, verifyCaptcha } from "@spacebar/api";
import { route, verifyCaptcha } from "@spacebar/api";
import { Config, FieldErrors, Invite, User, ValidRegistrationToken, generateToken, IpDataClient, AbuseIpDbClient } from "@spacebar/util";
import bcrypt from "bcrypt";
import { Request, Response, Router } from "express";
@@ -38,7 +38,7 @@ router.post(
async (req: Request, res: Response) => {
const body = req.body as RegisterSchema;
const { register, security, limits } = Config.get();
const ip = getIpAdress(req);
const ip = req.ip!;
// Reg tokens
// They're a one time use token that bypasses registration limits ( rates, disabled reg, etc )
@@ -143,7 +143,7 @@ router.post(
const ipData = await IpDataClient.getIpInfo(ip);
if (ipData) {
if(!ipData.threat) {
if (!ipData.threat) {
console.log("Invalid IPData.co response, missing threat field", ipData);
}
const categories = Object.entries(ipData.threat)
@@ -287,7 +287,7 @@ router.post(
},
})) >= limits.absoluteRate.register.limit
) {
console.log(`Global register ratelimit exceeded for ${getIpAdress(req)}, ${req.body.username}, ${req.body.invite || "No invite given"}`);
console.log(`Global register ratelimit exceeded for ${req.ip}, ${req.body.username}, ${req.body.invite || "No invite given"}`);
throw FieldErrors({
email: {
code: "TOO_MANY_REGISTRATIONS",
+2
View File
@@ -51,6 +51,8 @@ router.post(
try {
const userTokenData = await checkToken(token, {
select: ["email"],
fingerprint: req.fingerprint,
ipAddress: req.ip
});
user = userTokenData.user;
} catch {
+7 -10
View File
@@ -16,14 +16,8 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { getIpAdress, route, verifyCaptcha } from "@spacebar/api";
import {
checkToken,
Config,
FieldErrors,
generateToken,
User,
} from "@spacebar/util";
import { route, verifyCaptcha } from "@spacebar/api";
import { checkToken, Config, FieldErrors, generateToken, User } from "@spacebar/util";
import { Request, Response, Router } from "express";
const router = Router({ mergeParams: true });
@@ -67,7 +61,7 @@ router.post(
});
}
const ip = getIpAdress(req);
const ip = req.ip;
const verify = await verifyCaptcha(captcha_key, ip);
if (!verify.success) {
return res.status(400).json({
@@ -81,7 +75,10 @@ router.post(
let user;
try {
const userTokenData = await checkToken(token);
const userTokenData = await checkToken(token, {
fingerprint: req.fingerprint,
ipAddress: req.ip
});
user = userTokenData.user;
} catch {
throw FieldErrors({
@@ -548,7 +548,7 @@ router.delete(
// TODO: handle other read state types
if (body.read_state_type != ReadStateType.CHANNEL) return res.status(204).send();
const readState = await ReadState.findOne({where: {channel_id}});
const readState = await ReadState.findOne({ where: { channel_id, user_id: req.user_id } });
if (readState) {
await readState.remove();
}
+14 -3
View File
@@ -16,11 +16,11 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { getIpAdress, route } from "@spacebar/api";
import { route } from "@spacebar/api";
import { Ban, DiscordApiErrors, GuildBanAddEvent, GuildBanRemoveEvent, Member, User, emitEvent } from "@spacebar/util";
import { Request, Response, Router } from "express";
import { HTTPError } from "lambert-server";
import { APIBansArray, BanRegistrySchema, GuildBansResponse } from "@spacebar/schemas";
import { APIBansArray, BanCreateSchema, BanRegistrySchema, GuildBansResponse } from "@spacebar/schemas";
const router: Router = Router({ mergeParams: true });
@@ -198,6 +198,15 @@ router.put(
async (req: Request, res: Response) => {
const { guild_id } = req.params;
const banned_user_id = req.params.user_id;
const opts = req.body as BanCreateSchema;
let deleteMessagesMs = opts.delete_message_days
? (opts.delete_message_days as number) * 86400000
: opts.delete_message_seconds
? (opts.delete_message_seconds as number) * 1000
: 0;
if (deleteMessagesMs < 0) deleteMessagesMs = 0;
if (req.user_id === banned_user_id && banned_user_id === req.permission?.cache.guild?.owner_id)
throw new HTTPError("You are the guild owner, hence can't ban yourself", 403);
@@ -207,6 +216,7 @@ router.put(
const existingBan = await Ban.findOne({
where: { guild_id: guild_id, user_id: banned_user_id },
});
// Bans on already banned users are silently ignored
if (existingBan) return res.status(204).send();
@@ -227,6 +237,7 @@ router.put(
data: {
guild_id: guild_id,
user: banned_user.toPublicUser(),
delete_message_secs: Math.floor(deleteMessagesMs / 1000),
},
guild_id: guild_id,
} as GuildBanAddEvent),
@@ -279,4 +290,4 @@ router.delete(
},
);
export default router;
export default router;
+2 -2
View File
@@ -16,7 +16,7 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { getIpAdress, route } from "@spacebar/api";
import { route } from "@spacebar/api";
import { Ban, DiscordApiErrors, GuildBanAddEvent, Member, User, emitEvent } from "@spacebar/util";
import { Request, Response, Router } from "express";
import { HTTPError } from "lambert-server";
@@ -81,7 +81,7 @@ router.post(
const ban = Ban.create({
user_id: banned_user_id,
guild_id: guild_id,
ip: getIpAdress(req),
ip: req.ip,
executor_id: req.user_id,
reason: req.body.reason, // || otherwise empty
});
+2 -7
View File
@@ -16,7 +16,7 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { getIpAdress, getVoiceRegions, route } from "@spacebar/api";
import { getVoiceRegions, route } from "@spacebar/api";
import { Guild } from "@spacebar/util";
import { Request, Response, Router } from "express";
@@ -38,12 +38,7 @@ router.get(
const { guild_id } = req.params;
const guild = await Guild.findOneOrFail({ where: { id: guild_id } });
//TODO we should use an enum for guild's features and not hardcoded strings
return res.json(
await getVoiceRegions(
getIpAdress(req),
guild.features.includes("VIP_REGIONS"),
),
);
return res.json(await getVoiceRegions(req.ip!, guild.features.includes("VIP_REGIONS")));
},
);
+2 -2
View File
@@ -16,7 +16,7 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { getIpAdress, route } from "@spacebar/api";
import { route } from "@spacebar/api";
import { Ban, DiscordApiErrors, emitEvent, getPermission, Guild, Invite, InviteDeleteEvent, PublicInviteRelation, User } from "@spacebar/util";
import { Request, Response, Router } from "express";
import { HTTPError } from "lambert-server";
@@ -83,7 +83,7 @@ router.post(
const ban = await Ban.findOne({
where: [
{ guild_id: guild_id, user_id: req.user_id },
{ guild_id: guild_id, ip: getIpAdress(req) },
{ guild_id: guild_id, ip: req.ip },
],
});
+9 -40
View File
@@ -17,18 +17,9 @@
*/
import { route } from "@spacebar/api";
import {
ApiError,
Application,
DiscordApiErrors,
FieldErrors,
Member,
Permissions,
User,
getPermission,
} from "@spacebar/util";
import { ApiError, Application, DiscordApiErrors, FieldErrors, Member, Permissions, User, getPermission } from "@spacebar/util";
import { Request, Response, Router } from "express";
import { ApplicationAuthorizeSchema } from "@spacebar/schemas"
import { ApplicationAuthorizeSchema } from "@spacebar/schemas";
const router = Router({ mergeParams: true });
// TODO: scopes, other oauth types
@@ -84,26 +75,18 @@ router.get(
id: req.user_id,
bot: false,
},
select: [
"id",
"username",
"avatar",
"discriminator",
"public_flags",
],
select: ["id", "username", "avatar", "discriminator", "public_flags"],
});
const guilds = await Member.find({
where: {
user: {
id: req.user_id,
},
id: req.user_id,
},
relations: ["guild", "roles", "user"],
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
// prettier-ignore
select: ["guild.id", "guild.name", "guild.icon", "guild.mfa_level", "guild.owner_id", "roles.id", "communication_disabled_until", "user.flags"],
select: ["guild.id", "guild.name", "guild.icon", "guild.mfa_level", "guild.owner_id", "roles.id", "user.flags"],
});
const guildsWithPermissions = guilds.map((x) => {
@@ -112,7 +95,7 @@ router.get(
id: user.id,
roles: x.roles?.map((x) => x.id) || [],
communication_disabled_until: x.communication_disabled_until,
flags: x.user.flags
flags: x.user.flags,
},
guild: {
roles: x?.roles || [],
@@ -211,18 +194,9 @@ router.post(
// TODO: captcha verification
// TODO: MFA verification
const perms = await getPermission(
req.user_id,
body.guild_id,
undefined,
{ member_relations: ["user"] },
);
const perms = await getPermission(req.user_id, body.guild_id, undefined, { member_relations: ["user"] });
// getPermission cache won't exist if we're owner
if (
Object.keys(perms.cache || {}).length > 0 &&
perms.cache.member?.user.bot
)
throw DiscordApiErrors.UNAUTHORIZED;
if (Object.keys(perms.cache || {}).length > 0 && perms.cache.member?.user.bot) throw DiscordApiErrors.UNAUTHORIZED;
perms.hasThrow("MANAGE_GUILD");
const app = await Application.findOne({
@@ -235,12 +209,7 @@ router.post(
// TODO: use DiscordApiErrors
// findOneOrFail throws code 404
if (!app) throw new ApiError("Unknown Application", 10002, 404);
if (!app.bot)
throw new ApiError(
"OAuth2 application does not have a bot",
50010,
400,
);
if (!app.bot) throw new ApiError("OAuth2 application does not have a bot", 50010, 400);
await Member.addToGuild(app.id, body.guild_id);
+142 -4
View File
@@ -17,9 +17,25 @@
*/
import { route } from "@spacebar/api";
import { emitEvent, Member, User, UserDeleteEvent } from "@spacebar/util";
import {
Channel,
ChannelDeleteEvent,
ChannelRecipientRemoveEvent,
emitEvent,
Emoji,
Guild,
InstanceBan,
Member,
Recipient,
Sticker,
Stopwatch,
User,
UserDeleteEvent,
UserSettingsProtos,
} from "@spacebar/util";
import { Request, Response, Router } from "express";
import { PrivateUserProjection } from "@spacebar/schemas";
import { ChannelType, InstanceUserDeleteSchema, PrivateUserProjection } from "@spacebar/schemas";
import { Not } from "typeorm";
const router = Router({ mergeParams: true });
@@ -27,6 +43,7 @@ router.post(
"/",
route({
right: "MANAGE_USERS",
requestBody: "InstanceUserDeleteSchema",
responses: {
204: {},
403: {
@@ -38,12 +55,132 @@ router.post(
},
}),
async (req: Request, res: Response) => {
await User.findOneOrFail({
const sw = Stopwatch.startNew();
const body = req.body as InstanceUserDeleteSchema | undefined;
const user = await User.findOneOrFail({
where: { id: req.params.user_id },
select: [...PrivateUserProjection, "data"],
});
if (!(await InstanceBan.findOne({ where: { user_id: user.id } })))
await InstanceBan.create({ user_id: user.id, reason: body?.reason ?? "<legacy instance ban API - no reason specified>" }).save();
// prevent bugginess with clients - delete all DMs, only having half of the conversation is quite useless anyhow
const dmChannels = await user.getDmChannels();
for (const channel of dmChannels) {
console.log(`[Instance ban] Deleting DM channel ${channel.id} for user ${user.id}`);
await emitEvent({
event: "CHANNEL_DELETE",
data: channel.toJSON(),
channel_id: channel.id,
} as ChannelDeleteEvent);
await Recipient.delete({ channel_id: channel.id });
await Channel.deleteChannel(channel);
}
//leave all group channels
const groupChannels = await Channel.find({
where: { type: ChannelType.GROUP_DM },
relations: ["recipients"],
select: {
id: true,
owner_id: true,
recipients: {
id: true,
user_id: true,
},
},
});
await Promise.all(
groupChannels.map(async (channel) => {
const recipient = channel.recipients!.find((r) => r.user_id === user.id);
if (recipient) {
await Recipient.delete({ id: recipient.id });
await emitEvent({
event: "CHANNEL_RECIPIENT_REMOVE",
data: {
user: user.toPublicUser(),
channel_id: channel.id,
},
channel_id: channel.id,
} as ChannelRecipientRemoveEvent);
console.log(`[Instance ban] Removed user ${user.id} from group channel ${channel.id}`);
}
// if no recipients remain, delete the channel
const remainingRecipients = await Recipient.find({ where: { channel_id: channel.id } });
if (remainingRecipients.length === 0) {
await emitEvent({
event: "CHANNEL_DELETE",
data: channel.toJSON(),
channel_id: channel.id,
} as ChannelDeleteEvent);
await Channel.deleteChannel(channel);
console.log(`[Instance ban] Deleted empty group channel ${channel.id}`);
} else {
// otherwise, if the banned user was the owner, reassign ownership
if (channel.owner_id === user.id) {
channel.owner_id = remainingRecipients[0].user_id;
await channel.save();
console.log(`[Instance ban] Reassigned ownership of group channel ${channel.id} to user ${channel.owner_id}`);
}
}
}),
);
// change ownership on guilds
const guilds = await Guild.find({ where: { owner_id: req.params.user_id } });
await Promise.all(
guilds.map(async (guild) => {
const members = await Member.find({
where: { guild_id: guild.id, id: Not(req.params.user_id) },
relations: { roles: true },
select: { id: true, roles: { id: true, position: true } },
});
const sortedMembers = members
.filter((m) => m.id !== req.params.user_id)
.sort((a, b) => {
const aHighestRole = a.roles.reduce((prev, curr) => (curr.position > prev.position ? curr : prev), { position: -1 } as { position: number });
const bHighestRole = b.roles.reduce((prev, curr) => (curr.position > prev.position ? curr : prev), { position: -1 } as { position: number });
return bHighestRole.position - aHighestRole.position;
});
if (sortedMembers.length === 0) {
// no members left, delete guild
await guild.remove();
console.log(`[Instance ban] Deleted guild ${guild.id} as user ${user.id} was the last member`);
} else {
// assign new owner
guild.owner_id = sortedMembers[0].id;
await guild.save();
console.log(`[Instance ban] Transferred ownership of guild ${guild.id} to user ${guild.owner_id}`);
// safety - reassign emojis/stickers owned by the old owner
const stickers = await Sticker.find({ where: { guild_id: guild.id, user_id: req.params.user_id } });
await Promise.all(
stickers.map(async (sticker) => {
sticker.user_id = guild.owner_id;
await sticker.save();
console.log(`[Instance ban] Reassigned sticker ${sticker.id} ownership to user ${guild.owner_id}`);
}),
);
const emojis = await Emoji.find({ where: { guild_id: guild.id, user_id: req.params.user_id } });
await Promise.all(
emojis.map(async (emoji) => {
emoji.user_id = guild.owner_id!;
await emoji.save();
console.log(`[Instance ban] Reassigned emoji ${emoji.id} ownership to user ${guild.owner_id}`);
}),
);
}
}),
);
const members = await Member.find({ where: { id: req.params.user_id } });
await Promise.all([...members.map((member) => Member.removeFromGuild(member.id, member.guild_id)), User.delete({ id: req.params.user_id })]);
await Promise.all([...members.map((member) => Member.removeFromGuild(member.id, member.guild_id))]);
await UserSettingsProtos.delete({ user_id: req.params.user_id });
await User.delete({ id: req.params.user_id });
// TODO: respect intents as USER_DELETE has potential to cause privacy issues
await emitEvent({
@@ -52,6 +189,7 @@ router.post(
data: { user_id: req.params.user_id },
} as UserDeleteEvent);
console.log(`[Instance ban] Deleted user ${user.id} from instance in ${sw.elapsed().toString()}`);
res.sendStatus(204);
},
);
+2 -2
View File
@@ -16,7 +16,7 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { getIpAdress, getVoiceRegions, route } from "@spacebar/api";
import { getVoiceRegions, route } from "@spacebar/api";
import { Request, Response, Router } from "express";
const router: Router = Router({ mergeParams: true });
@@ -31,7 +31,7 @@ router.get(
},
}),
async (req: Request, res: Response) => {
res.json(await getVoiceRegions(getIpAdress(req), true)); //vip true?
res.json(await getVoiceRegions(req.ip!, true)); //vip true?
},
);
+4 -29
View File
@@ -16,41 +16,16 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Config } from "@spacebar/util";
import { Request } from "express";
export function getIpAdress(req: Request): string {
// TODO: express can do this (trustProxies: true)?
return req.ip!;
}
type Location = { latitude: number; longitude: number };
export function distanceBetweenLocations(
loc1: Location,
loc2: Location,
): number {
return distanceBetweenCoords(
loc1.latitude,
loc1.longitude,
loc2.latitude,
loc2.longitude,
);
export function distanceBetweenLocations(loc1: Location, loc2: Location): number {
return distanceBetweenCoords(loc1.latitude, loc1.longitude, loc2.latitude, loc2.longitude);
}
//Haversine function
function distanceBetweenCoords(
lat1: number,
lon1: number,
lat2: number,
lon2: number,
) {
function distanceBetweenCoords(lat1: number, lon1: number, lat2: number, lon2: number) {
const p = 0.017453292519943295; // Math.PI / 180
const c = Math.cos;
const a =
0.5 -
c((lat2 - lat1) * p) / 2 +
(c(lat1 * p) * c(lat2 * p) * (1 - c((lon2 - lon1) * p))) / 2;
const a = 0.5 - c((lat2 - lat1) * p) / 2 + (c(lat1 * p) * c(lat2 * p) * (1 - c((lon2 - lon1) * p))) / 2;
return 12742 * Math.asin(Math.sqrt(a)); // 2 * R; R = 6371 km
}
+5
View File
@@ -28,6 +28,7 @@ import ws from "ws";
import { Connection } from "./events/Connection";
import http from "http";
import { cleanupOnStartup } from "./util/Utils";
import { randomString } from "@spacebar/api";
export class Server {
public ws: ws.Server;
@@ -50,6 +51,10 @@ export class Server {
if (server) this.server = server;
else {
this.server = http.createServer(function (req, res) {
if(!req.headers.cookie?.split("; ").find(x => x.startsWith("__sb_sessid="))) {
res.setHeader("Set-Cookie", `__sb_sessid=${randomString(32)}; Secure; HttpOnly; SameSite=None; Path=/`);
}
res.writeHead(200).end("Online");
});
}
+7
View File
@@ -72,6 +72,13 @@ export async function Connection(
);
}
if (request.headers.cookie?.split("; ").find(x => x.startsWith("__sb_sessid="))) {
socket.fingerprint = request.headers.cookie
.split("; ")
.find((x) => x.startsWith("__sb_sessid="))
?.split("=")[1];
}
//Create session ID when the connection is opened. This allows gateway dump to group the initial websocket messages with the rest of the conversation.
const session_id = genSessionId();
socket.session_id = session_id; //Set the session of the WebSocket object
+50 -73
View File
@@ -29,6 +29,7 @@ import {
Message,
NewUrlUserSignatureData,
GuildMemberAddEvent,
Ban,
} from "@spacebar/util";
import { OPCODES } from "../util/Constants";
import { Send } from "../util/Send";
@@ -36,7 +37,7 @@ import { WebSocket } from "@spacebar/gateway";
import { Channel as AMQChannel } from "amqplib";
import { Recipient } from "@spacebar/util";
import * as console from "node:console";
import { PublicMember, RelationshipType } from "@spacebar/schemas"
import { PublicMember, RelationshipType } from "@spacebar/schemas";
import { bgRedBright } from "picocolors";
// TODO: close connection on Invalidated Token
@@ -46,10 +47,7 @@ import { bgRedBright } from "picocolors";
// 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,
) {
export function handlePresenceUpdate(this: WebSocket, { event, acknowledge, data }: EventOpts) {
acknowledge?.();
if (event === EVENTEnum.PresenceUpdate) {
return Send(this, {
@@ -92,32 +90,24 @@ export async function setupListener(this: WebSocket) {
this.listen_options = opts;
const consumer = consume.bind(this);
const handleChannelError = (err: unknown) => {
console.error(`[RabbitMQ] [user-${this.user_id}] Channel Error (Handled):`, err);
};
console.log("[RabbitMQ] setupListener: open for ", this.user_id);
if (RabbitMQ.connection) {
console.log(
"[RabbitMQ] setupListener: opts.channel = ",
typeof opts.channel,
"with channel id",
opts.channel?.ch,
);
console.log("[RabbitMQ] setupListener: opts.channel = ", typeof opts.channel, "with channel id", opts.channel?.ch);
opts.channel = await RabbitMQ.connection.createChannel();
opts.channel.on("error", handleChannelError);
opts.channel.queues = {};
console.log(
"[RabbitMQ] channel created: ",
typeof opts.channel,
"with channel id",
opts.channel?.ch,
);
console.log("[RabbitMQ] channel created: ", typeof opts.channel, "with channel id", opts.channel?.ch);
}
this.events[this.user_id] = await listenEvent(this.user_id, consumer, opts);
relationships.forEach(async (relationship) => {
this.events[relationship.to_id] = await listenEvent(
relationship.to_id,
handlePresenceUpdate.bind(this),
opts,
);
this.events[relationship.to_id] = await listenEvent(relationship.to_id, handlePresenceUpdate.bind(this), opts);
});
dm_channels.forEach(async (channel) => {
@@ -130,33 +120,27 @@ export async function setupListener(this: WebSocket) {
this.events[guild.id] = await listenEvent(guild.id, consumer, opts);
guild.channels.forEach(async (channel) => {
if (
permission
.overwriteChannel(channel.permission_overwrites ?? [])
.has("VIEW_CHANNEL")
) {
this.events[channel.id] = await listenEvent(
channel.id,
consumer,
opts,
);
if (permission.overwriteChannel(channel.permission_overwrites ?? []).has("VIEW_CHANNEL")) {
this.events[channel.id] = await listenEvent(channel.id, consumer, opts);
}
});
});
this.once("close", () => {
console.log(
"[RabbitMQ] setupListener: close for",
this.user_id,
"=",
typeof opts.channel,
"with channel id",
opts.channel?.ch,
this.once("close", async () => {
console.log("[RabbitMQ] setupListener: close for", this.user_id, "=", typeof opts.channel, "with channel id", opts.channel?.ch);
// wait for event consumer cancellation
await Promise.all(
Object.values(this.events).map((x) => {
if (x) return x();
else return Promise.resolve();
}),
);
if (opts.channel) opts.channel.close();
else {
Object.values(this.events).forEach((x) => x?.());
Object.values(this.member_events).forEach((x) => x());
await Promise.all(Object.values(this.member_events).map((x) => x()));
if (opts.channel) {
await opts.channel.close();
opts.channel.off("error", handleChannelError);
}
});
}
@@ -180,11 +164,7 @@ async function consume(this: WebSocket, opts: EventOpts) {
break;
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,
);
this.member_events[data.user.id] = await listenEvent(data.user.id, handlePresenceUpdate.bind(this), this.listen_options);
break;
case "GUILD_MEMBER_UPDATE":
if (!this.member_events[data.user.id]) break;
@@ -195,34 +175,32 @@ async function consume(this: WebSocket, opts: EventOpts) {
case "GUILD_DELETE":
this.events[id]?.();
delete this.events[id];
if (event === "GUILD_DELETE" && this.ipAddress) {
const ban = await Ban.findOne({
where: { guild_id: id, user_id: this.user_id },
});
if (ban) {
ban.ip = this.ipAddress || undefined;
await ban.save();
}
}
break;
case "CHANNEL_CREATE":
if (
!permission
.overwriteChannel(data.permission_overwrites)
.has("VIEW_CHANNEL")
) {
if (!permission.overwriteChannel(data.permission_overwrites).has("VIEW_CHANNEL")) {
return;
}
this.events[id] = await listenEvent(id, consumer, listenOpts);
break;
case "RELATIONSHIP_ADD":
this.events[data.user.id] = await listenEvent(
data.user.id,
handlePresenceUpdate.bind(this),
this.listen_options,
);
this.events[data.user.id] = await listenEvent(data.user.id, handlePresenceUpdate.bind(this), this.listen_options);
break;
case "GUILD_CREATE":
this.events[id] = await listenEvent(id, consumer, listenOpts);
break;
case "CHANNEL_UPDATE": {
const exists = this.events[id];
if (
permission
.overwriteChannel(data.permission_overwrites)
.has("VIEW_CHANNEL")
) {
if (permission.overwriteChannel(data.permission_overwrites).has("VIEW_CHANNEL")) {
if (exists) break;
this.events[id] = await listenEvent(id, consumer, listenOpts);
} else {
@@ -294,20 +272,19 @@ async function consume(this: WebSocket, opts: EventOpts) {
case "MESSAGE_UPDATE":
// console.log(this.request)
if (data["attachments"])
data["attachments"] =
Message.prototype.withSignedAttachments.call(
data,
new NewUrlUserSignatureData({
ip: this.ipAddress,
userAgent: this.userAgent,
}),
).attachments;
data["attachments"] = Message.prototype.withSignedAttachments.call(
data,
new NewUrlUserSignatureData({
ip: this.ipAddress,
userAgent: this.userAgent,
}),
).attachments;
break;
default:
break;
}
if(event === "GUILD_MEMBER_ADD") {
if (event === "GUILD_MEMBER_ADD") {
if ((data as PublicMember).roles === undefined || (data as PublicMember).roles === null) {
console.log(bgRedBright("[Gateway]"), "[GUILD_MEMBER_ADD] roles is undefined, setting to empty array!", opts.origin ?? "(Event origin not defined)", data);
(data as PublicMember).roles = [];
+3 -2
View File
@@ -33,6 +33,7 @@ export interface WebSocket extends WS {
compress?: "zlib-stream" | "zstd-stream";
ipAddress?: string;
userAgent?: string; // for cdn request signing
fingerprint?: string;
shard_count?: bigint;
shard_id?: bigint;
deflate?: Deflate;
@@ -44,8 +45,8 @@ export interface WebSocket extends WS {
intents: Intents;
sequence: number;
permissions: Record<string, Permissions>;
events: Record<string, undefined | (() => unknown)>;
member_events: Record<string, () => unknown>;
events: Record<string, undefined | (() => Promise<unknown>)>;
member_events: Record<string, () => Promise<unknown>>;
listen_options: ListenEventOpts;
capabilities?: Capabilities;
large_threshold: number;
@@ -0,0 +1,6 @@
import { ConnectedAccount } from "@spacebar/util";
export type InstanceUserDeleteSchema = InstanceUserDeleteSchemaContent | undefined; //unsure if this a correct way to make the body optional
export interface InstanceUserDeleteSchemaContent {
reason?: string;
}
+1
View File
@@ -16,6 +16,7 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export * from "./ConnectedAccount";
export * from "./InstanceUserDeleteSchema";
export * from "./Member";
export * from "./User";
export * from "./UserSettings";
+2 -2
View File
@@ -17,7 +17,7 @@
*/
export interface BanCreateSchema {
delete_message_seconds?: string;
delete_message_days?: string;
delete_message_seconds?: number;
delete_message_days?: number;
reason?: string;
}
@@ -24,7 +24,7 @@ export class SecurityConfiguration {
twoFactor: TwoFactorConfiguration = new TwoFactorConfiguration();
autoUpdate: boolean | number = true;
requestSignature: string = crypto.randomBytes(32).toString("base64");
jwtSecret: string = crypto.randomBytes(256).toString("base64");
jwtSecret: string | null = null;
// header to get the real user ip address
// X-Forwarded-For for nginx/reverse proxies
// CF-Connecting-IP for cloudflare
@@ -36,7 +36,7 @@ export class SecurityConfiguration {
// https://docs.abuseipdb.com/#api-daily-rate-limits
abuseipdbBlacklistRatelimit: number = 5;
abuseipdbConfidenceScoreTreshold: number = 50;
ipdataApiKey: string | null = "eca677b284b3bac29eb72f5e496aa9047f26543605efe99ff2ce35c9"; // isnt even valid anymore it seems?
ipdataApiKey: string | null = null;
mfaBackupCodeCount: number = 10;
statsWorldReadable: boolean = true;
defaultRegistrationTokenExpiration: number = 1000 * 60 * 60 * 24 * 7; //1 week
+2 -1
View File
@@ -20,6 +20,7 @@ import { Column, Entity, JoinColumn, ManyToOne } from "typeorm";
import { BaseClass } from "./BaseClass";
import { User } from "./User";
import crypto from "crypto";
import { Config } from "../util";
@Entity({
name: "backup_codes",
@@ -41,7 +42,7 @@ export class BackupCode extends BaseClass {
export function generateMfaBackupCodes(user_id: string) {
const backup_codes: BackupCode[] = [];
for (let i = 0; i < 10; i++) {
for (let i = 0; i < Config.get().security.mfaBackupCodeCount; i++) {
const code = BackupCode.create({
user: { id: user_id },
code: crypto.randomBytes(4).toString("hex"), // 8 characters
+8 -6
View File
@@ -379,13 +379,15 @@ export class Channel extends BaseClass {
// TODO Delete attachments from the CDN for messages in the channel
await Channel.delete({ id: channel.id });
const guild = await Guild.findOneOrFail({
where: { id: channel.guild_id },
select: { channel_ordering: true },
});
if (channel.guild_id) {
const guild = await Guild.findOneOrFail({
where: { id: channel.guild_id },
select: { channel_ordering: true },
});
const updatedOrdering = guild.channel_ordering.filter((id) => id != channel.id);
await Guild.update({ id: channel.guild_id }, { channel_ordering: updatedOrdering });
const updatedOrdering = guild.channel_ordering.filter((id) => id != channel.id);
await Guild.update({ id: channel.guild_id }, { channel_ordering: updatedOrdering });
}
}
static async calculatePosition(channel_id: string, guild_id: string, guild?: Guild) {
+111
View File
@@ -0,0 +1,111 @@
/*
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/>.
*/
import { Column, CreateDateColumn, Entity, FindOptionsWhere, JoinColumn, ManyToOne, OneToOne, RelationId } from "typeorm";
import { BaseClass } from "./BaseClass";
import { Team } from "./Team";
import { User } from "./User";
import { Guild } from "./Guild";
@Entity({
name: "instance_bans",
})
export class InstanceBan extends BaseClass {
@Column({ type: "bigint" })
@CreateDateColumn()
created_at: Date = new Date();
@Column()
reason: string;
@Column({ nullable: true })
user_id?: string;
@Column({ nullable: true })
fingerprint?: string;
@Column({ nullable: true })
ip_address?: string;
// chain of trust type tracking
@Column({ default: false })
is_allowlisted: boolean = false;
@Column({ default: false })
is_from_other_instance_ban: boolean = false;
@Column({ nullable: true })
@RelationId((instance_ban: InstanceBan) => instance_ban.origin_instance_ban)
origin_instance_ban_id?: string;
@JoinColumn({ name: "origin_instance_ban_id" })
@OneToOne(() => InstanceBan, { nullable: true, onDelete: "SET NULL" })
origin_instance_ban?: InstanceBan;
static async findInstanceBans(opts: { userId?: string; ipAddress?: string; fingerprint?: string; propagateBan?: boolean }) {
const optionalChecks: FindOptionsWhere<InstanceBan>[] = [{ user_id: opts.userId }];
if (opts?.ipAddress) optionalChecks.push({ ip_address: opts.ipAddress });
if (opts?.fingerprint) optionalChecks.push({ fingerprint: opts.fingerprint });
const instanceBans = await InstanceBan.find({ where: optionalChecks });
const banReasons = [];
for (const ban of instanceBans) {
if (ban.is_allowlisted) continue;
if (opts?.fingerprint && ban.fingerprint === opts.fingerprint) banReasons.push("fingerprint");
if (opts?.ipAddress && ban.ip_address === opts.ipAddress) banReasons.push("ipAddress");
if (opts?.userId && ban.user_id === opts?.userId) banReasons.push("userId");
}
const banViralityPromises: Promise<InstanceBan>[] = [];
if (opts.propagateBan && banReasons.length > 0) {
if (opts?.ipAddress && !instanceBans.find((b) => b.ip_address === opts.ipAddress))
banViralityPromises.push(
InstanceBan.create({
user_id: opts.userId,
ip_address: opts.ipAddress,
reason: "Propagated from other instance ban",
is_from_other_instance_ban: true,
origin_instance_ban: instanceBans[0],
}).save(),
);
if (opts?.fingerprint && !instanceBans.find((b) => b.fingerprint === opts.fingerprint))
banViralityPromises.push(
InstanceBan.create({
user_id: opts.userId,
fingerprint: opts.fingerprint,
reason: "Propagated from other instance ban",
is_from_other_instance_ban: true,
origin_instance_ban: instanceBans[0],
}).save(),
);
if (opts?.userId && !instanceBans.find((b) => b.user_id === opts.userId))
banViralityPromises.push(
InstanceBan.create({
user_id: opts.userId,
reason: "Propagated from other instance ban",
is_from_other_instance_ban: true,
origin_instance_ban: instanceBans[0],
}).save(),
);
}
await Promise.all(banViralityPromises);
return banReasons;
}
}
+14
View File
@@ -359,4 +359,18 @@ export class User extends BaseClass {
return qry[0];
}
async getDmChannels() {
const qry = await Channel.getRepository()
.createQueryBuilder("channel")
.leftJoinAndSelect("channel.recipients", "rcp")
.where("channel.type = :type", { type: ChannelType.DM })
.andWhere("rcp.user_id = :user_id", { user_id: this.id })
.groupBy("channel.id")
.addGroupBy("rcp.id")
.having("COUNT(rcp.id) = 2")
.getMany();
return qry;
}
}
+1
View File
@@ -36,6 +36,7 @@ export * from "./EmbedCache";
export * from "./Emoji";
export * from "./Encryption";
export * from "./Guild";
export * from "./InstanceBan";
export * from "./Invite";
export * from "./Member";
export * from "./Message";
+1
View File
@@ -219,6 +219,7 @@ export interface GuildBanAddEvent extends Event {
data: {
guild_id: string;
user: User;
delete_message_secs?: number
};
}
+727
View File
@@ -0,0 +1,727 @@
// @prettier-ignore
/*
Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
Copyright (C) 2025 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/>.
*/
import { MigrationInterface, QueryRunner } from "typeorm";
export class initial0 implements MigrationInterface {
name = "initial0";
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TABLE public.applications (
id character varying NOT NULL,
name character varying NOT NULL,
icon character varying,
description character varying,
summary character varying,
type text,
hook boolean NOT NULL,
bot_public boolean NOT NULL,
bot_require_code_grant boolean NOT NULL,
verify_key character varying NOT NULL,
flags integer NOT NULL,
redirect_uris text,
rpc_application_state integer,
store_application_state integer,
verification_state integer,
interactions_endpoint_url character varying,
integration_public boolean,
integration_require_code_grant boolean,
discoverability_state integer,
discovery_eligibility_flags integer,
tags text,
cover_image character varying,
install_params text,
terms_of_service_url character varying,
privacy_policy_url character varying,
owner_id character varying,
bot_user_id character varying,
team_id character varying
);`);
await queryRunner.query(`CREATE TABLE public.attachments (
id character varying NOT NULL,
filename character varying NOT NULL,
size integer NOT NULL,
url character varying NOT NULL,
proxy_url character varying NOT NULL,
height integer,
width integer,
content_type character varying,
message_id character varying
);`);
await queryRunner.query(`CREATE TABLE public.audit_logs (
id character varying NOT NULL,
user_id character varying,
action_type integer NOT NULL,
options text,
changes text NOT NULL,
reason character varying,
target_id character varying
);`);
await queryRunner.query(`CREATE TABLE public.backup_codes (
id character varying NOT NULL,
code character varying NOT NULL,
consumed boolean NOT NULL,
expired boolean NOT NULL,
user_id character varying
);`);
await queryRunner.query(`CREATE TABLE public.bans (
id character varying NOT NULL,
user_id character varying,
guild_id character varying,
executor_id character varying,
ip character varying NOT NULL,
reason character varying
);`);
await queryRunner.query(`CREATE TABLE public.categories (
id integer NOT NULL,
name character varying,
localizations text NOT NULL,
is_primary boolean
);`);
await queryRunner.query(`CREATE TABLE public.channels (
id character varying NOT NULL,
created_at timestamp without time zone NOT NULL,
name character varying,
icon text,
type integer NOT NULL,
last_message_id character varying,
guild_id character varying,
parent_id character varying,
owner_id character varying,
last_pin_timestamp integer,
default_auto_archive_duration integer,
permission_overwrites text,
video_quality_mode integer,
bitrate integer,
user_limit integer,
nsfw boolean NOT NULL,
rate_limit_per_user integer,
topic character varying,
retention_policy_id character varying,
flags integer NOT NULL,
default_thread_rate_limit_per_user integer NOT NULL,
position integer NOT NULL DEFAULT 0
);`);
await queryRunner.query(`CREATE TABLE public.client_release (
id character varying NOT NULL,
name character varying NOT NULL,
pub_date timestamp without time zone NOT NULL,
url character varying NOT NULL,
platform character varying NOT NULL,
enabled boolean NOT NULL,
notes character varying
);`);
await queryRunner.query(`CREATE TABLE public.config (
key character varying NOT NULL,
value text
);`);
await queryRunner.query(`CREATE TABLE public.connected_accounts (
id character varying NOT NULL,
external_id character varying NOT NULL,
user_id character varying,
friend_sync boolean NOT NULL,
name character varying NOT NULL,
revoked boolean NOT NULL,
show_activity integer NOT NULL,
type character varying NOT NULL,
verified boolean NOT NULL,
visibility integer NOT NULL,
integrations text NOT NULL,
metadata text,
metadata_visibility integer NOT NULL,
two_way_link boolean NOT NULL,
token_data text
);`);
await queryRunner.query(`CREATE TABLE public.connection_config (
key character varying NOT NULL,
value text
);`);
await queryRunner.query(`CREATE TABLE public.embed_cache (
id character varying NOT NULL,
url character varying NOT NULL,
embed text NOT NULL
);`);
await queryRunner.query(`CREATE TABLE public.emojis (
id character varying NOT NULL,
animated boolean NOT NULL,
available boolean NOT NULL,
guild_id character varying NOT NULL,
user_id character varying,
managed boolean NOT NULL,
name character varying NOT NULL,
require_colons boolean NOT NULL,
roles text NOT NULL,
groups text
);`);
await queryRunner.query(`CREATE TABLE public.guilds (
id character varying NOT NULL,
afk_channel_id character varying,
afk_timeout integer,
banner character varying,
default_message_notifications integer,
description character varying,
discovery_splash character varying,
explicit_content_filter integer,
features text NOT NULL,
primary_category_id character varying,
icon character varying,
large boolean NOT NULL,
max_members integer,
max_presences integer,
max_video_channel_users integer,
member_count integer,
presence_count integer,
template_id character varying,
mfa_level integer,
name character varying NOT NULL,
owner_id character varying,
preferred_locale character varying,
premium_subscription_count integer,
premium_tier integer NOT NULL,
public_updates_channel_id character varying,
rules_channel_id character varying,
region character varying,
splash character varying,
system_channel_id character varying,
system_channel_flags integer,
unavailable boolean NOT NULL,
verification_level integer,
welcome_screen text NOT NULL,
widget_channel_id character varying,
widget_enabled boolean NOT NULL,
nsfw_level integer,
nsfw boolean NOT NULL,
parent character varying,
premium_progress_bar_enabled boolean
);`);
await queryRunner.query(`CREATE TABLE public.invites (
code character varying NOT NULL,
temporary boolean NOT NULL,
uses integer NOT NULL,
max_uses integer NOT NULL,
max_age integer NOT NULL,
created_at timestamp without time zone NOT NULL,
expires_at timestamp without time zone,
guild_id character varying,
channel_id character varying,
inviter_id character varying,
target_user_id character varying,
target_user_type integer,
vanity_url boolean,
flags integer NOT NULL
);`);
await queryRunner.query(`CREATE TABLE public.member_roles (
index integer NOT NULL,
role_id character varying NOT NULL
);`);
await queryRunner.query(`CREATE TABLE public.members (
index integer NOT NULL,
id character varying NOT NULL,
guild_id character varying NOT NULL,
nick character varying,
joined_at timestamp without time zone NOT NULL,
premium_since bigint,
deaf boolean NOT NULL,
mute boolean NOT NULL,
pending boolean NOT NULL,
settings text NOT NULL,
last_message_id character varying,
joined_by character varying,
avatar character varying,
banner character varying,
bio character varying NOT NULL,
theme_colors text,
pronouns character varying,
communication_disabled_until timestamp without time zone
);`);
await queryRunner.query(`CREATE SEQUENCE public.members_index_seq
AS integer
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;`);
await queryRunner.query(`ALTER SEQUENCE public.members_index_seq OWNED BY public.members.index;`);
await queryRunner.query(`CREATE TABLE public.message_channel_mentions (
"messagesId" character varying NOT NULL,
"channelsId" character varying NOT NULL
);`);
await queryRunner.query(`CREATE TABLE public.message_role_mentions (
"messagesId" character varying NOT NULL,
"rolesId" character varying NOT NULL
);`);
await queryRunner.query(`CREATE TABLE public.message_stickers (
"messagesId" character varying NOT NULL,
"stickersId" character varying NOT NULL
);`);
await queryRunner.query(`CREATE TABLE public.message_user_mentions (
"messagesId" character varying NOT NULL,
"usersId" character varying NOT NULL
);`);
await queryRunner.query(`CREATE TABLE public.messages (
id character varying NOT NULL,
channel_id character varying,
guild_id character varying,
author_id character varying,
member_id character varying,
webhook_id character varying,
application_id character varying,
content character varying,
"timestamp" timestamp without time zone DEFAULT now() NOT NULL,
edited_timestamp timestamp without time zone,
tts boolean,
mention_everyone boolean,
embeds text NOT NULL,
reactions text NOT NULL,
nonce text,
type integer NOT NULL,
activity text,
message_reference text,
interaction text,
components text,
message_reference_id character varying,
pinned boolean,
flags integer
);`);
await queryRunner.query(`CREATE TABLE public.migrations (
id integer NOT NULL,
"timestamp" bigint NOT NULL,
name character varying NOT NULL
);`);
await queryRunner.query(`CREATE SEQUENCE public.migrations_id_seq
AS integer
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;`);
await queryRunner.query(`ALTER SEQUENCE public.migrations_id_seq OWNED BY public.migrations.id;`);
await queryRunner.query(`CREATE TABLE public.notes (
id character varying NOT NULL,
content character varying NOT NULL,
owner_id character varying,
target_id character varying
);`);
await queryRunner.query(`CREATE TABLE public.rate_limits (
id character varying NOT NULL,
executor_id character varying NOT NULL,
hits integer NOT NULL,
blocked boolean NOT NULL,
expires_at timestamp without time zone NOT NULL
);`);
await queryRunner.query(`CREATE TABLE public.read_states (
id character varying NOT NULL,
channel_id character varying NOT NULL,
user_id character varying NOT NULL,
last_message_id character varying,
public_ack character varying,
notifications_cursor character varying,
last_pin_timestamp timestamp without time zone,
mention_count integer
);`);
await queryRunner.query(`CREATE TABLE public.recipients (
id character varying NOT NULL,
channel_id character varying NOT NULL,
user_id character varying NOT NULL,
closed boolean DEFAULT false NOT NULL
);`);
await queryRunner.query(`CREATE TABLE public.relationships (
id character varying NOT NULL,
from_id character varying NOT NULL,
to_id character varying NOT NULL,
nickname character varying,
type integer NOT NULL
);`);
await queryRunner.query(`CREATE TABLE public.roles (
id character varying NOT NULL,
guild_id character varying NOT NULL,
color integer NOT NULL,
hoist boolean NOT NULL,
managed boolean NOT NULL,
mentionable boolean NOT NULL,
name character varying NOT NULL,
permissions character varying NOT NULL,
"position" integer NOT NULL,
icon character varying,
unicode_emoji character varying,
tags text,
flags integer DEFAULT 0 NOT NULL
);`);
await queryRunner.query(`CREATE TABLE public.security_settings (
id character varying NOT NULL,
guild_id character varying,
channel_id character varying,
encryption_permission_mask integer NOT NULL,
allowed_algorithms text NOT NULL,
current_algorithm character varying NOT NULL,
used_since_message character varying
);`);
await queryRunner.query(`CREATE TABLE public.sessions (
id character varying NOT NULL,
user_id character varying,
session_id character varying NOT NULL,
activities text,
client_info text NOT NULL,
status character varying NOT NULL
);`);
await queryRunner.query(`CREATE TABLE public.sticker_packs (
id character varying NOT NULL,
name character varying NOT NULL,
description character varying,
banner_asset_id character varying,
cover_sticker_id character varying,
"coverStickerId" character varying
);`);
await queryRunner.query(`CREATE TABLE public.stickers (
id character varying NOT NULL,
name character varying NOT NULL,
description character varying,
available boolean,
tags character varying,
pack_id character varying,
guild_id character varying,
user_id character varying,
type integer NOT NULL,
format_type integer NOT NULL
);`);
await queryRunner.query(`CREATE TABLE public.team_members (
id character varying NOT NULL,
membership_state integer NOT NULL,
permissions text NOT NULL,
team_id character varying,
user_id character varying
);`);
await queryRunner.query(`CREATE TABLE public.teams (
id character varying NOT NULL,
icon character varying,
name character varying NOT NULL,
owner_user_id character varying
);`);
await queryRunner.query(`CREATE TABLE public.templates (
id character varying NOT NULL,
code character varying NOT NULL,
name character varying NOT NULL,
description character varying,
usage_count integer,
creator_id character varying,
created_at timestamp without time zone NOT NULL,
updated_at timestamp without time zone NOT NULL,
source_guild_id character varying,
serialized_source_guild text NOT NULL
);`);
await queryRunner.query(`CREATE TABLE public.user_settings (
index integer NOT NULL,
afk_timeout integer,
allow_accessibility_detection boolean,
animate_emoji boolean,
animate_stickers integer,
contact_sync_enabled boolean,
convert_emoticons boolean,
custom_status text,
default_guilds_restricted boolean,
detect_platform_accounts boolean,
developer_mode boolean,
disable_games_tab boolean,
enable_tts_command boolean,
explicit_content_filter integer,
friend_source_flags text,
gateway_connected boolean,
gif_auto_play boolean,
guild_folders text,
guild_positions text,
inline_attachment_media boolean,
inline_embed_media boolean,
locale character varying,
message_display_compact boolean,
native_phone_integration_enabled boolean,
render_embeds boolean,
render_reactions boolean,
restricted_guilds text,
show_current_game boolean,
status character varying,
stream_notifications_enabled boolean,
theme character varying,
timezone_offset integer
);`);
await queryRunner.query(`CREATE SEQUENCE public.user_settings_index_seq
AS integer
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;`
);
await queryRunner.query(`ALTER SEQUENCE public.user_settings_index_seq OWNED BY public.user_settings.index;`);
await queryRunner.query(`CREATE TABLE public.users (
id character varying NOT NULL,
username character varying NOT NULL,
discriminator character varying NOT NULL,
avatar character varying,
accent_color integer,
banner character varying,
theme_colors text,
pronouns character varying,
phone character varying,
desktop boolean NOT NULL,
mobile boolean NOT NULL,
premium boolean NOT NULL,
premium_type integer NOT NULL,
bot boolean NOT NULL,
bio character varying NOT NULL,
system boolean NOT NULL,
nsfw_allowed boolean NOT NULL,
mfa_enabled boolean NOT NULL,
totp_secret character varying,
totp_last_ticket character varying,
created_at timestamp without time zone NOT NULL,
premium_since timestamp without time zone,
verified boolean NOT NULL,
disabled boolean NOT NULL,
deleted boolean NOT NULL,
email character varying,
flags bigint NOT NULL,
public_flags bigint NOT NULL,
purchased_flags bigint NOT NULL,
premium_usage_flags integer NOT NULL,
rights bigint NOT NULL,
data text NOT NULL,
fingerprints text NOT NULL,
extended_settings text NOT NULL,
"settingsIndex" integer
);`);
await queryRunner.query(`CREATE TABLE public.valid_registration_tokens (
token character varying NOT NULL,
created_at timestamp without time zone NOT NULL,
expires_at timestamp without time zone NOT NULL
);`);
await queryRunner.query(`CREATE TABLE public.voice_states (
id character varying NOT NULL,
guild_id character varying,
channel_id character varying,
user_id character varying,
session_id character varying NOT NULL,
token character varying,
deaf boolean NOT NULL,
mute boolean NOT NULL,
self_deaf boolean NOT NULL,
self_mute boolean NOT NULL,
self_stream boolean,
self_video boolean NOT NULL,
suppress boolean NOT NULL,
request_to_speak_timestamp timestamp without time zone
);`);
await queryRunner.query(`CREATE TABLE public.webhooks (
id character varying NOT NULL,
type integer NOT NULL,
name character varying,
avatar character varying,
token character varying,
guild_id character varying,
channel_id character varying,
application_id character varying,
user_id character varying,
source_guild_id character varying
);`);
await queryRunner.query(`ALTER TABLE ONLY public.members ALTER COLUMN index SET DEFAULT nextval('public.members_index_seq'::regclass);`);
await queryRunner.query(`ALTER TABLE ONLY public.migrations ALTER COLUMN id SET DEFAULT nextval('public.migrations_id_seq'::regclass);`);
await queryRunner.query(`ALTER TABLE ONLY public.user_settings ALTER COLUMN index SET DEFAULT nextval('public.user_settings_index_seq'::regclass);`);
await queryRunner.query(`ALTER TABLE ONLY public.embed_cache ADD CONSTRAINT "PK_0abb7581d4efc5a8b1361389c5e" PRIMARY KEY (id);`);
await queryRunner.query(`ALTER TABLE ONLY public.messages ADD CONSTRAINT "PK_18325f38ae6de43878487eff986" PRIMARY KEY (id);`);
await queryRunner.query(`ALTER TABLE ONLY public.audit_logs ADD CONSTRAINT "PK_1bb179d048bbc581caa3b013439" PRIMARY KEY (id);`);
await queryRunner.query(`ALTER TABLE ONLY public.categories ADD CONSTRAINT "PK_24dbc6126a28ff948da33e97d3b" PRIMARY KEY (id);`);
await queryRunner.query(`ALTER TABLE ONLY public.config ADD CONSTRAINT "PK_26489c99ddbb4c91631ef5cc791" PRIMARY KEY (key);`);
await queryRunner.query(`ALTER TABLE ONLY public.sessions ADD CONSTRAINT "PK_3238ef96f18b355b671619111bc" PRIMARY KEY (id);`);
await queryRunner.query(`ALTER TABLE ONLY public.invites ADD CONSTRAINT "PK_33fd8a248db1cd832baa8aa25bf" PRIMARY KEY (code);`);
await queryRunner.query(`ALTER TABLE ONLY public.backup_codes ADD CONSTRAINT "PK_34ab957382dbc57e8fb53f1638f" PRIMARY KEY (id);`);
await queryRunner.query(`ALTER TABLE ONLY public.rate_limits ADD CONSTRAINT "PK_3b4449f1f5fc167d921ee619f65" PRIMARY KEY (id);`);
await queryRunner.query(`ALTER TABLE ONLY public.security_settings ADD CONSTRAINT "PK_4aec436cf81177ae97a1bcec3c7" PRIMARY KEY (id);`);
await queryRunner.query(`ALTER TABLE ONLY public.client_release ADD CONSTRAINT "PK_4c4ea258342d2d6ba1be0a71a43" PRIMARY KEY (id);`);
await queryRunner.query(`ALTER TABLE ONLY public.templates ADD CONSTRAINT "PK_515948649ce0bbbe391de702ae5" PRIMARY KEY (id);`);
await queryRunner.query(`ALTER TABLE ONLY public.attachments ADD CONSTRAINT "PK_5e1f050bcff31e3084a1d662412" PRIMARY KEY (id);`);
await queryRunner.query(`ALTER TABLE ONLY public.connected_accounts ADD CONSTRAINT "PK_70416f1da0be645bb31da01c774" PRIMARY KEY (id);`);
await queryRunner.query(`ALTER TABLE ONLY public.message_role_mentions ADD CONSTRAINT "PK_74dba92cc300452a6e14b83ed44" PRIMARY KEY ("messagesId", "rolesId");`);
await queryRunner.query(`ALTER TABLE ONLY public.teams ADD CONSTRAINT "PK_7e5523774a38b08a6236d322403" PRIMARY KEY (id);`);
await queryRunner.query(`ALTER TABLE ONLY public.message_channel_mentions ADD CONSTRAINT "PK_85cb45351497cd9d06a79ced65e" PRIMARY KEY ("messagesId", "channelsId");`);
await queryRunner.query(`ALTER TABLE ONLY public.migrations ADD CONSTRAINT "PK_8c82d7f526340ab734260ea46be" PRIMARY KEY (id);`);
await queryRunner.query(`ALTER TABLE ONLY public.applications ADD CONSTRAINT "PK_938c0a27255637bde919591888f" PRIMARY KEY (id);`);
await queryRunner.query(`ALTER TABLE ONLY public.member_roles ADD CONSTRAINT "PK_951c1d72a0fd1da8760b4a1fd66" PRIMARY KEY (index, role_id);`);
await queryRunner.query(`ALTER TABLE ONLY public.emojis ADD CONSTRAINT "PK_9adb96a675f555c6169bad7ba62" PRIMARY KEY (id);`);
await queryRunner.query(`ALTER TABLE ONLY public.message_user_mentions ADD CONSTRAINT "PK_9b9b6e245ad47a48dbd7605d4fb" PRIMARY KEY ("messagesId", "usersId");`);
await queryRunner.query(`ALTER TABLE ONLY public.webhooks ADD CONSTRAINT "PK_9e8795cfc899ab7bdaa831e8527" PRIMARY KEY (id);`);
await queryRunner.query(`ALTER TABLE ONLY public.sticker_packs ADD CONSTRAINT "PK_a27381efea0f876f5d3233af655" PRIMARY KEY (id);`);
await queryRunner.query(`ALTER TABLE ONLY public.users ADD CONSTRAINT "PK_a3ffb1c0c8416b9fc6f907b7433" PRIMARY KEY (id);`);
await queryRunner.query(`ALTER TABLE ONLY public.bans ADD CONSTRAINT "PK_a4d6f261bffa4615c62d756566a" PRIMARY KEY (id);`);
await queryRunner.query(`ALTER TABLE ONLY public.voice_states ADD CONSTRAINT "PK_ada09a50c134fad1369b510e3ce" PRIMARY KEY (id);`);
await queryRunner.query(`ALTER TABLE ONLY public.notes ADD CONSTRAINT "PK_af6206538ea96c4e77e9f400c3d" PRIMARY KEY (id);`);
await queryRunner.query(`ALTER TABLE ONLY public.members ADD CONSTRAINT "PK_b4a6b8c2478e5df990909c6cf6a" PRIMARY KEY (index);`);
await queryRunner.query(`ALTER TABLE ONLY public.relationships ADD CONSTRAINT "PK_ba20e2f5cf487408e08e4dcecaf" PRIMARY KEY (id);`);
await queryRunner.query(`ALTER TABLE ONLY public.connection_config ADD CONSTRAINT "PK_bc0554f736ad71dde346549488a" PRIMARY KEY (key);`);
await queryRunner.query(`ALTER TABLE ONLY public.channels ADD CONSTRAINT "PK_bc603823f3f741359c2339389f9" PRIMARY KEY (id);`);
await queryRunner.query(`ALTER TABLE ONLY public.roles ADD CONSTRAINT "PK_c1433d71a4838793a49dcad46ab" PRIMARY KEY (id);`);
await queryRunner.query(`ALTER TABLE ONLY public.team_members ADD CONSTRAINT "PK_ca3eae89dcf20c9fd95bf7460aa" PRIMARY KEY (id);`);
await queryRunner.query(`ALTER TABLE ONLY public.recipients ADD CONSTRAINT "PK_de8fc5a9c364568f294798fe1e9" PRIMARY KEY (id);`);
await queryRunner.query(`ALTER TABLE ONLY public.valid_registration_tokens ADD CONSTRAINT "PK_e0f5c8e3fcefe3134a092c50485" PRIMARY KEY (token);`);
await queryRunner.query(`ALTER TABLE ONLY public.stickers ADD CONSTRAINT "PK_e1dafa4063a5532645cc2810374" PRIMARY KEY (id);`);
await queryRunner.query(`ALTER TABLE ONLY public.read_states ADD CONSTRAINT "PK_e6956a804978f01b713b1ed58e2" PRIMARY KEY (id);`);
await queryRunner.query(`ALTER TABLE ONLY public.guilds ADD CONSTRAINT "PK_e7e7f2a51bd6d96a9ac2aa560f9" PRIMARY KEY (id);`);
await queryRunner.query(`ALTER TABLE ONLY public.user_settings ADD CONSTRAINT "PK_e81f8bb92802737337d35c00981" PRIMARY KEY (index);`);
await queryRunner.query(`ALTER TABLE ONLY public.message_stickers ADD CONSTRAINT "PK_ed820c4093d0b8cd1d2bcf66087" PRIMARY KEY ("messagesId", "stickersId");`);
await queryRunner.query(`ALTER TABLE ONLY public.users ADD CONSTRAINT "REL_0c14beb78d8c5ccba66072adbc" UNIQUE ("settingsIndex");`);
await queryRunner.query(`ALTER TABLE ONLY public.applications ADD CONSTRAINT "REL_2ce5a55796fe4c2f77ece57a64" UNIQUE (bot_user_id);`);
await queryRunner.query(`ALTER TABLE ONLY public.notes ADD CONSTRAINT "UQ_74e6689b9568cc965b8bfc9150b" UNIQUE (owner_id, target_id);`);
await queryRunner.query(`ALTER TABLE ONLY public.templates ADD CONSTRAINT "UQ_be38737bf339baf63b1daeffb55" UNIQUE (code);`);
await queryRunner.query(`CREATE INDEX "IDX_05535bc695e9f7ee104616459d" ON public.messages USING btree (author_id);`);
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_0abf8b443321bd3cf7f81ee17a" ON public.read_states USING btree (channel_id, user_id);`);
await queryRunner.query(`CREATE INDEX "IDX_29d63eb1a458200851bc37d074" ON public.message_role_mentions USING btree ("rolesId");`);
await queryRunner.query(`CREATE INDEX "IDX_2a27102ecd1d81b4582a436092" ON public.message_channel_mentions USING btree ("messagesId");`);
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_3ed7a60fb7dbe04e1ba9332a8b" ON public.messages USING btree (channel_id, id);`);
await queryRunner.query(`CREATE INDEX "IDX_40bb6f23e7cc133292e92829d2" ON public.message_stickers USING btree ("messagesId");`);
await queryRunner.query(`CREATE INDEX "IDX_5d7ddc8a5f9c167f548625e772" ON public.member_roles USING btree (index);`);
await queryRunner.query(`CREATE INDEX "IDX_86b9109b155eb70c0a2ca3b4b6" ON public.messages USING btree (channel_id);`);
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_a0b2ff0a598df0b0d055934a17" ON public.relationships USING btree (from_id, to_id);`);
await queryRunner.query(`CREATE INDEX "IDX_a343387fc560ef378760681c23" ON public.message_user_mentions USING btree ("messagesId");`);
await queryRunner.query(`CREATE INDEX "IDX_a8242cf535337a490b0feaea0b" ON public.message_role_mentions USING btree ("messagesId");`);
await queryRunner.query(`CREATE INDEX "IDX_b831eb18ceebd28976239b1e2f" ON public.message_user_mentions USING btree ("usersId");`);
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_bb2bf9386ac443afbbbf9f12d3" ON public.members USING btree (id, guild_id);`);
await queryRunner.query(`CREATE INDEX "IDX_bdb8c09e1464cabf62105bf4b9" ON public.message_channel_mentions USING btree ("channelsId");`);
await queryRunner.query(`CREATE INDEX "IDX_e22a70819d07659c7a71c112a1" ON public.message_stickers USING btree ("stickersId");`);
await queryRunner.query(`CREATE INDEX "IDX_e9080e7a7997a0170026d5139c" ON public.member_roles USING btree (role_id);`);
await queryRunner.query(`ALTER TABLE ONLY public.voice_states ADD CONSTRAINT "FK_03779ef216d4b0358470d9cb748" FOREIGN KEY (guild_id) REFERENCES public.guilds(id) ON DELETE CASCADE;`);
await queryRunner.query(`ALTER TABLE ONLY public.messages ADD CONSTRAINT "FK_05535bc695e9f7ee104616459d3" FOREIGN KEY (author_id) REFERENCES public.users(id) ON DELETE CASCADE;`);
await queryRunner.query(`ALTER TABLE ONLY public.bans ADD CONSTRAINT "FK_07ad88c86d1f290d46748410d58" FOREIGN KEY (executor_id) REFERENCES public.users(id);`);
await queryRunner.query(`ALTER TABLE ONLY public.sessions ADD CONSTRAINT "FK_085d540d9f418cfbdc7bd55bb19" FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE;`);
await queryRunner.query(`ALTER TABLE ONLY public.users ADD CONSTRAINT "FK_0c14beb78d8c5ccba66072adbc7" FOREIGN KEY ("settingsIndex") REFERENCES public.user_settings(index);`);
await queryRunner.query(`ALTER TABLE ONLY public.webhooks ADD CONSTRAINT "FK_0d523f6f997c86e052c49b1455f" FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE;`);
await queryRunner.query(`ALTER TABLE ONLY public.invites ADD CONSTRAINT "FK_11a0d394f8fc649c19ce5f16b59" FOREIGN KEY (target_user_id) REFERENCES public.users(id) ON DELETE CASCADE;`);
await queryRunner.query(`ALTER TABLE ONLY public.teams ADD CONSTRAINT "FK_13f00abf7cb6096c43ecaf8c108" FOREIGN KEY (owner_user_id) REFERENCES public.users(id);`);
await queryRunner.query(`ALTER TABLE ONLY public.invites ADD CONSTRAINT "FK_15c35422032e0b22b4ada95f48f" FOREIGN KEY (inviter_id) REFERENCES public.users(id) ON DELETE CASCADE;`);
await queryRunner.query(`ALTER TABLE ONLY public.members ADD CONSTRAINT "FK_16aceddd5b89825b8ed6029ad1c" FOREIGN KEY (guild_id) REFERENCES public.guilds(id) ON DELETE CASCADE;`);
await queryRunner.query(`ALTER TABLE ONLY public.stickers ADD CONSTRAINT "FK_193d551d852aca5347ef5c9f205" FOREIGN KEY (guild_id) REFERENCES public.guilds(id) ON DELETE CASCADE;`);
await queryRunner.query(`ALTER TABLE ONLY public.read_states ADD CONSTRAINT "FK_195f92e4dd1254a4e348c043763" FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE;`);
await queryRunner.query(`ALTER TABLE ONLY public.notes ADD CONSTRAINT "FK_23e08e5b4481711d573e1abecdc" FOREIGN KEY (target_id) REFERENCES public.users(id) ON DELETE CASCADE;`);
await queryRunner.query(`ALTER TABLE ONLY public.members ADD CONSTRAINT "FK_28b53062261b996d9c99fa12404" FOREIGN KEY (id) REFERENCES public.users(id) ON DELETE CASCADE;`);
await queryRunner.query(`ALTER TABLE ONLY public.message_role_mentions ADD CONSTRAINT "FK_29d63eb1a458200851bc37d074b" FOREIGN KEY ("rolesId") REFERENCES public.roles(id) ON UPDATE CASCADE ON DELETE CASCADE;`);
await queryRunner.query(`ALTER TABLE ONLY public.message_channel_mentions ADD CONSTRAINT "FK_2a27102ecd1d81b4582a4360921" FOREIGN KEY ("messagesId") REFERENCES public.messages(id) ON UPDATE CASCADE ON DELETE CASCADE;`);
await queryRunner.query(`ALTER TABLE ONLY public.applications ADD CONSTRAINT "FK_2ce5a55796fe4c2f77ece57a647" FOREIGN KEY (bot_user_id) REFERENCES public.users(id) ON DELETE CASCADE;`);
await queryRunner.query(`ALTER TABLE ONLY public.recipients ADD CONSTRAINT "FK_2f18ee1ba667f233ae86c0ea60e" FOREIGN KEY (channel_id) REFERENCES public.channels(id) ON DELETE CASCADE;`);
await queryRunner.query(`ALTER TABLE ONLY public.channels ADD CONSTRAINT "FK_3274522d14af40540b1a883fc80" FOREIGN KEY (parent_id) REFERENCES public.channels(id);`);
await queryRunner.query(`ALTER TABLE ONLY public.channels ADD CONSTRAINT "FK_3873ed438575cce703ecff4fc7b" FOREIGN KEY (owner_id) REFERENCES public.users(id);`);
await queryRunner.query(`ALTER TABLE ONLY public.webhooks ADD CONSTRAINT "FK_3a285f4f49c40e0706d3018bc9f" FOREIGN KEY (source_guild_id) REFERENCES public.guilds(id) ON DELETE CASCADE;`);
await queryRunner.query(`ALTER TABLE ONLY public.audit_logs ADD CONSTRAINT "FK_3cd01cd3ae7aab010310d96ac8e" FOREIGN KEY (target_id) REFERENCES public.users(id);`);
await queryRunner.query(`ALTER TABLE ONLY public.invites ADD CONSTRAINT "FK_3f4939aa1461e8af57fea3fb05d" FOREIGN KEY (guild_id) REFERENCES public.guilds(id) ON DELETE CASCADE;`);
await queryRunner.query(`ALTER TABLE ONLY public.message_stickers ADD CONSTRAINT "FK_40bb6f23e7cc133292e92829d28" FOREIGN KEY ("messagesId") REFERENCES public.messages(id) ON UPDATE CASCADE ON DELETE CASCADE;`);
await queryRunner.query(`ALTER TABLE ONLY public.read_states ADD CONSTRAINT "FK_40da2fca4e0eaf7a23b5bfc5d34" FOREIGN KEY (channel_id) REFERENCES public.channels(id) ON DELETE CASCADE;`);
await queryRunner.query(`ALTER TABLE ONLY public.templates ADD CONSTRAINT "FK_445d00eaaea0e60a017a5ed0c11" FOREIGN KEY (source_guild_id) REFERENCES public.guilds(id);`);
await queryRunner.query(`ALTER TABLE ONLY public.sticker_packs ADD CONSTRAINT "FK_448fafba4355ee1c837bbc865f1" FOREIGN KEY ("coverStickerId") REFERENCES public.stickers(id);`);
await queryRunner.query(`ALTER TABLE ONLY public.webhooks ADD CONSTRAINT "FK_487a7af59d189f744fe394368fc" FOREIGN KEY (guild_id) REFERENCES public.guilds(id) ON DELETE CASCADE;`);
await queryRunner.query(`ALTER TABLE ONLY public.emojis ADD CONSTRAINT "FK_4b988e0db89d94cebcf07f598cc" FOREIGN KEY (guild_id) REFERENCES public.guilds(id) ON DELETE CASCADE;`);
await queryRunner.query(`ALTER TABLE ONLY public.bans ADD CONSTRAINT "FK_5999e8e449f80a236ff72023559" FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE;`);
await queryRunner.query(`ALTER TABLE ONLY public.messages ADD CONSTRAINT "FK_5d3ec1cb962de6488637fd779d6" FOREIGN KEY (application_id) REFERENCES public.applications(id);`);
await queryRunner.query(`ALTER TABLE ONLY public.member_roles ADD CONSTRAINT "FK_5d7ddc8a5f9c167f548625e772e" FOREIGN KEY (index) REFERENCES public.members(index) ON UPDATE CASCADE ON DELETE CASCADE;`);
await queryRunner.query(`ALTER TABLE ONLY public.voice_states ADD CONSTRAINT "FK_5fe1d5f931a67e85039c640001b" FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE;`);
await queryRunner.query(`ALTER TABLE ONLY public.recipients ADD CONSTRAINT "FK_6157e8b6ba4e6e3089616481fe2" FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE;`);
await queryRunner.query(`ALTER TABLE ONLY public.messages ADD CONSTRAINT "FK_61a92bb65b302a76d9c1fcd3174" FOREIGN KEY (message_reference_id) REFERENCES public.messages(id);`);
await queryRunner.query(`ALTER TABLE ONLY public.attachments ADD CONSTRAINT "FK_623e10eec51ada466c5038979e3" FOREIGN KEY (message_id) REFERENCES public.messages(id) ON DELETE CASCADE;`);
await queryRunner.query(`ALTER TABLE ONLY public.invites ADD CONSTRAINT "FK_6a15b051fe5050aa00a4b9ff0f6" FOREIGN KEY (channel_id) REFERENCES public.channels(id) ON DELETE CASCADE;`);
await queryRunner.query(`ALTER TABLE ONLY public.backup_codes ADD CONSTRAINT "FK_70066ea80d2f4b871beda32633b" FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE;`);
await queryRunner.query(`ALTER TABLE ONLY public.messages ADD CONSTRAINT "FK_86b9109b155eb70c0a2ca3b4b6d" FOREIGN KEY (channel_id) REFERENCES public.channels(id) ON DELETE CASCADE;`);
await queryRunner.query(`ALTER TABLE ONLY public.guilds ADD CONSTRAINT "FK_8d450b016dc8bec35f36729e4b0" FOREIGN KEY (public_updates_channel_id) REFERENCES public.channels(id);`);
await queryRunner.query(`ALTER TABLE ONLY public.stickers ADD CONSTRAINT "FK_8f4ee73f2bb2325ff980502e158" FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE;`);
await queryRunner.query(`ALTER TABLE ONLY public.guilds ADD CONSTRAINT "FK_95828668aa333460582e0ca6396" FOREIGN KEY (rules_channel_id) REFERENCES public.channels(id);`);
await queryRunner.query(`ALTER TABLE ONLY public.relationships ADD CONSTRAINT "FK_9af4194bab1250b1c584ae4f1d7" FOREIGN KEY (from_id) REFERENCES public.users(id) ON DELETE CASCADE;`);
await queryRunner.query(`ALTER TABLE ONLY public.relationships ADD CONSTRAINT "FK_9c7f6b98a9843b76dce1b0c878b" FOREIGN KEY (to_id) REFERENCES public.users(id) ON DELETE CASCADE;`);
await queryRunner.query(`ALTER TABLE ONLY public.guilds ADD CONSTRAINT "FK_9d1d665379eefde7876a17afa99" FOREIGN KEY (widget_channel_id) REFERENCES public.channels(id);`);
await queryRunner.query(`ALTER TABLE ONLY public.bans ADD CONSTRAINT "FK_9d3ab7dd180ebdd245cdb66ecad" FOREIGN KEY (guild_id) REFERENCES public.guilds(id) ON DELETE CASCADE;`);
await queryRunner.query(`ALTER TABLE ONLY public.voice_states ADD CONSTRAINT "FK_9f8d389866b40b6657edd026dd4" FOREIGN KEY (channel_id) REFERENCES public.channels(id) ON DELETE CASCADE;`);
await queryRunner.query(`ALTER TABLE ONLY public.message_user_mentions ADD CONSTRAINT "FK_a343387fc560ef378760681c236" FOREIGN KEY ("messagesId") REFERENCES public.messages(id) ON UPDATE CASCADE ON DELETE CASCADE;`);
await queryRunner.query(`ALTER TABLE ONLY public.applications ADD CONSTRAINT "FK_a36ed02953077f408d0f3ebc424" FOREIGN KEY (team_id) REFERENCES public.teams(id) ON DELETE CASCADE;`);
await queryRunner.query(`ALTER TABLE ONLY public.message_role_mentions ADD CONSTRAINT "FK_a8242cf535337a490b0feaea0b4" FOREIGN KEY ("messagesId") REFERENCES public.messages(id) ON UPDATE CASCADE ON DELETE CASCADE;`);
await queryRunner.query(`ALTER TABLE ONLY public.messages ADD CONSTRAINT "FK_b0525304f2262b7014245351c76" FOREIGN KEY (member_id) REFERENCES public.users(id) ON DELETE CASCADE;`);
await queryRunner.query(`ALTER TABLE ONLY public.messages ADD CONSTRAINT "FK_b193588441b085352a4c0109423" FOREIGN KEY (guild_id) REFERENCES public.guilds(id) ON DELETE CASCADE;`);
await queryRunner.query(`ALTER TABLE ONLY public.message_user_mentions ADD CONSTRAINT "FK_b831eb18ceebd28976239b1e2f8" FOREIGN KEY ("usersId") REFERENCES public.users(id) ON UPDATE CASCADE ON DELETE CASCADE;`);
await queryRunner.query(`ALTER TABLE ONLY public.audit_logs ADD CONSTRAINT "FK_bd2726fd31b35443f2245b93ba0" FOREIGN KEY (user_id) REFERENCES public.users(id);`);
await queryRunner.query(`ALTER TABLE ONLY public.message_channel_mentions ADD CONSTRAINT "FK_bdb8c09e1464cabf62105bf4b9d" FOREIGN KEY ("channelsId") REFERENCES public.channels(id) ON UPDATE CASCADE ON DELETE CASCADE;`);
await queryRunner.query(`ALTER TABLE ONLY public.channels ADD CONSTRAINT "FK_c253dafe5f3a03ec00cd8fb4581" FOREIGN KEY (guild_id) REFERENCES public.guilds(id) ON DELETE CASCADE;`);
await queryRunner.query(`ALTER TABLE ONLY public.team_members ADD CONSTRAINT "FK_c2bf4967c8c2a6b845dadfbf3d4" FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE;`);
await queryRunner.query(`ALTER TABLE ONLY public.roles ADD CONSTRAINT "FK_c32c1ab1c4dc7dcb0278c4b1b8b" FOREIGN KEY (guild_id) REFERENCES public.guilds(id) ON DELETE CASCADE;`);
await queryRunner.query(`ALTER TABLE ONLY public.webhooks ADD CONSTRAINT "FK_c3e5305461931763b56aa905f1c" FOREIGN KEY (application_id) REFERENCES public.applications(id) ON DELETE CASCADE;`);
await queryRunner.query(`ALTER TABLE ONLY public.guilds ADD CONSTRAINT "FK_cfc3d3ad260f8121c95b31a1fce" FOREIGN KEY (system_channel_id) REFERENCES public.channels(id);`);
await queryRunner.query(`ALTER TABLE ONLY public.templates ADD CONSTRAINT "FK_d7374b7f8f5fbfdececa4fb62e1" FOREIGN KEY (creator_id) REFERENCES public.users(id);`);
await queryRunner.query(`ALTER TABLE ONLY public.webhooks ADD CONSTRAINT "FK_df528cf77e82f8032230e7e37d8" FOREIGN KEY (channel_id) REFERENCES public.channels(id) ON DELETE CASCADE;`);
await queryRunner.query(`ALTER TABLE ONLY public.message_stickers ADD CONSTRAINT "FK_e22a70819d07659c7a71c112a1f" FOREIGN KEY ("stickersId") REFERENCES public.stickers(id) ON UPDATE CASCADE ON DELETE CASCADE;`);
await queryRunner.query(`ALTER TABLE ONLY public.guilds ADD CONSTRAINT "FK_e2a2f873a64a5cf62526de42325" FOREIGN KEY (template_id) REFERENCES public.templates(id);`);
await queryRunner.query(`ALTER TABLE ONLY public.applications ADD CONSTRAINT "FK_e57508958bf92b9d9d25231b5e8" FOREIGN KEY (owner_id) REFERENCES public.users(id) ON DELETE CASCADE;`);
await queryRunner.query(`ALTER TABLE ONLY public.stickers ADD CONSTRAINT "FK_e7cfa5cefa6661b3fb8fda8ce69" FOREIGN KEY (pack_id) REFERENCES public.sticker_packs(id) ON DELETE CASCADE;`);
await queryRunner.query(`ALTER TABLE ONLY public.member_roles ADD CONSTRAINT "FK_e9080e7a7997a0170026d5139c1" FOREIGN KEY (role_id) REFERENCES public.roles(id) ON UPDATE CASCADE ON DELETE CASCADE;`);
await queryRunner.query(`ALTER TABLE ONLY public.connected_accounts ADD CONSTRAINT "FK_f47244225a6a1eac04a3463dd90" FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE;`);
await queryRunner.query(`ALTER TABLE ONLY public.guilds ADD CONSTRAINT "FK_f591a66b8019d87b0fe6c12dad6" FOREIGN KEY (afk_channel_id) REFERENCES public.channels(id);`);
await queryRunner.query(`ALTER TABLE ONLY public.messages ADD CONSTRAINT "FK_f83c04bcf1df4e5c0e7a52ed348" FOREIGN KEY (webhook_id) REFERENCES public.webhooks(id);`);
await queryRunner.query(`ALTER TABLE ONLY public.notes ADD CONSTRAINT "FK_f9e103f8ae67cb1787063597925" FOREIGN KEY (owner_id) REFERENCES public.users(id) ON DELETE CASCADE;`);
await queryRunner.query(`ALTER TABLE ONLY public.emojis ADD CONSTRAINT "FK_fa7ddd5f9a214e28ce596548421" FOREIGN KEY (user_id) REFERENCES public.users(id);`);
await queryRunner.query(`ALTER TABLE ONLY public.guilds ADD CONSTRAINT "FK_fc1a451727e3643ca572a3bb394" FOREIGN KEY (owner_id) REFERENCES public.users(id);`);
await queryRunner.query(`ALTER TABLE ONLY public.team_members ADD CONSTRAINT "FK_fdad7d5768277e60c40e01cdcea" FOREIGN KEY (team_id) REFERENCES public.teams(id) ON DELETE CASCADE;`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
throw new Error("Can't revert this: just throw away your database lol");
}
}
@@ -34,7 +34,29 @@ export class guildChannelOrdering1696420827239 implements MigrationInterface {
await queryRunner.query(`ALTER TABLE channels DROP COLUMN position`);
}
public async down(): Promise<void> {
// don't care actually, sorry.
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE channels ADD position integer NOT NULL DEFAULT 0`);
const guilds = await queryRunner.query(
`SELECT id, channel_ordering FROM guilds`,
undefined,
true,
);
for (const guild of guilds.records) {
const channel_ordering: string[] = JSON.parse(guild.channel_ordering);
for (let i = 0; i < channel_ordering.length; i++) {
const channel_id = channel_ordering[i];
await queryRunner.query(
`UPDATE channels SET position = $1 WHERE id = $2`,
[i, channel_id],
);
}
}
await queryRunner.query(
`ALTER TABLE guilds DROP COLUMN channel_ordering`,
);
}
}
@@ -16,8 +16,16 @@ export class MessageFlagsNotNull1713116476900 implements MigrationInterface {
await queryRunner.query("ALTER TABLE messages DROP COLUMN flags_old;");
}
public async down(): Promise<void> {
// dont care
throw new Error("Migration down is not implemented.");
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
"ALTER TABLE messages RENAME COLUMN flags TO flags_new;",
);
await queryRunner.query(
"ALTER TABLE messages ADD COLUMN flags integer;",
);
await queryRunner.query(
"UPDATE messages SET flags = flags_new;",
);
await queryRunner.query("ALTER TABLE messages DROP COLUMN flags_new;");
}
}
@@ -12,5 +12,6 @@ export class Badges1720628601997 implements MigrationInterface {
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "badge_ids"`);
await queryRunner.query(`DROP TABLE "badges"`);
}
}
@@ -5,6 +5,8 @@ export class RoleColors1752321571508 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "roles" ADD "colors" text`);
await queryRunner.query(`UPDATE "roles" SET "colors" = jsonb_build_object('primary_color', "color") WHERE "colors" IS NULL`);
await queryRunner.query(`ALTER TABLE "roles" ALTER COLUMN "colors" SET NOT NULL`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
@@ -1,15 +0,0 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class RoleColorsSolidColor1752342900886 implements MigrationInterface {
name = 'RoleColorsSolidColor1752342900886'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`UPDATE "roles" SET "colors" = jsonb_build_object('primary_color', "color") WHERE "colors" IS NULL`);
await queryRunner.query(`ALTER TABLE "roles" ALTER COLUMN "colors" SET NOT NULL`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "roles" ALTER COLUMN "colors" DROP NOT NULL`);
}
}
@@ -14,12 +14,12 @@ export class MessagePinnedAt1752383879533 implements MigrationInterface {
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "messages" DROP COLUMN "pinned_at"`,
);
await queryRunner.query(`ALTER TABLE "messages" ADD "pinned" boolean`);
await queryRunner.query(
`UPDATE "messages" SET "pinned" = true WHERE "pinned_at" IS NOT NULL`,
);
await queryRunner.query(
`ALTER TABLE "messages" DROP COLUMN "pinned_at"`,
);
}
}
@@ -1,6 +1,7 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class DeleteBotUsersWithoutAnApplication1761113394664 implements MigrationInterface {
name = "DeleteBotUsersWithoutAnApplication1761113394664";
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DELETE FROM users WHERE bot = true AND id NOT IN (SELECT bot_user_id FROM applications);`);
}
@@ -1,6 +1,7 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class FixGifStickersFormatType1762611552514 implements MigrationInterface {
name = "FixGifStickersFormatType1762611552514";
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`UPDATE "stickers" SET "format_type" = 4 WHERE "format_type" = 0;`);
}
@@ -0,0 +1,16 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class InstanceBanTable1765578570423 implements MigrationInterface {
name = 'InstanceBanTable1765578570423'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TABLE "instance_bans" ("id" character varying NOT NULL, "created_at" TIMESTAMP NOT NULL DEFAULT now(), "reason" character varying NOT NULL, "user_id" character varying, "fingerprint" character varying, "ip_address" character varying, "is_from_other_instance_ban" boolean NOT NULL DEFAULT false, "origin_instance_ban_id" character varying, CONSTRAINT "REL_0b02d18d0d830f160c921192a3" UNIQUE ("origin_instance_ban_id"), CONSTRAINT "PK_3aa6e80a6d325601054892b1340" PRIMARY KEY ("id"))`);
await queryRunner.query(`ALTER TABLE "instance_bans" ADD CONSTRAINT "FK_0b02d18d0d830f160c921192a30" FOREIGN KEY ("origin_instance_ban_id") REFERENCES "instance_bans"("id") ON DELETE SET NULL ON UPDATE NO ACTION`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "instance_bans" DROP CONSTRAINT "FK_0b02d18d0d830f160c921192a30"`);
await queryRunner.query(`DROP TABLE "instance_bans"`);
}
}
@@ -0,0 +1,14 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class InstanceBanAllowlist1765587835846 implements MigrationInterface {
name = 'InstanceBanAllowlist1765587835846'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "instance_bans" ADD "is_allowlisted" boolean NOT NULL DEFAULT false`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "instance_bans" DROP COLUMN "is_allowlisted"`);
}
}
@@ -0,0 +1,14 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class DropDefaultIPDataKey1765665440000 implements MigrationInterface {
name = 'DropDefaultIPDataKey1765665440000'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`UPDATE "config" SET "value" = NULL WHERE "key" = 'security_ipdataApiKey' AND "value" = '"eca677b284b3bac29eb72f5e496aa9047f26543605efe99ff2ce35c9"'`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`UPDATE "config" SET "value" = '"eca677b284b3bac29eb72f5e496aa9047f26543605efe99ff2ce35c9"' WHERE "key" = 'security_ipdataApiKey' AND "value" IS NULL`);
}
}
+19 -28
View File
@@ -22,6 +22,7 @@ import { green, red, yellow } from "picocolors";
import { DataSource } from "typeorm";
import { ConfigEntity } from "../entities/Config";
import { Migration } from "../entities/Migration";
import fs from "fs";
// UUID extension option is only supported with postgres
// We want to generate all id's with Snowflakes that's why we have our own BaseEntity class
@@ -33,12 +34,9 @@ if (!process.env) {
config({ quiet: true });
}
const dbConnectionString =
process.env.DATABASE || path.join(process.cwd(), "database.db");
const dbConnectionString = process.env.DATABASE || path.join(process.cwd(), "database.db");
export const DatabaseType = dbConnectionString.includes("://")
? dbConnectionString.split(":")[0]?.replace("+srv", "")
: "sqlite";
export const DatabaseType = dbConnectionString.includes("://") ? dbConnectionString.split(":")[0]?.replace("+srv", "") : "sqlite";
const isSqlite = DatabaseType.includes("sqlite");
export const DataSourceOptions = new DataSource({
@@ -69,11 +67,7 @@ export async function initDatabase(): Promise<DataSource> {
if (dbConnection) return dbConnection;
if (isSqlite) {
console.log(
`[Database] ${red(
`You are running sqlite! Please keep in mind that we recommend setting up a dedicated database!`,
)}`,
);
console.log(`[Database] ${red(`You are running sqlite! Please keep in mind that we recommend setting up a dedicated database!`)}`);
}
if (!process.env.DB_SYNC) {
@@ -94,6 +88,13 @@ export async function initDatabase(): Promise<DataSource> {
dbConnection = await DataSourceOptions.initialize();
if (DatabaseType === "sqlite") {
console.log(`[Database] ${yellow("Warning: SQLite is not supported. Forcing sync, this may lead to data loss!")}`);
await dbConnection.synchronize();
console.log(`[Database] ${green("Connected")}`);
return dbConnection;
}
// Crude way of detecting if the migrations table exists.
const dbExists = async () => {
try {
@@ -103,27 +104,17 @@ export async function initDatabase(): Promise<DataSource> {
return false;
}
};
if (!(await dbExists())) {
console.log(
"[Database] This appears to be a fresh database. Synchronising.",
);
await dbConnection.synchronize();
// On next start, typeorm will try to run all the migrations again from beginning.
// Manually insert every current migration to prevent this:
await Promise.all(
dbConnection.migrations.map((migration) =>
Migration.insert({
name: migration.name,
timestamp: Date.now(),
}),
),
);
} else {
console.log("[Database] Applying missing migrations, if any.");
await dbConnection.runMigrations();
if (!(await dbExists())) {
console.log("[Database] This appears to be a fresh database. Running initial DDL.");
const qr = dbConnection.createQueryRunner();
if (fs.existsSync(path.join(__dirname, "..", "migration", DatabaseType, "initial0.js")))
await new (require(`../migration/${DatabaseType}-initial`).initial0)().up(qr);
}
console.log("[Database] Applying missing migrations, if any.");
await dbConnection.runMigrations();
console.log(`[Database] ${green("Connected")}`);
return dbConnection;
+13
View File
@@ -68,4 +68,17 @@ export class ElapsedTime {
get days(): number {
return this.totalDays;
}
toString(): string {
// Format: "DD.HH:MM:SS.mmmuuuNNN", with days being optional
const daysPart = Math.floor(this.days) > 0 ? `${Math.floor(this.days)}.` : "";
const hoursPart = Math.floor(this.hours).toString().padStart(2, "0");
const minutesPart = Math.floor(this.minutes).toString().padStart(2, "0");
const secondsPart = Math.floor(this.seconds).toString().padStart(2, "0");
const millisecondsPart = Math.floor(this.milliseconds).toString().padStart(3, "0");
const microsecondsPart = Math.floor(this.microseconds).toString().padStart(3, "0");
const nanosecondsPart = Math.floor(this.nanoseconds).toString().padStart(3, "0");
return `${daysPart}${hoursPart}:${minutesPart}:${secondsPart}.${millisecondsPart}${microsecondsPart}${nanosecondsPart}`;
}
}
+25 -38
View File
@@ -20,31 +20,28 @@ import { Channel } from "amqplib";
import { RabbitMQ } from "./RabbitMQ";
import EventEmitter from "events";
import { EVENT, Event } from "../interfaces";
import { randomUUID } from "crypto";
export const events = new EventEmitter();
export async function emitEvent(payload: Omit<Event, "created_at">) {
const id = (payload.guild_id ||
payload.channel_id ||
payload.user_id) as string;
const id = (payload.guild_id || payload.channel_id || payload.user_id) as string;
if (!id) return console.error("event doesn't contain any id", payload);
if (RabbitMQ.connection) {
const data =
typeof payload.data === "object"
? JSON.stringify(payload.data)
: payload.data; // use rabbitmq for event transmission
await RabbitMQ.channel?.assertExchange(id, "fanout", {
durable: false,
});
const data = typeof payload.data === "object" ? JSON.stringify(payload.data) : payload.data; // use rabbitmq for event transmission
const channel = await RabbitMQ.getSafeChannel();
try {
await channel.assertExchange(id, "fanout", {
durable: false,
});
// assertQueue isn't needed, because a queue will automatically created if it doesn't exist
const successful = RabbitMQ.channel?.publish(
id,
"",
Buffer.from(`${data}`),
{ type: payload.event },
);
if (!successful) throw new Error("failed to send event");
// assertQueue isn't needed, because a queue will automatically created if it doesn't exist
const successful = channel.publish(id, "", Buffer.from(`${data}`), { type: payload.event });
if (!successful) throw new Error("failed to send event");
} catch (e) {
// todo: should we retry publishng the event?
console.log("[RabbitMQ] ", e);
}
} else if (process.env.EVENT_TRANSMISSION === "process") {
process.send?.({ type: "event", event: payload, id } as ProcessEvent);
} else {
@@ -79,17 +76,11 @@ export interface ProcessEvent {
id: string;
}
export async function listenEvent(
event: string,
callback: (event: EventOpts) => unknown,
opts?: ListenEventOpts,
) {
export async function listenEvent(event: string, callback: (event: EventOpts) => unknown, opts?: ListenEventOpts): Promise<() => Promise<void>> {
if (RabbitMQ.connection) {
const channel = opts?.channel || RabbitMQ.channel;
if (!channel)
throw new Error(
"[Events] An event was sent without an associated channel",
);
const rabbitMQChannel = await RabbitMQ.getSafeChannel();
const channel = opts?.channel || rabbitMQChannel;
if (!channel) throw new Error("[Events] An event was sent without an associated channel");
return await rabbitListen(channel, event, callback, {
acknowledge: opts?.acknowledge,
});
@@ -101,9 +92,7 @@ export async function listenEvent(
const listener = (msg: ProcessEvent) => {
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
msg.type === "event" &&
msg.id === event &&
callback({ ...msg.event, cancel });
msg.type === "event" && msg.id === event && callback({ ...msg.event, cancel });
};
// TODO: assert the type is correct?
@@ -124,20 +113,17 @@ export async function listenEvent(
}
}
async function rabbitListen(
channel: Channel,
id: string,
callback: (event: EventOpts) => unknown,
opts?: { acknowledge?: boolean },
) {
async function rabbitListen(channel: Channel, id: string, callback: (event: EventOpts) => unknown, opts?: { acknowledge?: boolean }): Promise<() => Promise<void>> {
await channel.assertExchange(id, "fanout", { durable: false });
const q = await channel.assertQueue("", {
exclusive: true,
autoDelete: true,
});
const consumerTag = randomUUID();
const cancel = async () => {
await channel.cancel(q.queue);
await channel.cancel(consumerTag);
await channel.unbindQueue(q.queue, id, "");
};
@@ -163,6 +149,7 @@ async function rabbitListen(
},
{
noAck: !opts?.acknowledge,
consumerTag: consumerTag,
},
);
+32
View File
@@ -23,6 +23,7 @@ export const RabbitMQ: {
connection: ChannelModel | null;
channel: Channel | null;
init: () => Promise<void>;
getSafeChannel: () => Promise<Channel>;
} = {
connection: null,
channel: null,
@@ -34,7 +35,38 @@ export const RabbitMQ: {
timeout: 1000 * 60,
});
console.log(`[RabbitMQ] connected`);
// log connection errors
this.connection.on("error", (err) => {
console.error("[RabbitMQ] Connection Error:", err);
});
this.connection.on("close", () => {
console.error("[RabbitMQ] connection closed");
// TODO: Add reconnection logic here if the connection crashes??
// will be a pain since we will have to reconstruct entire state
});
await this.getSafeChannel();
},
getSafeChannel: async function () {
if (!this.connection) return Promise.reject();
if (this.channel) return this.channel;
this.channel = await this.connection.createChannel();
console.log(`[RabbitMQ] channel created`);
// log channel errors
this.channel.on("error", (err) => {
console.error("[RabbitMQ] Channel Error:", err);
});
this.channel.on("close", () => {
console.log("[RabbitMQ] channel closed");
this.channel = null;
});
return this.channel;
},
};
+22 -46
View File
@@ -18,15 +18,12 @@
import jwt, { VerifyOptions } from "jsonwebtoken";
import { Config } from "./Config";
import { User } from "../entities";
import { InstanceBan, User } from "../entities";
import crypto from "node:crypto";
import fs from "fs/promises";
import { existsSync } from "fs";
// TODO: dont use deprecated APIs lol
import {
FindOptionsRelationByString,
FindOptionsSelectByString,
} from "typeorm";
import { FindManyOptions, FindOptions, FindOptionsRelationByString, FindOptionsSelect, FindOptionsSelectByString, FindOptionsWhere } from "typeorm";
import * as console from "node:console";
export const JWTOptions: VerifyOptions = { algorithms: ["HS256"] };
@@ -37,7 +34,7 @@ export type UserTokenData = {
};
function logAuth(text: string) {
if(process.env.LOG_AUTH !== "true") return;
if (process.env.LOG_AUTH !== "true") return;
console.log(`[AUTH] ${text}`);
}
@@ -51,6 +48,8 @@ export const checkToken = (
opts?: {
select?: FindOptionsSelectByString<User>;
relations?: FindOptionsRelationByString;
ipAddress?: string;
fingerprint?: string;
},
): Promise<UserTokenData> =>
new Promise((resolve, reject) => {
@@ -66,15 +65,7 @@ export const checkToken = (
const user = await User.findOne({
where: { id: decoded.id },
select: [
...(opts?.select || []),
"id",
"bot",
"disabled",
"deleted",
"rights",
"data",
],
select: [...(opts?.select || []), "id", "bot", "disabled", "deleted", "rights", "data"],
relations: opts?.relations,
});
@@ -84,10 +75,7 @@ export const checkToken = (
}
// we need to round it to seconds as it saved as seconds in jwt iat and valid_tokens_since is stored in milliseconds
if (
decoded.iat * 1000 <
new Date(user.data.valid_tokens_since).setSeconds(0, 0)
) {
if (decoded.iat * 1000 < new Date(user.data.valid_tokens_since).setSeconds(0, 0)) {
logAuth("validateUser rejected: Token not yet valid");
return rejectAndLog(reject, "Invalid Token");
}
@@ -96,11 +84,18 @@ export const checkToken = (
logAuth("validateUser rejected: User disabled");
return rejectAndLog(reject, "User disabled");
}
if (user.deleted) {
logAuth("validateUser rejected: User deleted");
return rejectAndLog(reject, "User not found");
}
const banReasons = await InstanceBan.findInstanceBans({ userId: user.id, ipAddress: opts?.ipAddress, fingerprint: opts?.fingerprint, propagateBan: true });
if (banReasons.length > 0) {
logAuth("validateUser rejected: User banned for reasons: " + banReasons.join(", "));
return rejectAndLog(reject, "Invalid Token");
}
logAuth("validateUser success: " + JSON.stringify({ decoded, user }));
return resolve({ decoded, user });
};
@@ -109,21 +104,11 @@ export const checkToken = (
if (!dec) return reject("Could not parse token");
logAuth("Decoded token: " + JSON.stringify(dec));
if (dec.header.alg == "HS256") {
jwt.verify(
token,
Config.get().security.jwtSecret,
JWTOptions,
validateUser,
);
if (dec.header.alg == "HS256" && Config.get().security.jwtSecret !== null) {
jwt.verify(token, Config.get().security.jwtSecret!, JWTOptions, validateUser);
} else if (dec.header.alg == "ES512") {
loadOrGenerateKeypair().then((keyPair) => {
jwt.verify(
token,
keyPair.publicKey,
{ algorithms: ["ES512"] },
validateUser,
);
jwt.verify(token, keyPair.publicKey, { algorithms: ["ES512"] }, validateUser);
});
} else return reject("Invalid token algorithm");
});
@@ -152,7 +137,7 @@ let cachedKeypair: {
privateKey: crypto.KeyObject;
publicKey: crypto.KeyObject;
fingerprint: string;
}
};
// Get ECDSA keypair from file or generate it
export async function loadOrGenerateKeypair() {
@@ -176,10 +161,7 @@ export async function loadOrGenerateKeypair() {
let publicKey: crypto.KeyObject;
if (existsSync("jwt.key") && existsSync("jwt.key.pub")) {
const [loadedPrivateKey, loadedPublicKey] = await Promise.all([
fs.readFile("jwt.key"),
fs.readFile("jwt.key.pub"),
]);
const [loadedPrivateKey, loadedPublicKey] = await Promise.all([fs.readFile("jwt.key"), fs.readFile("jwt.key.pub")]);
privateKey = crypto.createPrivateKey(loadedPrivateKey);
publicKey = crypto.createPublicKey(loadedPublicKey);
@@ -192,14 +174,8 @@ export async function loadOrGenerateKeypair() {
publicKey = res.publicKey;
await Promise.all([
fs.writeFile(
"jwt.key",
privateKey.export({ format: "pem", type: "sec1" }),
),
fs.writeFile(
"jwt.key.pub",
publicKey.export({ format: "pem", type: "spki" }),
),
fs.writeFile("jwt.key", privateKey.export({ format: "pem", type: "sec1" })),
fs.writeFile("jwt.key.pub", publicKey.export({ format: "pem", type: "spki" })),
]);
}
@@ -209,5 +185,5 @@ export async function loadOrGenerateKeypair() {
.digest("hex");
lastFsCheck = Date.now();
return cachedKeypair = { privateKey, publicKey, fingerprint };
return (cachedKeypair = { privateKey, publicKey, fingerprint });
}
+23 -18
View File
@@ -19,13 +19,14 @@
import { Fido2Lib } from "fido2-lib";
import jwt from "jsonwebtoken";
import { Config } from "./Config";
import { loadOrGenerateKeypair } from "./Token";
const jwtSignOptions: jwt.SignOptions = {
algorithm: "HS256",
algorithm: "ES512",
expiresIn: "5m",
};
const jwtVerifyOptions: jwt.VerifyOptions = {
algorithms: ["HS256"],
algorithms: ["ES512"],
};
export const WebAuthn: {
@@ -44,28 +45,32 @@ export async function generateWebAuthnTicket(
challenge: string,
): Promise<string> {
return new Promise((res, rej) => {
jwt.sign(
{ challenge },
Config.get().security.jwtSecret,
jwtSignOptions,
(err, token) => {
if (err || !token) return rej(err || "no token");
return res(token);
},
loadOrGenerateKeypair().then(kp=>
jwt.sign(
{ challenge },
kp.privateKey,
jwtSignOptions,
(err, token) => {
if (err || !token) return rej(err || "no token");
return res(token);
},
)
);
});
}
export async function verifyWebAuthnToken(token: string) {
return new Promise((res, rej) => {
jwt.verify(
token,
Config.get().security.jwtSecret,
jwtVerifyOptions,
async (err, decoded) => {
if (err) return rej(err);
return res(decoded);
},
loadOrGenerateKeypair().then(kp=>
jwt.verify(
token,
kp.publicKey,
jwtVerifyOptions,
async (err, decoded) => {
if (err) return rej(err);
return res(decoded);
},
)
);
});
}