mirror of
https://github.com/spacebarchat/server.git
synced 2026-10-22 11:32:29 +00:00
Merge branch 'spacebarchat:master' into fix/gh-webhook-events-more
This commit is contained in:
195 files changed
+3917
-788
No files matched your search
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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}`);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -51,6 +51,8 @@ router.post(
|
||||
try {
|
||||
const userTokenData = await checkToken(token, {
|
||||
select: ["email"],
|
||||
fingerprint: req.fingerprint,
|
||||
ipAddress: req.ip
|
||||
});
|
||||
user = userTokenData.user;
|
||||
} catch {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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
|
||||
});
|
||||
|
||||
@@ -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")));
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -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 },
|
||||
],
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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?
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
Reference in new issue
Block a user