mirror of
https://github.com/spacebarchat/server.git
synced 2024-11-10 12:42:44 +01:00
Merge branch 'master' into tsnode
This commit is contained in:
commit
d84b2544a2
3
.github/relase_body_template.md
vendored
3
.github/relase_body_template.md
vendored
@ -1,9 +1,10 @@
|
||||
## Notes
|
||||
|
||||
## Additions
|
||||
-
|
||||
|
||||
## Fixes
|
||||
|
||||
-
|
||||
## Download
|
||||
- [Windows]()
|
||||
- [MacOS]()
|
||||
|
@ -1,5 +1,5 @@
|
||||
<p align="center">
|
||||
<img width="100" src="https://raw.githubusercontent.com/fosscord/fosscord/master/assets/logo_big_transparent.png" />
|
||||
<img width="100" src="https://raw.githubusercontent.com/fosscord/fosscord/master/assets-rebrand/svg/Fosscord-Icon-Rounded-Subtract.svg" />
|
||||
</p>
|
||||
<h1 align="center">Fosscord Server</h1>
|
||||
|
||||
|
@ -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%;
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
828
api/package-lock.json
generated
828
api/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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",
|
||||
|
20
api/scripts/stresstest/package-lock.json
generated
20
api/scripts/stresstest/package-lock.json
generated
@ -279,14 +279,22 @@
|
||||
}
|
||||
},
|
||||
"node_modules/node-fetch": {
|
||||
"version": "2.6.6",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.6.tgz",
|
||||
"integrity": "sha512-Z8/6vRlTUChSdIgMa51jxQ4lrw/Jy5SOW10ObaA47/RElsAN2c5Pn8bTgFGWn/ibwzXTE8qwr1Yzx28vsecXEA==",
|
||||
"version": "2.6.7",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
|
||||
"integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==",
|
||||
"dependencies": {
|
||||
"whatwg-url": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "4.x || >=6.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"encoding": "^0.1.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"encoding": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/oauth-sign": {
|
||||
@ -695,9 +703,9 @@
|
||||
}
|
||||
},
|
||||
"node-fetch": {
|
||||
"version": "2.6.6",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.6.tgz",
|
||||
"integrity": "sha512-Z8/6vRlTUChSdIgMa51jxQ4lrw/Jy5SOW10ObaA47/RElsAN2c5Pn8bTgFGWn/ibwzXTE8qwr1Yzx28vsecXEA==",
|
||||
"version": "2.6.7",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
|
||||
"integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==",
|
||||
"requires": {
|
||||
"whatwg-url": "^5.0.0"
|
||||
}
|
||||
|
@ -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<Message> & { where: { id?: any } } = {
|
||||
var query: FindManyOptions<Message> & { 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);
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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<Message> {
|
||||
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<Message> {
|
||||
}
|
||||
|
||||
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<Message> {
|
||||
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<Message> {
|
||||
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;
|
||||
}
|
||||
|
38
bundle/package-lock.json
generated
38
bundle/package-lock.json
generated
@ -46,7 +46,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",
|
||||
@ -100,7 +100,7 @@
|
||||
"name": "@fosscord/api",
|
||||
"version": "1.0.0",
|
||||
"hasInstallScript": true,
|
||||
"license": "ISC",
|
||||
"license": "GPLV3",
|
||||
"dependencies": {
|
||||
"@babel/preset-env": "^7.15.8",
|
||||
"@babel/preset-typescript": "^7.15.0",
|
||||
@ -126,7 +126,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",
|
||||
@ -163,7 +163,7 @@
|
||||
"../cdn": {
|
||||
"name": "@fosscord/cdn",
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"license": "GPLV3",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.36.1",
|
||||
"@aws-sdk/node-http-handler": "^3.36.0",
|
||||
@ -184,7 +184,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"
|
||||
},
|
||||
@ -207,7 +207,7 @@
|
||||
"name": "@fosscord/gateway",
|
||||
"version": "1.0.0",
|
||||
"hasInstallScript": true,
|
||||
"license": "ISC",
|
||||
"license": "GPLV3",
|
||||
"dependencies": {
|
||||
"@fosscord/util": "file:../util",
|
||||
"amqplib": "^0.8.0",
|
||||
@ -215,7 +215,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"
|
||||
@ -7909,13 +7909,22 @@
|
||||
"integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A=="
|
||||
},
|
||||
"node_modules/node-fetch": {
|
||||
"version": "2.6.5",
|
||||
"integrity": "sha512-mmlIVHJEu5rnIxgEgez6b9GgWXbkZj5YZ7fx+2r94a2E+Uirsp6HsPTPlomfdHtpt/B0cdKviwkoaM6pyvUOpQ==",
|
||||
"version": "2.6.7",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
|
||||
"integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==",
|
||||
"dependencies": {
|
||||
"whatwg-url": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "4.x || >=6.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"encoding": "^0.1.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"encoding": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/node-fetch/node_modules/tr46": {
|
||||
@ -13392,7 +13401,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",
|
||||
@ -13438,7 +13447,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",
|
||||
"ts-patch": "^1.4.4",
|
||||
"typescript": "^4.1.2"
|
||||
@ -13460,7 +13469,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",
|
||||
"ts-node-dev": "^1.1.6",
|
||||
"ts-patch": "^1.4.4",
|
||||
@ -17366,8 +17375,9 @@
|
||||
"integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A=="
|
||||
},
|
||||
"node-fetch": {
|
||||
"version": "2.6.5",
|
||||
"integrity": "sha512-mmlIVHJEu5rnIxgEgez6b9GgWXbkZj5YZ7fx+2r94a2E+Uirsp6HsPTPlomfdHtpt/B0cdKviwkoaM6pyvUOpQ==",
|
||||
"version": "2.6.7",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
|
||||
"integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==",
|
||||
"requires": {
|
||||
"whatwg-url": "^5.0.0"
|
||||
},
|
||||
|
@ -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",
|
||||
|
28
cdn/package-lock.json
generated
28
cdn/package-lock.json
generated
@ -7,7 +7,7 @@
|
||||
"": {
|
||||
"name": "@fosscord/cdn",
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"license": "GPLV3",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.36.1",
|
||||
"@aws-sdk/node-http-handler": "^3.36.0",
|
||||
@ -28,7 +28,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"
|
||||
},
|
||||
@ -59,10 +59,10 @@
|
||||
"lambert-server": "^1.2.12",
|
||||
"missing-native-js-functions": "^1.2.18",
|
||||
"multer": "^1.4.3",
|
||||
"nanocolors": "^0.2.12",
|
||||
"node-fetch": "^2.6.1",
|
||||
"patch-package": "^6.4.7",
|
||||
"pg": "^8.7.1",
|
||||
"picocolors": "^1.0.0",
|
||||
"proxy-agent": "^5.0.0",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"typeorm": "^0.2.38",
|
||||
@ -6101,14 +6101,22 @@
|
||||
}
|
||||
},
|
||||
"node_modules/node-fetch": {
|
||||
"version": "2.6.6",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.6.tgz",
|
||||
"integrity": "sha512-Z8/6vRlTUChSdIgMa51jxQ4lrw/Jy5SOW10ObaA47/RElsAN2c5Pn8bTgFGWn/ibwzXTE8qwr1Yzx28vsecXEA==",
|
||||
"version": "2.6.7",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
|
||||
"integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==",
|
||||
"dependencies": {
|
||||
"whatwg-url": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "4.x || >=6.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"encoding": "^0.1.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"encoding": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/node-fetch/node_modules/tr46": {
|
||||
@ -9140,10 +9148,10 @@
|
||||
"lambert-server": "^1.2.12",
|
||||
"missing-native-js-functions": "^1.2.18",
|
||||
"multer": "^1.4.3",
|
||||
"nanocolors": "^0.2.12",
|
||||
"node-fetch": "^2.6.1",
|
||||
"patch-package": "^6.4.7",
|
||||
"pg": "^8.7.1",
|
||||
"picocolors": "^1.0.0",
|
||||
"proxy-agent": "^5.0.0",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"ts-node": "^10.2.1",
|
||||
@ -12574,9 +12582,9 @@
|
||||
"integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw=="
|
||||
},
|
||||
"node-fetch": {
|
||||
"version": "2.6.6",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.6.tgz",
|
||||
"integrity": "sha512-Z8/6vRlTUChSdIgMa51jxQ4lrw/Jy5SOW10ObaA47/RElsAN2c5Pn8bTgFGWn/ibwzXTE8qwr1Yzx28vsecXEA==",
|
||||
"version": "2.6.7",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
|
||||
"integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==",
|
||||
"requires": {
|
||||
"whatwg-url": "^5.0.0"
|
||||
},
|
||||
|
@ -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"
|
||||
},
|
||||
|
@ -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");
|
||||
|
||||
|
101
cdn/src/routes/role-icons.ts
Normal file
101
cdn/src/routes/role-icons.ts
Normal file
@ -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;
|
14
dashboard/LICENSE
Normal file
14
dashboard/LICENSE
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
@ -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"
|
||||
}
|
||||
|
0
dashboard/src/index.ts
Normal file
0
dashboard/src/index.ts
Normal file
2011
gateway/package-lock.json
generated
2011
gateway/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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"
|
||||
|
@ -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) {
|
||||
|
14
rtc/LICENSE
Normal file
14
rtc/LICENSE
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
423
util/package-lock.json
generated
423
util/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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",
|
||||
|
@ -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<Channel>,
|
||||
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<Channel>,
|
||||
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,
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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, {
|
||||
|
@ -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;
|
||||
|
@ -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<string | undefined> {
|
||||
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",
|
||||
|
56
util/src/util/InvisibleCharacters.ts
Normal file
56
util/src/util/InvisibleCharacters.ts
Normal file
@ -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
|
||||
];
|
@ -18,3 +18,4 @@ export * from "./Snowflake";
|
||||
export * from "./String";
|
||||
export * from "./Array";
|
||||
export * from "./TraverseDirectory";
|
||||
export * from "./InvisibleCharacters";
|
@ -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 <https://www.gnu.org/licenses/>.
|
Loading…
Reference in New Issue
Block a user