diff --git a/.github/relase_body_template.md b/.github/relase_body_template.md
index 34b468804..994e83d35 100644
--- a/.github/relase_body_template.md
+++ b/.github/relase_body_template.md
@@ -1,9 +1,10 @@
## Notes
## Additions
+-
## Fixes
-
+-
## Download
- [Windows]()
- [MacOS]()
diff --git a/README.md b/README.md
index cc831e627..1bcea1e32 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,5 @@
-
+
Fosscord Server
diff --git a/api/assets/fosscord-login.css b/api/assets/fosscord-login.css
index bc32b82ea..34cf542bf 100644
--- a/api/assets/fosscord-login.css
+++ b/api/assets/fosscord-login.css
@@ -22,10 +22,10 @@ h3.title-jXR8lp.marginBottom8-AtZOdT.base-1x0h_U.size24-RIRrxO::after {
/* Logo in top left when bg removed */
#app-mount > div.app-1q1i1E > div > a {
/* replace me: original dimensions: 130x36 */
- background: url(https://raw.githubusercontent.com/fosscord/fosscord/9900329e5ef2c17bdeb6893e04c0511f72027f97/assets/logo/temp.svg);
+ background: url(https://raw.githubusercontent.com/fosscord/fosscord/master/assets-rebrand/svg/Fosscord-Wordmark-Gradient.svg);
+ width: 130px;
+ height: 23px;
background-size: contain;
- width: 128px;
- height: 128px;
border-radius: 50%;
}
diff --git a/api/assets/fosscord.css b/api/assets/fosscord.css
index 6a8b4c64e..6078fdeb4 100644
--- a/api/assets/fosscord.css
+++ b/api/assets/fosscord.css
@@ -13,10 +13,14 @@
/* home button icon */
#app-mount > div.app-1q1i1E > div > div.layers-3iHuyZ.layers-3q14ss > div > div > nav > ul > div.scroller-1Bvpku.none-2Eo-qx.scrollerBase-289Jih > div.tutorialContainer-2sGCg9 > div > div.listItemWrapper-KhRmzM > div > svg > foreignObject > div > div
{
- background-image: url(https://raw.githubusercontent.com/fosscord/fosscord/9900329e5ef2c17bdeb6893e04c0511f72027f97/assets/logo/temp.svg);
+ background-image: url(https://raw.githubusercontent.com/fosscord/fosscord/master/assets-rebrand/svg/Fosscord-Icon-Rounded-Subtract.svg);
background-size: contain;
border-radius: 50%;
}
+
+#app-mount > div.app-1q1i1E > div > div.layers-3iHuyZ.layers-3q14ss > div > div > nav > ul > div.scroller-1Bvpku.none-2Eo-qx.scrollerBase-289Jih > div.tutorialContainer-2sGCg9 > div > div.listItemWrapper-KhRmzM > div > svg > foreignObject > div > div, #app-mount > div.app-1q1i1E > div > div.layers-3iHuyZ.layers-3q14ss > div > div > nav > ul > div.scroller-1Bvpku.none-2Eo-qx.scrollerBase-289Jih > div.tutorialContainer-2sGCg9 > div > div.listItemWrapper-KhRmzM > div > svg > foreignObject > div > div:hover {
+ background-color: white;
+}
/* Login QR */
#app-mount > div.app-1q1i1E > div > div > div > div > form > div > div > div.transitionGroup-aR7y1d.qrLogin-1AOZMt,
#app-mount > div.app-1q1i1E > div > div > div > div > form > div > div > div.verticalSeparator-3huAjp,
diff --git a/api/package-lock.json b/api/package-lock.json
index aa0c07c5a..28af2e8b1 100644
Binary files a/api/package-lock.json and b/api/package-lock.json differ
diff --git a/api/package.json b/api/package.json
index 303d58716..f4614c90b 100644
--- a/api/package.json
+++ b/api/package.json
@@ -30,7 +30,7 @@
"discord-open-source"
],
"author": "Fosscord",
- "license": "ISC",
+ "license": "GPLV3",
"bugs": {
"url": "https://github.com/fosscord/fosscord-server/issues"
},
@@ -86,7 +86,7 @@
"missing-native-js-functions": "^1.2.18",
"morgan": "^1.10.0",
"multer": "^1.4.2",
- "node-fetch": "^2.6.1",
+ "node-fetch": "^3.1.1",
"patch-package": "^6.4.7",
"picocolors": "^1.0.0",
"proxy-agent": "^5.0.0",
diff --git a/api/scripts/stresstest/package-lock.json b/api/scripts/stresstest/package-lock.json
index ca84a8cf4..81c9b817a 100644
Binary files a/api/scripts/stresstest/package-lock.json and b/api/scripts/stresstest/package-lock.json differ
diff --git a/api/src/routes/channels/#channel_id/messages/index.ts b/api/src/routes/channels/#channel_id/messages/index.ts
index b2fb615c3..1ae9d6761 100644
--- a/api/src/routes/channels/#channel_id/messages/index.ts
+++ b/api/src/routes/channels/#channel_id/messages/index.ts
@@ -37,7 +37,11 @@ export function isTextChannel(type: ChannelType): boolean {
case ChannelType.GUILD_PUBLIC_THREAD:
case ChannelType.GUILD_PRIVATE_THREAD:
case ChannelType.GUILD_TEXT:
+ case ChannelType.ENCRYPTED:
+ case ChannelType.ENCRYPTED_THREAD:
return true;
+ default:
+ throw new HTTPError("unimplemented", 400);
}
}
@@ -87,7 +91,7 @@ router.get("/", async (req: Request, res: Response) => {
permissions.hasThrow("VIEW_CHANNEL");
if (!permissions.has("READ_MESSAGE_HISTORY")) return res.json([]);
- var query: FindManyOptions & { where: { id?: any } } = {
+ var query: FindManyOptions & { where: { id?: any; }; } = {
order: { id: "DESC" },
take: limit,
where: { channel_id },
@@ -172,7 +176,7 @@ router.post(
}
const channel = await Channel.findOneOrFail({ where: { id: channel_id }, relations: ["recipients", "recipients.user"] });
- const embeds = [];
+ const embeds = body.embeds || [];
if (body.embed) embeds.push(body.embed);
let message = await handleMessage({
...body,
@@ -216,7 +220,7 @@ router.post(
channel.save()
]);
- postHandleMessage(message).catch((e) => {}); // no await as it shouldnt block the message send function and silently catch error
+ postHandleMessage(message).catch((e) => { }); // no await as it shouldnt block the message send function and silently catch error
return res.json(message);
}
diff --git a/api/src/routes/guilds/#guild_id/roles.ts b/api/src/routes/guilds/#guild_id/roles.ts
index b1875598b..b6894e3f2 100644
--- a/api/src/routes/guilds/#guild_id/roles.ts
+++ b/api/src/routes/guilds/#guild_id/roles.ts
@@ -8,7 +8,8 @@ import {
GuildRoleDeleteEvent,
emitEvent,
Config,
- DiscordApiErrors
+ DiscordApiErrors,
+ handleFile
} from "@fosscord/util";
import { HTTPError } from "lambert-server";
import { route } from "@fosscord/api";
@@ -22,6 +23,8 @@ export interface RoleModifySchema {
hoist?: boolean; // whether the role should be displayed separately in the sidebar
mentionable?: boolean; // whether the role should be mentionable
position?: number;
+ icon?: string;
+ unicode_emoji?: string;
}
export type RolePositionUpdateSchema = {
@@ -58,7 +61,9 @@ router.post("/", route({ body: "RoleModifySchema", permission: "MANAGE_ROLES" })
guild_id: guild_id,
managed: false,
permissions: String(req.permission!.bitfield & BigInt(body.permissions || "0")),
- tags: undefined
+ tags: undefined,
+ icon: null,
+ unicode_emoji: null
});
await Promise.all([
@@ -105,6 +110,8 @@ router.patch("/:role_id", route({ body: "RoleModifySchema", permission: "MANAGE_
const { role_id, guild_id } = req.params;
const body = req.body as RoleModifySchema;
+ if (body.icon) body.icon = await handleFile(`/role-icons/${role_id}`, body.icon as string);
+
const role = new Role({
...body,
id: role_id,
diff --git a/api/src/routes/invites/index.ts b/api/src/routes/invites/index.ts
index a2cf4cb5e..37e9e05a3 100644
--- a/api/src/routes/invites/index.ts
+++ b/api/src/routes/invites/index.ts
@@ -19,7 +19,8 @@ router.post("/:code", route({}), async (req: Request, res: Response) => {
const { features } = await Guild.findOneOrFail({ id: guild_id});
const { public_flags } = await User.findOneOrFail({ id: req.user_id });
- if(features.includes("INTERNAL_EMPLOYEE_ONLY") && (public_flags & 1) !== 1) throw new HTTPError("You are not allowed to join this guild.", 401)
+ if(features.includes("INTERNAL_EMPLOYEE_ONLY") && (public_flags & 1) !== 1) throw new HTTPError("Only intended for the staff of this server.", 401);
+ if(features.includes("INVITES_CLOSED")) throw new HTTPError("Sorry, this guild has joins closed.", 403);
const invite = await Invite.joinGuild(req.user_id, code);
diff --git a/api/src/util/handlers/Message.ts b/api/src/util/handlers/Message.ts
index ba6763948..216643686 100644
--- a/api/src/util/handlers/Message.ts
+++ b/api/src/util/handlers/Message.ts
@@ -2,6 +2,7 @@ import {
Channel,
Embed,
emitEvent,
+ Guild,
Message,
MessageCreateEvent,
MessageUpdateEvent,
@@ -17,13 +18,14 @@ import {
User,
Application,
Webhook,
- Attachment
+ Attachment,
+ Config,
} from "@fosscord/util";
import { HTTPError } from "lambert-server";
import fetch from "node-fetch";
import cheerio from "cheerio";
import { MessageCreateSchema } from "../../routes/channels/#channel_id/messages";
-
+const allow_empty = false;
// TODO: check webhook, application, system author, stickers
// TODO: embed gifs/videos/images
@@ -55,6 +57,10 @@ export async function handleMessage(opts: MessageOptions): Promise {
type: opts.type ?? 0
});
+ if (message.content && message.content.length > Config.get().limits.message.maxCharacters) {
+ throw new HTTPError("Content length over max character limit")
+ }
+
// TODO: are tts messages allowed in dm channels? should permission be checked?
if (opts.author_id) {
message.author = await User.getPublicUser(opts.author_id);
@@ -67,7 +73,7 @@ export async function handleMessage(opts: MessageOptions): Promise {
}
const permission = await getPermission(opts.author_id, channel.guild_id, opts.channel_id);
- permission.hasThrow("SEND_MESSAGES");
+ permission.hasThrow("SEND_MESSAGES"); // TODO: add the rights check
if (permission.cache.member) {
message.member = permission.cache.member;
}
@@ -75,15 +81,19 @@ export async function handleMessage(opts: MessageOptions): Promise {
if (opts.tts) permission.hasThrow("SEND_TTS_MESSAGES");
if (opts.message_reference) {
permission.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");
+ // code below has to be redone when we add custom message routing and cross-channel replies
+ const guild = await Guild.findOneOrFail({ id: channel.guild_id });
+ if (!guild.features.includes("CROSS_CHANNEL_REPLIES")) {
+ 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
message.type = MessageType.REPLY;
}
// TODO: stickers/activity
- if (!opts.content && !opts.embeds?.length && !opts.attachments?.length && !opts.sticker_ids?.length) {
+ if (!allow_empty && (!opts.content && !opts.embeds?.length && !opts.attachments?.length && !opts.sticker_ids?.length)) {
throw new HTTPError("Empty messages are not allowed", 50006);
}
@@ -93,7 +103,7 @@ export async function handleMessage(opts: MessageOptions): Promise {
var mention_user_ids = [] as string[];
var mention_everyone = false;
- if (content) {
+ if (content) { // TODO: explicit-only mentions
message.content = content.trim();
for (const [_, mention] of content.matchAll(CHANNEL_MENTION)) {
if (!mention_channel_ids.includes(mention)) mention_channel_ids.push(mention);
@@ -135,7 +145,7 @@ export async function postHandleMessage(message: Message) {
const data = { ...message };
data.embeds = data.embeds.filter((x) => x.type !== "link");
- links = links.slice(0, 5); // embed max 5 links
+ links = links.slice(0, 20); // embed max 20 links — TODO: make this configurable with instance policies
for (const link of links) {
try {
@@ -188,7 +198,7 @@ export async function sendMessage(opts: MessageOptions) {
emitEvent({ event: "MESSAGE_CREATE", channel_id: opts.channel_id, data: message.toJSON() } as MessageCreateEvent)
]);
- postHandleMessage(message).catch((e) => {}); // no await as it shouldnt block the message send function and silently catch error
+ postHandleMessage(message).catch((e) => {}); // no await as it should catch error non-blockingly
return message;
}
diff --git a/bundle/package-lock.json b/bundle/package-lock.json
index 294dfe1f9..1b1455487 100644
Binary files a/bundle/package-lock.json and b/bundle/package-lock.json differ
diff --git a/bundle/package.json b/bundle/package.json
index 7dc9a776b..9acbf6298 100644
--- a/bundle/package.json
+++ b/bundle/package.json
@@ -94,7 +94,7 @@
"morgan": "^1.10.0",
"multer": "^1.4.2",
"nanocolors": "^0.2.12",
- "node-fetch": "^2.6.1",
+ "node-fetch": "^2.6.7",
"node-os-utils": "^1.3.5",
"patch-package": "^6.4.7",
"pg": "^8.7.1",
diff --git a/cdn/package-lock.json b/cdn/package-lock.json
index 367f411e6..a6c2df2d4 100644
Binary files a/cdn/package-lock.json and b/cdn/package-lock.json differ
diff --git a/cdn/package.json b/cdn/package.json
index fec43785c..aedcc4bff 100644
--- a/cdn/package.json
+++ b/cdn/package.json
@@ -15,7 +15,7 @@
},
"keywords": [],
"author": "",
- "license": "ISC",
+ "license": "GPLV3",
"bugs": {
"url": "https://github.com/fosscord/fosscord-server/issues"
},
@@ -54,7 +54,7 @@
"missing-native-js-functions": "^1.2.17",
"multer": "^1.4.2",
"nanocolors": "^0.2.12",
- "node-fetch": "^2.6.1",
+ "node-fetch": "^2.6.7",
"supertest": "^6.1.6",
"typescript": "^4.1.2"
},
diff --git a/cdn/src/Server.ts b/cdn/src/Server.ts
index cac34a80d..b8d71fa95 100644
--- a/cdn/src/Server.ts
+++ b/cdn/src/Server.ts
@@ -2,6 +2,7 @@ import { Server, ServerOptions } from "lambert-server";
import { Config, initDatabase, registerRoutes } from "@fosscord/util";
import path from "path";
import avatarsRoute from "./routes/avatars";
+import iconsRoute from "./routes/role-icons";
import bodyParser from "body-parser";
export interface CDNServerOptions extends ServerOptions {}
@@ -40,6 +41,9 @@ export class CDNServer extends Server {
this.app.use("/icons/", avatarsRoute);
this.log("verbose", "[Server] Route /icons registered");
+ this.app.use("/role-icons/", iconsRoute);
+ this.log("verbose", "[Server] Route /role-icons registered");
+
this.app.use("/emojis/", avatarsRoute);
this.log("verbose", "[Server] Route /emojis registered");
diff --git a/cdn/src/routes/role-icons.ts b/cdn/src/routes/role-icons.ts
new file mode 100644
index 000000000..12aae8a4e
--- /dev/null
+++ b/cdn/src/routes/role-icons.ts
@@ -0,0 +1,101 @@
+import { Router, Response, Request } from "express";
+import { Config, Snowflake } from "@fosscord/util";
+import { storage } from "../util/Storage";
+import FileType from "file-type";
+import { HTTPError } from "lambert-server";
+import crypto from "crypto";
+import { multer } from "../util/multer";
+
+//Role icons ---> avatars.ts modified
+
+// TODO: check user rights and perks and animated pfp are allowed in the policies
+// TODO: generate different sizes of icon
+// TODO: generate different image types of icon
+
+const STATIC_MIME_TYPES = [
+ "image/png",
+ "image/jpeg",
+ "image/webp",
+ "image/svg+xml",
+ "image/svg",
+];
+const ALLOWED_MIME_TYPES = [...STATIC_MIME_TYPES];
+
+const router = Router();
+
+router.post(
+ "/:role_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("Missing file");
+ const { buffer, mimetype, size, originalname, fieldname } = req.file;
+ const { role_id } = req.params;
+
+ var hash = crypto
+ .createHash("md5")
+ .update(Snowflake.generate())
+ .digest("hex");
+
+ const type = await FileType.fromBuffer(buffer);
+ if (!type || !ALLOWED_MIME_TYPES.includes(type.mime))
+ throw new HTTPError("Invalid file type");
+
+ const path = `role-icons/${role_id}/${hash}.png`;
+ const endpoint =
+ Config.get().cdn.endpointPublic || "http://localhost:3003";
+
+ await storage.set(path, buffer);
+
+ return res.json({
+ id: hash,
+ content_type: type.mime,
+ size,
+ url: `${endpoint}${req.baseUrl}/${role_id}/${hash}`,
+ });
+ }
+);
+
+router.get("/:role_id", async (req: Request, res: Response) => {
+ var { role_id } = req.params;
+ //role_id = role_id.split(".")[0]; // remove .file extension
+ const path = `role-icons/${role_id}`;
+
+ const file = await storage.get(path);
+ if (!file) throw new HTTPError("not found", 404);
+ const type = await FileType.fromBuffer(file);
+
+ res.set("Content-Type", type?.mime);
+ res.set("Cache-Control", "public, max-age=31536000, must-revalidate");
+
+ return res.send(file);
+});
+
+router.get("/:role_id/:hash", async (req: Request, res: Response) => {
+ var { role_id, hash } = req.params;
+ //hash = hash.split(".")[0]; // remove .file extension
+ const path = `role-icons/${role_id}/${hash}`;
+
+ const file = await storage.get(path);
+ if (!file) throw new HTTPError("not found", 404);
+ const type = await FileType.fromBuffer(file);
+
+ res.set("Content-Type", type?.mime);
+ res.set("Cache-Control", "public, max-age=31536000, must-revalidate");
+
+ return res.send(file);
+});
+
+router.delete("/:role_id/:id", async (req: Request, res: Response) => {
+ if (req.headers.signature !== Config.get().security.requestSignature)
+ throw new HTTPError("Invalid request signature");
+ const { role_id, id } = req.params;
+ const path = `role-icons/${role_id}/${id}`;
+
+ await storage.delete(path);
+
+ return res.send({ success: true });
+});
+
+export default router;
diff --git a/dashboard/LICENSE b/dashboard/LICENSE
new file mode 100644
index 000000000..f19bf5202
--- /dev/null
+++ b/dashboard/LICENSE
@@ -0,0 +1,14 @@
+Copyright (C) 2021 Fosscord and 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 .
\ No newline at end of file
diff --git a/dashboard/package.json b/dashboard/package.json
index 0967ef424..1009d658e 100644
--- a/dashboard/package.json
+++ b/dashboard/package.json
@@ -1 +1,23 @@
-{}
+{
+ "name": "@fosscord/dashboard",
+ "version": "1.0.0",
+ "description": "Dashboard for Fosscord",
+ "main": "dist/index.js",
+ "types": "src/index.ts",
+ "scripts": {
+ "test": "npm run build && jest --coverage ./tests",
+ "build": "npx tsc -p .",
+ "start": "node dist/start.js"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/fosscord/fosscord-server.git"
+ },
+ "keywords": [],
+ "author": "",
+ "license": "GPLV3",
+ "bugs": {
+ "url": "https://github.com/fosscord/fosscord-server/issues"
+ },
+ "homepage": "https://github.com/fosscord/fosscord-server#readme"
+}
diff --git a/dashboard/src/index.ts b/dashboard/src/index.ts
new file mode 100644
index 000000000..e69de29bb
diff --git a/gateway/package-lock.json b/gateway/package-lock.json
index a9813b6f2..d02106f80 100644
Binary files a/gateway/package-lock.json and b/gateway/package-lock.json differ
diff --git a/gateway/package.json b/gateway/package.json
index e02a0000f..7daddfc0d 100644
--- a/gateway/package.json
+++ b/gateway/package.json
@@ -13,7 +13,7 @@
},
"keywords": [],
"author": "Fosscord",
- "license": "ISC",
+ "license": "GPLV3",
"devDependencies": {
"@types/amqplib": "^0.8.1",
"@types/jsonwebtoken": "^8.5.0",
@@ -32,7 +32,7 @@
"jsonwebtoken": "^8.5.1",
"lambert-server": "^1.2.11",
"missing-native-js-functions": "^1.2.18",
- "node-fetch": "^2.6.1",
+ "node-fetch": "^3.1.1",
"proxy-agent": "^5.0.0",
"typeorm": "^0.2.37",
"ws": "^7.4.2"
diff --git a/gateway/src/util/Send.ts b/gateway/src/util/Send.ts
index 196d4205c..c8627b032 100644
--- a/gateway/src/util/Send.ts
+++ b/gateway/src/util/Send.ts
@@ -1,7 +1,9 @@
var erlpack: any;
try {
erlpack = require("@yukikaze-bot/erlpack");
-} catch (error) {}
+} catch (error) {
+ console.log("Missing @yukikaze-bot/erlpack, electron-based desktop clients designed for discord.com will not be able to connect!");
+}
import { Payload, WebSocket } from "@fosscord/gateway";
export async function Send(socket: WebSocket, data: Payload) {
diff --git a/rtc/LICENSE b/rtc/LICENSE
new file mode 100644
index 000000000..f19bf5202
--- /dev/null
+++ b/rtc/LICENSE
@@ -0,0 +1,14 @@
+Copyright (C) 2021 Fosscord and 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 .
\ No newline at end of file
diff --git a/util/package-lock.json b/util/package-lock.json
index c6f2ed6fc..2b33b7de4 100644
Binary files a/util/package-lock.json and b/util/package-lock.json differ
diff --git a/util/package.json b/util/package.json
index aef5dcfc5..1ef3031e8 100644
--- a/util/package.json
+++ b/util/package.json
@@ -44,7 +44,7 @@
"lambert-server": "^1.2.12",
"missing-native-js-functions": "^1.2.18",
"multer": "^1.4.3",
- "node-fetch": "^2.6.1",
+ "node-fetch": "^2.6.7",
"patch-package": "^6.4.7",
"pg": "^8.7.1",
"picocolors": "^1.0.0",
diff --git a/util/src/entities/Channel.ts b/util/src/entities/Channel.ts
index 4036b5d66..1cc4a5386 100644
--- a/util/src/entities/Channel.ts
+++ b/util/src/entities/Channel.ts
@@ -1,332 +1,357 @@
-import { Column, Entity, JoinColumn, ManyToOne, OneToMany, RelationId } from "typeorm";
-import { BaseClass } from "./BaseClass";
-import { Guild } from "./Guild";
-import { PublicUserProjection, User } from "./User";
-import { HTTPError } from "lambert-server";
-import { containsAll, emitEvent, getPermission, Snowflake, trimSpecial } from "../util";
-import { ChannelCreateEvent, ChannelRecipientRemoveEvent } from "../interfaces";
-import { Recipient } from "./Recipient";
-import { Message } from "./Message";
-import { ReadState } from "./ReadState";
-import { Invite } from "./Invite";
-import { VoiceState } from "./VoiceState";
-import { Webhook } from "./Webhook";
-import { DmChannelDTO } from "../dtos";
-
-export enum ChannelType {
- GUILD_TEXT = 0, // a text channel within a server
- DM = 1, // a direct message between users
- GUILD_VOICE = 2, // a voice channel within a server
- GROUP_DM = 3, // a direct message between multiple users
- GUILD_CATEGORY = 4, // an organizational category that contains up to 50 channels
- GUILD_NEWS = 5, // a channel that users can follow and crosspost into their own server
- GUILD_STORE = 6, // a channel in which game developers can sell their game on Discord
- // TODO: what are channel types between 7-9?
- GUILD_NEWS_THREAD = 10, // a temporary sub-channel within a GUILD_NEWS channel
- GUILD_PUBLIC_THREAD = 11, // a temporary sub-channel within a GUILD_TEXT channel
- GUILD_PRIVATE_THREAD = 12, // a temporary sub-channel within a GUILD_TEXT channel that is only viewable by those invited and those with the MANAGE_THREADS permission
- GUILD_STAGE_VOICE = 13, // a voice channel for hosting events with an audience
-}
-
-@Entity("channels")
-export class Channel extends BaseClass {
- @Column()
- created_at: Date;
-
- @Column({ nullable: true })
- name?: string;
-
- @Column({ type: "text", nullable: true })
- icon?: string | null;
-
- @Column({ type: "int" })
- type: ChannelType;
-
- @OneToMany(() => Recipient, (recipient: Recipient) => recipient.channel, {
- cascade: true,
- orphanedRowAction: "delete",
- })
- recipients?: Recipient[];
-
- @Column({ nullable: true })
- last_message_id: string;
-
- @Column({ nullable: true })
- @RelationId((channel: Channel) => channel.guild)
- guild_id?: string;
-
- @JoinColumn({ name: "guild_id" })
- @ManyToOne(() => Guild, {
- onDelete: "CASCADE",
- })
- guild: Guild;
-
- @Column({ nullable: true })
- @RelationId((channel: Channel) => channel.parent)
- parent_id: string;
-
- @JoinColumn({ name: "parent_id" })
- @ManyToOne(() => Channel)
- parent?: Channel;
-
- // only for group dms
- @Column({ nullable: true })
- @RelationId((channel: Channel) => channel.owner)
- owner_id: string;
-
- @JoinColumn({ name: "owner_id" })
- @ManyToOne(() => User)
- owner: User;
-
- @Column({ nullable: true })
- last_pin_timestamp?: number;
-
- @Column({ nullable: true })
- default_auto_archive_duration?: number;
-
- @Column({ nullable: true })
- position?: number;
-
- @Column({ type: "simple-json", nullable: true })
- permission_overwrites?: ChannelPermissionOverwrite[];
-
- @Column({ nullable: true })
- video_quality_mode?: number;
-
- @Column({ nullable: true })
- bitrate?: number;
-
- @Column({ nullable: true })
- user_limit?: number;
-
- @Column({ nullable: true })
- nsfw?: boolean;
-
- @Column({ nullable: true })
- rate_limit_per_user?: number;
-
- @Column({ nullable: true })
- topic?: string;
-
- @OneToMany(() => Invite, (invite: Invite) => invite.channel, {
- cascade: true,
- orphanedRowAction: "delete",
- })
- invites?: Invite[];
-
- @OneToMany(() => Message, (message: Message) => message.channel, {
- cascade: true,
- orphanedRowAction: "delete",
- })
- messages?: Message[];
-
- @OneToMany(() => VoiceState, (voice_state: VoiceState) => voice_state.channel, {
- cascade: true,
- orphanedRowAction: "delete",
- })
- voice_states?: VoiceState[];
-
- @OneToMany(() => ReadState, (read_state: ReadState) => read_state.channel, {
- cascade: true,
- orphanedRowAction: "delete",
- })
- read_states?: ReadState[];
-
- @OneToMany(() => Webhook, (webhook: Webhook) => webhook.channel, {
- cascade: true,
- orphanedRowAction: "delete",
- })
- webhooks?: Webhook[];
-
- // TODO: DM channel
- static async createChannel(
- channel: Partial,
- user_id: string = "0",
- opts?: {
- keepId?: boolean;
- skipExistsCheck?: boolean;
- skipPermissionCheck?: boolean;
- skipEventEmit?: boolean;
- }
- ) {
- if (!opts?.skipPermissionCheck) {
- // 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 && !opts?.skipExistsCheck) {
- const exists = await Channel.findOneOrFail({ id: channel.parent_id });
- 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 = {
- ...channel,
- ...(!opts?.keepId && { id: Snowflake.generate() }),
- created_at: new Date(),
- position: channel.position || 0,
- };
-
- await Promise.all([
- new Channel(channel).save(),
- !opts?.skipEventEmit
- ? emitEvent({
- event: "CHANNEL_CREATE",
- data: channel,
- guild_id: channel.guild_id,
- } as ChannelCreateEvent)
- : Promise.resolve(),
- ]);
-
- return channel;
- }
-
- static async createDMChannel(recipients: string[], creator_user_id: string, name?: string) {
- recipients = recipients.unique().filter((x) => x !== creator_user_id);
- const otherRecipientsUsers = await User.find({ where: recipients.map((x) => ({ id: x })) });
-
- // TODO: check config for max number of recipients
- if (otherRecipientsUsers.length !== recipients.length) {
- throw new HTTPError("Recipient/s not found");
- }
-
- const type = recipients.length === 1 ? ChannelType.DM : ChannelType.GROUP_DM;
-
- let channel = null;
-
- const channelRecipients = [...recipients, creator_user_id];
-
- const userRecipients = await Recipient.find({
- where: { user_id: creator_user_id },
- relations: ["channel", "channel.recipients"],
- });
-
- for (let ur of userRecipients) {
- let re = ur.channel.recipients!.map((r) => r.user_id);
- if (re.length === channelRecipients.length) {
- if (containsAll(re, channelRecipients)) {
- if (channel == null) {
- channel = ur.channel;
- await ur.assign({ closed: false }).save();
- }
- }
- }
- }
-
- if (channel == null) {
- name = trimSpecial(name);
-
- channel = await new Channel({
- name,
- type,
- owner_id: type === ChannelType.DM ? undefined : creator_user_id,
- created_at: new Date(),
- last_message_id: null,
- recipients: channelRecipients.map(
- (x) =>
- new Recipient({ user_id: x, closed: !(type === ChannelType.GROUP_DM || x === creator_user_id) })
- ),
- }).save();
- }
-
- const channel_dto = await DmChannelDTO.from(channel);
-
- if (type === ChannelType.GROUP_DM) {
- for (let recipient of channel.recipients!) {
- await emitEvent({
- event: "CHANNEL_CREATE",
- data: channel_dto.excludedRecipients([recipient.user_id]),
- user_id: recipient.user_id,
- });
- }
- } else {
- await emitEvent({ event: "CHANNEL_CREATE", data: channel_dto, user_id: creator_user_id });
- }
-
- return channel_dto.excludedRecipients([creator_user_id]);
- }
-
- static async removeRecipientFromChannel(channel: Channel, user_id: string) {
- await Recipient.delete({ channel_id: channel.id, user_id: user_id });
- channel.recipients = channel.recipients?.filter((r) => r.user_id !== user_id);
-
- if (channel.recipients?.length === 0) {
- await Channel.deleteChannel(channel);
- await emitEvent({
- event: "CHANNEL_DELETE",
- data: await DmChannelDTO.from(channel, [user_id]),
- user_id: user_id,
- });
- return;
- }
-
- await emitEvent({
- event: "CHANNEL_DELETE",
- data: await DmChannelDTO.from(channel, [user_id]),
- user_id: user_id,
- });
-
- //If the owner leave we make the first recipient in the list the new owner
- if (channel.owner_id === user_id) {
- channel.owner_id = channel.recipients!.find((r) => r.user_id !== user_id)!.user_id; //Is there a criteria to choose the new owner?
- await emitEvent({
- event: "CHANNEL_UPDATE",
- data: await DmChannelDTO.from(channel, [user_id]),
- channel_id: channel.id,
- });
- }
-
- await channel.save();
-
- await emitEvent({
- event: "CHANNEL_RECIPIENT_REMOVE",
- data: {
- channel_id: channel.id,
- user: await User.findOneOrFail({ where: { id: user_id }, select: PublicUserProjection }),
- },
- channel_id: channel.id,
- } as ChannelRecipientRemoveEvent);
- }
-
- static async deleteChannel(channel: Channel) {
- await Message.delete({ channel_id: channel.id }); //TODO we should also delete the attachments from the cdn but to do that we need to move cdn.ts in util
- //TODO before deleting the channel we should check and delete other relations
- await Channel.delete({ id: channel.id });
- }
-
- isDm() {
- return this.type === ChannelType.DM || this.type === ChannelType.GROUP_DM;
- }
-}
-
-export interface ChannelPermissionOverwrite {
- allow: string;
- deny: string;
- id: string;
- type: ChannelPermissionOverwriteType;
-}
-
-export enum ChannelPermissionOverwriteType {
- role = 0,
- member = 1,
-}
+import { Column, Entity, JoinColumn, ManyToOne, OneToMany, RelationId } from "typeorm";
+import { BaseClass } from "./BaseClass";
+import { Guild } from "./Guild";
+import { PublicUserProjection, User } from "./User";
+import { HTTPError } from "lambert-server";
+import { containsAll, emitEvent, getPermission, Snowflake, trimSpecial, InvisibleCharacters } from "../util";
+import { ChannelCreateEvent, ChannelRecipientRemoveEvent } from "../interfaces";
+import { Recipient } from "./Recipient";
+import { Message } from "./Message";
+import { ReadState } from "./ReadState";
+import { Invite } from "./Invite";
+import { VoiceState } from "./VoiceState";
+import { Webhook } from "./Webhook";
+import { DmChannelDTO } from "../dtos";
+
+export enum ChannelType {
+ GUILD_TEXT = 0, // a text channel within a server
+ DM = 1, // a direct message between users
+ GUILD_VOICE = 2, // a voice channel within a server
+ GROUP_DM = 3, // a direct message between multiple users
+ GUILD_CATEGORY = 4, // an organizational category that contains up to 50 channels
+ GUILD_NEWS = 5, // a channel that users can follow and crosspost into their own server
+ GUILD_STORE = 6, // a channel in which game developers can sell their game on Discord
+ ENCRYPTED = 7, // end-to-end encrypted channel
+ ENCRYPTED_THREAD = 8, // end-to-end encrypted thread channel
+ GUILD_NEWS_THREAD = 10, // a temporary sub-channel within a GUILD_NEWS channel
+ GUILD_PUBLIC_THREAD = 11, // a temporary sub-channel within a GUILD_TEXT channel
+ GUILD_PRIVATE_THREAD = 12, // a temporary sub-channel within a GUILD_TEXT channel that is only viewable by those invited and those with the MANAGE_THREADS permission
+ GUILD_STAGE_VOICE = 13, // a voice channel for hosting events with an audience
+ CUSTOM_START = 64, // start custom channel types from here
+ UNHANDLED = 255 // unhandled unowned pass-through channel type
+}
+
+@Entity("channels")
+export class Channel extends BaseClass {
+ @Column()
+ created_at: Date;
+
+ @Column({ nullable: true })
+ name?: string;
+
+ @Column({ type: "text", nullable: true })
+ icon?: string | null;
+
+ @Column({ type: "int" })
+ type: ChannelType;
+
+ @OneToMany(() => Recipient, (recipient: Recipient) => recipient.channel, {
+ cascade: true,
+ orphanedRowAction: "delete",
+ })
+ recipients?: Recipient[];
+
+ @Column({ nullable: true })
+ last_message_id: string;
+
+ @Column({ nullable: true })
+ @RelationId((channel: Channel) => channel.guild)
+ guild_id?: string;
+
+ @JoinColumn({ name: "guild_id" })
+ @ManyToOne(() => Guild, {
+ onDelete: "CASCADE",
+ })
+ guild: Guild;
+
+ @Column({ nullable: true })
+ @RelationId((channel: Channel) => channel.parent)
+ parent_id: string;
+
+ @JoinColumn({ name: "parent_id" })
+ @ManyToOne(() => Channel)
+ parent?: Channel;
+
+ // only for group dms
+ @Column({ nullable: true })
+ @RelationId((channel: Channel) => channel.owner)
+ owner_id: string;
+
+ @JoinColumn({ name: "owner_id" })
+ @ManyToOne(() => User)
+ owner: User;
+
+ @Column({ nullable: true })
+ last_pin_timestamp?: number;
+
+ @Column({ nullable: true })
+ default_auto_archive_duration?: number;
+
+ @Column({ nullable: true })
+ position?: number;
+
+ @Column({ type: "simple-json", nullable: true })
+ permission_overwrites?: ChannelPermissionOverwrite[];
+
+ @Column({ nullable: true })
+ video_quality_mode?: number;
+
+ @Column({ nullable: true })
+ bitrate?: number;
+
+ @Column({ nullable: true })
+ user_limit?: number;
+
+ @Column({ nullable: true })
+ nsfw?: boolean;
+
+ @Column({ nullable: true })
+ rate_limit_per_user?: number;
+
+ @Column({ nullable: true })
+ topic?: string;
+
+ @OneToMany(() => Invite, (invite: Invite) => invite.channel, {
+ cascade: true,
+ orphanedRowAction: "delete",
+ })
+ invites?: Invite[];
+
+ @OneToMany(() => Message, (message: Message) => message.channel, {
+ cascade: true,
+ orphanedRowAction: "delete",
+ })
+ messages?: Message[];
+
+ @OneToMany(() => VoiceState, (voice_state: VoiceState) => voice_state.channel, {
+ cascade: true,
+ orphanedRowAction: "delete",
+ })
+ voice_states?: VoiceState[];
+
+ @OneToMany(() => ReadState, (read_state: ReadState) => read_state.channel, {
+ cascade: true,
+ orphanedRowAction: "delete",
+ })
+ read_states?: ReadState[];
+
+ @OneToMany(() => Webhook, (webhook: Webhook) => webhook.channel, {
+ cascade: true,
+ orphanedRowAction: "delete",
+ })
+ webhooks?: Webhook[];
+
+ // TODO: DM channel
+ static async createChannel(
+ channel: Partial,
+ user_id: string = "0",
+ opts?: {
+ keepId?: boolean;
+ skipExistsCheck?: boolean;
+ skipPermissionCheck?: boolean;
+ skipEventEmit?: boolean;
+ skipNameChecks?: boolean;
+ }
+ ) {
+ if (!opts?.skipPermissionCheck) {
+ // Always check if user has permission first
+ const permissions = await getPermission(user_id, channel.guild_id);
+ permissions.hasThrow("MANAGE_CHANNELS");
+ }
+
+ if (!opts?.skipNameChecks) {
+ const guild = await Guild.findOneOrFail({ id: channel.guild_id });
+ if (!guild.features.includes("ALLOW_INVALID_CHANNEL_NAMES") && channel.name) {
+ for (var character of InvisibleCharacters)
+ if (channel.name.includes(character))
+ throw new HTTPError("Channel name cannot include invalid characters", 403);
+
+ if (channel.name.match(/\-\-+/g))
+ throw new HTTPError("Channel name cannot include multiple adjacent dashes.", 403)
+
+ if (channel.name.charAt(0) === "-" ||
+ channel.name.charAt(channel.name.length - 1) === "-")
+ throw new HTTPError("Channel name cannot start/end with dash.", 403)
+ }
+
+ if (!guild.features.includes("ALLOW_UNNAMED_CHANNELS")) {
+ if (!channel.name)
+ throw new HTTPError("Channel name cannot be empty.", 403);
+ }
+ }
+
+ switch (channel.type) {
+ case ChannelType.GUILD_TEXT:
+ case ChannelType.GUILD_VOICE:
+ if (channel.parent_id && !opts?.skipExistsCheck) {
+ const exists = await Channel.findOneOrFail({ id: channel.parent_id });
+ 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 = {
+ ...channel,
+ ...(!opts?.keepId && { id: Snowflake.generate() }),
+ created_at: new Date(),
+ position: channel.position || 0,
+ };
+
+ await Promise.all([
+ new Channel(channel).save(),
+ !opts?.skipEventEmit
+ ? emitEvent({
+ event: "CHANNEL_CREATE",
+ data: channel,
+ guild_id: channel.guild_id,
+ } as ChannelCreateEvent)
+ : Promise.resolve(),
+ ]);
+
+ return channel;
+ }
+
+ static async createDMChannel(recipients: string[], creator_user_id: string, name?: string) {
+ recipients = recipients.unique().filter((x) => x !== creator_user_id);
+ const otherRecipientsUsers = await User.find({ where: recipients.map((x) => ({ id: x })) });
+
+ // TODO: check config for max number of recipients
+ if (otherRecipientsUsers.length !== recipients.length) {
+ throw new HTTPError("Recipient/s not found");
+ }
+
+ const type = recipients.length === 1 ? ChannelType.DM : ChannelType.GROUP_DM;
+
+ let channel = null;
+
+ const channelRecipients = [...recipients, creator_user_id];
+
+ const userRecipients = await Recipient.find({
+ where: { user_id: creator_user_id },
+ relations: ["channel", "channel.recipients"],
+ });
+
+ for (let ur of userRecipients) {
+ let re = ur.channel.recipients!.map((r) => r.user_id);
+ if (re.length === channelRecipients.length) {
+ if (containsAll(re, channelRecipients)) {
+ if (channel == null) {
+ channel = ur.channel;
+ await ur.assign({ closed: false }).save();
+ }
+ }
+ }
+ }
+
+ if (channel == null) {
+ name = trimSpecial(name);
+
+ channel = await new Channel({
+ name,
+ type,
+ owner_id: type === ChannelType.DM ? undefined : null, // 1:1 DMs are ownerless in fosscord-server
+ created_at: new Date(),
+ last_message_id: null,
+ recipients: channelRecipients.map(
+ (x) =>
+ new Recipient({ user_id: x, closed: !(type === ChannelType.GROUP_DM || x === creator_user_id) })
+ ),
+ }).save();
+ }
+
+ const channel_dto = await DmChannelDTO.from(channel);
+
+ if (type === ChannelType.GROUP_DM) {
+ for (let recipient of channel.recipients!) {
+ await emitEvent({
+ event: "CHANNEL_CREATE",
+ data: channel_dto.excludedRecipients([recipient.user_id]),
+ user_id: recipient.user_id,
+ });
+ }
+ } else {
+ await emitEvent({ event: "CHANNEL_CREATE", data: channel_dto, user_id: creator_user_id });
+ }
+
+ return channel_dto.excludedRecipients([creator_user_id]);
+ }
+
+ static async removeRecipientFromChannel(channel: Channel, user_id: string) {
+ await Recipient.delete({ channel_id: channel.id, user_id: user_id });
+ channel.recipients = channel.recipients?.filter((r) => r.user_id !== user_id);
+
+ if (channel.recipients?.length === 0) {
+ await Channel.deleteChannel(channel);
+ await emitEvent({
+ event: "CHANNEL_DELETE",
+ data: await DmChannelDTO.from(channel, [user_id]),
+ user_id: user_id,
+ });
+ return;
+ }
+
+ await emitEvent({
+ event: "CHANNEL_DELETE",
+ data: await DmChannelDTO.from(channel, [user_id]),
+ user_id: user_id,
+ });
+
+ //If the owner leave the server user is the new owner
+ if (channel.owner_id === user_id) {
+ channel.owner_id = "1"; // The channel is now owned by the server user
+ await emitEvent({
+ event: "CHANNEL_UPDATE",
+ data: await DmChannelDTO.from(channel, [user_id]),
+ channel_id: channel.id,
+ });
+ }
+
+ await channel.save();
+
+ await emitEvent({
+ event: "CHANNEL_RECIPIENT_REMOVE",
+ data: {
+ channel_id: channel.id,
+ user: await User.findOneOrFail({ where: { id: user_id }, select: PublicUserProjection }),
+ },
+ channel_id: channel.id,
+ } as ChannelRecipientRemoveEvent);
+ }
+
+ static async deleteChannel(channel: Channel) {
+ await Message.delete({ channel_id: channel.id }); //TODO we should also delete the attachments from the cdn but to do that we need to move cdn.ts in util
+ //TODO before deleting the channel we should check and delete other relations
+ await Channel.delete({ id: channel.id });
+ }
+
+ isDm() {
+ return this.type === ChannelType.DM || this.type === ChannelType.GROUP_DM;
+ }
+}
+
+export interface ChannelPermissionOverwrite {
+ allow: string;
+ deny: string;
+ id: string;
+ type: ChannelPermissionOverwriteType;
+}
+
+export enum ChannelPermissionOverwriteType {
+ role = 0,
+ member = 1,
+}
diff --git a/util/src/entities/Config.ts b/util/src/entities/Config.ts
index 9e25c7379..6993cc095 100644
--- a/util/src/entities/Config.ts
+++ b/util/src/entities/Config.ts
@@ -149,6 +149,7 @@ export interface ConfigValue {
minUpperCase: number;
minSymbols: number;
};
+ incrementingDiscriminators: boolean; // random otherwise
};
regions: {
default: string;
@@ -335,6 +336,7 @@ export const DefaultConfigOptions: ConfigValue = {
minUpperCase: 2,
minSymbols: 0,
},
+ incrementingDiscriminators: false,
},
regions: {
default: "fosscord",
diff --git a/util/src/entities/Emoji.ts b/util/src/entities/Emoji.ts
index 03218375b..32d39234f 100644
--- a/util/src/entities/Emoji.ts
+++ b/util/src/entities/Emoji.ts
@@ -10,7 +10,7 @@ export class Emoji extends BaseClass {
animated: boolean;
@Column()
- available: boolean; // whether this emoji can be used, may be false due to loss of Server Boosts
+ available: boolean; // whether this emoji can be used, may be false due to various reasons
@Column()
guild_id: string;
@@ -40,4 +40,7 @@ export class Emoji extends BaseClass {
@Column({ type: "simple-array" })
roles: string[]; // roles this emoji is whitelisted to (new discord feature?)
+
+ @Column({ type: "simple-array" })
+ groups: string[]; // user groups this emoji is whitelisted to (Fosscord extension)
}
diff --git a/util/src/entities/Guild.ts b/util/src/entities/Guild.ts
index 157f0921b..6a1df4d6f 100644
--- a/util/src/entities/Guild.ts
+++ b/util/src/entities/Guild.ts
@@ -213,7 +213,7 @@ export class Guild extends BaseClass {
owner: User;
@Column({ nullable: true })
- preferred_locale?: string; // only community guilds can choose this
+ preferred_locale?: string;
@Column({ nullable: true })
premium_subscription_count?: number;
@@ -301,22 +301,22 @@ export class Guild extends BaseClass {
name: body.name || "Fosscord",
icon: await handleFile(`/icons/${guild_id}`, body.icon as string),
region: Config.get().regions.default,
- owner_id: body.owner_id,
+ owner_id: body.owner_id, // TODO: need to figure out a way for ownerless guilds and multiply-owned guilds
afk_timeout: 300,
- default_message_notifications: 0,
+ default_message_notifications: 1, // defaults effect: setting the push default at mentions-only will save a lot
explicit_content_filter: 0,
features: [],
id: guild_id,
max_members: 250000,
max_presences: 250000,
- max_video_channel_users: 25,
+ max_video_channel_users: 200,
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,
- system_channel_flags: 0,
+ system_channel_flags: 4, // defaults effect: suppress the setup tips to save performance
unavailable: false,
nsfw: false,
nsfw_level: 0,
@@ -326,20 +326,24 @@ export class Guild extends BaseClass {
description: "No description",
welcome_channels: [],
},
- widget_enabled: false,
+ widget_enabled: true, // NB: don't set it as false to prevent artificial restrictions
}).save();
// we have to create the role _after_ the guild because else we would get a "SQLITE_CONSTRAINT: FOREIGN KEY constraint failed" error
+ // TODO: make the @everyone a pseudorole that is dynamically generated at runtime so we can save storage
await new Role({
id: guild_id,
guild_id: guild_id,
color: 0,
hoist: false,
managed: false,
+ // NB: in Fosscord, every role will be non-managed, as we use user-groups instead of roles for managed groups
mentionable: false,
name: "@everyone",
permissions: String("2251804225"),
position: 0,
+ icon: null,
+ unicode_emoji: null
}).save();
if (!body.channels || !body.channels.length) body.channels = [{ id: "01", type: 0, name: "general" }];
@@ -355,7 +359,6 @@ export class Guild extends BaseClass {
for (const channel of body.channels?.sort((a, b) => (a.parent_id ? 1 : -1))) {
var id = ids.get(channel.id) || Snowflake.generate();
- // TODO: should we abort if parent_id is a category? (to disallow sub category channels)
var parent_id = ids.get(channel.parent_id);
await Channel.createChannel({ ...channel, guild_id, id, parent_id }, body.owner_id, {
diff --git a/util/src/entities/Role.ts b/util/src/entities/Role.ts
index 9fca99a5b..4b721b5bb 100644
--- a/util/src/entities/Role.ts
+++ b/util/src/entities/Role.ts
@@ -36,6 +36,12 @@ export class Role extends BaseClass {
@Column()
position: number;
+ @Column({ nullable: true })
+ icon: string;
+
+ @Column({ nullable: true })
+ unicode_emoji: string;
+
@Column({ type: "simple-json", nullable: true })
tags?: {
bot_id?: string;
diff --git a/util/src/entities/User.ts b/util/src/entities/User.ts
index bc852616d..5f2618e0e 100644
--- a/util/src/entities/User.ts
+++ b/util/src/entities/User.ts
@@ -64,7 +64,7 @@ export class User extends BaseClass {
setDiscriminator(val: string) {
const number = Number(val);
if (isNaN(number)) throw new Error("invalid discriminator");
- if (number <= 0 || number > 10000) throw new Error("discriminator must be between 1 and 9999");
+ if (number <= 0 || number >= 10000) throw new Error("discriminator must be between 1 and 9999");
this.discriminator = val.toString().padStart(4, "0");
}
@@ -178,6 +178,35 @@ export class User extends BaseClass {
);
}
+ private static async generateDiscriminator(username: string): Promise {
+ if (Config.get().register.incrementingDiscriminators) {
+ // discriminator will be incrementally generated
+
+ // First we need to figure out the currently highest discrimnator for the given username and then increment it
+ const users = await User.find({ where: { username }, select: ["discriminator"] });
+ const highestDiscriminator = Math.max(0, ...users.map((u) => Number(u.discriminator)));
+
+ const discriminator = highestDiscriminator + 1;
+ if (discriminator >= 10000) {
+ return undefined;
+ }
+
+ return discriminator.toString().padStart(4, "0");
+ } else {
+ // discriminator will be randomly generated
+
+ // randomly generates a discriminator between 1 and 9999 and checks max five times if it already exists
+ // TODO: is there any better way to generate a random discriminator only once, without checking if it already exists in the database?
+ for (let tries = 0; tries < 5; tries++) {
+ const discriminator = Math.randomIntBetween(1, 9999).toString().padStart(4, "0");
+ const exists = await User.findOne({ where: { discriminator, username: username }, select: ["id"] });
+ if (!exists) return discriminator;
+ }
+
+ return undefined;
+ }
+ }
+
static async register({
email,
username,
@@ -194,21 +223,9 @@ export class User extends BaseClass {
// trim special uf8 control characters -> Backspace, Newline, ...
username = trimSpecial(username);
- // discriminator will be randomly generated
- let discriminator = "";
-
- 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 database?
- for (let tries = 0; tries < 5; tries++) {
- discriminator = Math.randomIntBetween(1, 9999).toString().padStart(4, "0");
- exists = await User.findOne({ where: { discriminator, username: username }, select: ["id"] });
- if (!exists) break;
- }
-
- if (exists) {
+ const discriminator = await User.generateDiscriminator(username);
+ if (!discriminator) {
+ // We've failed to generate a valid and unused discriminator
throw FieldErrors({
username: {
code: "USERNAME_TOO_MANY_USERS",
diff --git a/util/src/util/InvisibleCharacters.ts b/util/src/util/InvisibleCharacters.ts
new file mode 100644
index 000000000..2b014e146
--- /dev/null
+++ b/util/src/util/InvisibleCharacters.ts
@@ -0,0 +1,56 @@
+// List from https://invisible-characters.com/
+export const InvisibleCharacters = [
+ '\u{9}', //Tab
+ '\u{20}', //Space
+ '\u{ad}', //Soft hyphen
+ '\u{34f}', //Combining grapheme joiner
+ '\u{61c}', //Arabic letter mark
+ '\u{115f}', //Hangul choseong filler
+ '\u{1160}', //Hangul jungseong filler
+ '\u{17b4}', //Khmer vowel inherent AQ
+ '\u{17b5}', //Khmer vowel inherent AA
+ '\u{180e}', //Mongolian vowel separator
+ '\u{2000}', //En quad
+ '\u{2001}', //Em quad
+ '\u{2002}', //En space
+ '\u{2003}', //Em space
+ '\u{2004}', //Three-per-em space
+ '\u{2005}', //Four-per-em space
+ '\u{2006}', //Six-per-em space
+ '\u{2007}', //Figure space
+ '\u{2008}', //Punctuation space
+ '\u{2009}', //Thin space
+ '\u{200a}', //Hair space
+ '\u{200b}', //Zero width space
+ '\u{200c}', //Zero width non-joiner
+ '\u{200d}', //Zero width joiner
+ '\u{200e}', //Left-to-right mark
+ '\u{200f}', //Right-to-left mark
+ '\u{202f}', //Narrow no-break space
+ '\u{205f}', //Medium mathematical space
+ '\u{2060}', //Word joiner
+ '\u{2061}', //Function application
+ '\u{2062}', //Invisible times
+ '\u{2063}', //Invisible separator
+ '\u{2064}', //Invisible plus
+ '\u{206a}', //Inhibit symmetric swapping
+ '\u{206b}', //Activate symmetric swapping
+ '\u{206c}', //Inhibit arabic form shaping
+ '\u{206d}', //Activate arabic form shaping
+ '\u{206e}', //National digit shapes
+ '\u{206f}', //Nominal digit shapes
+ '\u{3000}', //Ideographic space
+ '\u{2800}', //Braille pattern blank
+ '\u{3164}', //Hangul filler
+ '\u{feff}', //Zero width no-break space
+ '\u{ffa0}', //Haldwidth hangul filler
+ '\u{1d159}', //Musical symbol null notehead
+ '\u{1d173}', //Musical symbol begin beam
+ '\u{1d174}', //Musical symbol end beam
+ '\u{1d175}', //Musical symbol begin tie
+ '\u{1d176}', //Musical symbol end tie
+ '\u{1d177}', //Musical symbol begin slur
+ '\u{1d178}', //Musical symbol end slur
+ '\u{1d179}', //Musical symbol begin phrase
+ '\u{1d17a}' //Musical symbol end phrase
+];
\ No newline at end of file
diff --git a/util/src/util/index.ts b/util/src/util/index.ts
index c57034685..98e1146ca 100644
--- a/util/src/util/index.ts
+++ b/util/src/util/index.ts
@@ -18,3 +18,4 @@ export * from "./Snowflake";
export * from "./String";
export * from "./Array";
export * from "./TraverseDirectory";
+export * from "./InvisibleCharacters";
\ No newline at end of file
diff --git a/webrtc/LICENSE b/webrtc/LICENSE
index e8ea215a4..f19bf5202 100644
--- a/webrtc/LICENSE
+++ b/webrtc/LICENSE
@@ -1,21 +1,14 @@
-MIT License
+Copyright (C) 2021 Fosscord and contributors
-Copyright (c) 2021 Fosscord (former Discord Open Source)
+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.
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
+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.
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see .
\ No newline at end of file