diff --git a/src/api/routes/auth/generate-registration-tokens.ts b/src/api/routes/auth/generate-registration-tokens.ts
index 303c6ab5..ca5020fb 100644
--- a/src/api/routes/auth/generate-registration-tokens.ts
+++ b/src/api/routes/auth/generate-registration-tokens.ts
@@ -16,7 +16,7 @@
along with this program. If not, see .
*/
-import { random, route } from "@spacebar/api";
+import { randomString, route } from "@spacebar/api";
import { Config, ValidRegistrationToken } from "@spacebar/util";
import { Request, Response, Router } from "express";
@@ -51,7 +51,7 @@ router.get(
for (let i = 0; i < count; i++) {
const token = ValidRegistrationToken.create({
- token: random(length),
+ token: randomString(length),
expires_at: new Date(
Date.now() +
Config.get().security
diff --git a/src/api/routes/channels/#channel_id/attachments.ts b/src/api/routes/channels/#channel_id/attachments.ts
new file mode 100644
index 00000000..5da1a778
--- /dev/null
+++ b/src/api/routes/channels/#channel_id/attachments.ts
@@ -0,0 +1,100 @@
+/*
+ Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
+ Copyright (C) 2025 Spacebar and Spacebar Contributors
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+*/
+
+import { generateCode, randomString, route } from "@spacebar/api";
+import {
+ Attachment,
+ Channel,
+ Config,
+ emitEvent,
+ GreetRequestSchema,
+ Message,
+ MessageCreateEvent,
+ MessageType,
+ Permissions,
+ Sticker,
+ UploadAttachmentRequestSchema,
+ UploadAttachmentResponseSchema,
+ User,
+} from "@spacebar/util";
+import { Request, Response, Router } from "express";
+import { In } from "typeorm";
+import { CloudAttachment } from "../../../../util/entities/CloudAttachment";
+
+const router: Router = Router();
+
+router.post(
+ "/",
+ route({
+ requestBody: "UploadAttachmentRequestSchema",
+ responses: {
+ 200: {
+ body: "UploadAttachmentResponseSchema",
+ },
+ 404: {},
+ 400: {
+ body: "APIErrorResponse",
+ },
+ },
+ }),
+ async (req: Request, res: Response) => {
+ const payload = req.body as UploadAttachmentRequestSchema;
+ const { channel_id } = req.params;
+
+ const user = await User.findOneOrFail({ where: { id: req.user_id } });
+ const channel = await Channel.findOneOrFail({ where: { id: channel_id } });
+
+ if (!(await channel.getUserPermissions({ user_id: req.user_id })).has(Permissions.FLAGS.ATTACH_FILES)) {
+ return res.status(403).json({
+ code: 403,
+ message: "Missing Permissions: ATTACH_FILES",
+ });
+ }
+
+ const cdnUrl = Config.get().cdn.endpointPublic;
+ const batchId = `CLOUD_${user.id}_${randomString(128)}`;
+ const attachments = await Promise.all(
+ payload.files.map(async (attachment) => {
+ attachment.filename = attachment.filename.replaceAll(" ", "_").replace(/[^a-zA-Z0-9._]+/g, "");
+ const uploadFilename = `${batchId}/${attachment.id}/${attachment.filename}`;
+ const newAttachment = CloudAttachment.create({
+ user: user,
+ channel: channel,
+ uploadFilename: uploadFilename,
+ userAttachmentId: attachment.id,
+ userFilename: attachment.filename,
+ userFileSize: attachment.file_size,
+ userIsClip: attachment.is_clip,
+ userOriginalContentType: attachment.original_content_type,
+ });
+ await newAttachment.save();
+ return newAttachment;
+ }),
+ );
+
+ res.send({attachments: attachments.map(a => {
+ return {
+ id: a.userAttachmentId,
+ upload_filename: a.uploadFilename,
+ upload_url: `${cdnUrl}/attachments/${a.uploadFilename}`,
+ }
+ })} as UploadAttachmentResponseSchema);
+ },
+);
+
+export default router;
diff --git a/src/api/routes/channels/#channel_id/invites.ts b/src/api/routes/channels/#channel_id/invites.ts
index ae32e80d..e8602729 100644
--- a/src/api/routes/channels/#channel_id/invites.ts
+++ b/src/api/routes/channels/#channel_id/invites.ts
@@ -16,7 +16,7 @@
along with this program. If not, see .
*/
-import { random, route } from "@spacebar/api";
+import { randomString, route } from "@spacebar/api";
import {
Channel,
Guild,
@@ -70,7 +70,7 @@ router.post(
: new Date(body.max_age * 1000 + Date.now());
const invite = await Invite.create({
- code: random(),
+ code: randomString(),
temporary: body.temporary || true,
uses: 0,
max_uses: body.max_uses ? Math.max(0, body.max_uses) : 0,
diff --git a/src/api/routes/guilds/#guild_id/widget.json.ts b/src/api/routes/guilds/#guild_id/widget.json.ts
index 5d9f28c2..f0518184 100644
--- a/src/api/routes/guilds/#guild_id/widget.json.ts
+++ b/src/api/routes/guilds/#guild_id/widget.json.ts
@@ -16,7 +16,7 @@
along with this program. If not, see .
*/
-import { random, route } from "@spacebar/api";
+import { randomString, route } from "@spacebar/api";
import {
Channel,
DiscordApiErrors,
@@ -77,7 +77,7 @@ router.get(
const expires_at = new Date(max_age * 1000 + Date.now());
invite = await Invite.create({
- code: random(),
+ code: randomString(),
temporary: false,
uses: 0,
max_uses: 0,
diff --git a/src/api/util/utility/RandomInviteID.ts b/src/api/util/utility/RandomInviteID.ts
index 926750d3..bd50ed92 100644
--- a/src/api/util/utility/RandomInviteID.ts
+++ b/src/api/util/utility/RandomInviteID.ts
@@ -22,7 +22,7 @@ import crypto from "crypto";
// TODO: 'random'? seriously? who named this?
// And why is this even here? Just use cryto.randomBytes?
-export function random(length = 6) {
+export function randomString(length = 6) {
// Declare all characters
const chars =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
diff --git a/src/cdn/routes/attachments.ts b/src/cdn/routes/attachments.ts
index 4dca803b..79fbed57 100644
--- a/src/cdn/routes/attachments.ts
+++ b/src/cdn/routes/attachments.ts
@@ -1,6 +1,6 @@
/*
Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
- Copyright (C) 2023 Spacebar and Spacebar Contributors
+ Copyright (C) 2025 Spacebar and Spacebar Contributors
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
@@ -16,133 +16,135 @@
along with this program. If not, see .
*/
-import {
- Config,
- hasValidSignature,
- NewUrlUserSignatureData,
- Snowflake,
- UrlSignResult,
-} from "@spacebar/util";
+import { Config, hasValidSignature, NewUrlUserSignatureData, Snowflake, UrlSignResult } from "@spacebar/util";
import { Request, Response, Router } from "express";
import FileType from "file-type";
import imageSize from "image-size";
import { HTTPError } from "lambert-server";
import { multer } from "../util/multer";
import { storage } from "../util/Storage";
+import { CloudAttachment } from "../../util/entities/CloudAttachment";
const router = Router();
-const SANITIZED_CONTENT_TYPE = [
- "text/html",
- "text/mhtml",
- "multipart/related",
- "application/xhtml+xml",
-];
+const SANITIZED_CONTENT_TYPE = ["text/html", "text/mhtml", "multipart/related", "application/xhtml+xml"];
-router.post(
- "/:channel_id",
- multer.single("file"),
- async (req: Request, res: Response) => {
- if (req.headers.signature !== Config.get().security.requestSignature)
- throw new HTTPError("Invalid request signature");
+router.post("/:channel_id", multer.single("file"), async (req: Request, res: Response) => {
+ if (req.headers.signature !== Config.get().security.requestSignature) throw new HTTPError("Invalid request signature");
- if (!req.file) throw new HTTPError("file missing");
+ if (!req.file) throw new HTTPError("file missing");
- const { buffer, mimetype, size, originalname } = req.file;
- const { channel_id } = req.params;
- const filename = originalname
- .replaceAll(" ", "_")
- .replace(/[^a-zA-Z0-9._]+/g, "");
- const id = Snowflake.generate();
- const path = `attachments/${channel_id}/${id}/${filename}`;
+ const { buffer, mimetype, size, originalname } = req.file;
+ const { channel_id } = req.params;
+ const filename = originalname.replaceAll(" ", "_").replace(/[^a-zA-Z0-9._]+/g, "");
+ const id = Snowflake.generate();
+ const path = `attachments/${channel_id}/${id}/${filename}`;
- const endpoint =
- Config.get()?.cdn.endpointPublic || "http://localhost:3001";
+ const endpoint = Config.get()?.cdn.endpointPublic || "http://localhost:3001";
- await storage.set(path, buffer);
- let width;
- let height;
- if (mimetype.includes("image")) {
- const dimensions = imageSize(buffer);
- if (dimensions) {
- width = dimensions.width;
- height = dimensions.height;
- }
+ await storage.set(path, buffer);
+ let width;
+ let height;
+ if (mimetype.includes("image")) {
+ const dimensions = imageSize(buffer);
+ if (dimensions) {
+ width = dimensions.width;
+ height = dimensions.height;
}
+ }
- const finalUrl = `${endpoint}/${path}`;
+ const finalUrl = `${endpoint}/${path}`;
- const file = {
- id,
- content_type: mimetype,
- filename: filename,
- size,
- url: finalUrl,
- path,
- width,
- height,
- };
+ const file = {
+ id,
+ content_type: mimetype,
+ filename: filename,
+ size,
+ url: finalUrl,
+ path,
+ width,
+ height,
+ };
- return res.json(file);
- },
-);
+ return res.json(file);
+});
-router.get(
- "/:channel_id/:id/:filename",
- async (req: Request, res: Response) => {
- const { channel_id, id, filename } = req.params;
- // const { format } = req.query;
+router.get("/:channel_id/:id/:filename", async (req: Request, res: Response) => {
+ const { channel_id, id, filename } = req.params;
+ // const { format } = req.query;
- const path = `attachments/${channel_id}/${id}/${filename}`;
+ const path = `attachments/${channel_id}/${id}/${filename}`;
- const fullUrl =
- (req.headers["x-forwarded-proto"] ?? req.protocol) +
- "://" +
- (req.headers["x-forwarded-host"] ?? req.hostname) +
- req.originalUrl;
+ const fullUrl = (req.headers["x-forwarded-proto"] ?? req.protocol) + "://" + (req.headers["x-forwarded-host"] ?? req.hostname) + req.originalUrl;
- if (
- Config.get().security.cdnSignUrls &&
- !hasValidSignature(
- new NewUrlUserSignatureData({
- ip: req.ip,
- userAgent: req.headers["user-agent"] as string,
- }),
- UrlSignResult.fromUrl(fullUrl),
- )
- ) {
- return res.status(404).send("This content is no longer available.");
+ if (
+ Config.get().security.cdnSignUrls &&
+ !hasValidSignature(
+ new NewUrlUserSignatureData({
+ ip: req.ip,
+ userAgent: req.headers["user-agent"] as string,
+ }),
+ UrlSignResult.fromUrl(fullUrl),
+ )
+ ) {
+ return res.status(404).send("This content is no longer available.");
+ }
+
+ const file = await storage.get(path);
+ if (!file) throw new HTTPError("File not found");
+ const type = await FileType.fromBuffer(file);
+ let content_type = type?.mime || "application/octet-stream";
+
+ if (SANITIZED_CONTENT_TYPE.includes(content_type)) {
+ content_type = "application/octet-stream";
+ }
+
+ res.set("Content-Type", content_type);
+ res.set("Cache-Control", "public, max-age=31536000");
+
+ return res.send(file);
+});
+
+router.delete("/:channel_id/:id/:filename", async (req: Request, res: Response) => {
+ if (req.headers.signature !== Config.get().security.requestSignature) throw new HTTPError("Invalid request signature");
+
+ const { channel_id, id, filename } = req.params;
+ const path = `attachments/${channel_id}/${id}/${filename}`;
+
+ await storage.delete(path);
+
+ return res.send({ success: true });
+});
+
+// "cloud attachments"
+router.put("/:channel_id/:batch_id/:attachment_id/:filename", multer.single("file"), async (req: Request, res: Response) => {
+ const { channel_id, batch_id, attachment_id, filename } = req.params;
+ const att = await CloudAttachment.findOneOrFail({
+ where: {
+ uploadFilename: `${channel_id}/${batch_id}/${attachment_id}/${filename}`,
+ },
+ });
+
+ if (!req.file) throw new HTTPError("file missing");
+
+ const { buffer, mimetype, size } = req.file;
+ const path = `${channel_id}/${batch_id}/${attachment_id}/${filename}`;
+
+ await storage.set(path, buffer);
+ if (mimetype.includes("image")) {
+ const dimensions = imageSize(buffer);
+ if (dimensions) {
+ att.width = dimensions.width;
+ att.height = dimensions.height;
}
+ }
- const file = await storage.get(path);
- if (!file) throw new HTTPError("File not found");
- const type = await FileType.fromBuffer(file);
- let content_type = type?.mime || "application/octet-stream";
+ att.size = size;
+ att.contentType = att.userOriginalContentType ?? mimetype;
- if (SANITIZED_CONTENT_TYPE.includes(content_type)) {
- content_type = "application/octet-stream";
- }
+ await att.save();
- res.set("Content-Type", content_type);
- res.set("Cache-Control", "public, max-age=31536000");
-
- return res.send(file);
- },
-);
-
-router.delete(
- "/:channel_id/:id/:filename",
- async (req: Request, res: Response) => {
- if (req.headers.signature !== Config.get().security.requestSignature)
- throw new HTTPError("Invalid request signature");
-
- const { channel_id, id, filename } = req.params;
- const path = `attachments/${channel_id}/${id}/${filename}`;
-
- await storage.delete(path);
-
- return res.send({ success: true });
- },
-);
+ return res.status(200);
+});
export default router;
diff --git a/src/util/entities/CloudAttachment.ts b/src/util/entities/CloudAttachment.ts
new file mode 100644
index 00000000..02c52743
--- /dev/null
+++ b/src/util/entities/CloudAttachment.ts
@@ -0,0 +1,85 @@
+/*
+ Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
+ Copyright (C) 2023 Spacebar and Spacebar Contributors
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+*/
+
+import { BeforeRemove, Column, Entity, JoinColumn, ManyToOne, OneToOne, RelationId } from "typeorm";
+import { URL } from "url";
+import { deleteFile } from "../util/cdn";
+import { BaseClass } from "./BaseClass";
+import { dbEngine } from "../util/Database";
+import { User } from "./User";
+import { Channel } from "./Channel";
+
+@Entity({
+ name: "cloud_attachments",
+ engine: dbEngine,
+})
+export class CloudAttachment extends BaseClass {
+ // Internal tracking metadata
+ @Column({ name: "user_id", nullable: true })
+ @RelationId((att: CloudAttachment) => att.user)
+ userId: string;
+
+ @JoinColumn({ name: "userId" })
+ @ManyToOne(() => User, { nullable: true, onDelete: "SET NULL" })
+ user?: User;
+
+ @Column({ name: "channel_id", nullable: true })
+ @RelationId((att: CloudAttachment) => att.channel)
+ channelId?: string; // channel the file is uploaded to
+
+ @JoinColumn({ name: "channelId" })
+ @ManyToOne(() => Channel, { nullable: true, onDelete: "SET NULL" })
+ channel?: Channel; // channel the file is uploaded to
+
+ @Column({ name: "upload_filename" })
+ uploadFilename: string;
+
+ // User-provided info
+ @Column({ name: "user_attachment_id", nullable: true })
+ userAttachmentId?: string;
+
+ @Column({ name: "user_filename" })
+ userFilename: string; // name of file attached
+
+ @Column({ name: "user_file_size", nullable: true })
+ userFileSize?: number; // size of file in bytes
+
+ @Column({ name: "user_original_content_type", nullable: true })
+ userOriginalContentType?: string;
+
+ @Column({ name: "user_is_clip", nullable: true })
+ userIsClip?: boolean; // whether the file is a clip
+
+ // Actual file info, initialised after upload
+ @Column({ nullable: true })
+ size?: number; // size of file in bytes
+
+ @Column({ nullable: true })
+ height?: number; // height of file (if image)
+
+ @Column({ nullable: true })
+ width?: number; // width of file (if image)
+
+ @Column({ name: "content_type", nullable: true })
+ contentType?: string;
+
+ // @BeforeRemove()
+ // onDelete() {
+ // return deleteFile(new URL(this.url).pathname);
+ // }
+}
diff --git a/src/util/migration/postgres/1758654246197-CloudAttachments.ts b/src/util/migration/postgres/1758654246197-CloudAttachments.ts
new file mode 100644
index 00000000..03f745b2
--- /dev/null
+++ b/src/util/migration/postgres/1758654246197-CloudAttachments.ts
@@ -0,0 +1,18 @@
+import { MigrationInterface, QueryRunner } from "typeorm";
+
+export class CloudAttachments1758654246197 implements MigrationInterface {
+ name = 'CloudAttachments1758654246197'
+
+ public async up(queryRunner: QueryRunner): Promise {
+ await queryRunner.query(`CREATE TABLE "cloud_attachments" ("id" character varying NOT NULL, "user_id" character varying, "channel_id" character varying, "upload_filename" character varying NOT NULL, "user_attachment_id" character varying, "user_filename" character varying NOT NULL, "user_file_size" integer, "user_original_content_type" character varying, "user_is_clip" boolean, "size" integer, "height" integer, "width" integer, "content_type" character varying, "userId" character varying, "channelId" character varying, CONSTRAINT "PK_5794827a3ee7c9318612dcb70c8" PRIMARY KEY ("id"))`);
+ await queryRunner.query(`ALTER TABLE "cloud_attachments" ADD CONSTRAINT "FK_e6b32df2004e8ad0f488b4a2019" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE NO ACTION`);
+ await queryRunner.query(`ALTER TABLE "cloud_attachments" ADD CONSTRAINT "FK_cab965a18f8ca30293bff3d50a8" FOREIGN KEY ("channelId") REFERENCES "channels"("id") ON DELETE SET NULL ON UPDATE NO ACTION`);
+ }
+
+ public async down(queryRunner: QueryRunner): Promise {
+ await queryRunner.query(`ALTER TABLE "cloud_attachments" DROP CONSTRAINT "FK_cab965a18f8ca30293bff3d50a8"`);
+ await queryRunner.query(`ALTER TABLE "cloud_attachments" DROP CONSTRAINT "FK_e6b32df2004e8ad0f488b4a2019"`);
+ await queryRunner.query(`DROP TABLE "cloud_attachments"`);
+ }
+
+}
diff --git a/src/util/schemas/UploadAttachmentRequestSchema.ts b/src/util/schemas/UploadAttachmentRequestSchema.ts
new file mode 100644
index 00000000..06cb65f1
--- /dev/null
+++ b/src/util/schemas/UploadAttachmentRequestSchema.ts
@@ -0,0 +1,30 @@
+/*
+ Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
+ Copyright (C) 2024 Spacebar and Spacebar Contributors
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+*/
+
+
+export interface UploadAttachmentRequestSchema {
+ files: UploadAttachmentRequest[];
+}
+
+export interface UploadAttachmentRequest {
+ id: string;
+ filename: string;
+ file_size: number;
+ is_clip?: boolean;
+ original_content_type?: string;
+}
diff --git a/src/util/schemas/index.ts b/src/util/schemas/index.ts
index 9bba9cb4..39710425 100644
--- a/src/util/schemas/index.ts
+++ b/src/util/schemas/index.ts
@@ -81,6 +81,7 @@ export * from "./TemplateModifySchema";
export * from "./TotpDisableSchema";
export * from "./TotpEnableSchema";
export * from "./TotpSchema";
+export * from "./UploadAttachmentRequestSchema";
export * from "./UserDeleteSchema";
export * from "./UserGuildSettingsSchema";
export * from "./UserModifySchema";
diff --git a/src/util/schemas/responses/UploadAttachmentResponseSchema.ts b/src/util/schemas/responses/UploadAttachmentResponseSchema.ts
new file mode 100644
index 00000000..322137da
--- /dev/null
+++ b/src/util/schemas/responses/UploadAttachmentResponseSchema.ts
@@ -0,0 +1,28 @@
+/*
+ Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
+ Copyright (C) 2024 Spacebar and Spacebar Contributors
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+*/
+
+
+export interface UploadAttachmentResponseSchema {
+ attachments: UploadAttachmentResponse[];
+}
+
+export interface UploadAttachmentResponse {
+ id?: string;
+ upload_url: string;
+ upload_filename: string;
+}
diff --git a/src/util/schemas/responses/index.ts b/src/util/schemas/responses/index.ts
index 8ca6fc61..561af184 100644
--- a/src/util/schemas/responses/index.ts
+++ b/src/util/schemas/responses/index.ts
@@ -58,6 +58,7 @@ export * from "./Tenor";
export * from "./TokenResponse";
export * from "./TypedResponses";
export * from "./UpdatesResponse";
+export * from "./UploadAttachmentResponseSchema";
export * from "./UserNoteResponse";
export * from "./UserProfileResponse";
export * from "./UserRelationshipsResponse";
diff --git a/src/util/util/Permissions.ts b/src/util/util/Permissions.ts
index 53a4c4f0..440be9df 100644
--- a/src/util/util/Permissions.ts
+++ b/src/util/util/Permissions.ts
@@ -20,7 +20,7 @@ export class Permissions extends BitField {
constructor(bits: BitFieldResolvable = 0) {
super(bits);
if (this.bitfield & Permissions.FLAGS.ADMINISTRATOR) {
- this.bitfield = ALL_PERMISSIONS;
+ this.bitfield = Permissions.ALL_PERMISSIONS;
}
}
@@ -73,6 +73,11 @@ export class Permissions extends BitField {
// CUSTOM_PERMISSION: BigInt(1) << BigInt(0) + CUSTOM_PERMISSION_OFFSET
};
+ static ALL_PERMISSIONS = Object.values(Permissions.FLAGS).reduce(
+ (total, val) => total | val,
+ BigInt(0),
+ );
+
any(permission: PermissionResolvable, checkAdmin = true) {
return (
(checkAdmin && super.any(Permissions.FLAGS.ADMINISTRATOR)) ||
@@ -207,11 +212,6 @@ export class Permissions extends BitField {
));
}
-const ALL_PERMISSIONS = Object.values(Permissions.FLAGS).reduce(
- (total, val) => total | val,
- BigInt(0),
-);
-
export type PermissionCache = {
channel?: Channel | undefined;
member?: Member | undefined;