mirror of
https://github.com/spacebarchat/server.git
synced 2026-05-14 07:35:12 +00:00
✨ api
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
import "missing-native-js-functions";
|
||||
import fs from "fs";
|
||||
import { Connection } from "mongoose";
|
||||
import { Server, ServerOptions } from "lambert-server";
|
||||
import { Authentication, CORS } from "./middlewares/";
|
||||
import { Config, db, RabbitMQ } from "@fosscord/server-util";
|
||||
import i18next from "i18next";
|
||||
import i18nextMiddleware, { I18next } from "i18next-http-middleware";
|
||||
import i18nextBackend from "i18next-node-fs-backend";
|
||||
import { ErrorHandler } from "./middlewares/ErrorHandler";
|
||||
import { BodyParser } from "./middlewares/BodyParser";
|
||||
import express, { Router, Request, Response } from "express";
|
||||
import mongoose from "mongoose";
|
||||
import path from "path";
|
||||
import { initRateLimits } from "./middlewares/RateLimit";
|
||||
import TestClient from "./middlewares/TestClient";
|
||||
|
||||
// this will return the new updated document for findOneAndUpdate
|
||||
mongoose.set("returnOriginal", false); // https://mongoosejs.com/docs/api/model.html#model_Model.findOneAndUpdate
|
||||
|
||||
export interface FosscordServerOptions extends ServerOptions {}
|
||||
|
||||
declare global {
|
||||
namespace Express {
|
||||
interface Request {
|
||||
// @ts-ignore
|
||||
server: FosscordServer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class FosscordServer extends Server {
|
||||
public declare options: FosscordServerOptions;
|
||||
|
||||
constructor(opts?: Partial<FosscordServerOptions>) {
|
||||
// @ts-ignore
|
||||
super({ ...opts, errorHandler: false, jsonBody: false });
|
||||
}
|
||||
|
||||
async setupSchema() {
|
||||
return Promise.all([
|
||||
db.collection("users").createIndex({ id: 1 }, { unique: true }),
|
||||
db.collection("messages").createIndex({ id: 1 }, { unique: true }),
|
||||
db.collection("channels").createIndex({ id: 1 }, { unique: true }),
|
||||
db.collection("guilds").createIndex({ id: 1 }, { unique: true }),
|
||||
db.collection("members").createIndex({ id: 1, guild_id: 1 }, { unique: true }),
|
||||
db.collection("roles").createIndex({ id: 1 }, { unique: true }),
|
||||
db.collection("emojis").createIndex({ id: 1 }, { unique: true }),
|
||||
db.collection("invites").createIndex({ code: 1 }, { unique: true }),
|
||||
db.collection("invites").createIndex({ expires_at: 1 }, { expireAfterSeconds: 0 }), // after 0 seconds of expires_at the invite will get delete
|
||||
db.collection("ratelimits").createIndex({ expires_at: 1 }, { expireAfterSeconds: 0 })
|
||||
]);
|
||||
}
|
||||
|
||||
async start() {
|
||||
// @ts-ignore
|
||||
await (db as Promise<Connection>);
|
||||
await this.setupSchema();
|
||||
console.log("[Database] connected");
|
||||
await Config.init();
|
||||
await RabbitMQ.init();
|
||||
|
||||
this.app.use(CORS);
|
||||
this.app.use(Authentication);
|
||||
this.app.use(BodyParser({ inflate: true, limit: 1024 * 1024 * 10 })); // 2MB
|
||||
const languages = fs.readdirSync(path.join(__dirname, "..", "locales"));
|
||||
const namespaces = fs.readdirSync(path.join(__dirname, "..", "locales", "en"));
|
||||
const ns = namespaces.filter((x) => x.endsWith(".json")).map((x) => x.slice(0, x.length - 5));
|
||||
|
||||
await i18next
|
||||
.use(i18nextBackend)
|
||||
.use(i18nextMiddleware.LanguageDetector)
|
||||
.init({
|
||||
preload: languages,
|
||||
// debug: true,
|
||||
fallbackLng: "en",
|
||||
ns,
|
||||
backend: {
|
||||
loadPath: __dirname + "/../locales/{{lng}}/{{ns}}.json"
|
||||
},
|
||||
load: "all"
|
||||
});
|
||||
this.app.use(i18nextMiddleware.handle(i18next, {}));
|
||||
|
||||
const app = this.app;
|
||||
const api = Router();
|
||||
// @ts-ignore
|
||||
this.app = api;
|
||||
|
||||
initRateLimits(api);
|
||||
this.routes = await this.registerRoutes(path.join(__dirname, "routes", "/"));
|
||||
app.use("/api/v8", api);
|
||||
app.use("/api/v9", api);
|
||||
app.use("/api", api); // allow unversioned requests
|
||||
|
||||
api.get("*", (req: Request, res: Response) => {
|
||||
res.status(404).json({
|
||||
message: "404: Not Found",
|
||||
code: 0
|
||||
});
|
||||
});
|
||||
|
||||
this.app = app;
|
||||
this.app.use(ErrorHandler);
|
||||
TestClient(this.app);
|
||||
|
||||
return super.start();
|
||||
}
|
||||
}
|
||||
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
declare global {
|
||||
namespace Express {
|
||||
interface Request {
|
||||
user_id: any;
|
||||
token: any;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
export * from "./Server";
|
||||
export * from "./middlewares/";
|
||||
export * from "./schema/Ban";
|
||||
export * from "./schema/Channel";
|
||||
export * from "./schema/Guild";
|
||||
export * from "./schema/Invite";
|
||||
export * from "./schema/Message";
|
||||
export * from "./util/Config";
|
||||
export * from "./util/Constants";
|
||||
export * from "./util/Event";
|
||||
export * from "./util/instanceOf";
|
||||
export * from "./util/Event";
|
||||
export * from "./util/instanceOf";
|
||||
export * from "./util/Member";
|
||||
export * from "./util/RandomInviteID";
|
||||
export * from "./util/String";
|
||||
export * from "./util/User";
|
||||
export { check as checkPassword } from "./util/passwordStrength";
|
||||
@@ -0,0 +1,48 @@
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import { HTTPError } from "lambert-server";
|
||||
import { checkToken, Config } from "@fosscord/server-util";
|
||||
|
||||
export const NO_AUTHORIZATION_ROUTES = [
|
||||
/^\/api(\/v\d+)?\/auth\/login/,
|
||||
/^\/api(\/v\d+)?\/auth\/register/,
|
||||
/^\/api(\/v\d+)?\/webhooks\//,
|
||||
/^\/api(\/v\d+)?\/ping/,
|
||||
/^\/api(\/v\d+)?\/gateway/,
|
||||
/^\/api(\/v\d+)?\/experiments/,
|
||||
/^\/api(\/v\d+)?\/guilds\/\d+\/widget\.(json|png)/
|
||||
];
|
||||
|
||||
export const API_PREFIX = /^\/api(\/v\d+)?/;
|
||||
export const API_PREFIX_TRAILING_SLASH = /^\/api(\/v\d+)?\//;
|
||||
|
||||
declare global {
|
||||
namespace Express {
|
||||
interface Request {
|
||||
user_id: any;
|
||||
user_bot: boolean;
|
||||
token: any;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function Authentication(req: Request, res: Response, next: NextFunction) {
|
||||
if (req.method === "OPTIONS") return res.sendStatus(204);
|
||||
if (!req.url.startsWith("/api")) return next();
|
||||
const apiPath = req.url.replace(API_PREFIX, "");
|
||||
if (apiPath.startsWith("/invites") && req.method === "GET") return next();
|
||||
if (NO_AUTHORIZATION_ROUTES.some((x) => x.test(req.url))) return next();
|
||||
if (!req.headers.authorization) return next(new HTTPError("Missing Authorization Header", 401));
|
||||
|
||||
try {
|
||||
const { jwtSecret } = Config.get().security;
|
||||
|
||||
const { decoded, user }: any = await checkToken(req.headers.authorization, jwtSecret);
|
||||
|
||||
req.token = decoded;
|
||||
req.user_id = decoded.id;
|
||||
req.user_bot = user.bot;
|
||||
return next();
|
||||
} catch (error) {
|
||||
return next(new HTTPError(error.toString(), 400));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import bodyParser, { OptionsJson } from "body-parser";
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import { HTTPError } from "lambert-server";
|
||||
|
||||
export function BodyParser(opts?: OptionsJson) {
|
||||
const jsonParser = bodyParser.json(opts);
|
||||
|
||||
return (req: Request, res: Response, next: NextFunction) => {
|
||||
jsonParser(req, res, (err) => {
|
||||
if (err) {
|
||||
// TODO: different errors for body parser (request size limit, wrong body type, invalid body, ...)
|
||||
return next(new HTTPError("Invalid Body", 400));
|
||||
}
|
||||
next();
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,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", "*");
|
||||
// TODO: use better CSP policy
|
||||
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") || "*");
|
||||
|
||||
next();
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import { HTTPError } from "lambert-server";
|
||||
import { FieldError } from "../util/instanceOf";
|
||||
|
||||
export function ErrorHandler(error: Error, req: Request, res: Response, next: NextFunction) {
|
||||
try {
|
||||
let code = 400;
|
||||
let httpcode = code;
|
||||
let message = error?.toString();
|
||||
let errors = undefined;
|
||||
|
||||
if (error instanceof HTTPError && error.code) code = httpcode = error.code;
|
||||
else if (error instanceof FieldError) {
|
||||
code = Number(error.code);
|
||||
message = error.message;
|
||||
errors = error.errors;
|
||||
} else {
|
||||
console.error(error);
|
||||
if (req.server?.options?.production) {
|
||||
message = "Internal Server Error";
|
||||
}
|
||||
code = httpcode = 500;
|
||||
}
|
||||
|
||||
if (httpcode > 511) httpcode = 400;
|
||||
|
||||
res.status(httpcode).json({ code: code, message, errors });
|
||||
|
||||
return;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return res.status(500).json({ code: 500, message: "Internal Server Error" });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
import { db, MongooseCache, Bucket, Config } from "@fosscord/server-util";
|
||||
import { NextFunction, Request, Response, Router } from "express";
|
||||
import { getIpAdress } from "../util/ipAddress";
|
||||
import { API_PREFIX_TRAILING_SLASH } from "./Authentication";
|
||||
|
||||
const Cache = new MongooseCache(
|
||||
db.collection("ratelimits"),
|
||||
[
|
||||
// TODO: uncomment $match and fix error: not receiving change events
|
||||
// { $match: { blocked: true } }
|
||||
],
|
||||
{ onlyEvents: false, array: true }
|
||||
);
|
||||
|
||||
// Docs: https://discord.com/developers/docs/topics/rate-limits
|
||||
|
||||
/*
|
||||
? bucket limit? Max actions/sec per bucket?
|
||||
|
||||
TODO: ip rate limit
|
||||
TODO: user rate limit
|
||||
TODO: different rate limit for bots/user/oauth/webhook
|
||||
TODO: delay database requests to include multiple queries
|
||||
TODO: different for methods (GET/POST)
|
||||
TODO: bucket major parameters (channel_id, guild_id, webhook_id)
|
||||
TODO: use config values
|
||||
|
||||
> IP addresses that make too many invalid HTTP requests are automatically and temporarily restricted from accessing the Discord API. Currently, this limit is 10,000 per 10 minutes. An invalid request is one that results in 401, 403, or 429 statuses.
|
||||
|
||||
> All bots can make up to 50 requests per second to our API. This is independent of any individual rate limit on a route. If your bot gets big enough, based on its functionality, it may be impossible to stay below 50 requests per second during normal operations.
|
||||
|
||||
*/
|
||||
|
||||
export default function RateLimit(opts: {
|
||||
bucket?: string;
|
||||
window: number;
|
||||
count: number;
|
||||
bot?: number;
|
||||
webhook?: number;
|
||||
oauth?: number;
|
||||
GET?: number;
|
||||
MODIFY?: number;
|
||||
error?: boolean;
|
||||
success?: boolean;
|
||||
onlyIp?: boolean;
|
||||
}): any {
|
||||
Cache.init(); // will only initalize it once
|
||||
|
||||
return async (req: Request, res: Response, next: NextFunction): Promise<any> => {
|
||||
const bucket_id = opts.bucket || req.originalUrl.replace(API_PREFIX_TRAILING_SLASH, "");
|
||||
var user_id = getIpAdress(req);
|
||||
if (!opts.onlyIp && req.user_id) user_id = req.user_id;
|
||||
|
||||
var 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;
|
||||
|
||||
const offender = Cache.data?.find((x: Bucket) => x.user_id == user_id && x.id === bucket_id) as Bucket | null;
|
||||
|
||||
if (offender && offender.blocked) {
|
||||
const reset = offender.expires_at.getTime();
|
||||
const resetAfterMs = reset - Date.now();
|
||||
const resetAfterSec = resetAfterMs / 1000;
|
||||
const global = bucket_id === "global";
|
||||
|
||||
if (resetAfterMs > 0) {
|
||||
console.log("blocked bucket: " + bucket_id, { resetAfterMs });
|
||||
return (
|
||||
res
|
||||
.status(429)
|
||||
.set("X-RateLimit-Limit", `${max_hits}`)
|
||||
.set("X-RateLimit-Remaining", "0")
|
||||
.set("X-RateLimit-Reset", `${reset}`)
|
||||
.set("X-RateLimit-Reset-After", `${resetAfterSec}`)
|
||||
.set("X-RateLimit-Global", `${global}`)
|
||||
.set("Retry-After", `${Math.ceil(resetAfterSec)}`)
|
||||
.set("X-RateLimit-Bucket", `${bucket_id}`)
|
||||
// TODO: error rate limit message translation
|
||||
.send({ message: "You are being rate limited.", retry_after: resetAfterSec, global })
|
||||
);
|
||||
} else {
|
||||
offender.hits = 0;
|
||||
offender.expires_at = new Date(Date.now() + opts.window * 1000);
|
||||
offender.blocked = false;
|
||||
// mongodb ttl didn't update yet -> manually update/delete
|
||||
db.collection("ratelimits").updateOne({ id: bucket_id, user_id }, { $set: offender });
|
||||
}
|
||||
}
|
||||
next();
|
||||
const hitRouteOpts = { bucket_id, user_id, max_hits, window: opts.window };
|
||||
|
||||
if (opts.error || opts.success) {
|
||||
res.once("finish", () => {
|
||||
// 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) {
|
||||
return hitRoute(hitRouteOpts);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
return hitRoute(hitRouteOpts);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function initRateLimits(app: Router) {
|
||||
const { routes, global, ip, error } = Config.get().limits.rate;
|
||||
|
||||
app.use(
|
||||
RateLimit({
|
||||
bucket: "global",
|
||||
onlyIp: true,
|
||||
...ip
|
||||
})
|
||||
);
|
||||
app.use(RateLimit({ bucket: "global", ...global }));
|
||||
app.use(
|
||||
RateLimit({
|
||||
bucket: "error",
|
||||
error: true,
|
||||
onlyIp: true,
|
||||
...error
|
||||
})
|
||||
);
|
||||
app.use("/guilds/:id", RateLimit(routes.guild));
|
||||
app.use("/webhooks/:id", RateLimit(routes.webhook));
|
||||
app.use("/channels/:id", RateLimit(routes.channel));
|
||||
app.use("/auth/login", RateLimit(routes.auth.login));
|
||||
app.use("/auth/register", RateLimit({ onlyIp: true, success: true, ...routes.auth.register }));
|
||||
}
|
||||
|
||||
function hitRoute(opts: { user_id: string; bucket_id: string; max_hits: number; window: number }) {
|
||||
return db.collection("ratelimits").updateOne(
|
||||
{ id: opts.bucket_id, user_id: opts.user_id },
|
||||
[
|
||||
{
|
||||
$replaceRoot: {
|
||||
newRoot: {
|
||||
// similar to $setOnInsert
|
||||
$mergeObjects: [
|
||||
{
|
||||
id: opts.bucket_id,
|
||||
user_id: opts.user_id,
|
||||
expires_at: new Date(Date.now() + opts.window * 1000)
|
||||
},
|
||||
"$$ROOT"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
hits: { $sum: [{ $ifNull: ["$hits", 0] }, 1] },
|
||||
blocked: { $gte: ["$hits", opts.max_hits] }
|
||||
}
|
||||
}
|
||||
],
|
||||
{ upsert: true }
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import bodyParser, { OptionsJson } from "body-parser";
|
||||
import express, { NextFunction, Request, Response, Application } from "express";
|
||||
import { HTTPError } from "lambert-server";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import fetch, { Response as FetchResponse } from "node-fetch";
|
||||
import { Config } from "@fosscord/server-util";
|
||||
|
||||
export default function TestClient(app: Application) {
|
||||
const assetCache = new Map<string, { response: FetchResponse; buffer: Buffer }>();
|
||||
const indexHTML = fs.readFileSync(path.join(__dirname, "..", "..", "client_test", "index.html"), { encoding: "utf8" });
|
||||
|
||||
app.use("/assets", express.static(path.join(__dirname, "..", "assets")));
|
||||
|
||||
app.get("/assets/:file", async (req: Request, res: Response) => {
|
||||
delete req.headers.host;
|
||||
var response: FetchResponse;
|
||||
var buffer: Buffer;
|
||||
const cache = assetCache.get(req.params.file);
|
||||
if (!cache) {
|
||||
response = await fetch(`https://discord.com/assets/${req.params.file}`, {
|
||||
// @ts-ignore
|
||||
headers: {
|
||||
...req.headers
|
||||
}
|
||||
});
|
||||
buffer = await response.buffer();
|
||||
} else {
|
||||
response = cache.response;
|
||||
buffer = cache.buffer;
|
||||
}
|
||||
|
||||
response.headers.forEach((value, name) => {
|
||||
if (
|
||||
[
|
||||
"content-length",
|
||||
"content-security-policy",
|
||||
"strict-transport-security",
|
||||
"set-cookie",
|
||||
"transfer-encoding",
|
||||
"expect-ct",
|
||||
"access-control-allow-origin",
|
||||
"content-encoding"
|
||||
].includes(name.toLowerCase())
|
||||
) {
|
||||
return;
|
||||
}
|
||||
res.set(name, value);
|
||||
});
|
||||
assetCache.set(req.params.file, { buffer, response });
|
||||
|
||||
return res.send(buffer);
|
||||
});
|
||||
app.get("*", (req: Request, res: Response) => {
|
||||
res.set("Cache-Control", "public, max-age=" + 60 * 60 * 24);
|
||||
res.set("content-type", "text/html");
|
||||
var html = indexHTML;
|
||||
const CDN_ENDPOINT = (Config.get()?.cdn.endpoint || process.env.CDN || "").replace(/(https?)?(:\/\/?)/g, "");
|
||||
const GATEWAY_ENDPOINT = Config.get()?.gateway.endpoint || process.env.GATEWAY || "";
|
||||
|
||||
if (CDN_ENDPOINT) html = html.replace(/CDN_HOST: .+/, `CDN_HOST: "${CDN_ENDPOINT}",`);
|
||||
if (GATEWAY_ENDPOINT) html = html.replace(/GATEWAY_ENDPOINT: .+/, `GATEWAY_ENDPOINT: "${GATEWAY_ENDPOINT}",`);
|
||||
|
||||
res.send(html);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export * from "./Authentication";
|
||||
export * from "./BodyParser";
|
||||
export * from "./CORS";
|
||||
export * from "./ErrorHandler";
|
||||
export * from "./RateLimit";
|
||||
@@ -0,0 +1,113 @@
|
||||
import { Request, Response, Router } from "express";
|
||||
import { check, FieldErrors, Length } from "../../util/instanceOf";
|
||||
import bcrypt from "bcrypt";
|
||||
import jwt from "jsonwebtoken";
|
||||
import { Config, UserModel } from "@fosscord/server-util";
|
||||
import { adjustEmail } from "./register";
|
||||
import RateLimit from "../../middlewares/RateLimit";
|
||||
|
||||
const router: Router = Router();
|
||||
export default router;
|
||||
|
||||
// TODO: check if user is deleted --> prohibit login
|
||||
|
||||
router.post(
|
||||
"/",
|
||||
check({
|
||||
login: new Length(String, 2, 100), // email or telephone
|
||||
password: new Length(String, 8, 72),
|
||||
$undelete: Boolean,
|
||||
$captcha_key: String,
|
||||
$login_source: String,
|
||||
$gift_code_sku_id: String
|
||||
}),
|
||||
async (req: Request, res: Response) => {
|
||||
const { login, password, captcha_key, undelete } = req.body;
|
||||
const email = adjustEmail(login);
|
||||
const query: any[] = [{ phone: login }];
|
||||
if (email) query.push({ email });
|
||||
|
||||
// TODO: Rewrite this to have the proper config syntax on the new method
|
||||
|
||||
const config = Config.get();
|
||||
|
||||
if (config.login.requireCaptcha && config.security.captcha.enabled) {
|
||||
if (!captcha_key) {
|
||||
const { sitekey, service } = config.security.captcha;
|
||||
return res.status(400).json({
|
||||
captcha_key: ["captcha-required"],
|
||||
captcha_sitekey: sitekey,
|
||||
captcha_service: service
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: check captcha
|
||||
}
|
||||
|
||||
const user = await UserModel.findOne(
|
||||
{ $or: query },
|
||||
{ user_data: { hash: true }, id: true, disabled: true, deleted: true, user_settings: { locale: true, theme: true } }
|
||||
)
|
||||
.exec()
|
||||
.catch((e) => {
|
||||
throw FieldErrors({ login: { message: req.t("auth:login.INVALID_LOGIN"), code: "INVALID_LOGIN" } });
|
||||
});
|
||||
|
||||
if (undelete) {
|
||||
// undelete refers to un'disable' here
|
||||
if (user.disabled) await UserModel.updateOne({ id: user.id }, { disabled: false }).exec();
|
||||
if (user.deleted) await UserModel.updateOne({ id: user.id }, { deleted: false }).exec();
|
||||
} else {
|
||||
if (user.deleted) return res.status(400).json({ message: "This account is scheduled for deletion.", code: 20011 });
|
||||
if (user.disabled) return res.status(400).json({ message: req.t("auth:login.ACCOUNT_DISABLED"), code: 20013 });
|
||||
}
|
||||
|
||||
// the salt is saved in the password refer to bcrypt docs
|
||||
const same_password = await bcrypt.compare(password, user.user_data.hash || "");
|
||||
if (!same_password) {
|
||||
throw FieldErrors({ password: { message: req.t("auth:login.INVALID_PASSWORD"), code: "INVALID_PASSWORD" } });
|
||||
}
|
||||
|
||||
const token = await generateToken(user.id);
|
||||
|
||||
// Notice this will have a different token structure, than discord
|
||||
// Discord header is just the user id as string, which is not possible with npm-jsonwebtoken package
|
||||
// https://user-images.githubusercontent.com/6506416/81051916-dd8c9900-8ec2-11ea-8794-daf12d6f31f0.png
|
||||
|
||||
res.json({ token, user_settings: user.user_settings });
|
||||
}
|
||||
);
|
||||
|
||||
export async function generateToken(id: string) {
|
||||
const iat = Math.floor(Date.now() / 1000);
|
||||
const algorithm = "HS256";
|
||||
|
||||
return new Promise((res, rej) => {
|
||||
jwt.sign(
|
||||
{ id: id, iat },
|
||||
Config.get().security.jwtSecret,
|
||||
{
|
||||
algorithm
|
||||
},
|
||||
(err, token) => {
|
||||
if (err) return rej(err);
|
||||
return res(token);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /auth/login
|
||||
* @argument { login: "email@gmail.com", password: "cleartextpassword", undelete: false, captcha_key: null, login_source: null, gift_code_sku_id: null, }
|
||||
|
||||
* MFA required:
|
||||
* @returns {"token": null, "mfa": true, "sms": true, "ticket": "SOME TICKET JWT TOKEN"}
|
||||
|
||||
* Captcha required:
|
||||
* @returns {"captcha_key": ["captcha-required"], "captcha_sitekey": null, "captcha_service": "recaptcha"}
|
||||
|
||||
* Sucess:
|
||||
* @returns {"token": "USERTOKEN", "user_settings": {"locale": "en", "theme": "dark"}}
|
||||
|
||||
*/
|
||||
@@ -0,0 +1,309 @@
|
||||
import { Request, Response, Router } from "express";
|
||||
import { trimSpecial, User, Snowflake, UserModel, Config } from "@fosscord/server-util";
|
||||
import bcrypt from "bcrypt";
|
||||
import { check, Email, EMAIL_REGEX, FieldErrors, Length } from "../../util/instanceOf";
|
||||
import "missing-native-js-functions";
|
||||
import { generateToken } from "./login";
|
||||
import { getIpAdress, IPAnalysis, isProxy } from "../../util/ipAddress";
|
||||
import { HTTPError } from "lambert-server";
|
||||
import RateLimit from "../../middlewares/RateLimit";
|
||||
|
||||
const router: Router = Router();
|
||||
|
||||
router.post(
|
||||
"/",
|
||||
check({
|
||||
username: new Length(String, 2, 32),
|
||||
// TODO: check min password length in config
|
||||
// prevent Denial of Service with max length of 72 chars
|
||||
password: new Length(String, 8, 72),
|
||||
consent: Boolean,
|
||||
$email: new Length(Email, 5, 100),
|
||||
$fingerprint: String,
|
||||
$invite: String,
|
||||
$date_of_birth: Date, // "2000-04-03"
|
||||
$gift_code_sku_id: String,
|
||||
$captcha_key: String
|
||||
}),
|
||||
async (req: Request, res: Response) => {
|
||||
const {
|
||||
email,
|
||||
username,
|
||||
password,
|
||||
consent,
|
||||
fingerprint,
|
||||
invite,
|
||||
date_of_birth,
|
||||
gift_code_sku_id, // ? what is this
|
||||
captcha_key
|
||||
} = req.body;
|
||||
|
||||
// get register Config
|
||||
const { register, security } = Config.get();
|
||||
const ip = getIpAdress(req);
|
||||
|
||||
if (register.blockProxies) {
|
||||
if (isProxy(await IPAnalysis(ip))) {
|
||||
console.log(`proxy ${ip} blocked from registration`);
|
||||
throw new HTTPError("Your IP is blocked from registration");
|
||||
}
|
||||
}
|
||||
|
||||
console.log("register", req.body.email, req.body.username, ip);
|
||||
// TODO: automatically join invite
|
||||
// TODO: gift_code_sku_id?
|
||||
// TODO: check password strength
|
||||
|
||||
// adjusted_email will be slightly modified version of the user supplied email -> e.g. protection against GMail Trick
|
||||
let adjusted_email: string | null = adjustEmail(email);
|
||||
|
||||
// adjusted_password will be the hash of the password
|
||||
let adjusted_password: string = "";
|
||||
|
||||
// trim special uf8 control characters -> Backspace, Newline, ...
|
||||
let adjusted_username: string = trimSpecial(username);
|
||||
|
||||
// discriminator will be randomly generated
|
||||
let discriminator = "";
|
||||
|
||||
// check if registration is allowed
|
||||
if (!register.allowNewRegistration) {
|
||||
throw FieldErrors({
|
||||
email: { code: "REGISTRATION_DISABLED", message: req.t("auth:register.REGISTRATION_DISABLED") }
|
||||
});
|
||||
}
|
||||
|
||||
// check if the user agreed to the Terms of Service
|
||||
if (!consent) {
|
||||
throw FieldErrors({
|
||||
consent: { code: "CONSENT_REQUIRED", message: req.t("auth:register.CONSENT_REQUIRED") }
|
||||
});
|
||||
}
|
||||
|
||||
// require invite to register -> e.g. for organizations to send invites to their employees
|
||||
if (register.requireInvite && !invite) {
|
||||
throw FieldErrors({
|
||||
email: { code: "INVITE_ONLY", message: req.t("auth:register.INVITE_ONLY") }
|
||||
});
|
||||
}
|
||||
|
||||
if (email) {
|
||||
// replace all dots and chars after +, if its a gmail.com email
|
||||
if (!adjusted_email) throw FieldErrors({ email: { code: "INVALID_EMAIL", message: req.t("auth:register.INVALID_EMAIL") } });
|
||||
|
||||
// check if there is already an account with this email
|
||||
const exists = await UserModel.findOne({ email: adjusted_email })
|
||||
.exec()
|
||||
.catch((e) => {});
|
||||
|
||||
if (exists) {
|
||||
throw FieldErrors({
|
||||
email: {
|
||||
code: "EMAIL_ALREADY_REGISTERED",
|
||||
message: req.t("auth:register.EMAIL_ALREADY_REGISTERED")
|
||||
}
|
||||
});
|
||||
}
|
||||
} else if (register.email.necessary) {
|
||||
throw FieldErrors({
|
||||
email: { code: "BASE_TYPE_REQUIRED", message: req.t("common:field.BASE_TYPE_REQUIRED") }
|
||||
});
|
||||
}
|
||||
|
||||
if (register.dateOfBirth.necessary && !date_of_birth) {
|
||||
throw FieldErrors({
|
||||
date_of_birth: { code: "BASE_TYPE_REQUIRED", message: req.t("common:field.BASE_TYPE_REQUIRED") }
|
||||
});
|
||||
} else if (register.dateOfBirth.minimum) {
|
||||
const minimum = new Date();
|
||||
minimum.setFullYear(minimum.getFullYear() - register.dateOfBirth.minimum);
|
||||
|
||||
// higher is younger
|
||||
if (date_of_birth > minimum) {
|
||||
throw FieldErrors({
|
||||
date_of_birth: {
|
||||
code: "DATE_OF_BIRTH_UNDERAGE",
|
||||
message: req.t("auth:register.DATE_OF_BIRTH_UNDERAGE", { years: register.dateOfBirth.minimum })
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!register.allowMultipleAccounts) {
|
||||
// TODO: check if fingerprint was eligible generated
|
||||
const exists = await UserModel.findOne({ fingerprints: fingerprint })
|
||||
.exec()
|
||||
.catch((e) => {});
|
||||
|
||||
if (exists) {
|
||||
throw FieldErrors({
|
||||
email: {
|
||||
code: "EMAIL_ALREADY_REGISTERED",
|
||||
message: req.t("auth:register.EMAIL_ALREADY_REGISTERED")
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (register.requireCaptcha && security.captcha.enabled) {
|
||||
if (!captcha_key) {
|
||||
const { sitekey, service } = security.captcha;
|
||||
return res.status(400).json({
|
||||
captcha_key: ["captcha-required"],
|
||||
captcha_sitekey: sitekey,
|
||||
captcha_service: service
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: check captcha
|
||||
}
|
||||
|
||||
// the salt is saved in the password refer to bcrypt docs
|
||||
adjusted_password = await bcrypt.hash(password, 12);
|
||||
|
||||
let exists;
|
||||
// randomly generates a discriminator between 1 and 9999 and checks max five times if it already exists
|
||||
// if it all five times already exists, abort with USERNAME_TOO_MANY_USERS error
|
||||
// else just continue
|
||||
// TODO: is there any better way to generate a random discriminator only once, without checking if it already exists in the mongodb database?
|
||||
for (let tries = 0; tries < 5; tries++) {
|
||||
discriminator = Math.randomIntBetween(1, 9999).toString().padStart(4, "0");
|
||||
try {
|
||||
exists = await UserModel.findOne({ discriminator, username: adjusted_username }, "id").exec();
|
||||
} catch (error) {
|
||||
// doesn't exist -> break
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (exists) {
|
||||
throw FieldErrors({
|
||||
username: {
|
||||
code: "USERNAME_TOO_MANY_USERS",
|
||||
message: req.t("auth:register.USERNAME_TOO_MANY_USERS")
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: save date_of_birth
|
||||
// appearently discord doesn't save the date of birth and just calculate if nsfw is allowed
|
||||
// if nsfw_allowed is null/undefined it'll require date_of_birth to set it to true/false
|
||||
|
||||
const user: User = {
|
||||
id: Snowflake.generate(),
|
||||
created_at: new Date(),
|
||||
username: adjusted_username,
|
||||
discriminator,
|
||||
avatar: null,
|
||||
accent_color: null,
|
||||
banner: null,
|
||||
bot: false,
|
||||
system: false,
|
||||
desktop: false,
|
||||
mobile: false,
|
||||
premium: true,
|
||||
premium_type: 2,
|
||||
phone: null,
|
||||
bio: "",
|
||||
mfa_enabled: false,
|
||||
verified: false,
|
||||
disabled: false,
|
||||
deleted: false,
|
||||
presence: {
|
||||
activities: [],
|
||||
client_status: {
|
||||
desktop: undefined,
|
||||
mobile: undefined,
|
||||
web: undefined
|
||||
},
|
||||
status: "offline"
|
||||
},
|
||||
email: adjusted_email,
|
||||
nsfw_allowed: true, // TODO: depending on age
|
||||
public_flags: 0n,
|
||||
flags: 0n, // TODO: generate default flags
|
||||
guilds: [],
|
||||
user_data: {
|
||||
hash: adjusted_password,
|
||||
valid_tokens_since: new Date(),
|
||||
relationships: [],
|
||||
connected_accounts: [],
|
||||
fingerprints: []
|
||||
},
|
||||
user_settings: {
|
||||
afk_timeout: 300,
|
||||
allow_accessibility_detection: true,
|
||||
animate_emoji: true,
|
||||
animate_stickers: 0,
|
||||
contact_sync_enabled: false,
|
||||
convert_emoticons: false,
|
||||
custom_status: {
|
||||
emoji_id: null,
|
||||
emoji_name: null,
|
||||
expires_at: null,
|
||||
text: null
|
||||
},
|
||||
default_guilds_restricted: false,
|
||||
detect_platform_accounts: true,
|
||||
developer_mode: false,
|
||||
disable_games_tab: false,
|
||||
enable_tts_command: true,
|
||||
explicit_content_filter: 0,
|
||||
friend_source_flags: { all: true },
|
||||
gateway_connected: false,
|
||||
gif_auto_play: true,
|
||||
guild_folders: [],
|
||||
guild_positions: [],
|
||||
inline_attachment_media: true,
|
||||
inline_embed_media: true,
|
||||
locale: req.language,
|
||||
message_display_compact: false,
|
||||
native_phone_integration_enabled: true,
|
||||
render_embeds: true,
|
||||
render_reactions: true,
|
||||
restricted_guilds: [],
|
||||
show_current_game: true,
|
||||
status: "offline",
|
||||
stream_notifications_enabled: true,
|
||||
theme: "dark",
|
||||
timezone_offset: 0
|
||||
// timezone_offset: // TODO: timezone from request
|
||||
}
|
||||
};
|
||||
|
||||
// insert user into database
|
||||
await new UserModel(user).save();
|
||||
|
||||
return res.json({ token: await generateToken(user.id) });
|
||||
}
|
||||
);
|
||||
|
||||
export function adjustEmail(email: string): string | null {
|
||||
// body parser already checked if it is a valid email
|
||||
const parts = <RegExpMatchArray>email.match(EMAIL_REGEX);
|
||||
// @ts-ignore
|
||||
if (!parts || parts.length < 5) return undefined;
|
||||
const domain = parts[5];
|
||||
const user = parts[1];
|
||||
|
||||
// TODO: check accounts with uncommon email domains
|
||||
if (domain === "gmail.com" || domain === "googlemail.com") {
|
||||
// replace .dots and +alternatives -> Gmail Dot Trick https://support.google.com/mail/answer/7436150 and https://generator.email/blog/gmail-generator
|
||||
return user.replace(/[.]|(\+.*)/g, "") + "@gmail.com";
|
||||
}
|
||||
|
||||
return email;
|
||||
}
|
||||
|
||||
export default router;
|
||||
|
||||
/**
|
||||
* POST /auth/register
|
||||
* @argument { "fingerprint":"805826570869932034.wR8vi8lGlFBJerErO9LG5NViJFw", "email":"qo8etzvaf@gmail.com", "username":"qp39gr98", "password":"wtp9gep9gw", "invite":null, "consent":true, "date_of_birth":"2000-04-04", "gift_code_sku_id":null, "captcha_key":null}
|
||||
*
|
||||
* Field Error
|
||||
* @returns { "code": 50035, "errors": { "consent": { "_errors": [{ "code": "CONSENT_REQUIRED", "message": "You must agree to Discord's Terms of Service and Privacy Policy." }]}}, "message": "Invalid Form Body"}
|
||||
*
|
||||
* Success 201:
|
||||
* @returns {token: "OMITTED"}
|
||||
*/
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Router, Response, Request } from "express";
|
||||
const router: Router = Router();
|
||||
// TODO:
|
||||
|
||||
export default router;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {"webhook_channel_id":"754001514330062952"}
|
||||
*
|
||||
* Creates a WebHook in the channel and returns the id of it
|
||||
*
|
||||
* @returns {"channel_id": "816382962056560690", "webhook_id": "834910735095037962"}
|
||||
*/
|
||||
@@ -0,0 +1,60 @@
|
||||
import { ChannelDeleteEvent, ChannelModel, ChannelUpdateEvent, getPermission, GuildUpdateEvent, toObject } from "@fosscord/server-util";
|
||||
import { Router, Response, Request } from "express";
|
||||
import { HTTPError } from "lambert-server";
|
||||
import { ChannelModifySchema } from "../../../schema/Channel";
|
||||
import { emitEvent } from "../../../util/Event";
|
||||
import { check } from "../../../util/instanceOf";
|
||||
const router: Router = Router();
|
||||
// TODO: delete channel
|
||||
// TODO: Get channel
|
||||
|
||||
router.get("/", async (req: Request, res: Response) => {
|
||||
const { channel_id } = req.params;
|
||||
|
||||
const channel = await ChannelModel.findOne({ id: channel_id }).exec();
|
||||
|
||||
const permission = await getPermission(req.user_id, channel.guild_id, channel_id);
|
||||
permission.hasThrow("VIEW_CHANNEL");
|
||||
|
||||
return res.send(toObject(channel));
|
||||
});
|
||||
|
||||
router.delete("/", async (req: Request, res: Response) => {
|
||||
const { channel_id } = req.params;
|
||||
|
||||
const channel = await ChannelModel.findOne({ id: channel_id }).exec();
|
||||
|
||||
const permission = await getPermission(req.user_id, channel?.guild_id, channel_id, { channel });
|
||||
permission.hasThrow("MANAGE_CHANNELS");
|
||||
|
||||
// TODO: Dm channel "close" not delete
|
||||
const data = toObject(channel);
|
||||
|
||||
await emitEvent({ event: "CHANNEL_DELETE", data, channel_id } as ChannelDeleteEvent);
|
||||
|
||||
await ChannelModel.deleteOne({ id: channel_id });
|
||||
|
||||
res.send(data);
|
||||
});
|
||||
|
||||
router.patch("/", check(ChannelModifySchema), async (req: Request, res: Response) => {
|
||||
var payload = req.body as ChannelModifySchema;
|
||||
const { channel_id } = req.params;
|
||||
|
||||
const permission = await getPermission(req.user_id, undefined, channel_id);
|
||||
permission.hasThrow("MANAGE_CHANNELS");
|
||||
|
||||
const channel = await ChannelModel.findOneAndUpdate({ id: channel_id }, payload).exec();
|
||||
|
||||
const data = toObject(channel);
|
||||
|
||||
await emitEvent({
|
||||
event: "CHANNEL_UPDATE",
|
||||
data,
|
||||
channel_id
|
||||
} as ChannelUpdateEvent);
|
||||
|
||||
res.send(data);
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,65 @@
|
||||
import { Router, Request, Response } from "express";
|
||||
import { HTTPError } from "lambert-server";
|
||||
|
||||
import { check } from "../../../util/instanceOf";
|
||||
import { random } from "../../../util/RandomInviteID";
|
||||
import { emitEvent } from "../../../util/Event";
|
||||
|
||||
import { InviteCreateSchema } from "../../../schema/Invite";
|
||||
|
||||
import { getPermission, ChannelModel, InviteModel, InviteCreateEvent, toObject } from "@fosscord/server-util";
|
||||
|
||||
const router: Router = Router();
|
||||
|
||||
router.post("/", check(InviteCreateSchema), async (req: Request, res: Response) => {
|
||||
const { user_id } = req;
|
||||
const { channel_id } = req.params;
|
||||
const channel = await ChannelModel.findOne({ id: channel_id }).exec();
|
||||
|
||||
if (!channel.guild_id) {
|
||||
throw new HTTPError("This channel doesn't exist", 404);
|
||||
}
|
||||
const { guild_id } = channel;
|
||||
|
||||
const permission = await getPermission(user_id, guild_id);
|
||||
permission.hasThrow("CREATE_INSTANT_INVITE");
|
||||
|
||||
const expires_at = new Date(req.body.max_age * 1000 + Date.now());
|
||||
|
||||
const invite = {
|
||||
code: random(),
|
||||
temporary: req.body.temporary,
|
||||
uses: 0,
|
||||
max_uses: req.body.max_uses,
|
||||
max_age: req.body.max_age,
|
||||
expires_at,
|
||||
created_at: new Date(),
|
||||
guild_id,
|
||||
channel_id: channel_id,
|
||||
inviter_id: user_id
|
||||
};
|
||||
|
||||
await new InviteModel(invite).save();
|
||||
|
||||
await emitEvent({ event: "INVITE_CREATE", data: invite, guild_id } as InviteCreateEvent);
|
||||
res.status(201).send(invite);
|
||||
});
|
||||
|
||||
router.get("/", async (req: Request, res: Response) => {
|
||||
const { user_id } = req;
|
||||
const { channel_id } = req.params;
|
||||
const channel = await ChannelModel.findOne({ id: channel_id }).exec();
|
||||
|
||||
if (!channel.guild_id) {
|
||||
throw new HTTPError("This channel doesn't exist", 404);
|
||||
}
|
||||
const { guild_id } = channel;
|
||||
const permission = await getPermission(user_id, guild_id);
|
||||
permission.hasThrow("MANAGE_CHANNELS");
|
||||
|
||||
const invites = await InviteModel.find({ guild_id }).exec();
|
||||
|
||||
res.status(200).send(toObject(invites));
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,35 @@
|
||||
import { getPermission, MessageAckEvent, ReadStateModel } from "@fosscord/server-util";
|
||||
import { Request, Response, Router } from "express";
|
||||
import { emitEvent } from "../../../../../util/Event";
|
||||
import { check } from "../../../../../util/instanceOf";
|
||||
|
||||
const router = Router();
|
||||
|
||||
// TODO: check if message exists
|
||||
// TODO: send read state event to all channel members
|
||||
|
||||
router.post("/", check({ $manual: Boolean, $mention_count: Number }), async (req: Request, res: Response) => {
|
||||
const { channel_id, message_id } = req.params;
|
||||
|
||||
const permission = await getPermission(req.user_id, undefined, channel_id);
|
||||
permission.hasThrow("VIEW_CHANNEL");
|
||||
|
||||
await ReadStateModel.updateOne(
|
||||
{ user_id: req.user_id, channel_id, message_id },
|
||||
{ user_id: req.user_id, channel_id, message_id }
|
||||
).exec();
|
||||
|
||||
await emitEvent({
|
||||
event: "MESSAGE_ACK",
|
||||
user_id: req.user_id,
|
||||
data: {
|
||||
channel_id,
|
||||
message_id,
|
||||
version: 496
|
||||
}
|
||||
} as MessageAckEvent);
|
||||
|
||||
res.sendStatus(204);
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,8 @@
|
||||
import { Router, Response, Request } from "express";
|
||||
|
||||
const router = Router();
|
||||
|
||||
// TODO:
|
||||
// router.post("/", (req: Request, res: Response) => {});
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,72 @@
|
||||
import { ChannelModel, getPermission, MessageDeleteEvent, MessageModel, MessageUpdateEvent, toObject } from "@fosscord/server-util";
|
||||
import { Router, Response, Request } from "express";
|
||||
import { HTTPError } from "lambert-server";
|
||||
import { MessageCreateSchema } from "../../../../../schema/Message";
|
||||
import { emitEvent } from "../../../../../util/Event";
|
||||
import { check } from "../../../../../util/instanceOf";
|
||||
import { handleMessage, postHandleMessage } from "../../../../../util/Message";
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.patch("/", check(MessageCreateSchema), async (req: Request, res: Response) => {
|
||||
const { message_id, channel_id } = req.params;
|
||||
var body = req.body as MessageCreateSchema;
|
||||
|
||||
var message = await MessageModel.findOne({ id: message_id, channel_id }, { author_id: true }).exec();
|
||||
|
||||
const permissions = await getPermission(req.user_id, undefined, channel_id);
|
||||
|
||||
if (req.user_id !== message.author_id) {
|
||||
permissions.hasThrow("MANAGE_MESSAGES");
|
||||
body = { flags: body.flags };
|
||||
}
|
||||
|
||||
const opts = await handleMessage({
|
||||
...body,
|
||||
author_id: message.author_id,
|
||||
channel_id,
|
||||
id: message_id,
|
||||
edited_timestamp: new Date()
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
message = await MessageModel.findOneAndUpdate({ id: message_id }, opts).populate("author").exec();
|
||||
|
||||
await emitEvent({
|
||||
event: "MESSAGE_UPDATE",
|
||||
channel_id,
|
||||
data: { ...toObject(message), nonce: undefined }
|
||||
} as MessageUpdateEvent);
|
||||
|
||||
postHandleMessage(message);
|
||||
|
||||
return res.json(toObject(message));
|
||||
});
|
||||
|
||||
// TODO: delete attachments in message
|
||||
|
||||
router.delete("/", async (req: Request, res: Response) => {
|
||||
const { message_id, channel_id } = req.params;
|
||||
|
||||
const channel = await ChannelModel.findOne({ id: channel_id }, { guild_id: true });
|
||||
const message = await MessageModel.findOne({ id: message_id }, { author_id: true }).exec();
|
||||
|
||||
const permission = await getPermission(req.user_id, channel.guild_id, channel_id);
|
||||
if (message.author_id !== req.user_id) permission.hasThrow("MANAGE_MESSAGES");
|
||||
|
||||
await MessageModel.deleteOne({ id: message_id }).exec();
|
||||
|
||||
await emitEvent({
|
||||
event: "MESSAGE_DELETE",
|
||||
channel_id,
|
||||
data: {
|
||||
id: message_id,
|
||||
channel_id,
|
||||
guild_id: channel.guild_id
|
||||
}
|
||||
} as MessageDeleteEvent);
|
||||
|
||||
res.sendStatus(204);
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,191 @@
|
||||
import {
|
||||
ChannelModel,
|
||||
EmojiModel,
|
||||
getPermission,
|
||||
MemberModel,
|
||||
MessageModel,
|
||||
MessageReactionAddEvent,
|
||||
MessageReactionRemoveAllEvent,
|
||||
MessageReactionRemoveEmojiEvent,
|
||||
MessageReactionRemoveEvent,
|
||||
PartialEmoji,
|
||||
PublicUserProjection,
|
||||
toObject,
|
||||
UserModel
|
||||
} from "@fosscord/server-util";
|
||||
import { Router, Response, Request } from "express";
|
||||
import { HTTPError } from "lambert-server";
|
||||
import { emitEvent } from "../../../../../util/Event";
|
||||
|
||||
const router = Router();
|
||||
// TODO: check if emoji is really an unicode emoji or a prperly encoded external emoji
|
||||
|
||||
function getEmoji(emoji: string): PartialEmoji {
|
||||
emoji = decodeURIComponent(emoji);
|
||||
const parts = emoji.includes(":") && emoji.split(":");
|
||||
if (parts)
|
||||
return {
|
||||
name: parts[0],
|
||||
id: parts[1]
|
||||
};
|
||||
|
||||
return {
|
||||
id: undefined,
|
||||
name: emoji
|
||||
};
|
||||
}
|
||||
|
||||
router.delete("/", async (req: Request, res: Response) => {
|
||||
const { message_id, channel_id } = req.params;
|
||||
|
||||
const channel = await ChannelModel.findOne({ id: channel_id }, { guild_id: true }).exec();
|
||||
|
||||
const permissions = await getPermission(req.user_id, undefined, channel_id);
|
||||
permissions.hasThrow("MANAGE_MESSAGES");
|
||||
|
||||
await MessageModel.findOneAndUpdate({ id: message_id, channel_id }, { reactions: [] }).exec();
|
||||
|
||||
await emitEvent({
|
||||
event: "MESSAGE_REACTION_REMOVE_ALL",
|
||||
channel_id,
|
||||
data: {
|
||||
channel_id,
|
||||
message_id,
|
||||
guild_id: channel.guild_id
|
||||
}
|
||||
} as MessageReactionRemoveAllEvent);
|
||||
|
||||
res.sendStatus(204);
|
||||
});
|
||||
|
||||
router.delete("/:emoji", async (req: Request, res: Response) => {
|
||||
const { message_id, channel_id } = req.params;
|
||||
const emoji = getEmoji(req.params.emoji);
|
||||
|
||||
const channel = await ChannelModel.findOne({ id: channel_id }, { guild_id: true }).exec();
|
||||
|
||||
const permissions = await getPermission(req.user_id, undefined, channel_id);
|
||||
permissions.hasThrow("MANAGE_MESSAGES");
|
||||
|
||||
const message = await MessageModel.findOne({ id: message_id, channel_id }).exec();
|
||||
|
||||
const already_added = message.reactions.find((x) => (x.emoji.id === emoji.id && emoji.id) || x.emoji.name === emoji.name);
|
||||
if (!already_added) throw new HTTPError("Reaction not found", 404);
|
||||
message.reactions.remove(already_added);
|
||||
|
||||
await MessageModel.updateOne({ id: message_id, channel_id }, message).exec();
|
||||
|
||||
await emitEvent({
|
||||
event: "MESSAGE_REACTION_REMOVE_EMOJI",
|
||||
channel_id,
|
||||
data: {
|
||||
channel_id,
|
||||
message_id,
|
||||
guild_id: channel.guild_id,
|
||||
emoji
|
||||
}
|
||||
} as MessageReactionRemoveEmojiEvent);
|
||||
|
||||
res.sendStatus(204);
|
||||
});
|
||||
|
||||
router.get("/:emoji", async (req: Request, res: Response) => {
|
||||
const { message_id, channel_id } = req.params;
|
||||
const emoji = getEmoji(req.params.emoji);
|
||||
|
||||
const message = await MessageModel.findOne({ id: message_id, channel_id }).exec();
|
||||
if (!message) throw new HTTPError("Message not found", 404);
|
||||
const reaction = message.reactions.find((x) => (x.emoji.id === emoji.id && emoji.id) || x.emoji.name === emoji.name);
|
||||
if (!reaction) throw new HTTPError("Reaction not found", 404);
|
||||
|
||||
const permissions = await getPermission(req.user_id, undefined, channel_id);
|
||||
permissions.hasThrow("VIEW_CHANNEL");
|
||||
|
||||
const users = await UserModel.find({ id: { $in: reaction.user_ids } }, PublicUserProjection).exec();
|
||||
|
||||
res.json(toObject(users));
|
||||
});
|
||||
|
||||
router.put("/:emoji/:user_id", async (req: Request, res: Response) => {
|
||||
const { message_id, channel_id, user_id } = req.params;
|
||||
if (user_id !== "@me") throw new HTTPError("Invalid user");
|
||||
const emoji = getEmoji(req.params.emoji);
|
||||
|
||||
const channel = await ChannelModel.findOne({ id: channel_id }, { guild_id: true }).exec();
|
||||
const message = await MessageModel.findOne({ id: message_id, channel_id }).exec();
|
||||
const already_added = message.reactions.find((x) => (x.emoji.id === emoji.id && emoji.id) || x.emoji.name === emoji.name);
|
||||
|
||||
const permissions = await getPermission(req.user_id, undefined, channel_id);
|
||||
permissions.hasThrow("READ_MESSAGE_HISTORY");
|
||||
if (!already_added) permissions.hasThrow("ADD_REACTIONS");
|
||||
|
||||
if (emoji.id) {
|
||||
const external_emoji = await EmojiModel.findOne({ id: emoji.id }).exec();
|
||||
if (!already_added) permissions.hasThrow("USE_EXTERNAL_EMOJIS");
|
||||
emoji.animated = external_emoji.animated;
|
||||
emoji.name = external_emoji.name;
|
||||
}
|
||||
|
||||
if (already_added) {
|
||||
if (already_added.user_ids.includes(req.user_id)) return res.sendStatus(204); // Do not throw an error ¯\_(ツ)_/¯ as discord also doesn't throw any error
|
||||
already_added.count++;
|
||||
} else message.reactions.push({ count: 1, emoji, user_ids: [req.user_id] });
|
||||
|
||||
await MessageModel.updateOne({ id: message_id, channel_id }, message).exec();
|
||||
|
||||
const member = channel.guild_id && (await MemberModel.findOne({ id: req.user_id }).exec());
|
||||
|
||||
await emitEvent({
|
||||
event: "MESSAGE_REACTION_ADD",
|
||||
channel_id,
|
||||
data: {
|
||||
user_id: req.user_id,
|
||||
channel_id,
|
||||
message_id,
|
||||
guild_id: channel.guild_id,
|
||||
emoji,
|
||||
member
|
||||
}
|
||||
} as MessageReactionAddEvent);
|
||||
|
||||
res.sendStatus(204);
|
||||
});
|
||||
|
||||
router.delete("/:emoji/:user_id", async (req: Request, res: Response) => {
|
||||
var { message_id, channel_id, user_id } = req.params;
|
||||
|
||||
const emoji = getEmoji(req.params.emoji);
|
||||
|
||||
const channel = await ChannelModel.findOne({ id: channel_id }, { guild_id: true }).exec();
|
||||
const message = await MessageModel.findOne({ id: message_id, channel_id }).exec();
|
||||
|
||||
const permissions = await getPermission(req.user_id, undefined, channel_id);
|
||||
|
||||
if (user_id === "@me") user_id = req.user_id;
|
||||
else permissions.hasThrow("MANAGE_MESSAGES");
|
||||
|
||||
const already_added = message.reactions.find((x) => (x.emoji.id === emoji.id && emoji.id) || x.emoji.name === emoji.name);
|
||||
if (!already_added || !already_added.user_ids.includes(user_id)) throw new HTTPError("Reaction not found", 404);
|
||||
|
||||
already_added.count--;
|
||||
|
||||
if (already_added.count <= 0) message.reactions.remove(already_added);
|
||||
|
||||
await MessageModel.updateOne({ id: message_id, channel_id }, message).exec();
|
||||
|
||||
await emitEvent({
|
||||
event: "MESSAGE_REACTION_REMOVE",
|
||||
channel_id,
|
||||
data: {
|
||||
user_id: req.user_id,
|
||||
channel_id,
|
||||
message_id,
|
||||
guild_id: channel.guild_id,
|
||||
emoji
|
||||
}
|
||||
} as MessageReactionRemoveEvent);
|
||||
|
||||
res.sendStatus(204);
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,37 @@
|
||||
import { Router, Response, Request } from "express";
|
||||
import { ChannelModel, Config, getPermission, MessageDeleteBulkEvent, MessageModel } from "@fosscord/server-util";
|
||||
import { HTTPError } from "lambert-server";
|
||||
import { emitEvent } from "../../../../util/Event";
|
||||
import { check } from "../../../../util/instanceOf";
|
||||
|
||||
const router: Router = Router();
|
||||
|
||||
export default router;
|
||||
|
||||
// TODO: should users be able to bulk delete messages or only bots?
|
||||
// TODO: should this request fail, if you provide messages older than 14 days/invalid ids?
|
||||
// https://discord.com/developers/docs/resources/channel#bulk-delete-messages
|
||||
router.post("/", check({ messages: [String] }), async (req: Request, res: Response) => {
|
||||
const { channel_id } = req.params;
|
||||
const channel = await ChannelModel.findOne({ id: channel_id }, { permission_overwrites: true, guild_id: true }).exec();
|
||||
if (!channel.guild_id) throw new HTTPError("Can't bulk delete dm channel messages", 400);
|
||||
|
||||
const permission = await getPermission(req.user_id, channel?.guild_id, channel_id, { channel });
|
||||
permission.hasThrow("MANAGE_MESSAGES");
|
||||
|
||||
const { maxBulkDelete } = Config.get().limits.message;
|
||||
|
||||
const { messages } = req.body as { messages: string[] };
|
||||
if (messages.length < 2) throw new HTTPError("You must at least specify 2 messages to bulk delete");
|
||||
if (messages.length > maxBulkDelete) throw new HTTPError(`You cannot delete more than ${maxBulkDelete} messages`);
|
||||
|
||||
await MessageModel.deleteMany({ id: { $in: messages } }).exec();
|
||||
|
||||
await emitEvent({
|
||||
event: "MESSAGE_DELETE_BULK",
|
||||
channel_id,
|
||||
data: { ids: messages, channel_id, guild_id: channel.guild_id }
|
||||
} as MessageDeleteBulkEvent);
|
||||
|
||||
res.sendStatus(204);
|
||||
});
|
||||
@@ -0,0 +1,146 @@
|
||||
import { Router, Response, Request } from "express";
|
||||
import { Attachment, ChannelModel, ChannelType, getPermission, MessageDocument, MessageModel, toObject } from "@fosscord/server-util";
|
||||
import { HTTPError } from "lambert-server";
|
||||
import { MessageCreateSchema } from "../../../../schema/Message";
|
||||
import { check, instanceOf, Length } from "../../../../util/instanceOf";
|
||||
import multer from "multer";
|
||||
import { Query } from "mongoose";
|
||||
import { sendMessage } from "../../../../util/Message";
|
||||
import { uploadFile } from "../../../../util/cdn";
|
||||
|
||||
const router: Router = Router();
|
||||
|
||||
export default router;
|
||||
|
||||
export function isTextChannel(type: ChannelType): boolean {
|
||||
switch (type) {
|
||||
case ChannelType.GUILD_VOICE:
|
||||
case ChannelType.GUILD_CATEGORY:
|
||||
throw new HTTPError("not a text channel", 400);
|
||||
case ChannelType.DM:
|
||||
case ChannelType.GROUP_DM:
|
||||
case ChannelType.GUILD_NEWS:
|
||||
case ChannelType.GUILD_STORE:
|
||||
case ChannelType.GUILD_TEXT:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// https://discord.com/developers/docs/resources/channel#create-message
|
||||
// get messages
|
||||
router.get("/", async (req: Request, res: Response) => {
|
||||
const channel_id = req.params.channel_id;
|
||||
const channel = await ChannelModel.findOne(
|
||||
{ id: channel_id },
|
||||
{ guild_id: true, type: true, permission_overwrites: true, recipient_ids: true, owner_id: true }
|
||||
)
|
||||
.lean() // lean is needed, because we don't want to populate .recipients that also auto deletes .recipient_ids
|
||||
.exec();
|
||||
if (!channel) throw new HTTPError("Channel not found", 404);
|
||||
|
||||
isTextChannel(channel.type);
|
||||
|
||||
try {
|
||||
instanceOf({ $around: String, $after: String, $before: String, $limit: new Length(Number, 1, 100) }, req.query, {
|
||||
path: "query",
|
||||
req
|
||||
});
|
||||
} catch (error) {
|
||||
return res.status(400).json({ code: 50035, message: "Invalid Query", success: false, errors: error });
|
||||
}
|
||||
var { around, after, before, limit }: { around?: string; after?: string; before?: string; limit?: number } = req.query;
|
||||
if (!limit) limit = 50;
|
||||
var halfLimit = Math.floor(limit / 2);
|
||||
|
||||
// @ts-ignore
|
||||
const permissions = await getPermission(req.user_id, channel.guild_id, channel_id, { channel });
|
||||
permissions.hasThrow("VIEW_CHANNEL");
|
||||
if (!permissions.has("READ_MESSAGE_HISTORY")) return res.json([]);
|
||||
|
||||
var query: Query<MessageDocument[], MessageDocument>;
|
||||
if (after) query = MessageModel.find({ channel_id, id: { $gt: after } });
|
||||
else if (before) query = MessageModel.find({ channel_id, id: { $lt: before } });
|
||||
else if (around)
|
||||
query = MessageModel.find({
|
||||
channel_id,
|
||||
id: { $gt: (BigInt(around) - BigInt(halfLimit)).toString(), $lt: (BigInt(around) + BigInt(halfLimit)).toString() }
|
||||
});
|
||||
else {
|
||||
query = MessageModel.find({ channel_id });
|
||||
}
|
||||
|
||||
query = query.sort({ id: -1 });
|
||||
|
||||
const messages = await query.limit(limit).exec();
|
||||
|
||||
return res.json(
|
||||
toObject(messages).map((x) => {
|
||||
(x.reactions || []).forEach((x) => {
|
||||
// @ts-ignore
|
||||
if ((x.user_ids || []).includes(req.user_id)) x.me = true;
|
||||
// @ts-ignore
|
||||
delete x.user_ids;
|
||||
});
|
||||
// @ts-ignore
|
||||
if (!x.author) x.author = { discriminator: "0000", username: "Deleted User", public_flags: 0n, avatar: null };
|
||||
|
||||
return x;
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
// TODO: config max upload size
|
||||
const messageUpload = multer({
|
||||
limits: {
|
||||
fileSize: 1024 * 1024 * 100,
|
||||
fields: 10,
|
||||
files: 1
|
||||
},
|
||||
storage: multer.memoryStorage()
|
||||
}); // max upload 50 mb
|
||||
|
||||
// TODO: dynamically change limit of MessageCreateSchema with config
|
||||
// TODO: check: sum of all characters in an embed structure must not exceed 6000 characters
|
||||
|
||||
// https://discord.com/developers/docs/resources/channel#create-message
|
||||
// TODO: text channel slowdown
|
||||
// TODO: trim and replace message content and every embed field
|
||||
// TODO: check allowed_mentions
|
||||
|
||||
// Send message
|
||||
router.post("/", messageUpload.single("file"), async (req: Request, res: Response) => {
|
||||
const { channel_id } = req.params;
|
||||
var body = req.body as MessageCreateSchema;
|
||||
const attachments: Attachment[] = [];
|
||||
|
||||
if (req.file) {
|
||||
try {
|
||||
const file = await uploadFile(`/attachments/${channel_id}`, req.file);
|
||||
attachments.push({ ...file, proxy_url: file.url });
|
||||
} catch (error) {
|
||||
return res.status(400).json(error);
|
||||
}
|
||||
}
|
||||
|
||||
if (body.payload_json) {
|
||||
body = JSON.parse(body.payload_json);
|
||||
}
|
||||
|
||||
const errors = instanceOf(MessageCreateSchema, body, { req });
|
||||
if (errors !== true) throw errors;
|
||||
|
||||
const embeds = [];
|
||||
if (body.embed) embeds.push(body.embed);
|
||||
const data = await sendMessage({
|
||||
...body,
|
||||
type: 0,
|
||||
pinned: false,
|
||||
author_id: req.user_id,
|
||||
embeds,
|
||||
channel_id,
|
||||
attachments,
|
||||
edited_timestamp: null
|
||||
});
|
||||
|
||||
return res.send(data);
|
||||
});
|
||||
@@ -0,0 +1,72 @@
|
||||
import { ChannelModel, ChannelPermissionOverwrite, ChannelUpdateEvent, getPermission, MemberModel, RoleModel } from "@fosscord/server-util";
|
||||
import { Router, Response, Request } from "express";
|
||||
import { HTTPError } from "lambert-server";
|
||||
import { emitEvent } from "../../../util/Event";
|
||||
import { check } from "../../../util/instanceOf";
|
||||
const router: Router = Router();
|
||||
|
||||
// TODO: Only permissions your bot has in the guild or channel can be allowed/denied (unless your bot has a MANAGE_ROLES overwrite in the channel)
|
||||
|
||||
router.put("/:overwrite_id", check({ allow: String, deny: String, type: Number, id: String }), async (req: Request, res: Response) => {
|
||||
const { channel_id, overwrite_id } = req.params;
|
||||
const body = req.body as { allow: bigint; deny: bigint; type: number; id: string };
|
||||
|
||||
var channel = await ChannelModel.findOne({ id: channel_id }, { guild_id: true, permission_overwrites: true }).exec();
|
||||
if (!channel.guild_id) throw new HTTPError("Channel not found", 404);
|
||||
|
||||
const permissions = await getPermission(req.user_id, channel.guild_id, channel_id);
|
||||
permissions.hasThrow("MANAGE_ROLES");
|
||||
|
||||
if (body.type === 0) {
|
||||
if (!(await RoleModel.exists({ id: overwrite_id }))) throw new HTTPError("role not found", 404);
|
||||
} else if (body.type === 1) {
|
||||
if (!(await MemberModel.exists({ id: overwrite_id }))) throw new HTTPError("user not found", 404);
|
||||
} else throw new HTTPError("type not supported", 501);
|
||||
|
||||
// @ts-ignore
|
||||
var overwrite: ChannelPermissionOverwrite = channel.permission_overwrites.find((x) => x.id === overwrite_id);
|
||||
if (!overwrite) {
|
||||
// @ts-ignore
|
||||
overwrite = {
|
||||
id: overwrite_id,
|
||||
type: body.type,
|
||||
allow: body.allow,
|
||||
deny: body.deny
|
||||
};
|
||||
channel.permission_overwrites.push(overwrite);
|
||||
}
|
||||
overwrite.allow = body.allow;
|
||||
overwrite.deny = body.deny;
|
||||
|
||||
// @ts-ignore
|
||||
channel = await ChannelModel.findOneAndUpdate({ id: channel_id }, channel).exec();
|
||||
|
||||
await emitEvent({
|
||||
event: "CHANNEL_UPDATE",
|
||||
channel_id,
|
||||
data: channel
|
||||
} as ChannelUpdateEvent);
|
||||
|
||||
return res.sendStatus(204);
|
||||
});
|
||||
|
||||
// TODO: check permission hierarchy
|
||||
router.delete("/:overwrite_id", async (req: Request, res: Response) => {
|
||||
const { channel_id, overwrite_id } = req.params;
|
||||
|
||||
const permissions = await getPermission(req.user_id, undefined, channel_id);
|
||||
permissions.hasThrow("MANAGE_ROLES");
|
||||
|
||||
const channel = await ChannelModel.findOneAndUpdate({ id: channel_id }, { $pull: { permission_overwrites: { id: overwrite_id } } });
|
||||
if (!channel.guild_id) throw new HTTPError("Channel not found", 404);
|
||||
|
||||
await emitEvent({
|
||||
event: "CHANNEL_UPDATE",
|
||||
channel_id,
|
||||
data: channel
|
||||
} as ChannelUpdateEvent);
|
||||
|
||||
return res.sendStatus(204);
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,93 @@
|
||||
import {
|
||||
ChannelModel,
|
||||
ChannelPinsUpdateEvent,
|
||||
Config,
|
||||
getPermission,
|
||||
MessageModel,
|
||||
MessageUpdateEvent,
|
||||
toObject
|
||||
} from "@fosscord/server-util";
|
||||
import { Router, Request, Response } from "express";
|
||||
import { HTTPError } from "lambert-server";
|
||||
import { emitEvent } from "../../../util/Event";
|
||||
|
||||
const router: Router = Router();
|
||||
|
||||
router.put("/:message_id", async (req: Request, res: Response) => {
|
||||
const { channel_id, message_id } = req.params;
|
||||
const channel = await ChannelModel.findOne({ id: channel_id }).exec();
|
||||
const permission = await getPermission(req.user_id, channel.guild_id, channel_id);
|
||||
permission.hasThrow("VIEW_CHANNEL");
|
||||
|
||||
// * in dm channels anyone can pin messages -> only check for guilds
|
||||
if (channel.guild_id) permission.hasThrow("MANAGE_MESSAGES");
|
||||
|
||||
const pinned_count = await MessageModel.count({ channel_id, pinned: true }).exec();
|
||||
const { maxPins } = Config.get().limits.channel;
|
||||
if (pinned_count >= maxPins) throw new HTTPError("Max pin count reached: " + maxPins);
|
||||
|
||||
await MessageModel.updateOne({ id: message_id }, { pinned: true }).exec();
|
||||
const message = toObject(await MessageModel.findOne({ id: message_id }).exec());
|
||||
|
||||
await emitEvent({
|
||||
event: "MESSAGE_UPDATE",
|
||||
channel_id,
|
||||
data: message
|
||||
} as MessageUpdateEvent);
|
||||
|
||||
await emitEvent({
|
||||
event: "CHANNEL_PINS_UPDATE",
|
||||
channel_id,
|
||||
data: {
|
||||
channel_id,
|
||||
guild_id: channel.guild_id,
|
||||
last_pin_timestamp: undefined
|
||||
}
|
||||
} as ChannelPinsUpdateEvent);
|
||||
|
||||
res.sendStatus(204);
|
||||
});
|
||||
|
||||
router.delete("/:message_id", async (req: Request, res: Response) => {
|
||||
const { channel_id, message_id } = req.params;
|
||||
|
||||
const channel = await ChannelModel.findOne({ id: channel_id }).exec();
|
||||
|
||||
const permission = await getPermission(req.user_id, channel.guild_id, channel_id);
|
||||
permission.hasThrow("VIEW_CHANNEL");
|
||||
if (channel.guild_id) permission.hasThrow("MANAGE_MESSAGES");
|
||||
|
||||
const message = toObject(await MessageModel.findOneAndUpdate({ id: message_id }, { pinned: false }).exec());
|
||||
|
||||
await emitEvent({
|
||||
event: "MESSAGE_UPDATE",
|
||||
channel_id,
|
||||
data: message
|
||||
} as MessageUpdateEvent);
|
||||
|
||||
await emitEvent({
|
||||
event: "CHANNEL_PINS_UPDATE",
|
||||
channel_id,
|
||||
data: {
|
||||
channel_id,
|
||||
guild_id: channel.guild_id,
|
||||
last_pin_timestamp: undefined
|
||||
}
|
||||
} as ChannelPinsUpdateEvent);
|
||||
|
||||
res.sendStatus(204);
|
||||
});
|
||||
|
||||
router.get("/", async (req: Request, res: Response) => {
|
||||
const { channel_id } = req.params;
|
||||
|
||||
const channel = await ChannelModel.findOne({ id: channel_id }).exec();
|
||||
const permission = await getPermission(req.user_id, channel.guild_id, channel_id);
|
||||
permission.hasThrow("VIEW_CHANNEL");
|
||||
|
||||
let pins = await MessageModel.find({ channel_id: channel_id, pinned: true }).exec();
|
||||
|
||||
res.send(toObject(pins));
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,5 @@
|
||||
import { Router, Response, Request } from "express";
|
||||
const router: Router = Router();
|
||||
// TODO:
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,31 @@
|
||||
import { ChannelModel, MemberModel, toObject, TypingStartEvent } from "@fosscord/server-util";
|
||||
import { Router, Request, Response } from "express";
|
||||
|
||||
import { HTTPError } from "lambert-server";
|
||||
import { emitEvent } from "../../../util/Event";
|
||||
|
||||
const router: Router = Router();
|
||||
|
||||
router.post("/", async (req: Request, res: Response) => {
|
||||
const { channel_id } = req.params;
|
||||
const user_id = req.user_id;
|
||||
const timestamp = Date.now();
|
||||
const channel = await ChannelModel.findOne({ id: channel_id });
|
||||
const member = await MemberModel.findOne({ id: user_id }).exec();
|
||||
|
||||
await emitEvent({
|
||||
event: "TYPING_START",
|
||||
channel_id: channel_id,
|
||||
data: {
|
||||
// this is the paylod
|
||||
member: toObject(member),
|
||||
channel_id,
|
||||
timestamp,
|
||||
user_id,
|
||||
guild_id: channel.guild_id
|
||||
}
|
||||
} as TypingStartEvent);
|
||||
res.sendStatus(204);
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Router, Response, Request } from "express";
|
||||
import { check, Length } from "../../../util/instanceOf";
|
||||
import { ChannelModel, getPermission, trimSpecial } from "@fosscord/server-util";
|
||||
import { HTTPError } from "lambert-server";
|
||||
import { isTextChannel } from "./messages/index";
|
||||
|
||||
const router: Router = Router();
|
||||
// TODO:
|
||||
|
||||
// TODO: use Image Data Type for avatar instead of String
|
||||
router.post("/", check({ name: new Length(String, 1, 80), $avatar: String }), async (req: Request, res: Response) => {
|
||||
const channel_id = req.params.channel_id;
|
||||
const channel = await ChannelModel.findOne({ id: channel_id }, { guild_id: true, type: true }).exec();
|
||||
|
||||
isTextChannel(channel.type);
|
||||
if (!channel.guild_id) throw new HTTPError("Not a guild channel", 400);
|
||||
|
||||
const permission = await getPermission(req.user_id, channel.guild_id);
|
||||
permission.hasThrow("MANAGE_WEBHOOKS");
|
||||
|
||||
var { avatar, name } = req.body as { name: string; avatar?: string };
|
||||
name = trimSpecial(name);
|
||||
if (name === "clyde") throw new HTTPError("Invalid name", 400);
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Router, Response, Request } from "express";
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get("/", (req: Request, res: Response) => {
|
||||
// TODO:
|
||||
res.send({ fingerprint: "", assignments: [] });
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Config } from "@fosscord/server-util";
|
||||
import { Router, Response, Request } from "express";
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get("/", (req: Request, res: Response) => {
|
||||
const { endpoint } = Config.get().gateway;
|
||||
res.json({ url: endpoint || process.env.GATEWAY || "ws://localhost:3002" });
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,90 @@
|
||||
import { Request, Response, Router } from "express";
|
||||
import { BanModel, getPermission, GuildBanAddEvent, GuildBanRemoveEvent, GuildModel, toObject } from "@fosscord/server-util";
|
||||
import { HTTPError } from "lambert-server";
|
||||
import { getIpAdress } from "../../../util/ipAddress";
|
||||
import { BanCreateSchema } from "../../../schema/Ban";
|
||||
import { emitEvent } from "../../../util/Event";
|
||||
import { check } from "../../../util/instanceOf";
|
||||
import { removeMember } from "../../../util/Member";
|
||||
import { getPublicUser } from "../../../util/User";
|
||||
|
||||
const router: Router = Router();
|
||||
|
||||
router.get("/", async (req: Request, res: Response) => {
|
||||
const { guild_id } = req.params;
|
||||
|
||||
const guild = await GuildModel.exists({ id: guild_id });
|
||||
if (!guild) throw new HTTPError("Guild not found", 404);
|
||||
|
||||
var bans = await BanModel.find({ guild_id: guild_id }, { user_id: true, reason: true }).exec();
|
||||
return res.json(toObject(bans));
|
||||
});
|
||||
|
||||
router.get("/:user", async (req: Request, res: Response) => {
|
||||
const { guild_id } = req.params;
|
||||
const user_id = req.params.ban;
|
||||
|
||||
var ban = await BanModel.findOne({ guild_id: guild_id, user_id: user_id }).exec();
|
||||
return res.json(ban);
|
||||
});
|
||||
|
||||
router.put("/:user_id", check(BanCreateSchema), async (req: Request, res: Response) => {
|
||||
const { guild_id } = req.params;
|
||||
const banned_user_id = req.params.user_id;
|
||||
|
||||
const banned_user = await getPublicUser(banned_user_id);
|
||||
const perms = await getPermission(req.user_id, guild_id);
|
||||
perms.hasThrow("BAN_MEMBERS");
|
||||
if (req.user_id === banned_user_id) throw new HTTPError("You can't ban yourself", 400);
|
||||
|
||||
await removeMember(banned_user_id, guild_id);
|
||||
|
||||
const ban = await new BanModel({
|
||||
user_id: banned_user_id,
|
||||
guild_id: guild_id,
|
||||
ip: getIpAdress(req),
|
||||
executor_id: req.user_id,
|
||||
reason: req.body.reason // || otherwise empty
|
||||
}).save();
|
||||
|
||||
await emitEvent({
|
||||
event: "GUILD_BAN_ADD",
|
||||
data: {
|
||||
guild_id: guild_id,
|
||||
user: banned_user
|
||||
},
|
||||
guild_id: guild_id
|
||||
} as GuildBanAddEvent);
|
||||
|
||||
return res.json(toObject(ban));
|
||||
});
|
||||
|
||||
router.delete("/:user_id", async (req: Request, res: Response) => {
|
||||
var { guild_id } = req.params;
|
||||
var banned_user_id = req.params.user_id;
|
||||
|
||||
const banned_user = await getPublicUser(banned_user_id);
|
||||
const guild = await GuildModel.exists({ id: guild_id });
|
||||
if (!guild) throw new HTTPError("Guild not found", 404);
|
||||
|
||||
const perms = await getPermission(req.user_id, guild_id);
|
||||
perms.hasThrow("BAN_MEMBERS");
|
||||
|
||||
await BanModel.deleteOne({
|
||||
user_id: banned_user_id,
|
||||
guild_id
|
||||
}).exec();
|
||||
|
||||
await emitEvent({
|
||||
event: "GUILD_BAN_REMOVE",
|
||||
data: {
|
||||
guild_id,
|
||||
user: banned_user
|
||||
},
|
||||
guild_id
|
||||
} as GuildBanRemoveEvent);
|
||||
|
||||
return res.status(204).send();
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,73 @@
|
||||
import { Router, Response, Request } from "express";
|
||||
import {
|
||||
ChannelCreateEvent,
|
||||
ChannelModel,
|
||||
ChannelType,
|
||||
GuildModel,
|
||||
Snowflake,
|
||||
toObject,
|
||||
ChannelUpdateEvent,
|
||||
AnyChannel,
|
||||
getPermission
|
||||
} from "@fosscord/server-util";
|
||||
import { HTTPError } from "lambert-server";
|
||||
import { ChannelModifySchema } from "../../../schema/Channel";
|
||||
import { emitEvent } from "../../../util/Event";
|
||||
import { check } from "../../../util/instanceOf";
|
||||
import { createChannel } from "../../../util/Channel";
|
||||
const router = Router();
|
||||
|
||||
router.get("/", async (req: Request, res: Response) => {
|
||||
const { guild_id } = req.params;
|
||||
const channels = await ChannelModel.find({ guild_id }).exec();
|
||||
|
||||
res.json(toObject(channels));
|
||||
});
|
||||
|
||||
// TODO: check if channel type is permitted
|
||||
// TODO: check if parent_id exists
|
||||
|
||||
router.post("/", check(ChannelModifySchema), async (req: Request, res: Response) => {
|
||||
// creates a new guild channel https://discord.com/developers/docs/resources/guild#create-guild-channel
|
||||
const { guild_id } = req.params;
|
||||
const body = req.body as ChannelModifySchema;
|
||||
|
||||
const channel = await createChannel({ ...body, guild_id }, req.user_id);
|
||||
|
||||
res.json(toObject(channel));
|
||||
});
|
||||
|
||||
// TODO: check if parent_id exists
|
||||
router.patch(
|
||||
"/",
|
||||
check([{ id: String, $position: Number, $lock_permissions: Boolean, $parent_id: String }]),
|
||||
async (req: Request, res: Response) => {
|
||||
// changes guild channel position
|
||||
const { guild_id } = req.params;
|
||||
const body = req.body as { id: string; position?: number; lock_permissions?: boolean; parent_id?: string };
|
||||
body.position = Math.floor(body.position || 0);
|
||||
if (!body.position && !body.parent_id) throw new HTTPError(`You need to at least specify position or parent_id`, 400);
|
||||
|
||||
const permission = await getPermission(req.user_id, guild_id);
|
||||
permission.hasThrow("MANAGE_CHANNELS");
|
||||
|
||||
const opts: any = {};
|
||||
if (body.position) opts.position = body.position;
|
||||
|
||||
if (body.parent_id) {
|
||||
opts.parent_id = body.parent_id;
|
||||
const parent_channel = await ChannelModel.findOne({ id: body.parent_id, guild_id }, { permission_overwrites: true }).exec();
|
||||
if (body.lock_permissions) {
|
||||
opts.permission_overwrites = parent_channel.permission_overwrites;
|
||||
}
|
||||
}
|
||||
|
||||
const channel = await ChannelModel.findOneAndUpdate({ id: req.body, guild_id }, opts).exec();
|
||||
|
||||
await emitEvent({ event: "CHANNEL_UPDATE", data: channel, channel_id: body.id, guild_id } as ChannelUpdateEvent);
|
||||
|
||||
res.json(toObject(channel));
|
||||
}
|
||||
);
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,48 @@
|
||||
import {
|
||||
ChannelModel,
|
||||
EmojiModel,
|
||||
GuildDeleteEvent,
|
||||
GuildModel,
|
||||
InviteModel,
|
||||
MemberModel,
|
||||
MessageModel,
|
||||
RoleModel,
|
||||
UserModel
|
||||
} from "@fosscord/server-util";
|
||||
import { Router, Request, Response } from "express";
|
||||
import { HTTPError } from "lambert-server";
|
||||
import { emitEvent } from "../../../util/Event";
|
||||
|
||||
const router = Router();
|
||||
|
||||
// discord prefixes this route with /delete instead of using the delete method
|
||||
// docs are wrong https://discord.com/developers/docs/resources/guild#delete-guild
|
||||
router.post("/", async (req: Request, res: Response) => {
|
||||
var { guild_id } = req.params;
|
||||
|
||||
const guild = await GuildModel.findOne({ id: guild_id }, "owner_id").exec();
|
||||
if (guild.owner_id !== req.user_id) throw new HTTPError("You are not the owner of this guild", 401);
|
||||
|
||||
await emitEvent({
|
||||
event: "GUILD_DELETE",
|
||||
data: {
|
||||
id: guild_id
|
||||
},
|
||||
guild_id: guild_id
|
||||
} as GuildDeleteEvent);
|
||||
|
||||
await Promise.all([
|
||||
GuildModel.deleteOne({ id: guild_id }).exec(),
|
||||
UserModel.updateMany({ guilds: guild_id }, { $pull: { guilds: guild_id } }).exec(),
|
||||
RoleModel.deleteMany({ guild_id }).exec(),
|
||||
ChannelModel.deleteMany({ guild_id }).exec(),
|
||||
EmojiModel.deleteMany({ guild_id }).exec(),
|
||||
InviteModel.deleteMany({ guild_id }).exec(),
|
||||
MessageModel.deleteMany({ guild_id }).exec(),
|
||||
MemberModel.deleteMany({ guild_id }).exec()
|
||||
]);
|
||||
|
||||
return res.sendStatus(204);
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,61 @@
|
||||
import { Request, Response, Router } from "express";
|
||||
import {
|
||||
ChannelModel,
|
||||
EmojiModel,
|
||||
getPermission,
|
||||
GuildDeleteEvent,
|
||||
GuildModel,
|
||||
GuildUpdateEvent,
|
||||
InviteModel,
|
||||
MemberModel,
|
||||
MessageModel,
|
||||
RoleModel,
|
||||
toObject,
|
||||
UserModel
|
||||
} from "@fosscord/server-util";
|
||||
import { HTTPError } from "lambert-server";
|
||||
import { GuildUpdateSchema } from "../../../schema/Guild";
|
||||
import { emitEvent } from "../../../util/Event";
|
||||
import { check } from "../../../util/instanceOf";
|
||||
import { handleFile } from "../../../util/cdn";
|
||||
import "missing-native-js-functions";
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get("/", async (req: Request, res: Response) => {
|
||||
const { guild_id } = req.params;
|
||||
|
||||
const guild = await GuildModel.findOne({ id: guild_id })
|
||||
.populate({ path: "joined_at", match: { id: req.user_id } })
|
||||
.exec();
|
||||
|
||||
const member = await MemberModel.exists({ guild_id: guild_id, id: req.user_id });
|
||||
if (!member) throw new HTTPError("You are not a member of the guild you are trying to access", 401);
|
||||
|
||||
return res.json(guild);
|
||||
});
|
||||
|
||||
router.patch("/", check(GuildUpdateSchema), async (req: Request, res: Response) => {
|
||||
const body = req.body as GuildUpdateSchema;
|
||||
const { guild_id } = req.params;
|
||||
// TODO: guild update check image
|
||||
|
||||
const perms = await getPermission(req.user_id, guild_id);
|
||||
perms.hasThrow("MANAGE_GUILD");
|
||||
|
||||
if (body.icon) body.icon = await handleFile(`/icons/${guild_id}`, body.icon);
|
||||
if (body.banner) body.banner = await handleFile(`/banners/${guild_id}`, body.banner);
|
||||
if (body.splash) body.splash = await handleFile(`/splashes/${guild_id}`, body.splash);
|
||||
|
||||
const guild = await GuildModel.findOneAndUpdate({ id: guild_id }, body)
|
||||
.populate({ path: "joined_at", match: { id: req.user_id } })
|
||||
.exec();
|
||||
|
||||
const data = toObject(guild);
|
||||
|
||||
emitEvent({ event: "GUILD_UPDATE", data: data, guild_id } as GuildUpdateEvent);
|
||||
|
||||
return res.json(data);
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,17 @@
|
||||
import { getPermission, InviteModel, toObject } from "@fosscord/server-util";
|
||||
import { Request, Response, Router } from "express";
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get("/", async (req: Request, res: Response) => {
|
||||
const { guild_id } = req.params;
|
||||
|
||||
const permissions = await getPermission(req.user_id, guild_id);
|
||||
permissions.hasThrow("MANAGE_GUILD");
|
||||
|
||||
const invites = await InviteModel.find({ guild_id }).exec();
|
||||
|
||||
return res.json(toObject(invites));
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,69 @@
|
||||
import { Request, Response, Router } from "express";
|
||||
import {
|
||||
GuildModel,
|
||||
MemberModel,
|
||||
UserModel,
|
||||
toObject,
|
||||
GuildMemberAddEvent,
|
||||
getPermission,
|
||||
PermissionResolvable,
|
||||
RoleModel,
|
||||
GuildMemberUpdateEvent
|
||||
} from "@fosscord/server-util";
|
||||
import { HTTPError } from "lambert-server";
|
||||
import { addMember, isMember, removeMember } from "../../../../../util/Member";
|
||||
import { check } from "../../../../../util/instanceOf";
|
||||
import { MemberChangeSchema } from "../../../../../schema/Member";
|
||||
import { emitEvent } from "../../../../../util/Event";
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get("/", async (req: Request, res: Response) => {
|
||||
const { guild_id, member_id } = req.params;
|
||||
await isMember(req.user_id, guild_id);
|
||||
|
||||
const member = await MemberModel.findOne({ id: member_id, guild_id }).exec();
|
||||
|
||||
return res.json(toObject(member));
|
||||
});
|
||||
|
||||
router.patch("/", check(MemberChangeSchema), async (req: Request, res: Response) => {
|
||||
const { guild_id, member_id } = req.params;
|
||||
const body = req.body as MemberChangeSchema;
|
||||
if (body.roles) {
|
||||
const roles = await RoleModel.find({ id: { $in: body.roles } }).exec();
|
||||
if (body.roles.length !== roles.length) throw new HTTPError("Roles not found", 404);
|
||||
// TODO: check if user has permission to add role
|
||||
}
|
||||
|
||||
const member = await MemberModel.findOneAndUpdate({ id: member_id, guild_id }, body).exec();
|
||||
|
||||
await emitEvent({
|
||||
event: "GUILD_MEMBER_UPDATE",
|
||||
guild_id,
|
||||
data: toObject(member)
|
||||
} as GuildMemberUpdateEvent);
|
||||
|
||||
res.json(toObject(member));
|
||||
});
|
||||
|
||||
router.put("/", async (req: Request, res: Response) => {
|
||||
const { guild_id, member_id } = req.params;
|
||||
|
||||
throw new HTTPError("Maintenance: Currently you can't add a member", 403);
|
||||
// TODO: only for oauth2 applications
|
||||
await addMember(member_id, guild_id);
|
||||
res.sendStatus(204);
|
||||
});
|
||||
|
||||
router.delete("/", async (req: Request, res: Response) => {
|
||||
const { guild_id, member_id } = req.params;
|
||||
|
||||
const perms = await getPermission(req.user_id, guild_id);
|
||||
perms.hasThrow("KICK_MEMBERS");
|
||||
|
||||
await removeMember(member_id, guild_id);
|
||||
res.sendStatus(204);
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,24 @@
|
||||
import { getPermission, PermissionResolvable } from "@fosscord/server-util";
|
||||
import { Request, Response, Router } from "express";
|
||||
import { check } from "lambert-server";
|
||||
import { MemberNickChangeSchema } from "../../../../../schema/Member";
|
||||
import { changeNickname } from "../../../../../util/Member";
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.patch("/", check(MemberNickChangeSchema), async (req: Request, res: Response) => {
|
||||
var { guild_id, member_id } = req.params;
|
||||
var permissionString: PermissionResolvable = "MANAGE_NICKNAMES";
|
||||
if (member_id === "@me") {
|
||||
member_id = req.user_id;
|
||||
permissionString = "CHANGE_NICKNAME";
|
||||
}
|
||||
|
||||
const perms = await getPermission(req.user_id, guild_id);
|
||||
perms.hasThrow(permissionString);
|
||||
|
||||
await changeNickname(member_id, guild_id, req.body.nickname);
|
||||
res.status(204);
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,27 @@
|
||||
import { getPermission } from "@fosscord/server-util";
|
||||
import { Request, Response, Router } from "express";
|
||||
import { addRole, removeRole } from "../../../../../../../util/Member";
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.delete("/:member_id/roles/:role_id", async (req: Request, res: Response) => {
|
||||
const { guild_id, role_id, member_id } = req.params;
|
||||
|
||||
const perms = await getPermission(req.user_id, guild_id);
|
||||
perms.hasThrow("MANAGE_ROLES");
|
||||
|
||||
await removeRole(member_id, guild_id, role_id);
|
||||
res.sendStatus(204);
|
||||
});
|
||||
|
||||
router.put("/:member_id/roles/:role_id", async (req: Request, res: Response) => {
|
||||
const { guild_id, role_id, member_id } = req.params;
|
||||
|
||||
const perms = await getPermission(req.user_id, guild_id);
|
||||
perms.hasThrow("MANAGE_ROLES");
|
||||
|
||||
await addRole(member_id, guild_id, role_id);
|
||||
res.sendStatus(204);
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,38 @@
|
||||
import { Request, Response, Router } from "express";
|
||||
import { GuildModel, MemberModel, toObject } from "@fosscord/server-util";
|
||||
import { HTTPError } from "lambert-server";
|
||||
import { instanceOf, Length } from "../../../../util/instanceOf";
|
||||
import { PublicMemberProjection, isMember } from "../../../../util/Member";
|
||||
|
||||
const router = Router();
|
||||
|
||||
// TODO: not allowed for user -> only allowed for bots with privileged intents
|
||||
// TODO: send over websocket
|
||||
router.get("/", async (req: Request, res: Response) => {
|
||||
const { guild_id } = req.params;
|
||||
const guild = await GuildModel.findOne({ id: guild_id }).exec();
|
||||
await isMember(req.user_id, guild_id);
|
||||
|
||||
try {
|
||||
instanceOf({ $limit: new Length(Number, 1, 1000), $after: String }, req.query, {
|
||||
path: "query",
|
||||
req,
|
||||
ref: { obj: null, key: "" }
|
||||
});
|
||||
} catch (error) {
|
||||
return res.status(400).json({ code: 50035, message: "Invalid Query", success: false, errors: error });
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
if (!req.query.limit) req.query.limit = 1;
|
||||
const { limit, after } = (<unknown>req.query) as { limit: number; after: string };
|
||||
const query = after ? { id: { $gt: after } } : {};
|
||||
|
||||
var members = await MemberModel.find({ guild_id, ...query }, PublicMemberProjection)
|
||||
.limit(limit)
|
||||
.exec();
|
||||
|
||||
return res.json(toObject(members));
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Config } from "@fosscord/server-util";
|
||||
import { Request, Response, Router } from "express";
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get("/", async (req: Request, res: Response) => {
|
||||
return res.json(Config.get().regions.available);
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,128 @@
|
||||
import { Request, Response, Router } from "express";
|
||||
import {
|
||||
RoleModel,
|
||||
GuildModel,
|
||||
getPermission,
|
||||
toObject,
|
||||
UserModel,
|
||||
Snowflake,
|
||||
MemberModel,
|
||||
GuildRoleCreateEvent,
|
||||
GuildRoleUpdateEvent,
|
||||
GuildRoleDeleteEvent
|
||||
} from "@fosscord/server-util";
|
||||
import { HTTPError } from "lambert-server";
|
||||
import { emitEvent } from "../../../util/Event";
|
||||
import { check } from "../../../util/instanceOf";
|
||||
import { RoleModifySchema } from "../../../schema/Roles";
|
||||
import { getPublicUser } from "../../../util/User";
|
||||
import { isMember } from "../../../util/Member";
|
||||
|
||||
const router: Router = Router();
|
||||
|
||||
router.get("/", async (req: Request, res: Response) => {
|
||||
const guild_id = req.params.guild_id;
|
||||
|
||||
await isMember(req.user_id, guild_id);
|
||||
|
||||
const roles = await RoleModel.find({ guild_id: guild_id }).exec();
|
||||
|
||||
return res.json(toObject(roles));
|
||||
});
|
||||
|
||||
router.post("/", check(RoleModifySchema), async (req: Request, res: Response) => {
|
||||
const guild_id = req.params.guild_id;
|
||||
const body = req.body as RoleModifySchema;
|
||||
|
||||
const guild = await GuildModel.findOne({ id: guild_id }, { id: true }).exec();
|
||||
const user = await UserModel.findOne({ id: req.user_id }).exec();
|
||||
|
||||
const perms = await getPermission(req.user_id, guild_id);
|
||||
perms.hasThrow("MANAGE_ROLES");
|
||||
if (!body.name) throw new HTTPError("You need to specify a name");
|
||||
|
||||
const role = await new RoleModel({
|
||||
...body,
|
||||
id: Snowflake.generate(),
|
||||
guild_id: guild_id,
|
||||
managed: false,
|
||||
position: 0,
|
||||
tags: null,
|
||||
permissions: body.permissions || 0n
|
||||
}).save();
|
||||
|
||||
await emitEvent({
|
||||
event: "GUILD_ROLE_CREATE",
|
||||
guild_id,
|
||||
data: {
|
||||
guild_id,
|
||||
role: toObject(role)
|
||||
}
|
||||
} as GuildRoleCreateEvent);
|
||||
|
||||
res.json(toObject(role));
|
||||
});
|
||||
|
||||
router.delete("/:role_id", async (req: Request, res: Response) => {
|
||||
const guild_id = req.params.guild_id;
|
||||
const { role_id } = req.params;
|
||||
|
||||
const guild = await GuildModel.findOne({ id: guild_id }, { id: true }).exec();
|
||||
const user = await UserModel.findOne({ id: req.user_id }).exec();
|
||||
|
||||
const perms = await getPermission(req.user_id, guild_id);
|
||||
|
||||
if (!perms.has("MANAGE_ROLES")) throw new HTTPError("You missing the MANAGE_ROLES permission", 401);
|
||||
|
||||
await RoleModel.findOneAndDelete({
|
||||
id: role_id,
|
||||
guild_id: guild_id
|
||||
}).exec();
|
||||
|
||||
await emitEvent({
|
||||
event: "GUILD_ROLE_DELETE",
|
||||
guild_id,
|
||||
data: {
|
||||
guild_id,
|
||||
role_id
|
||||
}
|
||||
} as GuildRoleDeleteEvent);
|
||||
|
||||
res.sendStatus(204);
|
||||
});
|
||||
|
||||
// TODO: check role hierarchy
|
||||
|
||||
router.patch("/:role_id", check(RoleModifySchema), async (req: Request, res: Response) => {
|
||||
const guild_id = req.params.guild_id;
|
||||
const { role_id } = req.params;
|
||||
const body = req.body as RoleModifySchema;
|
||||
|
||||
const guild = await GuildModel.findOne({ id: guild_id }, { id: true }).exec();
|
||||
const user = await UserModel.findOne({ id: req.user_id }).exec();
|
||||
|
||||
const perms = await getPermission(req.user_id, guild_id);
|
||||
perms.hasThrow("MANAGE_ROLES");
|
||||
|
||||
const role = await RoleModel.findOneAndUpdate(
|
||||
{
|
||||
id: role_id,
|
||||
guild_id: guild_id
|
||||
},
|
||||
// @ts-ignore
|
||||
body
|
||||
).exec();
|
||||
|
||||
await emitEvent({
|
||||
event: "GUILD_ROLE_UPDATE",
|
||||
guild_id,
|
||||
data: {
|
||||
guild_id,
|
||||
role
|
||||
}
|
||||
} as GuildRoleUpdateEvent);
|
||||
|
||||
res.json(toObject(role));
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,99 @@
|
||||
import { Request, Response, Router } from "express";
|
||||
import { TemplateModel, GuildModel, getPermission, toObject, UserModel, Snowflake } from "@fosscord/server-util";
|
||||
import { HTTPError } from "lambert-server";
|
||||
import { TemplateCreateSchema, TemplateModifySchema } from "../../../schema/Template";
|
||||
import { check } from "../../../util/instanceOf";
|
||||
import { generateCode } from "../../../util/String";
|
||||
|
||||
const router: Router = Router();
|
||||
|
||||
const TemplateGuildProjection = {
|
||||
name: true,
|
||||
description: true,
|
||||
region: true,
|
||||
verification_level: true,
|
||||
default_message_notifications: true,
|
||||
explicit_content_filter: true,
|
||||
preferred_locale: true,
|
||||
afk_timeout: true,
|
||||
roles: true,
|
||||
channels: true,
|
||||
afk_channel_id: true,
|
||||
system_channel_id: true,
|
||||
system_channel_flags: true,
|
||||
icon_hash: true
|
||||
};
|
||||
|
||||
router.get("/", async (req: Request, res: Response) => {
|
||||
const { guild_id } = req.params;
|
||||
|
||||
var templates = await TemplateModel.find({ source_guild_id: guild_id }).exec();
|
||||
|
||||
return res.json(toObject(templates));
|
||||
});
|
||||
|
||||
router.post("/", check(TemplateCreateSchema), async (req: Request, res: Response) => {
|
||||
const { guild_id } = req.params;
|
||||
const guild = await GuildModel.findOne({ id: guild_id }, TemplateGuildProjection).exec();
|
||||
const perms = await getPermission(req.user_id, guild_id);
|
||||
perms.hasThrow("MANAGE_GUILD");
|
||||
|
||||
const exists = await TemplateModel.findOne({ id: guild_id })
|
||||
.exec()
|
||||
.catch((e) => {});
|
||||
if (exists) throw new HTTPError("Template already exists", 400);
|
||||
|
||||
const template = await new TemplateModel({
|
||||
...req.body,
|
||||
code: generateCode(),
|
||||
creator_id: req.user_id,
|
||||
created_at: new Date(),
|
||||
updated_at: new Date(),
|
||||
source_guild_id: guild_id,
|
||||
serialized_source_guild: guild
|
||||
}).save();
|
||||
|
||||
res.json(toObject(template)).send();
|
||||
});
|
||||
|
||||
router.delete("/:code", async (req: Request, res: Response) => {
|
||||
const guild_id = req.params.guild_id;
|
||||
const { code } = req.params;
|
||||
|
||||
const perms = await getPermission(req.user_id, guild_id);
|
||||
perms.hasThrow("MANAGE_GUILD");
|
||||
|
||||
const template = await TemplateModel.findOneAndDelete({
|
||||
code
|
||||
}).exec();
|
||||
|
||||
res.send(toObject(template));
|
||||
});
|
||||
|
||||
router.put("/:code", async (req: Request, res: Response) => {
|
||||
const guild_id = req.params.guild_id;
|
||||
const { code } = req.params;
|
||||
|
||||
const guild = await GuildModel.findOne({ id: guild_id }, TemplateGuildProjection).exec();
|
||||
|
||||
const perms = await getPermission(req.user_id, guild_id);
|
||||
perms.hasThrow("MANAGE_GUILD");
|
||||
|
||||
const template = await TemplateModel.findOneAndUpdate({ code }, { serialized_source_guild: guild }).exec();
|
||||
|
||||
res.json(toObject(template)).send();
|
||||
});
|
||||
|
||||
router.patch("/:code", check(TemplateModifySchema), async (req: Request, res: Response) => {
|
||||
const { guild_id } = req.params;
|
||||
const { code } = req.params;
|
||||
|
||||
const perms = await getPermission(req.user_id, guild_id);
|
||||
perms.hasThrow("MANAGE_GUILD");
|
||||
|
||||
const template = await TemplateModel.findOneAndUpdate({ code }, { name: req.body.name, description: req.body.description }).exec();
|
||||
|
||||
res.json(toObject(template)).send();
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,45 @@
|
||||
import { getPermission, GuildModel, InviteModel, trimSpecial } from "@fosscord/server-util";
|
||||
import { Router, Request, Response } from "express";
|
||||
import { HTTPError } from "lambert-server";
|
||||
import { check, Length } from "../../../util/instanceOf";
|
||||
import { isMember } from "../../../util/Member";
|
||||
|
||||
const router = Router();
|
||||
|
||||
const InviteRegex = /\W/g;
|
||||
|
||||
router.get("/", async (req: Request, res: Response) => {
|
||||
const { guild_id } = req.params;
|
||||
|
||||
await isMember(req.user_id, guild_id);
|
||||
const guild = await GuildModel.findOne({ id: guild_id }).exec();
|
||||
if (!guild.vanity_url) throw new HTTPError("This guild has no vanity url", 204);
|
||||
|
||||
return res.json({ code: guild.vanity_url.code });
|
||||
});
|
||||
|
||||
// TODO: check if guild is elgible for vanity url
|
||||
router.patch("/", check({ code: new Length(String, 0, 20) }), async (req: Request, res: Response) => {
|
||||
const { guild_id } = req.params;
|
||||
var code = req.body.code.replace(InviteRegex);
|
||||
if (!code) code = null;
|
||||
|
||||
const permission = await getPermission(req.user_id, guild_id);
|
||||
permission.hasThrow("MANAGE_GUILD");
|
||||
|
||||
const alreadyExists = await Promise.all([
|
||||
GuildModel.findOne({ "vanity_url.code": code })
|
||||
.exec()
|
||||
.catch(() => null),
|
||||
InviteModel.findOne({ code: code })
|
||||
.exec()
|
||||
.catch(() => null)
|
||||
]);
|
||||
if (alreadyExists.some((x) => x)) throw new HTTPError("Vanity url already exists", 400);
|
||||
|
||||
await GuildModel.updateOne({ id: guild_id }, { "vanity_url.code": code }).exec();
|
||||
|
||||
return res.json({ code: code });
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,49 @@
|
||||
import { Request, Response, Router } from "express";
|
||||
import { GuildModel, getPermission, toObject, Snowflake } from "@fosscord/server-util";
|
||||
import { HTTPError } from "lambert-server";
|
||||
import { emitEvent } from "../../../util/Event";
|
||||
import { check } from "../../../util/instanceOf";
|
||||
import { isMember } from "../../../util/Member";
|
||||
import { GuildAddChannelToWelcomeScreenSchema } from "../../../schema/Guild";
|
||||
import { getPublicUser } from "../../../util/User";
|
||||
|
||||
const router: Router = Router();
|
||||
|
||||
router.get("/", async (req: Request, res: Response) => {
|
||||
const guild_id = req.params.guild_id;
|
||||
|
||||
const guild = await GuildModel.findOne({ id: guild_id });
|
||||
|
||||
await isMember(req.user_id, guild_id);
|
||||
|
||||
res.json(toObject(guild.welcome_screen));
|
||||
});
|
||||
|
||||
router.post("/", check(GuildAddChannelToWelcomeScreenSchema), async (req: Request, res: Response) => {
|
||||
const guild_id = req.params.guild_id;
|
||||
const body = req.body as GuildAddChannelToWelcomeScreenSchema;
|
||||
|
||||
const guild = await GuildModel.findOne({ id: guild_id }).exec();
|
||||
|
||||
var channelObject = {
|
||||
...body
|
||||
};
|
||||
|
||||
const perms = await getPermission(req.user_id, guild_id);
|
||||
perms.hasThrow("MANAGE_GUILD");
|
||||
|
||||
if (!guild.welcome_screen.enabled) throw new HTTPError("Welcome screen disabled", 400);
|
||||
if (guild.welcome_screen.welcome_channels.some((channel) => channel.channel_id === body.channel_id))
|
||||
throw new Error("Welcome Channel exists");
|
||||
|
||||
await GuildModel.findOneAndUpdate(
|
||||
{
|
||||
id: guild_id
|
||||
},
|
||||
{ $push: { "welcome_screen.welcome_channels": channelObject } }
|
||||
).exec();
|
||||
|
||||
res.sendStatus(204);
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,139 @@
|
||||
import { Request, Response, Router } from "express";
|
||||
import { Config, Permissions, GuildModel, InviteModel, ChannelModel, MemberModel } from "@fosscord/server-util";
|
||||
import { HTTPError } from "lambert-server";
|
||||
import { random } from "../../../util/RandomInviteID";
|
||||
|
||||
const router: Router = Router();
|
||||
|
||||
// Undocumented API notes:
|
||||
// An invite is created for the widget_channel_id on request (only if an existing one created by the widget doesn't already exist)
|
||||
// This invite created doesn't include an inviter object like user created ones and has a default expiry of 24 hours
|
||||
// Missing user object information is intentional (https://github.com/discord/discord-api-docs/issues/1287)
|
||||
// channels returns voice channel objects where @everyone has the CONNECT permission
|
||||
// members (max 100 returned) is a sample of all members, and bots par invisible status, there exists some alphabetical distribution pattern between the members returned
|
||||
|
||||
// https://discord.com/developers/docs/resources/guild#get-guild-widget
|
||||
// TODO: Cache the response for a guild for 5 minutes regardless of response
|
||||
router.get("/", async (req: Request, res: Response) => {
|
||||
const { guild_id } = req.params;
|
||||
|
||||
const guild = await GuildModel.findOne({ id: guild_id }).exec();
|
||||
if (!guild.widget_enabled) throw new HTTPError("Widget Disabled", 404);
|
||||
|
||||
// Fetch existing widget invite for widget channel
|
||||
var invite = await InviteModel.findOne({ channel_id: guild.widget_channel_id, inviter_id: { $type: 10 } }).exec();
|
||||
if (guild.widget_channel_id && !invite) {
|
||||
// Create invite for channel if none exists
|
||||
// TODO: Refactor invite create code to a shared function
|
||||
const max_age = 86400; // 24 hours
|
||||
const expires_at = new Date(max_age * 1000 + Date.now());
|
||||
const body = {
|
||||
code: random(),
|
||||
temporary: false,
|
||||
uses: 0,
|
||||
max_uses: 0,
|
||||
max_age: max_age,
|
||||
expires_at,
|
||||
created_at: new Date(),
|
||||
guild_id,
|
||||
channel_id: guild.widget_channel_id,
|
||||
inviter_id: null
|
||||
};
|
||||
|
||||
invite = await new InviteModel(body).save();
|
||||
}
|
||||
|
||||
// Fetch voice channels, and the @everyone permissions object
|
||||
let channels: any[] = [];
|
||||
await ChannelModel.find({ guild_id: guild_id, type: 2 }, { permission_overwrites: { $elemMatch: { id: guild_id } } })
|
||||
.lean()
|
||||
.select("id name position permission_overwrites")
|
||||
.sort({ position: 1 })
|
||||
.cursor()
|
||||
.eachAsync((doc) => {
|
||||
// Only return channels where @everyone has the CONNECT permission
|
||||
if (
|
||||
doc.permission_overwrites === undefined ||
|
||||
Permissions.channelPermission(doc.permission_overwrites, Permissions.FLAGS.CONNECT) === Permissions.FLAGS.CONNECT
|
||||
) {
|
||||
channels.push({
|
||||
id: doc.id,
|
||||
name: doc.name,
|
||||
position: doc.position
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Fetch members
|
||||
// TODO: Understand how Discord's max 100 random member sample works, and apply to here (see top of this file)
|
||||
let members: any[] = [];
|
||||
await MemberModel.find({ guild_id: guild_id })
|
||||
.lean()
|
||||
.populate({ path: "user", select: { _id: 0, username: 1, avatar: 1, presence: 1 } })
|
||||
.select("id user nick deaf mute")
|
||||
.cursor()
|
||||
.eachAsync((doc) => {
|
||||
const status = doc.user?.presence?.status || "offline";
|
||||
if (status == "offline") return;
|
||||
|
||||
let item = {};
|
||||
|
||||
item = {
|
||||
...item,
|
||||
id: null, // this is updated during the sort outside of the query
|
||||
username: doc.nick || doc.user?.username,
|
||||
discriminator: "0000", // intended (https://github.com/discord/discord-api-docs/issues/1287)
|
||||
avatar: null, // intended, avatar_url below will return a unique guild + user url to the avatar
|
||||
status: status
|
||||
};
|
||||
|
||||
const activity = doc.user?.presence?.activities?.[0];
|
||||
if (activity) {
|
||||
item = {
|
||||
...item,
|
||||
game: { name: activity.name }
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: If the member is in a voice channel, return extra widget details
|
||||
// Extra fields returned include deaf, mute, self_deaf, self_mute, supress, and channel_id (voice channel connected to)
|
||||
// Get this from VoiceState
|
||||
|
||||
// TODO: Implement a widget-avatar endpoint on the CDN, and implement logic here to request it
|
||||
// Get unique avatar url for guild user, cdn to serve the actual avatar image on this url
|
||||
/*
|
||||
const avatar = doc.user?.avatar;
|
||||
if (avatar) {
|
||||
const CDN_HOST = Config.get().cdn.endpoint || "http://localhost:3003";
|
||||
const avatar_url = "/widget-avatars/" + ;
|
||||
item = {
|
||||
...item,
|
||||
avatar_url: avatar_url
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
members.push(item);
|
||||
});
|
||||
|
||||
// Sort members, and update ids (Unable to do under the mongoose query due to https://mongoosejs.com/docs/faq.html#populate_sort_order)
|
||||
members = members.sort((first, second) => 0 - (first.username > second.username ? -1 : 1));
|
||||
members.forEach((x, i) => {
|
||||
x.id = i;
|
||||
});
|
||||
|
||||
// Construct object to respond with
|
||||
const data = {
|
||||
id: guild_id,
|
||||
name: guild.name,
|
||||
instant_invite: invite?.code,
|
||||
channels: channels,
|
||||
members: members,
|
||||
presence_count: guild.presence_count
|
||||
};
|
||||
|
||||
res.set("Cache-Control", "public, max-age=300");
|
||||
return res.json(data);
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,110 @@
|
||||
import { Request, Response, Router } from "express";
|
||||
import { GuildModel } from "@fosscord/server-util";
|
||||
import { HTTPError } from "lambert-server";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
const router: Router = Router();
|
||||
|
||||
// TODO: use svg templates instead of node-canvas for improved performance and to change it easily
|
||||
|
||||
// https://discord.com/developers/docs/resources/guild#get-guild-widget-image
|
||||
// TODO: Cache the response
|
||||
router.get("/", async (req: Request, res: Response) => {
|
||||
const { guild_id } = req.params;
|
||||
|
||||
const guild = await GuildModel.findOne({ id: guild_id }).exec();
|
||||
if (!guild.widget_enabled) throw new HTTPError("Unknown Guild", 404);
|
||||
|
||||
// Fetch guild information
|
||||
const icon = guild.icon;
|
||||
const name = guild.name;
|
||||
const presence = guild.presence_count + " ONLINE";
|
||||
|
||||
// Fetch parameter
|
||||
const style = req.query.style?.toString() || "shield";
|
||||
if (!["shield", "banner1", "banner2", "banner3", "banner4"].includes(style)) {
|
||||
throw new HTTPError("Value must be one of ('shield', 'banner1', 'banner2', 'banner3', 'banner4').", 400);
|
||||
}
|
||||
|
||||
// Setup canvas
|
||||
const { createCanvas } = require("canvas");
|
||||
const { loadImage } = require("canvas");
|
||||
const sizeOf = require("image-size");
|
||||
|
||||
// TODO: Widget style templates need Fosscord branding
|
||||
const source = path.join(__dirname, "..", "..", "..", "..", "assets", "widget", `${style}.png`);
|
||||
if (!fs.existsSync(source)) {
|
||||
throw new HTTPError("Widget template does not exist.", 400);
|
||||
}
|
||||
|
||||
// Create base template image for parameter
|
||||
const { width, height } = await sizeOf(source);
|
||||
const canvas = createCanvas(width, height);
|
||||
const ctx = canvas.getContext("2d");
|
||||
const template = await loadImage(source);
|
||||
ctx.drawImage(template, 0, 0);
|
||||
|
||||
// Add the guild specific information to the template asset image
|
||||
switch (style) {
|
||||
case "shield":
|
||||
ctx.textAlign = "center";
|
||||
await drawText(ctx, 73, 13, "#FFFFFF", "thin 10px Verdana", presence);
|
||||
break;
|
||||
case "banner1":
|
||||
if (icon) await drawIcon(ctx, 20, 27, 50, icon);
|
||||
await drawText(ctx, 83, 51, "#FFFFFF", "12px Verdana", name, 22);
|
||||
await drawText(ctx, 83, 66, "#C9D2F0FF", "thin 11px Verdana", presence);
|
||||
break;
|
||||
case "banner2":
|
||||
if (icon) await drawIcon(ctx, 13, 19, 36, icon);
|
||||
await drawText(ctx, 62, 34, "#FFFFFF", "12px Verdana", name, 15);
|
||||
await drawText(ctx, 62, 49, "#C9D2F0FF", "thin 11px Verdana", presence);
|
||||
break;
|
||||
case "banner3":
|
||||
if (icon) await drawIcon(ctx, 20, 20, 50, icon);
|
||||
await drawText(ctx, 83, 44, "#FFFFFF", "12px Verdana", name, 27);
|
||||
await drawText(ctx, 83, 58, "#C9D2F0FF", "thin 11px Verdana", presence);
|
||||
break;
|
||||
case "banner4":
|
||||
if (icon) await drawIcon(ctx, 21, 136, 50, icon);
|
||||
await drawText(ctx, 84, 156, "#FFFFFF", "13px Verdana", name, 27);
|
||||
await drawText(ctx, 84, 171, "#C9D2F0FF", "thin 12px Verdana", presence);
|
||||
break;
|
||||
default:
|
||||
throw new HTTPError("Value must be one of ('shield', 'banner1', 'banner2', 'banner3', 'banner4').", 400);
|
||||
}
|
||||
|
||||
// Return final image
|
||||
const buffer = canvas.toBuffer("image/png");
|
||||
res.set("Content-Type", "image/png");
|
||||
res.set("Cache-Control", "public, max-age=3600");
|
||||
return res.send(buffer);
|
||||
});
|
||||
|
||||
async function drawIcon(canvas: any, x: number, y: number, scale: number, icon: string) {
|
||||
// @ts-ignore
|
||||
const img = new require("canvas").Image();
|
||||
img.src = icon;
|
||||
|
||||
// Do some canvas clipping magic!
|
||||
canvas.save();
|
||||
canvas.beginPath();
|
||||
|
||||
const r = scale / 2; // use scale to determine radius
|
||||
canvas.arc(x + r, y + r, r, 0, 2 * Math.PI, false); // start circle at x, and y coords + radius to find center
|
||||
|
||||
canvas.clip();
|
||||
canvas.drawImage(img, x, y, scale, scale);
|
||||
|
||||
canvas.restore();
|
||||
}
|
||||
|
||||
async function drawText(canvas: any, x: number, y: number, color: string, font: string, text: string, maxcharacters?: number) {
|
||||
canvas.fillStyle = color;
|
||||
canvas.font = font;
|
||||
if (text.length > (maxcharacters || 0) && maxcharacters) text = text.slice(0, maxcharacters) + "...";
|
||||
canvas.fillText(text, x, y);
|
||||
}
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,35 @@
|
||||
import { Request, Response, Router } from "express";
|
||||
import { getPermission, GuildModel } from "@fosscord/server-util";
|
||||
import { HTTPError } from "lambert-server";
|
||||
import { check } from "../../../util/instanceOf";
|
||||
import { WidgetModifySchema } from "../../../schema/Widget";
|
||||
|
||||
const router: Router = Router();
|
||||
|
||||
// https://discord.com/developers/docs/resources/guild#get-guild-widget-settings
|
||||
router.get("/", async (req: Request, res: Response) => {
|
||||
const { guild_id } = req.params;
|
||||
|
||||
const perms = await getPermission(req.user_id, guild_id);
|
||||
perms.hasThrow("MANAGE_GUILD");
|
||||
|
||||
const guild = await GuildModel.findOne({ id: guild_id }).exec();
|
||||
|
||||
return res.json({ enabled: guild.widget_enabled || false, channel_id: guild.widget_channel_id || null });
|
||||
});
|
||||
|
||||
// https://discord.com/developers/docs/resources/guild#modify-guild-widget
|
||||
router.patch("/", check(WidgetModifySchema), async (req: Request, res: Response) => {
|
||||
const body = req.body as WidgetModifySchema;
|
||||
const { guild_id } = req.params;
|
||||
|
||||
const perms = await getPermission(req.user_id, guild_id);
|
||||
perms.hasThrow("MANAGE_GUILD");
|
||||
|
||||
await GuildModel.updateOne({ id: guild_id }, { widget_enabled: body.enabled, widget_channel_id: body.channel_id }).exec();
|
||||
// Widget invite for the widget_channel_id gets created as part of the /guilds/{guild.id}/widget.json request
|
||||
|
||||
return res.json(body);
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,89 @@
|
||||
import { Router, Request, Response } from "express";
|
||||
import { RoleModel, GuildModel, Snowflake, Guild, RoleDocument, Config } from "@fosscord/server-util";
|
||||
import { HTTPError } from "lambert-server";
|
||||
import { check } from "./../../util/instanceOf";
|
||||
import { GuildCreateSchema } from "../../schema/Guild";
|
||||
import { getPublicUser } from "../../util/User";
|
||||
import { addMember } from "../../util/Member";
|
||||
import { createChannel } from "../../util/Channel";
|
||||
|
||||
const router: Router = Router();
|
||||
|
||||
//TODO: create default channel
|
||||
|
||||
router.post("/", check(GuildCreateSchema), async (req: Request, res: Response) => {
|
||||
const body = req.body as GuildCreateSchema;
|
||||
|
||||
const { maxGuilds } = Config.get().limits.user;
|
||||
const user = await getPublicUser(req.user_id, { guilds: true });
|
||||
|
||||
if (user.guilds.length >= maxGuilds) {
|
||||
throw new HTTPError(`Maximum number of guilds reached ${maxGuilds}`, 403);
|
||||
}
|
||||
|
||||
const guild_id = Snowflake.generate();
|
||||
const guild: Guild = {
|
||||
name: body.name,
|
||||
region: Config.get().regions.default,
|
||||
owner_id: req.user_id,
|
||||
icon: undefined,
|
||||
afk_channel_id: undefined,
|
||||
afk_timeout: 300,
|
||||
application_id: undefined,
|
||||
banner: undefined,
|
||||
default_message_notifications: 0,
|
||||
description: undefined,
|
||||
splash: undefined,
|
||||
discovery_splash: undefined,
|
||||
explicit_content_filter: 0,
|
||||
features: [],
|
||||
id: guild_id,
|
||||
large: undefined,
|
||||
max_members: 250000,
|
||||
max_presences: 250000,
|
||||
max_video_channel_users: 25,
|
||||
presence_count: 0,
|
||||
member_count: 0, // will automatically be increased by addMember()
|
||||
mfa_level: 0,
|
||||
preferred_locale: "en-US",
|
||||
premium_subscription_count: 0,
|
||||
premium_tier: 0,
|
||||
public_updates_channel_id: undefined,
|
||||
rules_channel_id: undefined,
|
||||
system_channel_flags: 0,
|
||||
system_channel_id: undefined,
|
||||
unavailable: false,
|
||||
vanity_url: undefined,
|
||||
verification_level: 0,
|
||||
welcome_screen: {
|
||||
enabled: false,
|
||||
description: "No description",
|
||||
welcome_channels: []
|
||||
},
|
||||
widget_channel_id: undefined,
|
||||
widget_enabled: false
|
||||
};
|
||||
|
||||
const [guild_doc, role] = await Promise.all([
|
||||
new GuildModel(guild).save(),
|
||||
new RoleModel({
|
||||
id: guild_id,
|
||||
guild_id: guild_id,
|
||||
color: 0,
|
||||
hoist: false,
|
||||
managed: false,
|
||||
mentionable: false,
|
||||
name: "@everyone",
|
||||
permissions: 2251804225n,
|
||||
position: 0,
|
||||
tags: null
|
||||
}).save()
|
||||
]);
|
||||
|
||||
await createChannel({ name: "general", type: 0, guild_id, position: 0, permission_overwrites: [] }, req.user_id);
|
||||
await addMember(req.user_id, guild_id);
|
||||
|
||||
res.status(201).json({ id: guild.id });
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,61 @@
|
||||
import { Request, Response, Router } from "express";
|
||||
const router: Router = Router();
|
||||
import { TemplateModel, GuildModel, toObject, UserModel, RoleModel, Snowflake, Guild, Config } from "@fosscord/server-util";
|
||||
import { HTTPError } from "lambert-server";
|
||||
import { GuildTemplateCreateSchema } from "../../../schema/Guild";
|
||||
import { getPublicUser } from "../../../util/User";
|
||||
import { check } from "../../../util/instanceOf";
|
||||
import { addMember } from "../../../util/Member";
|
||||
|
||||
router.get("/:code", async (req: Request, res: Response) => {
|
||||
const { code } = req.params;
|
||||
|
||||
const template = await TemplateModel.findOne({ code: code }).exec();
|
||||
|
||||
res.json(toObject(template)).send();
|
||||
});
|
||||
|
||||
router.post("/:code", check(GuildTemplateCreateSchema), async (req: Request, res: Response) => {
|
||||
const { code } = req.params;
|
||||
const body = req.body as GuildTemplateCreateSchema;
|
||||
|
||||
const { maxGuilds } = Config.get().limits.user;
|
||||
const user = await getPublicUser(req.user_id, { guilds: true });
|
||||
|
||||
if (user.guilds.length >= maxGuilds) {
|
||||
throw new HTTPError(`Maximum number of guilds reached ${maxGuilds}`, 403);
|
||||
}
|
||||
|
||||
const template = await TemplateModel.findOne({ code: code }).exec();
|
||||
|
||||
const guild_id = Snowflake.generate();
|
||||
|
||||
const guild: Guild = {
|
||||
...body,
|
||||
...template.serialized_source_guild,
|
||||
id: guild_id,
|
||||
owner_id: req.user_id
|
||||
};
|
||||
|
||||
const [guild_doc, role] = await Promise.all([
|
||||
new GuildModel(guild).save(),
|
||||
new RoleModel({
|
||||
id: guild_id,
|
||||
guild_id: guild_id,
|
||||
color: 0,
|
||||
hoist: false,
|
||||
managed: true,
|
||||
mentionable: true,
|
||||
name: "@everyone",
|
||||
permissions: 2251804225n,
|
||||
position: 0,
|
||||
tags: null
|
||||
}).save()
|
||||
]);
|
||||
|
||||
await addMember(req.user_id, guild_id, { guild: guild_doc });
|
||||
|
||||
res.status(201).json({ id: guild.id });
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,44 @@
|
||||
import { Router, Request, Response } from "express";
|
||||
import { getPermission, InviteModel, toObject } from "@fosscord/server-util";
|
||||
import { HTTPError } from "lambert-server";
|
||||
import { addMember } from "../../util/Member";
|
||||
const router: Router = Router();
|
||||
|
||||
router.get("/:code", async (req: Request, res: Response) => {
|
||||
const { code } = req.params;
|
||||
|
||||
const invite = await InviteModel.findOne({ code }).exec();
|
||||
if (!invite) throw new HTTPError("Unknown Invite", 404);
|
||||
|
||||
res.status(200).send(toObject(invite));
|
||||
});
|
||||
|
||||
router.post("/:code", async (req: Request, res: Response) => {
|
||||
const { code } = req.params;
|
||||
|
||||
const invite = await InviteModel.findOneAndUpdate({ code }, { $inc: { uses: 1 } }).exec();
|
||||
if (!invite) throw new HTTPError("Unknown Invite", 404);
|
||||
|
||||
await addMember(req.user_id, invite.guild_id);
|
||||
|
||||
res.status(200).send(toObject(invite));
|
||||
});
|
||||
|
||||
router.delete("/:code", async (req: Request, res: Response) => {
|
||||
const { code } = req.params;
|
||||
const invite = await InviteModel.findOne({ code }).exec();
|
||||
|
||||
if (!invite) throw new HTTPError("Unknown Invite", 404);
|
||||
|
||||
const { guild_id, channel_id } = invite;
|
||||
const perms = await getPermission(req.user_id, guild_id, channel_id);
|
||||
|
||||
if (!perms.has("MANAGE_GUILD") && !perms.has("MANAGE_CHANNELS"))
|
||||
throw new HTTPError("You missing the MANAGE_GUILD or MANAGE_CHANNELS permission", 401);
|
||||
|
||||
await InviteModel.deleteOne({ code }).exec();
|
||||
|
||||
res.status(200).send({ invite: toObject(invite) });
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,9 @@
|
||||
import { Router, Response, Request } from "express";
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get("/", (req: Request, res: Response) => {
|
||||
res.send("pong");
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Router, Response, Request } from "express";
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.post("/", (req: Request, res: Response) => {
|
||||
// TODO:
|
||||
res.sendStatus(204);
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Router, Request, Response } from "express";
|
||||
import { getPublicUser } from "../../../util/User";
|
||||
import { HTTPError } from "lambert-server";
|
||||
|
||||
const router: Router = Router();
|
||||
|
||||
router.get("/", async (req: Request, res: Response) => {
|
||||
const { id } = req.params;
|
||||
|
||||
res.json(await getPublicUser(id));
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,27 @@
|
||||
import { Router, Request, Response } from "express";
|
||||
import { getPublicUser } from "../../../util/User";
|
||||
|
||||
const router: Router = Router();
|
||||
|
||||
router.get("/", async (req: Request, res: Response) => {
|
||||
const user = await getPublicUser(req.params.id, { user_data: true })
|
||||
|
||||
res.json({
|
||||
connected_accounts: user.user_data.connected_accounts,
|
||||
premium_guild_since: null, // TODO
|
||||
premium_since: null, // TODO
|
||||
user: {
|
||||
username: user.username,
|
||||
discriminator: user.discriminator,
|
||||
id: user.id,
|
||||
public_flags: user.public_flags,
|
||||
avatar: user.avatar,
|
||||
accent_color: user.accent_color,
|
||||
banner: user.banner,
|
||||
bio: req.user_bot ? null : user.bio,
|
||||
bot: user.bot,
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Router, Response, Request } from "express";
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get("/", (req: Request, res: Response) => {
|
||||
// TODO:
|
||||
res.status(200).send({ guild_affinities: [] });
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Router, Response, Request } from "express";
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get("/", (req: Request, res: Response) => {
|
||||
// TODO:
|
||||
res.status(200).send({ user_affinities: [], inverse_user_affinities: [] });
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,53 @@
|
||||
import { Router, Request, Response } from "express";
|
||||
import {
|
||||
ChannelModel,
|
||||
ChannelCreateEvent,
|
||||
toObject,
|
||||
ChannelType,
|
||||
Snowflake,
|
||||
trimSpecial,
|
||||
Channel,
|
||||
DMChannel,
|
||||
UserModel
|
||||
} from "@fosscord/server-util";
|
||||
import { HTTPError } from "lambert-server";
|
||||
import { emitEvent } from "../../../util/Event";
|
||||
import { DmChannelCreateSchema } from "../../../schema/Channel";
|
||||
import { check } from "../../../util/instanceOf";
|
||||
|
||||
const router: Router = Router();
|
||||
|
||||
router.get("/", async (req: Request, res: Response) => {
|
||||
var channels = await ChannelModel.find({ recipient_ids: req.user_id }).exec();
|
||||
|
||||
res.json(toObject(channels));
|
||||
});
|
||||
|
||||
router.post("/", check(DmChannelCreateSchema), async (req: Request, res: Response) => {
|
||||
const body = req.body as DmChannelCreateSchema;
|
||||
|
||||
body.recipients = body.recipients.filter((x) => x !== req.user_id).unique();
|
||||
|
||||
if (!(await Promise.all(body.recipients.map((x) => UserModel.exists({ id: x })))).every((x) => x)) {
|
||||
throw new HTTPError("Recipient not found");
|
||||
}
|
||||
|
||||
const type = body.recipients.length === 1 ? ChannelType.DM : ChannelType.GROUP_DM;
|
||||
const name = trimSpecial(body.name);
|
||||
|
||||
const channel = await new ChannelModel({
|
||||
name,
|
||||
type,
|
||||
owner_id: req.user_id,
|
||||
id: Snowflake.generate(),
|
||||
created_at: new Date(),
|
||||
last_message_id: null,
|
||||
recipient_ids: [...body.recipients, req.user_id]
|
||||
}).save();
|
||||
|
||||
await emitEvent({ event: "CHANNEL_CREATE", data: toObject(channel), user_id: req.user_id } as ChannelCreateEvent);
|
||||
|
||||
res.json(toObject(channel));
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Router, Request, Response } from "express";
|
||||
import { GuildModel, MemberModel, UserModel } from "@fosscord/server-util";
|
||||
import bcrypt from "bcrypt";
|
||||
const router = Router();
|
||||
|
||||
router.post("/", async (req: Request, res: Response) => {
|
||||
const user = await UserModel.findOne({ id: req.user_id }).exec(); //User object
|
||||
|
||||
let correctpass = await bcrypt.compare(req.body.password, user!.user_data.hash); //Not sure if user typed right password :/
|
||||
if (correctpass) {
|
||||
await Promise.all([
|
||||
UserModel.deleteOne({ id: req.user_id }).exec(), //Yeetus user deletus
|
||||
MemberModel.deleteMany({ id: req.user_id }).exec()
|
||||
]);
|
||||
|
||||
res.sendStatus(204);
|
||||
} else {
|
||||
res.sendStatus(401);
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,20 @@
|
||||
import { UserModel } from "@fosscord/server-util";
|
||||
import { Router, Response, Request } from "express";
|
||||
import bcrypt from "bcrypt";
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.post("/", async (req: Request, res: Response) => {
|
||||
const user = await UserModel.findOne({ id: req.user_id }).exec(); //User object
|
||||
|
||||
let correctpass = await bcrypt.compare(req.body.password, user!.user_data.hash); //Not sure if user typed right password :/
|
||||
if (correctpass) {
|
||||
await UserModel.updateOne({ id: req.user_id }, { disabled: true }).exec();
|
||||
|
||||
res.sendStatus(204);
|
||||
} else {
|
||||
res.status(400).json({ message: "Password does not match", code: 50018 });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,55 @@
|
||||
import { Router, Request, Response } from "express";
|
||||
import { GuildModel, MemberModel, UserModel, GuildDeleteEvent, GuildMemberRemoveEvent, toObject } from "@fosscord/server-util";
|
||||
import { HTTPError } from "lambert-server";
|
||||
import { emitEvent } from "../../../util/Event";
|
||||
import { getPublicUser } from "../../../util/User";
|
||||
|
||||
const router: Router = Router();
|
||||
|
||||
router.get("/", async (req: Request, res: Response) => {
|
||||
const user = await UserModel.findOne({ id: req.user_id }, { guilds: true }).exec();
|
||||
if (!user) throw new HTTPError("User not found", 404);
|
||||
|
||||
var guildIDs = user.guilds || [];
|
||||
var guild = await GuildModel.find({ id: { $in: guildIDs } })
|
||||
.populate({ path: "joined_at", match: { id: req.user_id } })
|
||||
.exec();
|
||||
|
||||
res.json(toObject(guild));
|
||||
});
|
||||
|
||||
// user send to leave a certain guild
|
||||
router.delete("/:id", async (req: Request, res: Response) => {
|
||||
const guild_id = req.params.id;
|
||||
const guild = await GuildModel.findOne({ id: guild_id }, { guild_id: true }).exec();
|
||||
|
||||
if (!guild) throw new HTTPError("Guild doesn't exist", 404);
|
||||
if (guild.owner_id === req.user_id) throw new HTTPError("You can't leave your own guild", 400);
|
||||
|
||||
await Promise.all([
|
||||
MemberModel.deleteOne({ id: req.user_id, guild_id: guild_id }).exec(),
|
||||
UserModel.updateOne({ id: req.user_id }, { $pull: { guilds: guild_id } }).exec(),
|
||||
emitEvent({
|
||||
event: "GUILD_DELETE",
|
||||
data: {
|
||||
id: guild_id,
|
||||
},
|
||||
user_id: req.user_id,
|
||||
} as GuildDeleteEvent),
|
||||
]);
|
||||
|
||||
const user = await getPublicUser(req.user_id);
|
||||
|
||||
await emitEvent({
|
||||
event: "GUILD_MEMBER_REMOVE",
|
||||
data: {
|
||||
guild_id: guild_id,
|
||||
user: user,
|
||||
},
|
||||
guild_id: guild_id,
|
||||
} as GuildMemberRemoveEvent);
|
||||
|
||||
return res.sendStatus(204);
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,48 @@
|
||||
import { Router, Request, Response } from "express";
|
||||
import { UserModel, toObject, PublicUserProjection } from "@fosscord/server-util";
|
||||
import { getPublicUser } from "../../../util/User";
|
||||
import { UserModifySchema } from "../../../schema/User";
|
||||
import { check } from "../../../util/instanceOf";
|
||||
import { handleFile } from "../../../util/cdn";
|
||||
|
||||
const router: Router = Router();
|
||||
|
||||
router.get("/", async (req: Request, res: Response) => {
|
||||
res.json(await getPublicUser(req.user_id));
|
||||
});
|
||||
|
||||
const UserUpdateProjection = {
|
||||
accent_color: true,
|
||||
avatar: true,
|
||||
banner: true,
|
||||
bio: true,
|
||||
bot: true,
|
||||
discriminator: true,
|
||||
email: true,
|
||||
flags: true,
|
||||
id: true,
|
||||
locale: true,
|
||||
mfa_enabled: true,
|
||||
nsfw_alllowed: true,
|
||||
phone: true,
|
||||
public_flags: true,
|
||||
purchased_flags: true,
|
||||
// token: true, // this isn't saved in the db and needs to be set manually
|
||||
username: true,
|
||||
verified: true
|
||||
};
|
||||
|
||||
router.patch("/", check(UserModifySchema), async (req: Request, res: Response) => {
|
||||
const body = req.body as UserModifySchema;
|
||||
|
||||
if (body.avatar) body.avatar = await handleFile(`/avatars/${req.user_id}`, body.avatar as string);
|
||||
if (body.banner) body.banner = await handleFile(`/banners/${req.user_id}`, body.banner as string);
|
||||
|
||||
const user = await UserModel.findOneAndUpdate({ id: req.user_id }, body, { projection: UserUpdateProjection }).exec();
|
||||
// TODO: dispatch user update event
|
||||
|
||||
res.json(toObject(user));
|
||||
});
|
||||
|
||||
export default router;
|
||||
// {"message": "Invalid two-factor code", "code": 60008}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Router, Response, Request } from "express";
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get("/", (req: Request, res: Response) => {
|
||||
// TODO:
|
||||
res.status(200).send([]);
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,27 @@
|
||||
import { Router, Request, Response } from "express";
|
||||
import { getPublicUser } from "../../../util/User";
|
||||
|
||||
const router: Router = Router();
|
||||
|
||||
router.get("/", async (req: Request, res: Response) => {
|
||||
const user = await getPublicUser(req.user_id, { user_data: true })
|
||||
|
||||
res.json({
|
||||
connected_accounts: user.user_data.connected_accounts,
|
||||
premium_guild_since: null, // TODO
|
||||
premium_since: null, // TODO
|
||||
user: {
|
||||
username: user.username,
|
||||
discriminator: user.discriminator,
|
||||
id: user.id,
|
||||
public_flags: user.public_flags,
|
||||
avatar: user.avatar,
|
||||
accent_color: user.accent_color,
|
||||
banner: user.banner,
|
||||
bio: user.bio,
|
||||
bot: user.bot,
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,176 @@
|
||||
import {
|
||||
RelationshipAddEvent,
|
||||
UserModel,
|
||||
PublicUserProjection,
|
||||
toObject,
|
||||
RelationshipType,
|
||||
RelationshipRemoveEvent,
|
||||
UserDocument
|
||||
} from "@fosscord/server-util";
|
||||
import { Router, Response, Request } from "express";
|
||||
import { HTTPError } from "lambert-server";
|
||||
import { emitEvent } from "../../../util/Event";
|
||||
import { check, Length } from "../../../util/instanceOf";
|
||||
|
||||
const router = Router();
|
||||
|
||||
const userProjection = { "user_data.relationships": true, ...PublicUserProjection };
|
||||
|
||||
router.get("/", async (req: Request, res: Response) => {
|
||||
const user = await UserModel.findOne({ id: req.user_id }, { user_data: { relationships: true } })
|
||||
.populate({ path: "user_data.relationships.id", model: UserModel })
|
||||
.exec();
|
||||
|
||||
return res.json(toObject(user.user_data.relationships));
|
||||
});
|
||||
|
||||
async function addRelationship(req: Request, res: Response, friend: UserDocument, type: RelationshipType) {
|
||||
const id = friend.id;
|
||||
if (id === req.user_id) throw new HTTPError("You can't add yourself as a friend");
|
||||
|
||||
const user = await UserModel.findOne({ id: req.user_id }, userProjection).exec();
|
||||
const newUserRelationships = [...user.user_data.relationships];
|
||||
const newFriendRelationships = [...friend.user_data.relationships];
|
||||
|
||||
var relationship = newUserRelationships.find((x) => x.id === id);
|
||||
const friendRequest = newFriendRelationships.find((x) => x.id === req.user_id);
|
||||
|
||||
if (type === RelationshipType.blocked) {
|
||||
if (relationship) {
|
||||
if (relationship.type === RelationshipType.blocked) throw new HTTPError("You already blocked the user");
|
||||
relationship.type = RelationshipType.blocked;
|
||||
} else {
|
||||
relationship = { id, type: RelationshipType.blocked };
|
||||
newUserRelationships.push(relationship);
|
||||
}
|
||||
|
||||
if (friendRequest && friendRequest.type !== RelationshipType.blocked) {
|
||||
newFriendRelationships.remove(friendRequest);
|
||||
await Promise.all([
|
||||
UserModel.updateOne({ id: friend.id }, { "user_data.relationships": newFriendRelationships }).exec(),
|
||||
emitEvent({
|
||||
event: "RELATIONSHIP_REMOVE",
|
||||
data: friendRequest,
|
||||
user_id: id
|
||||
} as RelationshipRemoveEvent)
|
||||
]);
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
UserModel.updateOne({ id: req.user_id }, { "user_data.relationships": newUserRelationships }).exec(),
|
||||
emitEvent({
|
||||
event: "RELATIONSHIP_ADD",
|
||||
data: {
|
||||
...toObject(relationship),
|
||||
user: { ...toObject(friend), user_data: undefined }
|
||||
},
|
||||
user_id: req.user_id
|
||||
} as RelationshipAddEvent)
|
||||
]);
|
||||
|
||||
return res.sendStatus(204);
|
||||
}
|
||||
|
||||
var incoming_relationship = { id: req.user_id, nickname: undefined, type: RelationshipType.incoming };
|
||||
var outgoing_relationship = { id, nickname: undefined, type: RelationshipType.outgoing };
|
||||
|
||||
if (friendRequest) {
|
||||
if (friendRequest.type === RelationshipType.blocked) throw new HTTPError("The user blocked you");
|
||||
// accept friend request
|
||||
// @ts-ignore
|
||||
incoming_relationship = friendRequest;
|
||||
incoming_relationship.type = RelationshipType.friends;
|
||||
outgoing_relationship.type = RelationshipType.friends;
|
||||
} else newFriendRelationships.push(incoming_relationship);
|
||||
|
||||
if (relationship) {
|
||||
if (relationship.type === RelationshipType.outgoing) throw new HTTPError("You already sent a friend request");
|
||||
if (relationship.type === RelationshipType.blocked) throw new HTTPError("Unblock the user before sending a friend request");
|
||||
if (relationship.type === RelationshipType.friends) throw new HTTPError("You are already friends with the user");
|
||||
} else newUserRelationships.push(outgoing_relationship);
|
||||
|
||||
await Promise.all([
|
||||
UserModel.updateOne({ id: req.user_id }, { "user_data.relationships": newUserRelationships }).exec(),
|
||||
UserModel.updateOne({ id: friend.id }, { "user_data.relationships": newFriendRelationships }).exec(),
|
||||
emitEvent({
|
||||
event: "RELATIONSHIP_ADD",
|
||||
data: {
|
||||
...outgoing_relationship,
|
||||
user: { ...toObject(friend), user_data: undefined }
|
||||
},
|
||||
user_id: req.user_id
|
||||
} as RelationshipAddEvent),
|
||||
emitEvent({
|
||||
event: "RELATIONSHIP_ADD",
|
||||
data: {
|
||||
...toObject(incoming_relationship),
|
||||
should_notify: true,
|
||||
user: { ...toObject(user), user_data: undefined }
|
||||
},
|
||||
user_id: id
|
||||
} as RelationshipAddEvent)
|
||||
]);
|
||||
|
||||
return res.sendStatus(204);
|
||||
}
|
||||
|
||||
router.put("/:id", check({ $type: new Length(Number, 1, 4) }), async (req: Request, res: Response) => {
|
||||
return await addRelationship(req, res, await UserModel.findOne({ id: req.params.id }), req.body.type);
|
||||
});
|
||||
|
||||
router.post("/", check({ discriminator: String, username: String }), async (req: Request, res: Response) => {
|
||||
return await addRelationship(
|
||||
req,
|
||||
res,
|
||||
await UserModel.findOne(req.body as { discriminator: string; username: string }).exec(),
|
||||
req.body.type
|
||||
);
|
||||
});
|
||||
|
||||
router.delete("/:id", async (req: Request, res: Response) => {
|
||||
const { id } = req.params;
|
||||
if (id === req.user_id) throw new HTTPError("You can't remove yourself as a friend");
|
||||
|
||||
const user = await UserModel.findOne({ id: req.user_id }).exec();
|
||||
if (!user) throw new HTTPError("Invalid token", 400);
|
||||
|
||||
const friend = await UserModel.findOne({ id }, userProjection).exec();
|
||||
if (!friend) throw new HTTPError("User not found", 404);
|
||||
|
||||
const relationship = user.user_data.relationships.find((x) => x.id === id);
|
||||
const friendRequest = friend.user_data.relationships.find((x) => x.id === req.user_id);
|
||||
if (relationship?.type === RelationshipType.blocked) {
|
||||
// unblock user
|
||||
user.user_data.relationships.remove(relationship);
|
||||
|
||||
await Promise.all([
|
||||
user.save(),
|
||||
emitEvent({ event: "RELATIONSHIP_REMOVE", user_id: req.user_id, data: relationship } as RelationshipRemoveEvent)
|
||||
]);
|
||||
return res.sendStatus(204);
|
||||
}
|
||||
if (!relationship || !friendRequest) throw new HTTPError("You are not friends with the user", 404);
|
||||
if (friendRequest.type === RelationshipType.blocked) throw new HTTPError("The user blocked you");
|
||||
|
||||
user.user_data.relationships.remove(relationship);
|
||||
friend.user_data.relationships.remove(friendRequest);
|
||||
|
||||
await Promise.all([
|
||||
user.save(),
|
||||
friend.save(),
|
||||
emitEvent({
|
||||
event: "RELATIONSHIP_REMOVE",
|
||||
data: relationship,
|
||||
user_id: req.user_id
|
||||
} as RelationshipRemoveEvent),
|
||||
emitEvent({
|
||||
event: "RELATIONSHIP_REMOVE",
|
||||
data: friendRequest,
|
||||
user_id: id
|
||||
} as RelationshipRemoveEvent)
|
||||
]);
|
||||
|
||||
return res.sendStatus(204);
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Router, Response, Request } from "express";
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.patch("/", (req: Request, res: Response) => {
|
||||
// TODO:
|
||||
res.sendStatus(204);
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,9 @@
|
||||
export const BanCreateSchema = {
|
||||
$delete_message_days: String,
|
||||
$reason: String,
|
||||
};
|
||||
|
||||
export interface BanCreateSchema {
|
||||
delete_message_days?: string;
|
||||
reason?: string;
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import { ChannelType } from "@fosscord/server-util";
|
||||
import { Length } from "../util/instanceOf";
|
||||
|
||||
export const ChannelModifySchema = {
|
||||
name: new Length(String, 2, 100),
|
||||
type: new Length(Number, 0, 13),
|
||||
$topic: new Length(String, 0, 1024),
|
||||
$bitrate: Number,
|
||||
$user_limit: Number,
|
||||
$rate_limit_per_user: new Length(Number, 0, 21600),
|
||||
$position: Number,
|
||||
$permission_overwrites: [
|
||||
{
|
||||
id: String,
|
||||
type: new Length(Number, 0, 1), // either 0 (role) or 1 (member)
|
||||
allow: BigInt,
|
||||
deny: BigInt
|
||||
}
|
||||
],
|
||||
$parent_id: String,
|
||||
$nsfw: Boolean
|
||||
};
|
||||
|
||||
export const DmChannelCreateSchema = {
|
||||
$name: String,
|
||||
recipients: new Length([String], 1, 10)
|
||||
};
|
||||
|
||||
export interface DmChannelCreateSchema {
|
||||
name?: string;
|
||||
recipients: string[];
|
||||
}
|
||||
|
||||
export interface ChannelModifySchema {
|
||||
name: string;
|
||||
type: number;
|
||||
topic?: string;
|
||||
bitrate?: number;
|
||||
user_limit?: number;
|
||||
rate_limit_per_user?: number;
|
||||
position?: number;
|
||||
permission_overwrites?: {
|
||||
id: string;
|
||||
type: number;
|
||||
allow: bigint;
|
||||
deny: bigint;
|
||||
}[];
|
||||
parent_id?: string;
|
||||
nsfw?: boolean;
|
||||
}
|
||||
|
||||
export const ChannelGuildPositionUpdateSchema = [
|
||||
{
|
||||
id: String,
|
||||
$position: Number
|
||||
}
|
||||
];
|
||||
|
||||
export type ChannelGuildPositionUpdateSchema = {
|
||||
id: string;
|
||||
position?: number;
|
||||
}[];
|
||||
@@ -0,0 +1,131 @@
|
||||
import { ChannelSchema, GuildChannel } from "@fosscord/server-util";
|
||||
import { Length } from "../util/instanceOf";
|
||||
|
||||
export const GuildCreateSchema = {
|
||||
name: new Length(String, 2, 100),
|
||||
$region: String, // auto complete voice region of the user
|
||||
$icon: String,
|
||||
$channels: [Object],
|
||||
$guild_template_code: String,
|
||||
$system_channel_id: String,
|
||||
$rules_channel_id: String
|
||||
};
|
||||
|
||||
export interface GuildCreateSchema {
|
||||
name: string;
|
||||
region?: string;
|
||||
icon?: string;
|
||||
channels?: GuildChannel[];
|
||||
guild_template_code?: string;
|
||||
system_channel_id?: string;
|
||||
rules_channel_id?: string;
|
||||
}
|
||||
|
||||
export const GuildUpdateSchema = {
|
||||
...GuildCreateSchema,
|
||||
name: undefined,
|
||||
$name: new Length(String, 2, 100),
|
||||
$banner: String,
|
||||
$splash: String,
|
||||
$description: String,
|
||||
$features: [String],
|
||||
$icon: String,
|
||||
$verification_level: Number,
|
||||
$default_message_notifications: Number,
|
||||
$system_channel_flags: Number,
|
||||
$system_channel_id: String,
|
||||
$explicit_content_filter: Number,
|
||||
$public_updates_channel_id: String,
|
||||
$afk_timeout: Number,
|
||||
$afk_channel_id: String,
|
||||
$preferred_locale: String
|
||||
};
|
||||
// @ts-ignore
|
||||
delete GuildUpdateSchema.$channels;
|
||||
|
||||
export interface GuildUpdateSchema extends Omit<GuildCreateSchema, "channels"> {
|
||||
banner?: string;
|
||||
splash?: string;
|
||||
description?: string;
|
||||
features?: [string];
|
||||
verification_level?: number;
|
||||
default_message_notifications?: number;
|
||||
system_channel_flags?: number;
|
||||
explicit_content_filter?: number;
|
||||
public_updates_channel_id?: string;
|
||||
afk_timeout?: number;
|
||||
afk_channel_id?: string;
|
||||
preferred_locale?: string;
|
||||
}
|
||||
|
||||
export const GuildGetSchema = {
|
||||
id: true,
|
||||
name: true,
|
||||
icon: true,
|
||||
splash: true,
|
||||
discovery_splash: true,
|
||||
owner: true,
|
||||
owner_id: true,
|
||||
permissions: true,
|
||||
region: true,
|
||||
afk_channel_id: true,
|
||||
afk_timeout: true,
|
||||
widget_enabled: true,
|
||||
widget_channel_id: true,
|
||||
verification_level: true,
|
||||
default_message_notifications: true,
|
||||
explicit_content_filter: true,
|
||||
roles: true,
|
||||
emojis: true,
|
||||
features: true,
|
||||
mfa_level: true,
|
||||
application_id: true,
|
||||
system_channel_id: true,
|
||||
system_channel_flags: true,
|
||||
rules_channel_id: true,
|
||||
joined_at: true,
|
||||
// large: true,
|
||||
// unavailable: true,
|
||||
member_count: true,
|
||||
// voice_states: true,
|
||||
// members: true,
|
||||
// channels: true,
|
||||
// presences: true,
|
||||
max_presences: true,
|
||||
max_members: true,
|
||||
vanity_url_code: true,
|
||||
description: true,
|
||||
banner: true,
|
||||
premium_tier: true,
|
||||
premium_subscription_count: true,
|
||||
preferred_locale: true,
|
||||
public_updates_channel_id: true,
|
||||
max_video_channel_users: true,
|
||||
approximate_member_count: true,
|
||||
approximate_presence_count: true
|
||||
// welcome_screen: true,
|
||||
};
|
||||
|
||||
export const GuildTemplateCreateSchema = {
|
||||
name: String,
|
||||
$avatar: String
|
||||
};
|
||||
|
||||
export interface GuildTemplateCreateSchema {
|
||||
name: string;
|
||||
avatar?: string;
|
||||
}
|
||||
|
||||
export const GuildAddChannelToWelcomeScreenSchema = {
|
||||
channel_id: String,
|
||||
description: String,
|
||||
$emoji_id: String,
|
||||
emoji_name: String
|
||||
};
|
||||
|
||||
export interface GuildAddChannelToWelcomeScreenSchema {
|
||||
channel_id: string;
|
||||
description: string;
|
||||
emoji_id?: string;
|
||||
emoji_name: string;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
export const InviteCreateSchema = {
|
||||
$target_user_id: String,
|
||||
$target_type: String,
|
||||
$validate: String, //? wtf is this
|
||||
$max_age: Number,
|
||||
$max_uses: Number,
|
||||
$temporary: Boolean,
|
||||
$unique: Boolean,
|
||||
$target_user: String,
|
||||
$target_user_type: Number
|
||||
};
|
||||
export interface InviteCreateSchema {
|
||||
target_user_id?: String;
|
||||
target_type?: String;
|
||||
validate?: String; //? wtf is this
|
||||
max_age?: Number;
|
||||
max_uses?: Number;
|
||||
temporary?: Boolean;
|
||||
unique?: Boolean;
|
||||
target_user?: String;
|
||||
target_user_type?: Number;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
export const MemberCreateSchema = {
|
||||
id: String,
|
||||
nick: String,
|
||||
guild_id: String,
|
||||
joined_at: Date
|
||||
};
|
||||
|
||||
export interface MemberCreateSchema {
|
||||
id: string;
|
||||
nick: string;
|
||||
guild_id: string;
|
||||
joined_at: Date;
|
||||
}
|
||||
|
||||
export const MemberNickChangeSchema = {
|
||||
nick: String
|
||||
};
|
||||
|
||||
export interface MemberNickChangeSchema {
|
||||
nick: string;
|
||||
}
|
||||
|
||||
export const MemberChangeSchema = {
|
||||
$roles: [String]
|
||||
};
|
||||
|
||||
export interface MemberChangeSchema {
|
||||
roles?: string[];
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import { Embed, EmbedImage } from "@fosscord/server-util";
|
||||
import { Length } from "../util/instanceOf";
|
||||
|
||||
export const MessageCreateSchema = {
|
||||
$content: new Length(String, 0, 2000),
|
||||
$nonce: String,
|
||||
$tts: Boolean,
|
||||
$flags: BigInt,
|
||||
$embed: {
|
||||
$title: new Length(String, 0, 256), //title of embed
|
||||
$type: String, // type of embed (always "rich" for webhook embeds)
|
||||
$description: new Length(String, 0, 2048), // description of embed
|
||||
$url: String, // url of embed
|
||||
$timestamp: String, // ISO8601 timestamp
|
||||
$color: Number, // color code of the embed
|
||||
$footer: {
|
||||
text: new Length(String, 0, 2048),
|
||||
icon_url: String,
|
||||
proxy_icon_url: String
|
||||
}, // footer object footer information
|
||||
$image: EmbedImage, // image object image information
|
||||
$thumbnail: EmbedImage, // thumbnail object thumbnail information
|
||||
$video: EmbedImage, // video object video information
|
||||
$provider: {
|
||||
name: String,
|
||||
url: String
|
||||
}, // provider object provider information
|
||||
$author: {
|
||||
name: new Length(String, 0, 256),
|
||||
url: String,
|
||||
icon_url: String,
|
||||
proxy_icon_url: String
|
||||
}, // author object author information
|
||||
$fields: new Length(
|
||||
[
|
||||
{
|
||||
name: new Length(String, 0, 256),
|
||||
value: new Length(String, 0, 1024),
|
||||
$inline: Boolean
|
||||
}
|
||||
],
|
||||
0,
|
||||
25
|
||||
)
|
||||
},
|
||||
$allowed_mentions: {
|
||||
$parse: [String],
|
||||
$roles: [String],
|
||||
$users: [String],
|
||||
$replied_user: Boolean
|
||||
},
|
||||
$message_reference: {
|
||||
message_id: String,
|
||||
channel_id: String,
|
||||
$guild_id: String,
|
||||
$fail_if_not_exists: Boolean
|
||||
},
|
||||
$payload_json: String,
|
||||
$file: Object
|
||||
};
|
||||
|
||||
export interface MessageCreateSchema {
|
||||
content?: string;
|
||||
nonce?: string;
|
||||
tts?: boolean;
|
||||
flags?: bigint;
|
||||
embed?: Embed & { timestamp?: string };
|
||||
allowed_mentions?: {
|
||||
parse?: string[];
|
||||
roles?: string[];
|
||||
users?: string[];
|
||||
replied_user?: boolean;
|
||||
};
|
||||
message_reference?: {
|
||||
message_id: string;
|
||||
channel_id: string;
|
||||
guild_id?: string;
|
||||
fail_if_not_exists: boolean;
|
||||
};
|
||||
payload_json?: string;
|
||||
file?: any;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
export const RoleModifySchema = {
|
||||
$name: String,
|
||||
$permissions: BigInt,
|
||||
$color: Number,
|
||||
$hoist: Boolean, // whether the role should be displayed separately in the sidebar
|
||||
$mentionable: Boolean, // whether the role should be mentionable
|
||||
$position: Number
|
||||
};
|
||||
|
||||
export interface RoleModifySchema {
|
||||
name?: string;
|
||||
permissions?: BigInt;
|
||||
color?: number;
|
||||
hoist?: boolean; // whether the role should be displayed separately in the sidebar
|
||||
mentionable?: boolean; // whether the role should be mentionable
|
||||
position?: number;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
export const TemplateCreateSchema = {
|
||||
name: String,
|
||||
$description: String,
|
||||
};
|
||||
|
||||
export interface TemplateCreateSchema {
|
||||
name: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export const TemplateModifySchema = {
|
||||
name: String,
|
||||
$description: String,
|
||||
};
|
||||
|
||||
export interface TemplateModifySchema {
|
||||
name: string;
|
||||
description?: string;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Length } from "../util/instanceOf";
|
||||
|
||||
export const UserModifySchema = {
|
||||
$username: new Length(String, 2, 32),
|
||||
$avatar: String,
|
||||
$bio: new Length(String, 0, 190),
|
||||
$accent_color: Number,
|
||||
$banner: String,
|
||||
$password: String,
|
||||
$new_password: String,
|
||||
$code: String // 2fa code
|
||||
};
|
||||
|
||||
export interface UserModifySchema {
|
||||
username?: string;
|
||||
avatar?: string | null;
|
||||
bio?: string;
|
||||
accent_color?: number | null;
|
||||
banner?: string | null;
|
||||
password?: string;
|
||||
new_password?: string;
|
||||
code?: string;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
// https://discord.com/developers/docs/resources/guild#guild-widget-object
|
||||
export const WidgetModifySchema = {
|
||||
enabled: Boolean, // whether the widget is enabled
|
||||
channel_id: String // the widget channel id
|
||||
};
|
||||
|
||||
export interface WidgetModifySchema {
|
||||
enabled: boolean; // whether the widget is enabled
|
||||
channel_id: string; // the widget channel id
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
process.on("uncaughtException", console.error);
|
||||
process.on("unhandledRejection", console.error);
|
||||
|
||||
import "missing-native-js-functions";
|
||||
import { config } from "dotenv";
|
||||
config();
|
||||
import { FosscordServer } from "./Server";
|
||||
import cluster from "cluster";
|
||||
import os from "os";
|
||||
const cores = Number(process.env.threads) || os.cpus().length;
|
||||
|
||||
if (cluster.isMaster && process.env.NODE_ENV == "production") {
|
||||
console.log(`Primary ${process.pid} is running`);
|
||||
|
||||
// Fork workers.
|
||||
for (let i = 0; i < cores; i++) {
|
||||
cluster.fork();
|
||||
}
|
||||
|
||||
cluster.on("exit", (worker, code, signal) => {
|
||||
console.log(`worker ${worker.process.pid} died, restart worker`);
|
||||
cluster.fork();
|
||||
});
|
||||
} else {
|
||||
var port = Number(process.env.PORT) || 3001;
|
||||
|
||||
const server = new FosscordServer({ port });
|
||||
server.start().catch(console.error);
|
||||
|
||||
// @ts-ignore
|
||||
global.server = server;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
const jwa = require("jwa");
|
||||
|
||||
var STR64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_".split("");
|
||||
|
||||
function base64url(string: string, encoding: string) {
|
||||
// @ts-ignore
|
||||
return Buffer.from(string, encoding).toString("base64").replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
|
||||
}
|
||||
|
||||
function to64String(input: number, current = ""): string {
|
||||
if (input < 0 && current.length == 0) {
|
||||
input = input * -1;
|
||||
}
|
||||
var modify = input % 64;
|
||||
var remain = Math.floor(input / 64);
|
||||
var result = STR64[modify] + current;
|
||||
return remain <= 0 ? result : to64String(remain, result);
|
||||
}
|
||||
|
||||
function to64Parse(input: string) {
|
||||
var result = 0;
|
||||
var toProc = input.split("");
|
||||
var e;
|
||||
for (e in toProc) {
|
||||
result = result * 64 + STR64.indexOf(toProc[e]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
const start = `${base64url("311129357362135041")}.${to64String(Date.now())}`;
|
||||
const signature = jwa("HS256").sign(start, `test`);
|
||||
const token = `${start}.${signature}`;
|
||||
console.log(token);
|
||||
|
||||
// MzExMTI5MzU3MzYyMTM1MDQx.XdQb_rA.907VgF60kocnOTl32MSUWGSSzbAytQ0jbt36KjLaxuY
|
||||
// MzExMTI5MzU3MzYyMTM1MDQx.XdQbaPy.4vGx4L7IuFJGsRe6IL3BeybLIvbx4Vauvx12pwNsy2U
|
||||
@@ -0,0 +1,13 @@
|
||||
import jwt from "jsonwebtoken";
|
||||
|
||||
const algorithm = "HS256";
|
||||
const iat = Math.floor(Date.now() / 1000);
|
||||
|
||||
// @ts-ignore
|
||||
const token = jwt.sign({ id: "311129357362135041" }, "secret", {
|
||||
algorithm,
|
||||
});
|
||||
console.log(token);
|
||||
|
||||
const decoded = jwt.verify(token, "secret", { algorithms: [algorithm] });
|
||||
console.log(decoded);
|
||||
@@ -0,0 +1,40 @@
|
||||
import mongoose, { Schema, Types } from "mongoose";
|
||||
require("mongoose-long")(mongoose);
|
||||
|
||||
const userSchema = new Schema({
|
||||
id: String,
|
||||
});
|
||||
|
||||
const messageSchema = new Schema({
|
||||
id: String,
|
||||
content: String,
|
||||
});
|
||||
const message = mongoose.model("message", messageSchema, "messages");
|
||||
const user = mongoose.model("user", userSchema, "users");
|
||||
|
||||
messageSchema.virtual("u", {
|
||||
ref: user,
|
||||
localField: "id",
|
||||
foreignField: "id",
|
||||
justOne: true,
|
||||
});
|
||||
|
||||
messageSchema.set("toObject", { virtuals: true });
|
||||
messageSchema.set("toJSON", { virtuals: true });
|
||||
|
||||
async function main() {
|
||||
const conn = await mongoose.connect("mongodb://localhost:27017/lambert?readPreference=secondaryPreferred", {
|
||||
useNewUrlParser: true,
|
||||
useUnifiedTopology: false,
|
||||
});
|
||||
console.log("connected");
|
||||
|
||||
// const u = await new user({ name: "test" }).save();
|
||||
// await new message({ user: u._id, content: "test" }).save();
|
||||
|
||||
const test = await message.findOne({}).populate("u").exec();
|
||||
// @ts-ignore
|
||||
console.log(test?.toJSON());
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -0,0 +1,12 @@
|
||||
import { check } from "./../util/passwordStrength";
|
||||
|
||||
console.log(check("123456789012345"));
|
||||
// -> 0.25
|
||||
console.log(check("ABCDEFGHIJKLMOPQ"));
|
||||
// -> 0.25
|
||||
console.log(check("ABC123___...123"));
|
||||
// ->
|
||||
console.log(check(""));
|
||||
// ->
|
||||
// console.log(check(""));
|
||||
// // ->
|
||||
@@ -0,0 +1,39 @@
|
||||
// @ts-nocheck
|
||||
import "missing-native-js-functions";
|
||||
import { config } from "dotenv";
|
||||
config();
|
||||
import { DiscordServer } from "../Server";
|
||||
import fetch from "node-fetch";
|
||||
import { promises } from "fs";
|
||||
const count = 100;
|
||||
|
||||
async function main() {
|
||||
const server = new DiscordServer({ port: 3000 });
|
||||
await server.start();
|
||||
|
||||
const tasks = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
tasks.push(test());
|
||||
}
|
||||
|
||||
await Promise.all(tasks);
|
||||
|
||||
console.log("logging in 5secs");
|
||||
setTimeout(async () => {
|
||||
await test();
|
||||
|
||||
process.exit();
|
||||
}, 5000);
|
||||
}
|
||||
main();
|
||||
|
||||
async function test() {
|
||||
const res = await fetch("http://localhost:3000/api/v8/guilds/813524615463698433/members/813524464300982272", {
|
||||
headers: {
|
||||
authorization:
|
||||
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjgxMzUyNDQ2NDMwMDk4MjI3MiIsImlhdCI6MTYxNDAyOTc0Nn0.6WQiU4D5HHRi3sliHOQe1hsW-hZTEttvdtZuNIdviNI",
|
||||
},
|
||||
});
|
||||
|
||||
return await res.text();
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import { Snowflake } from "@fosscord/server-util";
|
||||
|
||||
console.log(Snowflake.deconstruct("0"));
|
||||
@@ -0,0 +1,47 @@
|
||||
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+";
|
||||
|
||||
// binary to string lookup table
|
||||
const b2s = alphabet.split("");
|
||||
|
||||
// string to binary lookup table
|
||||
// 123 == 'z'.charCodeAt(0) + 1
|
||||
const s2b = new Array(123);
|
||||
for (let i = 0; i < alphabet.length; i++) {
|
||||
s2b[alphabet.charCodeAt(i)] = i;
|
||||
}
|
||||
|
||||
// number to base64
|
||||
export const ntob = (n: number): string => {
|
||||
if (n < 0) return `-${ntob(-n)}`;
|
||||
|
||||
let lo = n >>> 0;
|
||||
let hi = (n / 4294967296) >>> 0;
|
||||
|
||||
let right = "";
|
||||
while (hi > 0) {
|
||||
right = b2s[0x3f & lo] + right;
|
||||
lo >>>= 6;
|
||||
lo |= (0x3f & hi) << 26;
|
||||
hi >>>= 6;
|
||||
}
|
||||
|
||||
let left = "";
|
||||
do {
|
||||
left = b2s[0x3f & lo] + left;
|
||||
lo >>>= 6;
|
||||
} while (lo > 0);
|
||||
|
||||
return left + right;
|
||||
};
|
||||
|
||||
// base64 to number
|
||||
export const bton = (base64: string) => {
|
||||
let number = 0;
|
||||
const sign = base64.charAt(0) === "-" ? 1 : 0;
|
||||
|
||||
for (let i = sign; i < base64.length; i++) {
|
||||
number = number * 64 + s2b[base64.charCodeAt(i)];
|
||||
}
|
||||
|
||||
return sign ? -number : number;
|
||||
};
|
||||
@@ -0,0 +1,56 @@
|
||||
import {
|
||||
ChannelCreateEvent,
|
||||
ChannelModel,
|
||||
ChannelType,
|
||||
getPermission,
|
||||
GuildModel,
|
||||
Snowflake,
|
||||
TextChannel,
|
||||
VoiceChannel
|
||||
} from "@fosscord/server-util";
|
||||
import { HTTPError } from "lambert-server";
|
||||
import { emitEvent } from "./Event";
|
||||
|
||||
// TODO: DM channel
|
||||
export async function createChannel(channel: Partial<TextChannel | VoiceChannel>, user_id: string = "0") {
|
||||
|
||||
// Always check if user has permission first
|
||||
const permissions = await getPermission(user_id, channel.guild_id);
|
||||
permissions.hasThrow("MANAGE_CHANNELS");
|
||||
|
||||
switch (channel.type) {
|
||||
case ChannelType.GUILD_TEXT:
|
||||
case ChannelType.GUILD_VOICE:
|
||||
if (channel.parent_id) {
|
||||
const exists = await ChannelModel.findOne({ id: channel.parent_id }, { guild_id: true }).exec();
|
||||
if (!exists) throw new HTTPError("Parent id channel doesn't exist", 400);
|
||||
if (exists.guild_id !== channel.guild_id) throw new HTTPError("The category channel needs to be in the guild");
|
||||
}
|
||||
break;
|
||||
case ChannelType.GUILD_CATEGORY:
|
||||
break;
|
||||
case ChannelType.DM:
|
||||
case ChannelType.GROUP_DM:
|
||||
throw new HTTPError("You can't create a dm channel in a guild");
|
||||
// TODO: check if guild is community server
|
||||
case ChannelType.GUILD_STORE:
|
||||
case ChannelType.GUILD_NEWS:
|
||||
default:
|
||||
throw new HTTPError("Not yet supported");
|
||||
}
|
||||
|
||||
if (!channel.permission_overwrites) channel.permission_overwrites = [];
|
||||
// TODO: auto generate position
|
||||
|
||||
channel = await new ChannelModel({
|
||||
...channel,
|
||||
id: Snowflake.generate(),
|
||||
created_at: new Date(),
|
||||
// @ts-ignore
|
||||
recipient_ids: null
|
||||
}).save();
|
||||
|
||||
await emitEvent({ event: "CHANNEL_CREATE", data: channel, guild_id: channel.guild_id } as ChannelCreateEvent);
|
||||
|
||||
return channel;
|
||||
}
|
||||
@@ -0,0 +1,372 @@
|
||||
// @ts-nocheck
|
||||
import Ajv, { JSONSchemaType } from "ajv";
|
||||
import { getConfigPathForFile } from "@fosscord/server-util/dist/util/Config";
|
||||
import { Config } from "@fosscord/server-util";
|
||||
|
||||
export interface RateLimitOptions {
|
||||
count: number;
|
||||
timespan: number;
|
||||
}
|
||||
|
||||
export interface DefaultOptions {
|
||||
gateway: string;
|
||||
general: {
|
||||
instance_id: string;
|
||||
};
|
||||
permissions: {
|
||||
user: {
|
||||
createGuilds: boolean;
|
||||
};
|
||||
};
|
||||
limits: {
|
||||
user: {
|
||||
maxGuilds: number;
|
||||
maxUsername: number;
|
||||
maxFriends: number;
|
||||
};
|
||||
guild: {
|
||||
maxRoles: number;
|
||||
maxMembers: number;
|
||||
maxChannels: number;
|
||||
maxChannelsInCategory: number;
|
||||
hideOfflineMember: number;
|
||||
};
|
||||
message: {
|
||||
characters: number;
|
||||
ttsCharacters: number;
|
||||
maxReactions: number;
|
||||
maxAttachmentSize: number;
|
||||
maxBulkDelete: number;
|
||||
};
|
||||
channel: {
|
||||
maxPins: number;
|
||||
maxTopic: number;
|
||||
};
|
||||
rate: {
|
||||
ip: {
|
||||
enabled: boolean;
|
||||
count: number;
|
||||
timespan: number;
|
||||
};
|
||||
routes: {
|
||||
auth?: {
|
||||
login?: RateLimitOptions;
|
||||
register?: RateLimitOptions;
|
||||
};
|
||||
channel?: string;
|
||||
// TODO: rate limit configuration for all routes
|
||||
};
|
||||
};
|
||||
};
|
||||
security: {
|
||||
jwtSecret: string;
|
||||
forwadedFor: string | null;
|
||||
captcha: {
|
||||
enabled: boolean;
|
||||
service: "recaptcha" | "hcaptcha" | null; // TODO: hcaptcha, custom
|
||||
sitekey: string | null;
|
||||
secret: string | null;
|
||||
};
|
||||
};
|
||||
login: {
|
||||
requireCaptcha: boolean;
|
||||
};
|
||||
register: {
|
||||
email: {
|
||||
necessary: boolean;
|
||||
allowlist: boolean;
|
||||
blocklist: boolean;
|
||||
domains: string[];
|
||||
};
|
||||
dateOfBirth: {
|
||||
necessary: boolean;
|
||||
minimum: number; // in years
|
||||
};
|
||||
requireCaptcha: boolean;
|
||||
requireInvite: boolean;
|
||||
allowNewRegistration: boolean;
|
||||
allowMultipleAccounts: boolean;
|
||||
password: {
|
||||
minLength: number;
|
||||
minNumbers: number;
|
||||
minUpperCase: number;
|
||||
minSymbols: number;
|
||||
blockInsecureCommonPasswords: boolean; // TODO: efficiently save password blocklist in database
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
const schema: JSONSchemaType<DefaultOptions> & {
|
||||
definitions: {
|
||||
rateLimitOptions: JSONSchemaType<RateLimitOptions>;
|
||||
};
|
||||
} = {
|
||||
type: "object",
|
||||
definitions: {
|
||||
rateLimitOptions: {
|
||||
type: "object",
|
||||
properties: {
|
||||
count: { type: "number" },
|
||||
timespan: { type: "number" }
|
||||
},
|
||||
required: ["count", "timespan"]
|
||||
}
|
||||
},
|
||||
properties: {
|
||||
gateway: {
|
||||
type: "string"
|
||||
},
|
||||
general: {
|
||||
type: "object",
|
||||
properties: {
|
||||
instance_id: {
|
||||
type: "string"
|
||||
}
|
||||
},
|
||||
required: ["instance_id"],
|
||||
additionalProperties: false
|
||||
},
|
||||
permissions: {
|
||||
type: "object",
|
||||
properties: {
|
||||
user: {
|
||||
type: "object",
|
||||
properties: {
|
||||
createGuilds: {
|
||||
type: "boolean"
|
||||
}
|
||||
},
|
||||
required: ["createGuilds"],
|
||||
additionalProperties: false
|
||||
}
|
||||
},
|
||||
required: ["user"],
|
||||
additionalProperties: false
|
||||
},
|
||||
limits: {
|
||||
type: "object",
|
||||
properties: {
|
||||
user: {
|
||||
type: "object",
|
||||
properties: {
|
||||
maxFriends: {
|
||||
type: "number"
|
||||
},
|
||||
maxGuilds: {
|
||||
type: "number"
|
||||
},
|
||||
maxUsername: {
|
||||
type: "number"
|
||||
}
|
||||
},
|
||||
required: ["maxFriends", "maxGuilds", "maxUsername"],
|
||||
additionalProperties: false
|
||||
},
|
||||
guild: {
|
||||
type: "object",
|
||||
properties: {
|
||||
maxRoles: {
|
||||
type: "number"
|
||||
},
|
||||
maxMembers: {
|
||||
type: "number"
|
||||
},
|
||||
maxChannels: {
|
||||
type: "number"
|
||||
},
|
||||
maxChannelsInCategory: {
|
||||
type: "number"
|
||||
},
|
||||
hideOfflineMember: {
|
||||
type: "number"
|
||||
}
|
||||
},
|
||||
required: ["maxRoles", "maxMembers", "maxChannels", "maxChannelsInCategory", "hideOfflineMember"],
|
||||
additionalProperties: false
|
||||
},
|
||||
message: {
|
||||
type: "object",
|
||||
properties: {
|
||||
characters: {
|
||||
type: "number"
|
||||
},
|
||||
ttsCharacters: {
|
||||
type: "number"
|
||||
},
|
||||
maxReactions: {
|
||||
type: "number"
|
||||
},
|
||||
maxAttachmentSize: {
|
||||
type: "number"
|
||||
},
|
||||
maxBulkDelete: {
|
||||
type: "number"
|
||||
}
|
||||
},
|
||||
required: ["characters", "ttsCharacters", "maxReactions", "maxAttachmentSize", "maxBulkDelete"],
|
||||
additionalProperties: false
|
||||
},
|
||||
channel: {
|
||||
type: "object",
|
||||
properties: {
|
||||
maxPins: {
|
||||
type: "number"
|
||||
},
|
||||
maxTopic: {
|
||||
type: "number"
|
||||
}
|
||||
},
|
||||
required: ["maxPins", "maxTopic"],
|
||||
additionalProperties: false
|
||||
},
|
||||
rate: {
|
||||
type: "object",
|
||||
properties: {
|
||||
ip: {
|
||||
type: "object",
|
||||
properties: {
|
||||
enabled: { type: "boolean" },
|
||||
count: { type: "number" },
|
||||
timespan: { type: "number" }
|
||||
},
|
||||
required: ["enabled", "count", "timespan"],
|
||||
additionalProperties: false
|
||||
},
|
||||
routes: {
|
||||
type: "object",
|
||||
properties: {
|
||||
auth: {
|
||||
type: "object",
|
||||
properties: {
|
||||
login: { $ref: "#/definitions/rateLimitOptions" },
|
||||
register: { $ref: "#/definitions/rateLimitOptions" }
|
||||
},
|
||||
nullable: true,
|
||||
required: [],
|
||||
additionalProperties: false
|
||||
},
|
||||
channel: {
|
||||
type: "string",
|
||||
nullable: true
|
||||
}
|
||||
},
|
||||
required: [],
|
||||
additionalProperties: false
|
||||
}
|
||||
},
|
||||
required: ["ip", "routes"]
|
||||
}
|
||||
},
|
||||
required: ["channel", "guild", "message", "rate", "user"],
|
||||
additionalProperties: false
|
||||
},
|
||||
security: {
|
||||
type: "object",
|
||||
properties: {
|
||||
jwtSecret: {
|
||||
type: "string"
|
||||
},
|
||||
forwadedFor: {
|
||||
type: "string",
|
||||
nullable: true
|
||||
},
|
||||
captcha: {
|
||||
type: "object",
|
||||
properties: {
|
||||
enabled: { type: "boolean" },
|
||||
service: {
|
||||
type: "string",
|
||||
enum: ["hcaptcha", "recaptcha", null],
|
||||
nullable: true
|
||||
},
|
||||
sitekey: {
|
||||
type: "string",
|
||||
nullable: true
|
||||
},
|
||||
secret: {
|
||||
type: "string",
|
||||
nullable: true
|
||||
}
|
||||
},
|
||||
required: ["enabled", "secret", "service", "sitekey"],
|
||||
additionalProperties: false
|
||||
}
|
||||
},
|
||||
required: ["captcha", "forwadedFor", "jwtSecret"],
|
||||
additionalProperties: false
|
||||
},
|
||||
login: {
|
||||
type: "object",
|
||||
properties: {
|
||||
requireCaptcha: { type: "boolean" }
|
||||
},
|
||||
required: ["requireCaptcha"],
|
||||
additionalProperties: false
|
||||
},
|
||||
register: {
|
||||
type: "object",
|
||||
properties: {
|
||||
email: {
|
||||
type: "object",
|
||||
properties: {
|
||||
necessary: { type: "boolean" },
|
||||
allowlist: { type: "boolean" },
|
||||
blocklist: { type: "boolean" },
|
||||
domains: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ["allowlist", "blocklist", "domains", "necessary"],
|
||||
additionalProperties: false
|
||||
},
|
||||
dateOfBirth: {
|
||||
type: "object",
|
||||
properties: {
|
||||
necessary: { type: "boolean" },
|
||||
minimum: { type: "number" }
|
||||
},
|
||||
required: ["minimum", "necessary"],
|
||||
additionalProperties: false
|
||||
},
|
||||
requireCaptcha: { type: "boolean" },
|
||||
requireInvite: { type: "boolean" },
|
||||
allowNewRegistration: { type: "boolean" },
|
||||
allowMultipleAccounts: { type: "boolean" },
|
||||
password: {
|
||||
type: "object",
|
||||
properties: {
|
||||
minLength: { type: "number" },
|
||||
minNumbers: { type: "number" },
|
||||
minUpperCase: { type: "number" },
|
||||
minSymbols: { type: "number" },
|
||||
blockInsecureCommonPasswords: { type: "boolean" }
|
||||
},
|
||||
required: ["minLength", "minNumbers", "minUpperCase", "minSymbols", "blockInsecureCommonPasswords"],
|
||||
additionalProperties: false
|
||||
}
|
||||
},
|
||||
required: [
|
||||
"allowMultipleAccounts",
|
||||
"allowNewRegistration",
|
||||
"dateOfBirth",
|
||||
"email",
|
||||
"password",
|
||||
"requireCaptcha",
|
||||
"requireInvite"
|
||||
],
|
||||
additionalProperties: false
|
||||
}
|
||||
},
|
||||
required: ["gateway", "general", "limits", "login", "permissions", "register", "security"],
|
||||
additionalProperties: false
|
||||
};
|
||||
|
||||
const ajv = new Ajv();
|
||||
const validator = ajv.compile(schema);
|
||||
|
||||
const configPath = getConfigPathForFile("fosscord", "api", ".json");
|
||||
|
||||
export const apiConfig = new Config<DefaultOptions>({ path: configPath, schemaValidator: validator, schema: schema });
|
||||
@@ -0,0 +1,593 @@
|
||||
export const WSCodes = {
|
||||
1000: "WS_CLOSE_REQUESTED",
|
||||
4004: "TOKEN_INVALID",
|
||||
4010: "SHARDING_INVALID",
|
||||
4011: "SHARDING_REQUIRED",
|
||||
4013: "INVALID_INTENTS",
|
||||
4014: "DISALLOWED_INTENTS",
|
||||
};
|
||||
|
||||
const AllowedImageFormats = ["webp", "png", "jpg", "jpeg", "gif"];
|
||||
|
||||
const AllowedImageSizes = Array.from({ length: 9 }, (e, i) => 2 ** (i + 4));
|
||||
|
||||
function makeImageUrl(root: string, { format = "webp", size = 512 } = {}) {
|
||||
if (format && !AllowedImageFormats.includes(format)) throw new Error("IMAGE_FORMAT: " + format);
|
||||
if (size && !AllowedImageSizes.includes(size)) throw new RangeError("IMAGE_SIZE: " + size);
|
||||
return `${root}.${format}${size ? `?size=${size}` : ""}`;
|
||||
}
|
||||
/**
|
||||
* Options for Image URLs.
|
||||
* @typedef {Object} ImageURLOptions
|
||||
* @property {string} [format] One of `webp`, `png`, `jpg`, `jpeg`, `gif`. If no format is provided,
|
||||
* defaults to `webp`.
|
||||
* @property {boolean} [dynamic] If true, the format will dynamically change to `gif` for
|
||||
* animated avatars; the default is false.
|
||||
* @property {number} [size] One of `16`, `32`, `64`, `128`, `256`, `512`, `1024`, `2048`, `4096`
|
||||
*/
|
||||
|
||||
export const Endpoints = {
|
||||
CDN(root: string) {
|
||||
return {
|
||||
Emoji: (emojiID: string, format = "png") => `${root}/emojis/${emojiID}.${format}`,
|
||||
Asset: (name: string) => `${root}/assets/${name}`,
|
||||
DefaultAvatar: (discriminator: string) => `${root}/embed/avatars/${discriminator}.png`,
|
||||
Avatar: (user_id: string, hash: string, format = "webp", size: number, dynamic = false) => {
|
||||
if (dynamic) format = hash.startsWith("a_") ? "gif" : format;
|
||||
return makeImageUrl(`${root}/avatars/${user_id}/${hash}`, { format, size });
|
||||
},
|
||||
Banner: (guildID: string, hash: string, format = "webp", size: number) =>
|
||||
makeImageUrl(`${root}/banners/${guildID}/${hash}`, { format, size }),
|
||||
Icon: (guildID: string, hash: string, format = "webp", size: number, dynamic = false) => {
|
||||
if (dynamic) format = hash.startsWith("a_") ? "gif" : format;
|
||||
return makeImageUrl(`${root}/icons/${guildID}/${hash}`, { format, size });
|
||||
},
|
||||
AppIcon: (clientID: string, hash: string, { format = "webp", size }: { format?: string; size?: number } = {}) =>
|
||||
makeImageUrl(`${root}/app-icons/${clientID}/${hash}`, { size, format }),
|
||||
AppAsset: (clientID: string, hash: string, { format = "webp", size }: { format?: string; size?: number } = {}) =>
|
||||
makeImageUrl(`${root}/app-assets/${clientID}/${hash}`, { size, format }),
|
||||
GDMIcon: (channelID: string, hash: string, format = "webp", size: number) =>
|
||||
makeImageUrl(`${root}/channel-icons/${channelID}/${hash}`, { size, format }),
|
||||
Splash: (guildID: string, hash: string, format = "webp", size: number) =>
|
||||
makeImageUrl(`${root}/splashes/${guildID}/${hash}`, { size, format }),
|
||||
DiscoverySplash: (guildID: string, hash: string, format = "webp", size: number) =>
|
||||
makeImageUrl(`${root}/discovery-splashes/${guildID}/${hash}`, { size, format }),
|
||||
TeamIcon: (teamID: string, hash: string, { format = "webp", size }: { format?: string; size?: number } = {}) =>
|
||||
makeImageUrl(`${root}/team-icons/${teamID}/${hash}`, { size, format }),
|
||||
};
|
||||
},
|
||||
invite: (root: string, code: string) => `${root}/${code}`,
|
||||
botGateway: "/gateway/bot",
|
||||
};
|
||||
|
||||
/**
|
||||
* The current status of the client. Here are the available statuses:
|
||||
* * READY: 0
|
||||
* * CONNECTING: 1
|
||||
* * RECONNECTING: 2
|
||||
* * IDLE: 3
|
||||
* * NEARLY: 4
|
||||
* * DISCONNECTED: 5
|
||||
* * WAITING_FOR_GUILDS: 6
|
||||
* * IDENTIFYING: 7
|
||||
* * RESUMING: 8
|
||||
* @typedef {number} Status
|
||||
*/
|
||||
export const Status = {
|
||||
READY: 0,
|
||||
CONNECTING: 1,
|
||||
RECONNECTING: 2,
|
||||
IDLE: 3,
|
||||
NEARLY: 4,
|
||||
DISCONNECTED: 5,
|
||||
WAITING_FOR_GUILDS: 6,
|
||||
IDENTIFYING: 7,
|
||||
RESUMING: 8,
|
||||
};
|
||||
|
||||
/**
|
||||
* The current status of a voice connection. Here are the available statuses:
|
||||
* * CONNECTED: 0
|
||||
* * CONNECTING: 1
|
||||
* * AUTHENTICATING: 2
|
||||
* * RECONNECTING: 3
|
||||
* * DISCONNECTED: 4
|
||||
* @typedef {number} VoiceStatus
|
||||
*/
|
||||
export const VoiceStatus = {
|
||||
CONNECTED: 0,
|
||||
CONNECTING: 1,
|
||||
AUTHENTICATING: 2,
|
||||
RECONNECTING: 3,
|
||||
DISCONNECTED: 4,
|
||||
};
|
||||
|
||||
export const OPCodes = {
|
||||
DISPATCH: 0,
|
||||
HEARTBEAT: 1,
|
||||
IDENTIFY: 2,
|
||||
STATUS_UPDATE: 3,
|
||||
VOICE_STATE_UPDATE: 4,
|
||||
VOICE_GUILD_PING: 5,
|
||||
RESUME: 6,
|
||||
RECONNECT: 7,
|
||||
REQUEST_GUILD_MEMBERS: 8,
|
||||
INVALID_SESSION: 9,
|
||||
HELLO: 10,
|
||||
HEARTBEAT_ACK: 11,
|
||||
};
|
||||
|
||||
export const VoiceOPCodes = {
|
||||
IDENTIFY: 0,
|
||||
SELECT_PROTOCOL: 1,
|
||||
READY: 2,
|
||||
HEARTBEAT: 3,
|
||||
SESSION_DESCRIPTION: 4,
|
||||
SPEAKING: 5,
|
||||
HELLO: 8,
|
||||
CLIENT_CONNECT: 12,
|
||||
CLIENT_DISCONNECT: 13,
|
||||
};
|
||||
|
||||
export const Events = {
|
||||
RATE_LIMIT: "rateLimit",
|
||||
CLIENT_READY: "ready",
|
||||
GUILD_CREATE: "guildCreate",
|
||||
GUILD_DELETE: "guildDelete",
|
||||
GUILD_UPDATE: "guildUpdate",
|
||||
GUILD_UNAVAILABLE: "guildUnavailable",
|
||||
GUILD_AVAILABLE: "guildAvailable",
|
||||
GUILD_MEMBER_ADD: "guildMemberAdd",
|
||||
GUILD_MEMBER_REMOVE: "guildMemberRemove",
|
||||
GUILD_MEMBER_UPDATE: "guildMemberUpdate",
|
||||
GUILD_MEMBER_AVAILABLE: "guildMemberAvailable",
|
||||
GUILD_MEMBER_SPEAKING: "guildMemberSpeaking",
|
||||
GUILD_MEMBERS_CHUNK: "guildMembersChunk",
|
||||
GUILD_INTEGRATIONS_UPDATE: "guildIntegrationsUpdate",
|
||||
GUILD_ROLE_CREATE: "roleCreate",
|
||||
GUILD_ROLE_DELETE: "roleDelete",
|
||||
INVITE_CREATE: "inviteCreate",
|
||||
INVITE_DELETE: "inviteDelete",
|
||||
GUILD_ROLE_UPDATE: "roleUpdate",
|
||||
GUILD_EMOJI_CREATE: "emojiCreate",
|
||||
GUILD_EMOJI_DELETE: "emojiDelete",
|
||||
GUILD_EMOJI_UPDATE: "emojiUpdate",
|
||||
GUILD_BAN_ADD: "guildBanAdd",
|
||||
GUILD_BAN_REMOVE: "guildBanRemove",
|
||||
CHANNEL_CREATE: "channelCreate",
|
||||
CHANNEL_DELETE: "channelDelete",
|
||||
CHANNEL_UPDATE: "channelUpdate",
|
||||
CHANNEL_PINS_UPDATE: "channelPinsUpdate",
|
||||
MESSAGE_CREATE: "message",
|
||||
MESSAGE_DELETE: "messageDelete",
|
||||
MESSAGE_UPDATE: "messageUpdate",
|
||||
MESSAGE_BULK_DELETE: "messageDeleteBulk",
|
||||
MESSAGE_REACTION_ADD: "messageReactionAdd",
|
||||
MESSAGE_REACTION_REMOVE: "messageReactionRemove",
|
||||
MESSAGE_REACTION_REMOVE_ALL: "messageReactionRemoveAll",
|
||||
MESSAGE_REACTION_REMOVE_EMOJI: "messageReactionRemoveEmoji",
|
||||
USER_UPDATE: "userUpdate",
|
||||
PRESENCE_UPDATE: "presenceUpdate",
|
||||
VOICE_SERVER_UPDATE: "voiceServerUpdate",
|
||||
VOICE_STATE_UPDATE: "voiceStateUpdate",
|
||||
VOICE_BROADCAST_SUBSCRIBE: "subscribe",
|
||||
VOICE_BROADCAST_UNSUBSCRIBE: "unsubscribe",
|
||||
TYPING_START: "typingStart",
|
||||
TYPING_STOP: "typingStop",
|
||||
WEBHOOKS_UPDATE: "webhookUpdate",
|
||||
ERROR: "error",
|
||||
WARN: "warn",
|
||||
DEBUG: "debug",
|
||||
SHARD_DISCONNECT: "shardDisconnect",
|
||||
SHARD_ERROR: "shardError",
|
||||
SHARD_RECONNECTING: "shardReconnecting",
|
||||
SHARD_READY: "shardReady",
|
||||
SHARD_RESUME: "shardResume",
|
||||
INVALIDATED: "invalidated",
|
||||
RAW: "raw",
|
||||
};
|
||||
|
||||
export const ShardEvents = {
|
||||
CLOSE: "close",
|
||||
DESTROYED: "destroyed",
|
||||
INVALID_SESSION: "invalidSession",
|
||||
READY: "ready",
|
||||
RESUMED: "resumed",
|
||||
ALL_READY: "allReady",
|
||||
};
|
||||
|
||||
/**
|
||||
* The type of Structure allowed to be a partial:
|
||||
* * USER
|
||||
* * CHANNEL (only affects DMChannels)
|
||||
* * GUILD_MEMBER
|
||||
* * MESSAGE
|
||||
* * REACTION
|
||||
* <warn>Partials require you to put checks in place when handling data, read the Partials topic listed in the
|
||||
* sidebar for more information.</warn>
|
||||
* @typedef {string} PartialType
|
||||
*/
|
||||
export const PartialTypes = keyMirror(["USER", "CHANNEL", "GUILD_MEMBER", "MESSAGE", "REACTION"]);
|
||||
|
||||
/**
|
||||
* The type of a websocket message event, e.g. `MESSAGE_CREATE`. Here are the available events:
|
||||
* * READY
|
||||
* * RESUMED
|
||||
* * GUILD_CREATE
|
||||
* * GUILD_DELETE
|
||||
* * GUILD_UPDATE
|
||||
* * INVITE_CREATE
|
||||
* * INVITE_DELETE
|
||||
* * GUILD_MEMBER_ADD
|
||||
* * GUILD_MEMBER_REMOVE
|
||||
* * GUILD_MEMBER_UPDATE
|
||||
* * GUILD_MEMBERS_CHUNK
|
||||
* * GUILD_INTEGRATIONS_UPDATE
|
||||
* * GUILD_ROLE_CREATE
|
||||
* * GUILD_ROLE_DELETE
|
||||
* * GUILD_ROLE_UPDATE
|
||||
* * GUILD_BAN_ADD
|
||||
* * GUILD_BAN_REMOVE
|
||||
* * GUILD_EMOJIS_UPDATE
|
||||
* * CHANNEL_CREATE
|
||||
* * CHANNEL_DELETE
|
||||
* * CHANNEL_UPDATE
|
||||
* * CHANNEL_PINS_UPDATE
|
||||
* * MESSAGE_CREATE
|
||||
* * MESSAGE_DELETE
|
||||
* * MESSAGE_UPDATE
|
||||
* * MESSAGE_DELETE_BULK
|
||||
* * MESSAGE_REACTION_ADD
|
||||
* * MESSAGE_REACTION_REMOVE
|
||||
* * MESSAGE_REACTION_REMOVE_ALL
|
||||
* * MESSAGE_REACTION_REMOVE_EMOJI
|
||||
* * USER_UPDATE
|
||||
* * PRESENCE_UPDATE
|
||||
* * TYPING_START
|
||||
* * VOICE_STATE_UPDATE
|
||||
* * VOICE_SERVER_UPDATE
|
||||
* * WEBHOOKS_UPDATE
|
||||
* @typedef {string} WSEventType
|
||||
*/
|
||||
export const WSEvents = keyMirror([
|
||||
"READY",
|
||||
"RESUMED",
|
||||
"GUILD_CREATE",
|
||||
"GUILD_DELETE",
|
||||
"GUILD_UPDATE",
|
||||
"INVITE_CREATE",
|
||||
"INVITE_DELETE",
|
||||
"GUILD_MEMBER_ADD",
|
||||
"GUILD_MEMBER_REMOVE",
|
||||
"GUILD_MEMBER_UPDATE",
|
||||
"GUILD_MEMBERS_CHUNK",
|
||||
"GUILD_INTEGRATIONS_UPDATE",
|
||||
"GUILD_ROLE_CREATE",
|
||||
"GUILD_ROLE_DELETE",
|
||||
"GUILD_ROLE_UPDATE",
|
||||
"GUILD_BAN_ADD",
|
||||
"GUILD_BAN_REMOVE",
|
||||
"GUILD_EMOJIS_UPDATE",
|
||||
"CHANNEL_CREATE",
|
||||
"CHANNEL_DELETE",
|
||||
"CHANNEL_UPDATE",
|
||||
"CHANNEL_PINS_UPDATE",
|
||||
"MESSAGE_CREATE",
|
||||
"MESSAGE_DELETE",
|
||||
"MESSAGE_UPDATE",
|
||||
"MESSAGE_DELETE_BULK",
|
||||
"MESSAGE_REACTION_ADD",
|
||||
"MESSAGE_REACTION_REMOVE",
|
||||
"MESSAGE_REACTION_REMOVE_ALL",
|
||||
"MESSAGE_REACTION_REMOVE_EMOJI",
|
||||
"USER_UPDATE",
|
||||
"PRESENCE_UPDATE",
|
||||
"TYPING_START",
|
||||
"VOICE_STATE_UPDATE",
|
||||
"VOICE_SERVER_UPDATE",
|
||||
"WEBHOOKS_UPDATE",
|
||||
]);
|
||||
|
||||
/**
|
||||
* The type of a message, e.g. `DEFAULT`. Here are the available types:
|
||||
* * DEFAULT
|
||||
* * RECIPIENT_ADD
|
||||
* * RECIPIENT_REMOVE
|
||||
* * CALL
|
||||
* * CHANNEL_NAME_CHANGE
|
||||
* * CHANNEL_ICON_CHANGE
|
||||
* * PINS_ADD
|
||||
* * GUILD_MEMBER_JOIN
|
||||
* * USER_PREMIUM_GUILD_SUBSCRIPTION
|
||||
* * USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_1
|
||||
* * USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_2
|
||||
* * USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_3
|
||||
* * CHANNEL_FOLLOW_ADD
|
||||
* * GUILD_DISCOVERY_DISQUALIFIED
|
||||
* * GUILD_DISCOVERY_REQUALIFIED
|
||||
* * REPLY
|
||||
* @typedef {string} MessageType
|
||||
*/
|
||||
export const MessageTypes = [
|
||||
"DEFAULT",
|
||||
"RECIPIENT_ADD",
|
||||
"RECIPIENT_REMOVE",
|
||||
"CALL",
|
||||
"CHANNEL_NAME_CHANGE",
|
||||
"CHANNEL_ICON_CHANGE",
|
||||
"PINS_ADD",
|
||||
"GUILD_MEMBER_JOIN",
|
||||
"USER_PREMIUM_GUILD_SUBSCRIPTION",
|
||||
"USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_1",
|
||||
"USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_2",
|
||||
"USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_3",
|
||||
"CHANNEL_FOLLOW_ADD",
|
||||
null,
|
||||
"GUILD_DISCOVERY_DISQUALIFIED",
|
||||
"GUILD_DISCOVERY_REQUALIFIED",
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
"REPLY",
|
||||
];
|
||||
|
||||
/**
|
||||
* The types of messages that are `System`. The available types are `MessageTypes` excluding:
|
||||
* * DEFAULT
|
||||
* * REPLY
|
||||
* @typedef {string} SystemMessageType
|
||||
*/
|
||||
export const SystemMessageTypes = MessageTypes.filter((type: string | null) => type && type !== "DEFAULT" && type !== "REPLY");
|
||||
|
||||
/**
|
||||
* <info>Bots cannot set a `CUSTOM_STATUS`, it is only for custom statuses received from users</info>
|
||||
* The type of an activity of a users presence, e.g. `PLAYING`. Here are the available types:
|
||||
* * PLAYING
|
||||
* * STREAMING
|
||||
* * LISTENING
|
||||
* * WATCHING
|
||||
* * CUSTOM_STATUS
|
||||
* * COMPETING
|
||||
* @typedef {string} ActivityType
|
||||
*/
|
||||
export const ActivityTypes = ["PLAYING", "STREAMING", "LISTENING", "WATCHING", "CUSTOM_STATUS", "COMPETING"];
|
||||
|
||||
export const ChannelTypes = {
|
||||
TEXT: 0,
|
||||
DM: 1,
|
||||
VOICE: 2,
|
||||
GROUP: 3,
|
||||
CATEGORY: 4,
|
||||
NEWS: 5,
|
||||
STORE: 6,
|
||||
};
|
||||
|
||||
export const ClientApplicationAssetTypes = {
|
||||
SMALL: 1,
|
||||
BIG: 2,
|
||||
};
|
||||
|
||||
export const Colors = {
|
||||
DEFAULT: 0x000000,
|
||||
WHITE: 0xffffff,
|
||||
AQUA: 0x1abc9c,
|
||||
GREEN: 0x2ecc71,
|
||||
BLUE: 0x3498db,
|
||||
YELLOW: 0xffff00,
|
||||
PURPLE: 0x9b59b6,
|
||||
LUMINOUS_VIVID_PINK: 0xe91e63,
|
||||
GOLD: 0xf1c40f,
|
||||
ORANGE: 0xe67e22,
|
||||
RED: 0xe74c3c,
|
||||
GREY: 0x95a5a6,
|
||||
NAVY: 0x34495e,
|
||||
DARK_AQUA: 0x11806a,
|
||||
DARK_GREEN: 0x1f8b4c,
|
||||
DARK_BLUE: 0x206694,
|
||||
DARK_PURPLE: 0x71368a,
|
||||
DARK_VIVID_PINK: 0xad1457,
|
||||
DARK_GOLD: 0xc27c0e,
|
||||
DARK_ORANGE: 0xa84300,
|
||||
DARK_RED: 0x992d22,
|
||||
DARK_GREY: 0x979c9f,
|
||||
DARKER_GREY: 0x7f8c8d,
|
||||
LIGHT_GREY: 0xbcc0c0,
|
||||
DARK_NAVY: 0x2c3e50,
|
||||
BLURPLE: 0x7289da,
|
||||
GREYPLE: 0x99aab5,
|
||||
DARK_BUT_NOT_BLACK: 0x2c2f33,
|
||||
NOT_QUITE_BLACK: 0x23272a,
|
||||
};
|
||||
|
||||
/**
|
||||
* The value set for the explicit content filter levels for a guild:
|
||||
* * DISABLED
|
||||
* * MEMBERS_WITHOUT_ROLES
|
||||
* * ALL_MEMBERS
|
||||
* @typedef {string} ExplicitContentFilterLevel
|
||||
*/
|
||||
export const ExplicitContentFilterLevels = ["DISABLED", "MEMBERS_WITHOUT_ROLES", "ALL_MEMBERS"];
|
||||
|
||||
/**
|
||||
* The value set for the verification levels for a guild:
|
||||
* * NONE
|
||||
* * LOW
|
||||
* * MEDIUM
|
||||
* * HIGH
|
||||
* * VERY_HIGH
|
||||
* @typedef {string} VerificationLevel
|
||||
*/
|
||||
export const VerificationLevels = ["NONE", "LOW", "MEDIUM", "HIGH", "VERY_HIGH"];
|
||||
|
||||
/**
|
||||
* An error encountered while performing an API request. Here are the potential errors:
|
||||
* * UNKNOWN_ACCOUNT
|
||||
* * UNKNOWN_APPLICATION
|
||||
* * UNKNOWN_CHANNEL
|
||||
* * UNKNOWN_GUILD
|
||||
* * UNKNOWN_INTEGRATION
|
||||
* * UNKNOWN_INVITE
|
||||
* * UNKNOWN_MEMBER
|
||||
* * UNKNOWN_MESSAGE
|
||||
* * UNKNOWN_OVERWRITE
|
||||
* * UNKNOWN_PROVIDER
|
||||
* * UNKNOWN_ROLE
|
||||
* * UNKNOWN_TOKEN
|
||||
* * UNKNOWN_USER
|
||||
* * UNKNOWN_EMOJI
|
||||
* * UNKNOWN_WEBHOOK
|
||||
* * UNKNOWN_BAN
|
||||
* * UNKNOWN_GUILD_TEMPLATE
|
||||
* * BOT_PROHIBITED_ENDPOINT
|
||||
* * BOT_ONLY_ENDPOINT
|
||||
* * CHANNEL_HIT_WRITE_RATELIMIT
|
||||
* * MAXIMUM_GUILDS
|
||||
* * MAXIMUM_FRIENDS
|
||||
* * MAXIMUM_PINS
|
||||
* * MAXIMUM_ROLES
|
||||
* * MAXIMUM_WEBHOOKS
|
||||
* * MAXIMUM_REACTIONS
|
||||
* * MAXIMUM_CHANNELS
|
||||
* * MAXIMUM_ATTACHMENTS
|
||||
* * MAXIMUM_INVITES
|
||||
* * GUILD_ALREADY_HAS_TEMPLATE
|
||||
* * UNAUTHORIZED
|
||||
* * ACCOUNT_VERIFICATION_REQUIRED
|
||||
* * REQUEST_ENTITY_TOO_LARGE
|
||||
* * FEATURE_TEMPORARILY_DISABLED
|
||||
* * USER_BANNED
|
||||
* * ALREADY_CROSSPOSTED
|
||||
* * MISSING_ACCESS
|
||||
* * INVALID_ACCOUNT_TYPE
|
||||
* * CANNOT_EXECUTE_ON_DM
|
||||
* * EMBED_DISABLED
|
||||
* * CANNOT_EDIT_MESSAGE_BY_OTHER
|
||||
* * CANNOT_SEND_EMPTY_MESSAGE
|
||||
* * CANNOT_MESSAGE_USER
|
||||
* * CANNOT_SEND_MESSAGES_IN_VOICE_CHANNEL
|
||||
* * CHANNEL_VERIFICATION_LEVEL_TOO_HIGH
|
||||
* * OAUTH2_APPLICATION_BOT_ABSENT
|
||||
* * MAXIMUM_OAUTH2_APPLICATIONS
|
||||
* * INVALID_OAUTH_STATE
|
||||
* * MISSING_PERMISSIONS
|
||||
* * INVALID_AUTHENTICATION_TOKEN
|
||||
* * NOTE_TOO_LONG
|
||||
* * INVALID_BULK_DELETE_QUANTITY
|
||||
* * CANNOT_PIN_MESSAGE_IN_OTHER_CHANNEL
|
||||
* * INVALID_OR_TAKEN_INVITE_CODE
|
||||
* * CANNOT_EXECUTE_ON_SYSTEM_MESSAGE
|
||||
* * INVALID_OAUTH_TOKEN
|
||||
* * BULK_DELETE_MESSAGE_TOO_OLD
|
||||
* * INVALID_FORM_BODY
|
||||
* * INVITE_ACCEPTED_TO_GUILD_NOT_CONTAINING_BOT
|
||||
* * INVALID_API_VERSION
|
||||
* * CANNOT_DELETE_COMMUNITY_REQUIRED_CHANNEL
|
||||
* * REACTION_BLOCKED
|
||||
* * RESOURCE_OVERLOADED
|
||||
* @typedef {string} APIError
|
||||
*/
|
||||
export const APIErrors = {
|
||||
UNKNOWN_ACCOUNT: 10001,
|
||||
UNKNOWN_APPLICATION: 10002,
|
||||
UNKNOWN_CHANNEL: 10003,
|
||||
UNKNOWN_GUILD: 10004,
|
||||
UNKNOWN_INTEGRATION: 10005,
|
||||
UNKNOWN_INVITE: 10006,
|
||||
UNKNOWN_MEMBER: 10007,
|
||||
UNKNOWN_MESSAGE: 10008,
|
||||
UNKNOWN_OVERWRITE: 10009,
|
||||
UNKNOWN_PROVIDER: 10010,
|
||||
UNKNOWN_ROLE: 10011,
|
||||
UNKNOWN_TOKEN: 10012,
|
||||
UNKNOWN_USER: 10013,
|
||||
UNKNOWN_EMOJI: 10014,
|
||||
UNKNOWN_WEBHOOK: 10015,
|
||||
UNKNOWN_BAN: 10026,
|
||||
UNKNOWN_GUILD_TEMPLATE: 10057,
|
||||
BOT_PROHIBITED_ENDPOINT: 20001,
|
||||
BOT_ONLY_ENDPOINT: 20002,
|
||||
CHANNEL_HIT_WRITE_RATELIMIT: 20028,
|
||||
MAXIMUM_GUILDS: 30001,
|
||||
MAXIMUM_FRIENDS: 30002,
|
||||
MAXIMUM_PINS: 30003,
|
||||
MAXIMUM_ROLES: 30005,
|
||||
MAXIMUM_WEBHOOKS: 30007,
|
||||
MAXIMUM_REACTIONS: 30010,
|
||||
MAXIMUM_CHANNELS: 30013,
|
||||
MAXIMUM_ATTACHMENTS: 30015,
|
||||
MAXIMUM_INVITES: 30016,
|
||||
GUILD_ALREADY_HAS_TEMPLATE: 30031,
|
||||
UNAUTHORIZED: 40001,
|
||||
ACCOUNT_VERIFICATION_REQUIRED: 40002,
|
||||
REQUEST_ENTITY_TOO_LARGE: 40005,
|
||||
FEATURE_TEMPORARILY_DISABLED: 40006,
|
||||
USER_BANNED: 40007,
|
||||
ALREADY_CROSSPOSTED: 40033,
|
||||
MISSING_ACCESS: 50001,
|
||||
INVALID_ACCOUNT_TYPE: 50002,
|
||||
CANNOT_EXECUTE_ON_DM: 50003,
|
||||
EMBED_DISABLED: 50004,
|
||||
CANNOT_EDIT_MESSAGE_BY_OTHER: 50005,
|
||||
CANNOT_SEND_EMPTY_MESSAGE: 50006,
|
||||
CANNOT_MESSAGE_USER: 50007,
|
||||
CANNOT_SEND_MESSAGES_IN_VOICE_CHANNEL: 50008,
|
||||
CHANNEL_VERIFICATION_LEVEL_TOO_HIGH: 50009,
|
||||
OAUTH2_APPLICATION_BOT_ABSENT: 50010,
|
||||
MAXIMUM_OAUTH2_APPLICATIONS: 50011,
|
||||
INVALID_OAUTH_STATE: 50012,
|
||||
MISSING_PERMISSIONS: 50013,
|
||||
INVALID_AUTHENTICATION_TOKEN: 50014,
|
||||
NOTE_TOO_LONG: 50015,
|
||||
INVALID_BULK_DELETE_QUANTITY: 50016,
|
||||
CANNOT_PIN_MESSAGE_IN_OTHER_CHANNEL: 50019,
|
||||
INVALID_OR_TAKEN_INVITE_CODE: 50020,
|
||||
CANNOT_EXECUTE_ON_SYSTEM_MESSAGE: 50021,
|
||||
INVALID_OAUTH_TOKEN: 50025,
|
||||
BULK_DELETE_MESSAGE_TOO_OLD: 50034,
|
||||
INVALID_FORM_BODY: 50035,
|
||||
INVITE_ACCEPTED_TO_GUILD_NOT_CONTAINING_BOT: 50036,
|
||||
INVALID_API_VERSION: 50041,
|
||||
CANNOT_DELETE_COMMUNITY_REQUIRED_CHANNEL: 50074,
|
||||
REACTION_BLOCKED: 90001,
|
||||
RESOURCE_OVERLOADED: 130000,
|
||||
};
|
||||
|
||||
/**
|
||||
* The value set for a guild's default message notifications, e.g. `ALL`. Here are the available types:
|
||||
* * ALL
|
||||
* * MENTIONS
|
||||
* @typedef {string} DefaultMessageNotifications
|
||||
*/
|
||||
export const DefaultMessageNotifications = ["ALL", "MENTIONS"];
|
||||
|
||||
/**
|
||||
* The value set for a team members's membership state:
|
||||
* * INVITED
|
||||
* * ACCEPTED
|
||||
* @typedef {string} MembershipStates
|
||||
*/
|
||||
export const MembershipStates = [
|
||||
// They start at 1
|
||||
null,
|
||||
"INVITED",
|
||||
"ACCEPTED",
|
||||
];
|
||||
|
||||
/**
|
||||
* The value set for a webhook's type:
|
||||
* * Incoming
|
||||
* * Channel Follower
|
||||
* @typedef {string} WebhookTypes
|
||||
*/
|
||||
export const WebhookTypes = [
|
||||
// They start at 1
|
||||
null,
|
||||
"Incoming",
|
||||
"Channel Follower",
|
||||
];
|
||||
|
||||
function keyMirror(arr: string[]) {
|
||||
let tmp = Object.create(null);
|
||||
for (const value of arr) tmp[value] = value;
|
||||
return tmp;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Config, Event, EventModel, RabbitMQ } from "@fosscord/server-util";
|
||||
|
||||
export async function emitEvent(payload: Omit<Event, "created_at">) {
|
||||
if (RabbitMQ.connection) {
|
||||
const id = (payload.channel_id || payload.user_id || payload.guild_id) as string;
|
||||
if (!id) console.error("event doesn't contain any id", payload);
|
||||
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 });
|
||||
|
||||
// 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");
|
||||
} else {
|
||||
// use mongodb for event transmission
|
||||
// TODO: use event emitter for local server bundle
|
||||
const obj = {
|
||||
created_at: new Date(), // in seconds
|
||||
...payload
|
||||
};
|
||||
// TODO: bigint isn't working
|
||||
|
||||
return await new EventModel(obj).save();
|
||||
}
|
||||
}
|
||||
|
||||
export async function emitAuditLog(payload: any) {}
|
||||
@@ -0,0 +1,218 @@
|
||||
import {
|
||||
Guild,
|
||||
GuildCreateEvent,
|
||||
GuildDeleteEvent,
|
||||
GuildMemberAddEvent,
|
||||
GuildMemberRemoveEvent,
|
||||
GuildMemberUpdateEvent,
|
||||
GuildModel,
|
||||
MemberModel,
|
||||
RoleModel,
|
||||
toObject,
|
||||
UserModel,
|
||||
GuildDocument,
|
||||
Config
|
||||
} from "@fosscord/server-util";
|
||||
|
||||
import { HTTPError } from "lambert-server";
|
||||
import { emitEvent } from "./Event";
|
||||
import { getPublicUser } from "./User";
|
||||
|
||||
export const PublicMemberProjection = {
|
||||
id: true,
|
||||
guild_id: true,
|
||||
nick: true,
|
||||
roles: true,
|
||||
joined_at: true,
|
||||
pending: true,
|
||||
deaf: true,
|
||||
mute: true,
|
||||
premium_since: true
|
||||
};
|
||||
|
||||
export async function isMember(user_id: string, guild_id: string) {
|
||||
const exists = await MemberModel.exists({ id: user_id, guild_id });
|
||||
if (!exists) throw new HTTPError("You are not a member of this guild", 403);
|
||||
return exists;
|
||||
}
|
||||
|
||||
export async function addMember(user_id: string, guild_id: string, cache?: { guild?: GuildDocument }) {
|
||||
const user = await getPublicUser(user_id, { guilds: true });
|
||||
|
||||
const { maxGuilds } = Config.get().limits.user;
|
||||
if (user.guilds.length >= maxGuilds) {
|
||||
throw new HTTPError(`You are at the ${maxGuilds} server limit.`, 403);
|
||||
}
|
||||
|
||||
const guild = cache?.guild || (await GuildModel.findOne({ id: guild_id }).exec());
|
||||
|
||||
if (!guild) throw new HTTPError("Guild not found", 404);
|
||||
|
||||
if (await MemberModel.exists({ id: user.id, guild_id })) throw new HTTPError("You are already a member of this guild", 400);
|
||||
|
||||
const member = {
|
||||
id: user_id,
|
||||
guild_id: guild_id,
|
||||
nick: undefined,
|
||||
roles: [guild_id], // @everyone role
|
||||
joined_at: new Date(),
|
||||
premium_since: undefined,
|
||||
deaf: false,
|
||||
mute: false,
|
||||
pending: false
|
||||
};
|
||||
|
||||
await Promise.all([
|
||||
new MemberModel({
|
||||
...member,
|
||||
read_state: {},
|
||||
settings: {
|
||||
channel_overrides: [],
|
||||
message_notifications: 0,
|
||||
mobile_push: true,
|
||||
mute_config: null,
|
||||
muted: false,
|
||||
suppress_everyone: false,
|
||||
suppress_roles: false,
|
||||
version: 0
|
||||
}
|
||||
}).save(),
|
||||
|
||||
UserModel.updateOne({ id: user_id }, { $push: { guilds: guild_id } }).exec(),
|
||||
GuildModel.updateOne({ id: guild_id }, { $inc: { member_count: 1 } }).exec(),
|
||||
|
||||
emitEvent({
|
||||
event: "GUILD_MEMBER_ADD",
|
||||
data: {
|
||||
...member,
|
||||
user,
|
||||
guild_id: guild_id
|
||||
},
|
||||
guild_id: guild_id
|
||||
} as GuildMemberAddEvent)
|
||||
]);
|
||||
|
||||
await emitEvent({
|
||||
event: "GUILD_CREATE",
|
||||
data: toObject(
|
||||
await guild
|
||||
.populate({ path: "members", match: { guild_id } })
|
||||
.populate({ path: "joined_at", match: { id: user.id } })
|
||||
.execPopulate()
|
||||
),
|
||||
user_id
|
||||
} as GuildCreateEvent);
|
||||
}
|
||||
|
||||
export async function removeMember(user_id: string, guild_id: string) {
|
||||
const user = await getPublicUser(user_id);
|
||||
|
||||
const guild = await GuildModel.findOne({ id: guild_id }, { owner_id: true }).exec();
|
||||
if (!guild) throw new HTTPError("Guild not found", 404);
|
||||
if (guild.owner_id === user_id) throw new Error("The owner cannot be removed of the guild");
|
||||
if (!(await MemberModel.exists({ id: user.id, guild_id }))) throw new HTTPError("Is not member of this guild", 404);
|
||||
|
||||
// use promise all to execute all promises at the same time -> save time
|
||||
return Promise.all([
|
||||
MemberModel.deleteOne({
|
||||
id: user_id,
|
||||
guild_id: guild_id
|
||||
}).exec(),
|
||||
UserModel.updateOne({ id: user.id }, { $pull: { guilds: guild_id } }).exec(),
|
||||
GuildModel.updateOne({ id: guild_id }, { $inc: { member_count: -1 } }).exec(),
|
||||
|
||||
emitEvent({
|
||||
event: "GUILD_DELETE",
|
||||
data: {
|
||||
id: guild_id
|
||||
},
|
||||
user_id: user_id
|
||||
} as GuildDeleteEvent),
|
||||
emitEvent({
|
||||
event: "GUILD_MEMBER_REMOVE",
|
||||
data: {
|
||||
guild_id: guild_id,
|
||||
user: user
|
||||
},
|
||||
guild_id: guild_id
|
||||
} as GuildMemberRemoveEvent)
|
||||
]);
|
||||
}
|
||||
|
||||
export async function addRole(user_id: string, guild_id: string, role_id: string) {
|
||||
const user = await getPublicUser(user_id);
|
||||
|
||||
const role = await RoleModel.findOne({ id: role_id, guild_id: guild_id }).exec();
|
||||
if (!role) throw new HTTPError("role not found", 404);
|
||||
|
||||
var memberObj = await MemberModel.findOneAndUpdate(
|
||||
{
|
||||
id: user_id,
|
||||
guild_id: guild_id
|
||||
},
|
||||
{ $push: { roles: role_id } }
|
||||
).exec();
|
||||
|
||||
if (!memberObj) throw new HTTPError("Member not found", 404);
|
||||
|
||||
await emitEvent({
|
||||
event: "GUILD_MEMBER_UPDATE",
|
||||
data: {
|
||||
guild_id: guild_id,
|
||||
user: user,
|
||||
roles: memberObj.roles
|
||||
},
|
||||
guild_id: guild_id
|
||||
} as GuildMemberUpdateEvent);
|
||||
}
|
||||
|
||||
export async function removeRole(user_id: string, guild_id: string, role_id: string) {
|
||||
const user = await getPublicUser(user_id);
|
||||
|
||||
const role = await RoleModel.findOne({ id: role_id, guild_id: guild_id }).exec();
|
||||
if (!role) throw new HTTPError("role not found", 404);
|
||||
|
||||
var memberObj = await MemberModel.findOneAndUpdate(
|
||||
{
|
||||
id: user_id,
|
||||
guild_id: guild_id
|
||||
},
|
||||
{ $pull: { roles: role_id } }
|
||||
).exec();
|
||||
|
||||
if (!memberObj) throw new HTTPError("Member not found", 404);
|
||||
|
||||
await emitEvent({
|
||||
event: "GUILD_MEMBER_UPDATE",
|
||||
data: {
|
||||
guild_id: guild_id,
|
||||
user: user,
|
||||
roles: memberObj.roles
|
||||
},
|
||||
guild_id: guild_id
|
||||
} as GuildMemberUpdateEvent);
|
||||
}
|
||||
|
||||
export async function changeNickname(user_id: string, guild_id: string, nickname: string) {
|
||||
const user = await getPublicUser(user_id);
|
||||
|
||||
var memberObj = await MemberModel.findOneAndUpdate(
|
||||
{
|
||||
id: user_id,
|
||||
guild_id: guild_id
|
||||
},
|
||||
{ nick: nickname }
|
||||
).exec();
|
||||
|
||||
if (!memberObj) throw new HTTPError("Member not found", 404);
|
||||
|
||||
await emitEvent({
|
||||
event: "GUILD_MEMBER_UPDATE",
|
||||
data: {
|
||||
guild_id: guild_id,
|
||||
user: user,
|
||||
nick: nickname
|
||||
},
|
||||
guild_id: guild_id
|
||||
} as GuildMemberUpdateEvent);
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
import { ChannelModel, Embed, Message, MessageCreateEvent, MessageUpdateEvent } from "@fosscord/server-util";
|
||||
import { Snowflake } from "@fosscord/server-util";
|
||||
import { MessageModel } from "@fosscord/server-util";
|
||||
import { PublicMemberProjection } from "@fosscord/server-util";
|
||||
import { toObject } from "@fosscord/server-util";
|
||||
import { getPermission } from "@fosscord/server-util";
|
||||
import { HTTPError } from "lambert-server";
|
||||
import fetch from "node-fetch";
|
||||
import cheerio from "cheerio";
|
||||
import { emitEvent } from "./Event";
|
||||
import { MessageType } from "@fosscord/server-util/dist/util/Constants";
|
||||
// TODO: check webhook, application, system author
|
||||
|
||||
const LINK_REGEX = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/g;
|
||||
|
||||
const DEFAULT_FETCH_OPTIONS: any = {
|
||||
redirect: "follow",
|
||||
follow: 1,
|
||||
headers: {
|
||||
"user-agent": "Mozilla/5.0 (compatible; Discordbot/2.0; +https://discordapp.com)"
|
||||
},
|
||||
size: 1024 * 1024 * 1,
|
||||
compress: true,
|
||||
method: "GET"
|
||||
};
|
||||
|
||||
export async function handleMessage(opts: Partial<Message>) {
|
||||
const channel = await ChannelModel.findOne(
|
||||
{ id: opts.channel_id },
|
||||
{ guild_id: true, type: true, permission_overwrites: true, recipient_ids: true, owner_id: true }
|
||||
)
|
||||
.lean() // lean is needed, because we don't want to populate .recipients that also auto deletes .recipient_ids
|
||||
.exec();
|
||||
if (!channel || !opts.channel_id) throw new HTTPError("Channel not found", 404);
|
||||
// TODO: are tts messages allowed in dm channels? should permission be checked?
|
||||
|
||||
// @ts-ignore
|
||||
const permissions = await getPermission(opts.author_id, channel.guild_id, opts.channel_id, { channel });
|
||||
permissions.hasThrow("SEND_MESSAGES");
|
||||
if (opts.tts) permissions.hasThrow("SEND_TTS_MESSAGES");
|
||||
if (opts.message_reference) {
|
||||
permissions.hasThrow("READ_MESSAGE_HISTORY");
|
||||
if (opts.message_reference.guild_id !== channel.guild_id) throw new HTTPError("You can only reference messages from this guild");
|
||||
if (opts.message_reference.channel_id !== opts.channel_id) throw new HTTPError("You can only reference messages from this channel");
|
||||
// TODO: should be checked if the referenced message exists?
|
||||
// @ts-ignore
|
||||
opts.type = MessageType.REPLY;
|
||||
}
|
||||
|
||||
if (!opts.content && !opts.embeds?.length && !opts.attachments?.length && !opts.stickers?.length && !opts.activity) {
|
||||
throw new HTTPError("Empty messages are not allowed", 50006);
|
||||
}
|
||||
|
||||
// TODO: check and put it all in the body
|
||||
return {
|
||||
...opts,
|
||||
guild_id: channel.guild_id,
|
||||
channel_id: opts.channel_id,
|
||||
// TODO: generate mentions and check permissions
|
||||
mention_channels_ids: [],
|
||||
mention_role_ids: [],
|
||||
mention_user_ids: [],
|
||||
attachments: opts.attachments || [], // TODO: message attachments
|
||||
embeds: opts.embeds || [],
|
||||
reactions: opts.reactions || [],
|
||||
type: opts.type ?? 0
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: cache link result in db
|
||||
export async function postHandleMessage(message: Message) {
|
||||
var links = message.content?.match(LINK_REGEX);
|
||||
if (!links) return;
|
||||
|
||||
const data = { ...message };
|
||||
data.embeds = data.embeds.filter((x) => x.type !== "link");
|
||||
|
||||
links = links.slice(0, 5); // embed max 5 links
|
||||
|
||||
for (const link of links) {
|
||||
try {
|
||||
const request = await fetch(link, DEFAULT_FETCH_OPTIONS);
|
||||
|
||||
const text = await request.text();
|
||||
const $ = cheerio.load(text);
|
||||
|
||||
const title = $('meta[property="og:title"]').attr("content");
|
||||
const provider_name = $('meta[property="og:site_name"]').text();
|
||||
const author_name = $('meta[property="article:author"]').attr("content");
|
||||
const description = $('meta[property="og:description"]').attr("content") || $('meta[property="description"]').attr("content");
|
||||
const image = $('meta[property="og:image"]').attr("content");
|
||||
const url = $('meta[property="og:url"]').attr("content");
|
||||
// TODO: color
|
||||
const embed: Embed = {
|
||||
provider: {
|
||||
url: link,
|
||||
name: provider_name
|
||||
}
|
||||
};
|
||||
|
||||
if (author_name) embed.author = { name: author_name };
|
||||
if (image) embed.thumbnail = { proxy_url: image, url: image };
|
||||
if (title) embed.title = title;
|
||||
if (url) embed.url = url;
|
||||
if (description) embed.description = description;
|
||||
|
||||
if (title || description) {
|
||||
data.embeds.push(embed);
|
||||
}
|
||||
} catch (error) {}
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
emitEvent({
|
||||
event: "MESSAGE_UPDATE",
|
||||
guild_id: message.guild_id,
|
||||
channel_id: message.channel_id,
|
||||
data
|
||||
} as MessageUpdateEvent),
|
||||
MessageModel.updateOne({ id: message.id, channel_id: message.channel_id }, data).exec()
|
||||
]);
|
||||
}
|
||||
|
||||
export async function sendMessage(opts: Partial<Message>) {
|
||||
const message = await handleMessage({ ...opts, id: Snowflake.generate(), timestamp: new Date() });
|
||||
|
||||
const data = toObject(
|
||||
await new MessageModel(message).populate({ path: "member", select: PublicMemberProjection }).populate("referenced_message").save()
|
||||
);
|
||||
|
||||
await emitEvent({ event: "MESSAGE_CREATE", channel_id: opts.channel_id, data, guild_id: message.guild_id } as MessageCreateEvent);
|
||||
|
||||
postHandleMessage(data).catch((e) => {}); // no await as it shouldnt block the message send function and silently catch error
|
||||
|
||||
return data;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
export function random(length = 6) {
|
||||
// Declare all characters
|
||||
let chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
|
||||
// Pick characers randomly
|
||||
let str = "";
|
||||
for (let i = 0; i < length; i++) {
|
||||
str += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
}
|
||||
|
||||
return str;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Request } from "express";
|
||||
import { ntob } from "./Base64";
|
||||
import { FieldErrors } from "./instanceOf";
|
||||
|
||||
export function checkLength(str: string, min: number, max: number, key: string, req: Request) {
|
||||
if (str.length < min || str.length > max) {
|
||||
throw FieldErrors({
|
||||
[key]: {
|
||||
code: "BASE_TYPE_BAD_LENGTH",
|
||||
message: req.t("common:field.BASE_TYPE_BAD_LENGTH", { length: `${min} - ${max}` }),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function generateCode() {
|
||||
return ntob(Date.now() + Math.randomIntBetween(0, 10000));
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { toObject, UserModel, PublicUserProjection } from "@fosscord/server-util";
|
||||
import { HTTPError } from "lambert-server";
|
||||
|
||||
export { PublicUserProjection };
|
||||
|
||||
export async function getPublicUser(user_id: string, additional_fields?: any) {
|
||||
const user = await UserModel.findOne(
|
||||
{ id: user_id },
|
||||
{
|
||||
...PublicUserProjection,
|
||||
...additional_fields
|
||||
}
|
||||
).exec();
|
||||
if (!user) throw new HTTPError("User not found", 404);
|
||||
return toObject(user);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,40 @@
|
||||
import { Config } from "@fosscord/server-util";
|
||||
import FormData from "form-data";
|
||||
import { HTTPError } from "lambert-server";
|
||||
import fetch from "node-fetch";
|
||||
|
||||
export async function uploadFile(path: string, file: Express.Multer.File) {
|
||||
const form = new FormData();
|
||||
form.append("file", file.buffer, {
|
||||
contentType: file.mimetype,
|
||||
filename: file.originalname
|
||||
});
|
||||
|
||||
const response = await fetch(`${Config.get().cdn.endpoint || "http://localhost:3003"}${path}`, {
|
||||
headers: {
|
||||
signature: Config.get().security.requestSignature,
|
||||
...form.getHeaders()
|
||||
},
|
||||
method: "POST",
|
||||
body: form
|
||||
});
|
||||
const result = await response.json();
|
||||
|
||||
if (response.status !== 200) throw result;
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function handleFile(path: string, body?: string): Promise<string | undefined> {
|
||||
if (!body || !body.startsWith("data:")) return body;
|
||||
try {
|
||||
const mimetype = body.split(":")[1].split(";")[0];
|
||||
const buffer = Buffer.from(body.split(",")[1], "base64");
|
||||
|
||||
// @ts-ignore
|
||||
const { id } = await uploadFile(path, { buffer, mimetype, originalname: "banner" });
|
||||
return id;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
throw new HTTPError("Invalid " + path);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
// different version of lambert-server instanceOf with discord error format
|
||||
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import { Tuple } from "lambert-server";
|
||||
import "missing-native-js-functions";
|
||||
|
||||
export const OPTIONAL_PREFIX = "$";
|
||||
export const EMAIL_REGEX =
|
||||
/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
|
||||
|
||||
export function check(schema: any) {
|
||||
return (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const result = instanceOf(schema, req.body, { path: "body", req, ref: { obj: null, key: "" } });
|
||||
if (result === true) return next();
|
||||
throw result;
|
||||
} catch (error) {
|
||||
return res.status(400).json({ code: 50035, message: "Invalid Form Body", success: false, errors: error });
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function FieldErrors(fields: Record<string, { code?: string; message: string }>) {
|
||||
return new FieldError(
|
||||
50035,
|
||||
"Invalid Form Body",
|
||||
fields.map(({ message, code }) => ({
|
||||
_errors: [
|
||||
{
|
||||
message,
|
||||
code: code || "BASE_TYPE_INVALID"
|
||||
}
|
||||
]
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: implement Image data type: Data URI scheme that supports JPG, GIF, and PNG formats. An example Data URI format is: data:image/jpeg;base64,BASE64_ENCODED_JPEG_IMAGE_DATA
|
||||
// Ensure you use the proper content type (image/jpeg, image/png, image/gif) that matches the image data being provided.
|
||||
|
||||
export class FieldError extends Error {
|
||||
constructor(public code: string | number, public message: string, public errors?: any) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
export class Email {
|
||||
constructor(public email: string) {}
|
||||
check() {
|
||||
return !!this.email.match(EMAIL_REGEX);
|
||||
}
|
||||
}
|
||||
|
||||
export class Length {
|
||||
constructor(public type: any, public min: number, public max: number) {}
|
||||
|
||||
check(value: string) {
|
||||
if (typeof value === "string" || Array.isArray(value)) return value.length >= this.min && value.length <= this.max;
|
||||
if (typeof value === "number" || typeof value === "bigint") return value >= this.min && value <= this.max;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function instanceOf(
|
||||
type: any,
|
||||
value: any,
|
||||
{
|
||||
path = "",
|
||||
optional = false,
|
||||
errors = {},
|
||||
req,
|
||||
ref
|
||||
}: { path?: string; optional?: boolean; errors?: any; req: Request; ref?: { key: string | number; obj: any } }
|
||||
): Boolean {
|
||||
if (!ref) ref = { obj: null, key: "" };
|
||||
if (!path) path = "body";
|
||||
if (!type) return true; // no type was specified
|
||||
|
||||
try {
|
||||
if (value == null) {
|
||||
if (optional) return true;
|
||||
throw new FieldError("BASE_TYPE_REQUIRED", req.t("common:field.BASE_TYPE_REQUIRED"));
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case String:
|
||||
value = `${value}`;
|
||||
ref.obj[ref.key] = value;
|
||||
if (typeof value === "string") return true;
|
||||
throw new FieldError("BASE_TYPE_STRING", req.t("common:field.BASE_TYPE_STRING"));
|
||||
case Number:
|
||||
value = Number(value);
|
||||
ref.obj[ref.key] = value;
|
||||
if (typeof value === "number" && !isNaN(value)) return true;
|
||||
throw new FieldError("BASE_TYPE_NUMBER", req.t("common:field.BASE_TYPE_NUMBER"));
|
||||
case BigInt:
|
||||
try {
|
||||
value = BigInt(value);
|
||||
ref.obj[ref.key] = value;
|
||||
if (typeof value === "bigint") return true;
|
||||
} catch (error) {}
|
||||
throw new FieldError("BASE_TYPE_BIGINT", req.t("common:field.BASE_TYPE_BIGINT"));
|
||||
case Boolean:
|
||||
if (value == "true") value = true;
|
||||
if (value == "false") value = false;
|
||||
ref.obj[ref.key] = value;
|
||||
if (typeof value === "boolean") return true;
|
||||
throw new FieldError("BASE_TYPE_BOOLEAN", req.t("common:field.BASE_TYPE_BOOLEAN"));
|
||||
|
||||
case Email:
|
||||
if (new Email(value).check()) return true;
|
||||
throw new FieldError("EMAIL_TYPE_INVALID_EMAIL", req.t("common:field.EMAIL_TYPE_INVALID_EMAIL"));
|
||||
case Date:
|
||||
value = new Date(value);
|
||||
ref.obj[ref.key] = value;
|
||||
// value.getTime() can be < 0, if it is before 1970
|
||||
if (!isNaN(value)) return true;
|
||||
throw new FieldError("DATE_TYPE_PARSE", req.t("common:field.DATE_TYPE_PARSE"));
|
||||
}
|
||||
|
||||
if (typeof type === "object") {
|
||||
if (Array.isArray(type)) {
|
||||
if (!Array.isArray(value)) throw new FieldError("BASE_TYPE_ARRAY", req.t("common:field.BASE_TYPE_ARRAY"));
|
||||
if (!type.length) return true; // type array didn't specify any type
|
||||
|
||||
return (
|
||||
value.every((val, i) => {
|
||||
errors[i] = {};
|
||||
|
||||
if (
|
||||
instanceOf(type[0], val, {
|
||||
path: `${path}[${i}]`,
|
||||
optional,
|
||||
errors: errors[i],
|
||||
req,
|
||||
ref: { key: i, obj: value }
|
||||
}) === true
|
||||
) {
|
||||
delete errors[i];
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}) || errors
|
||||
);
|
||||
} else if (type?.constructor?.name != "Object") {
|
||||
if (type instanceof Tuple) {
|
||||
if ((<Tuple>type).types.some((x) => instanceOf(x, value, { path, optional, errors, req, ref }))) return true;
|
||||
throw new FieldError("BASE_TYPE_CHOICES", req.t("common:field.BASE_TYPE_CHOICES", { types: type.types }));
|
||||
} else if (type instanceof Length) {
|
||||
let length = <Length>type;
|
||||
if (instanceOf(length.type, value, { path, optional, req, ref, errors }) !== true) return errors;
|
||||
let val = ref.obj[ref.key];
|
||||
if ((<Length>type).check(val)) return true;
|
||||
throw new FieldError(
|
||||
"BASE_TYPE_BAD_LENGTH",
|
||||
req.t("common:field.BASE_TYPE_BAD_LENGTH", {
|
||||
length: `${type.min} - ${type.max}`
|
||||
})
|
||||
);
|
||||
}
|
||||
try {
|
||||
if (value instanceof type) return true;
|
||||
} catch (error) {
|
||||
throw new FieldError("BASE_TYPE_CLASS", req.t("common:field.BASE_TYPE_CLASS", { type }));
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof value !== "object") throw new FieldError("BASE_TYPE_OBJECT", req.t("common:field.BASE_TYPE_OBJECT"));
|
||||
|
||||
const diff = Object.keys(value).missing(
|
||||
Object.keys(type).map((x) => (x.startsWith(OPTIONAL_PREFIX) ? x.slice(OPTIONAL_PREFIX.length) : x))
|
||||
);
|
||||
|
||||
if (diff.length) throw new FieldError("UNKOWN_FIELD", req.t("common:field.UNKOWN_FIELD", { key: diff }));
|
||||
|
||||
return (
|
||||
Object.keys(type).every((key) => {
|
||||
let newKey = key;
|
||||
const OPTIONAL = key.startsWith(OPTIONAL_PREFIX);
|
||||
if (OPTIONAL) newKey = newKey.slice(OPTIONAL_PREFIX.length);
|
||||
errors[newKey] = {};
|
||||
|
||||
if (
|
||||
instanceOf(type[key], value[newKey], {
|
||||
path: `${path}.${newKey}`,
|
||||
optional: OPTIONAL,
|
||||
errors: errors[newKey],
|
||||
req,
|
||||
ref: { key: newKey, obj: value }
|
||||
}) === true
|
||||
) {
|
||||
delete errors[newKey];
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}) || errors
|
||||
);
|
||||
} else if (typeof type === "number" || typeof type === "string" || typeof type === "boolean") {
|
||||
if (value === type) return true;
|
||||
throw new FieldError("BASE_TYPE_CONSTANT", req.t("common:field.BASE_TYPE_CONSTANT", { value: type }));
|
||||
} else if (typeof type === "bigint") {
|
||||
if (BigInt(value) === type) return true;
|
||||
throw new FieldError("BASE_TYPE_CONSTANT", req.t("common:field.BASE_TYPE_CONSTANT", { value: type }));
|
||||
}
|
||||
|
||||
return type == value;
|
||||
} catch (error) {
|
||||
let e = error as FieldError;
|
||||
errors._errors = [{ message: e.message, code: e.code }];
|
||||
return errors;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import { Config } from "@fosscord/server-util";
|
||||
import { Request } from "express";
|
||||
// use ipdata package instead of simple fetch because of integrated caching
|
||||
import fetch from "node-fetch";
|
||||
|
||||
const exampleData = {
|
||||
ip: "",
|
||||
is_eu: true,
|
||||
city: "",
|
||||
region: "",
|
||||
region_code: "",
|
||||
country_name: "",
|
||||
country_code: "",
|
||||
continent_name: "",
|
||||
continent_code: "",
|
||||
latitude: 0,
|
||||
longitude: 0,
|
||||
postal: "",
|
||||
calling_code: "",
|
||||
flag: "",
|
||||
emoji_flag: "",
|
||||
emoji_unicode: "",
|
||||
asn: {
|
||||
asn: "",
|
||||
name: "",
|
||||
domain: "",
|
||||
route: "",
|
||||
type: "isp"
|
||||
},
|
||||
languages: [
|
||||
{
|
||||
name: "",
|
||||
native: ""
|
||||
}
|
||||
],
|
||||
currency: {
|
||||
name: "",
|
||||
code: "",
|
||||
symbol: "",
|
||||
native: "",
|
||||
plural: ""
|
||||
},
|
||||
time_zone: {
|
||||
name: "",
|
||||
abbr: "",
|
||||
offset: "",
|
||||
is_dst: true,
|
||||
current_time: ""
|
||||
},
|
||||
threat: {
|
||||
is_tor: false,
|
||||
is_proxy: false,
|
||||
is_anonymous: false,
|
||||
is_known_attacker: false,
|
||||
is_known_abuser: false,
|
||||
is_threat: false,
|
||||
is_bogon: false
|
||||
},
|
||||
count: 0,
|
||||
status: 200
|
||||
};
|
||||
|
||||
export async function IPAnalysis(ip: string): Promise<typeof exampleData> {
|
||||
const { ipdataApiKey } = Config.get().security;
|
||||
if (!ipdataApiKey) return { ...exampleData, ip };
|
||||
|
||||
return (await fetch(`https://api.ipdata.co/${ip}?api-key=${ipdataApiKey}`)).json();
|
||||
}
|
||||
|
||||
export function isProxy(data: typeof exampleData) {
|
||||
if (!data || !data.asn || !data.threat) return false;
|
||||
if (data.asn.type !== "isp") return true;
|
||||
if (Object.values(data.threat).some((x) => x)) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export function getIpAdress(req: Request): string {
|
||||
// @ts-ignore
|
||||
return req.headers[Config.get().security.forwadedFor] || req.socket.remoteAddress;
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { Config } from "@fosscord/server-util";
|
||||
import "missing-native-js-functions";
|
||||
|
||||
const reNUMBER = /[0-9]/g;
|
||||
const reUPPERCASELETTER = /[A-Z]/g;
|
||||
const reSYMBOLS = /[A-Z,a-z,0-9]/g;
|
||||
|
||||
const blocklist: string[] = []; // TODO: update ones passwordblocklist is stored in db
|
||||
/*
|
||||
* https://en.wikipedia.org/wiki/Password_policy
|
||||
* password must meet following criteria, to be perfect:
|
||||
* - min <n> chars
|
||||
* - min <n> numbers
|
||||
* - min <n> symbols
|
||||
* - min <n> uppercase chars
|
||||
*
|
||||
* Returns: 0 > pw > 1
|
||||
*/
|
||||
export function check(password: string): number {
|
||||
const { minLength, minNumbers, minUpperCase, minSymbols } = Config.get().register.password;
|
||||
var strength = 0;
|
||||
|
||||
// checks for total password len
|
||||
if (password.length >= minLength - 1) {
|
||||
strength += 0.25;
|
||||
}
|
||||
|
||||
// checks for amount of Numbers
|
||||
if (password.count(reNUMBER) >= minNumbers - 1) {
|
||||
strength += 0.25;
|
||||
}
|
||||
|
||||
// checks for amount of Uppercase Letters
|
||||
if (password.count(reUPPERCASELETTER) >= minUpperCase - 1) {
|
||||
strength += 0.25;
|
||||
}
|
||||
|
||||
// checks for amount of symbols
|
||||
if (password.replace(reSYMBOLS, "").length >= minSymbols - 1) {
|
||||
strength += 0.25;
|
||||
}
|
||||
|
||||
// checks if password only consists of numbers or only consists of chars
|
||||
if (password.length == password.count(reNUMBER) || password.length === password.count(reUPPERCASELETTER)) {
|
||||
strength = 0;
|
||||
}
|
||||
|
||||
return strength;
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
aol.com
|
||||
att.net
|
||||
comcast.net
|
||||
facebook.com
|
||||
gmail.com
|
||||
gmx.com
|
||||
googlemail.com
|
||||
google.com
|
||||
hotmail.com
|
||||
hotmail.co.uk
|
||||
mac.com
|
||||
me.com
|
||||
mail.com
|
||||
msn.com
|
||||
live.com
|
||||
sbcglobal.net
|
||||
verizon.net
|
||||
yahoo.com
|
||||
yahoo.co.uk
|
||||
email.com
|
||||
fastmail.fm
|
||||
games.com
|
||||
gmx.net
|
||||
hush.com
|
||||
hushmail.com
|
||||
icloud.com
|
||||
iname.com
|
||||
inbox.com
|
||||
lavabit.com
|
||||
love.com
|
||||
outlook.com
|
||||
pobox.com
|
||||
protonmail.ch
|
||||
protonmail.com
|
||||
tutanota.de
|
||||
tutanota.com
|
||||
tutamail.com
|
||||
tuta.io
|
||||
keemail.me
|
||||
rocketmail.com
|
||||
safe-mail.net
|
||||
wow.com
|
||||
ygm.com
|
||||
ymail.com
|
||||
zoho.com
|
||||
yandex.com
|
||||
bellsouth.net
|
||||
charter.net
|
||||
cox.net
|
||||
earthlink.net
|
||||
juno.com
|
||||
btinternet.com
|
||||
virginmedia.com
|
||||
blueyonder.co.uk
|
||||
freeserve.co.uk
|
||||
live.co.uk
|
||||
ntlworld.com
|
||||
o2.co.uk
|
||||
orange.net
|
||||
sky.com
|
||||
talktalk.co.uk
|
||||
tiscali.co.uk
|
||||
virgin.net
|
||||
wanadoo.co.uk
|
||||
bt.com
|
||||
sina.com
|
||||
sina.cn
|
||||
qq.com
|
||||
naver.com
|
||||
hanmail.net
|
||||
daum.net
|
||||
nate.com
|
||||
yahoo.co.jp
|
||||
yahoo.co.kr
|
||||
yahoo.co.id
|
||||
yahoo.co.in
|
||||
yahoo.com.sg
|
||||
yahoo.com.ph
|
||||
163.com
|
||||
yeah.net
|
||||
126.com
|
||||
21cn.com
|
||||
aliyun.com
|
||||
foxmail.com
|
||||
hotmail.fr
|
||||
live.fr
|
||||
laposte.net
|
||||
yahoo.fr
|
||||
wanadoo.fr
|
||||
orange.fr
|
||||
gmx.fr
|
||||
sfr.fr
|
||||
neuf.fr
|
||||
free.fr
|
||||
gmx.de
|
||||
hotmail.de
|
||||
live.de
|
||||
online.de
|
||||
t-online.de
|
||||
web.de
|
||||
yahoo.de
|
||||
libero.it
|
||||
virgilio.it
|
||||
hotmail.it
|
||||
aol.it
|
||||
tiscali.it
|
||||
alice.it
|
||||
live.it
|
||||
yahoo.it
|
||||
email.it
|
||||
tin.it
|
||||
poste.it
|
||||
teletu.it
|
||||
mail.ru
|
||||
rambler.ru
|
||||
yandex.ru
|
||||
ya.ru
|
||||
list.ru
|
||||
hotmail.be
|
||||
live.be
|
||||
skynet.be
|
||||
voo.be
|
||||
tvcablenet.be
|
||||
telenet.be
|
||||
hotmail.com.ar
|
||||
live.com.ar
|
||||
yahoo.com.ar
|
||||
fibertel.com.ar
|
||||
speedy.com.ar
|
||||
arnet.com.ar
|
||||
yahoo.com.mx
|
||||
live.com.mx
|
||||
hotmail.es
|
||||
hotmail.com.mx
|
||||
prodigy.net.mx
|
||||
yahoo.ca
|
||||
hotmail.ca
|
||||
bell.net
|
||||
shaw.ca
|
||||
sympatico.ca
|
||||
rogers.com
|
||||
yahoo.com.br
|
||||
hotmail.com.br
|
||||
outlook.com.br
|
||||
uol.com.br
|
||||
bol.com.br
|
||||
terra.com.br
|
||||
ig.com.br
|
||||
itelefonica.com.br
|
||||
r7.com
|
||||
zipmail.com.br
|
||||
globo.com
|
||||
globomail.com
|
||||
oi.com.br
|
||||
Reference in New Issue
Block a user