1
0
mirror of https://github.com/spacebarchat/server.git synced 2024-09-20 17:51:35 +02:00

Merge branch 'master' into tsnode

This commit is contained in:
Madeline 2022-01-24 10:22:46 +11:00
commit ba79fe858c
35 changed files with 3554 additions and 894 deletions

View File

@ -1,9 +1,10 @@
## Notes
## Additions
-
## Fixes
-
## Download
- [Windows]()
- [MacOS]()

View File

@ -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>

View File

@ -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%;
}

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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",

View File

@ -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"
}

View File

@ -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);
}

View File

@ -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,

View File

@ -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);

View File

@ -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;
}

View File

@ -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"
},

View File

@ -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
View File

@ -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"
},

View File

@ -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"
},

View File

@ -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");

View 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
View 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/>.

View File

@ -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
View File

2011
gateway/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -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"

View File

@ -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
View 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

File diff suppressed because it is too large Load Diff

View File

@ -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",

View File

@ -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,
}

View File

@ -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",

View File

@ -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)
}

View File

@ -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, {

View File

@ -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;

View File

@ -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",

View 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
];

View File

@ -18,3 +18,4 @@ export * from "./Snowflake";
export * from "./String";
export * from "./Array";
export * from "./TraverseDirectory";
export * from "./InvisibleCharacters";

View File

@ -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/>.