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 ## Notes
## Additions ## Additions
-
## Fixes ## Fixes
-
## Download ## Download
- [Windows]() - [Windows]()
- [MacOS]() - [MacOS]()

View File

@ -1,5 +1,5 @@
<p align="center"> <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> </p>
<h1 align="center">Fosscord Server</h1> <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 */ /* Logo in top left when bg removed */
#app-mount > div.app-1q1i1E > div > a { #app-mount > div.app-1q1i1E > div > a {
/* replace me: original dimensions: 130x36 */ /* 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; background-size: contain;
width: 128px;
height: 128px;
border-radius: 50%; border-radius: 50%;
} }

View File

@ -13,10 +13,14 @@
/* home button icon */ /* 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 #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; background-size: contain;
border-radius: 50%; 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 */ /* 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.transitionGroup-aR7y1d.qrLogin-1AOZMt,
#app-mount > div.app-1q1i1E > div > div > div > div > form > div > div > div.verticalSeparator-3huAjp, #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" "discord-open-source"
], ],
"author": "Fosscord", "author": "Fosscord",
"license": "ISC", "license": "GPLV3",
"bugs": { "bugs": {
"url": "https://github.com/fosscord/fosscord-server/issues" "url": "https://github.com/fosscord/fosscord-server/issues"
}, },
@ -86,7 +86,7 @@
"missing-native-js-functions": "^1.2.18", "missing-native-js-functions": "^1.2.18",
"morgan": "^1.10.0", "morgan": "^1.10.0",
"multer": "^1.4.2", "multer": "^1.4.2",
"node-fetch": "^2.6.1", "node-fetch": "^3.1.1",
"patch-package": "^6.4.7", "patch-package": "^6.4.7",
"picocolors": "^1.0.0", "picocolors": "^1.0.0",
"proxy-agent": "^5.0.0", "proxy-agent": "^5.0.0",

View File

@ -279,14 +279,22 @@
} }
}, },
"node_modules/node-fetch": { "node_modules/node-fetch": {
"version": "2.6.6", "version": "2.6.7",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.6.tgz", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
"integrity": "sha512-Z8/6vRlTUChSdIgMa51jxQ4lrw/Jy5SOW10ObaA47/RElsAN2c5Pn8bTgFGWn/ibwzXTE8qwr1Yzx28vsecXEA==", "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==",
"dependencies": { "dependencies": {
"whatwg-url": "^5.0.0" "whatwg-url": "^5.0.0"
}, },
"engines": { "engines": {
"node": "4.x || >=6.0.0" "node": "4.x || >=6.0.0"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
} }
}, },
"node_modules/oauth-sign": { "node_modules/oauth-sign": {
@ -695,9 +703,9 @@
} }
}, },
"node-fetch": { "node-fetch": {
"version": "2.6.6", "version": "2.6.7",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.6.tgz", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
"integrity": "sha512-Z8/6vRlTUChSdIgMa51jxQ4lrw/Jy5SOW10ObaA47/RElsAN2c5Pn8bTgFGWn/ibwzXTE8qwr1Yzx28vsecXEA==", "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==",
"requires": { "requires": {
"whatwg-url": "^5.0.0" "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_PUBLIC_THREAD:
case ChannelType.GUILD_PRIVATE_THREAD: case ChannelType.GUILD_PRIVATE_THREAD:
case ChannelType.GUILD_TEXT: case ChannelType.GUILD_TEXT:
case ChannelType.ENCRYPTED:
case ChannelType.ENCRYPTED_THREAD:
return true; return true;
default:
throw new HTTPError("unimplemented", 400);
} }
} }
@ -87,7 +91,7 @@ router.get("/", async (req: Request, res: Response) => {
permissions.hasThrow("VIEW_CHANNEL"); permissions.hasThrow("VIEW_CHANNEL");
if (!permissions.has("READ_MESSAGE_HISTORY")) return res.json([]); 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" }, order: { id: "DESC" },
take: limit, take: limit,
where: { channel_id }, where: { channel_id },
@ -172,7 +176,7 @@ router.post(
} }
const channel = await Channel.findOneOrFail({ where: { id: channel_id }, relations: ["recipients", "recipients.user"] }); 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); if (body.embed) embeds.push(body.embed);
let message = await handleMessage({ let message = await handleMessage({
...body, ...body,
@ -216,7 +220,7 @@ router.post(
channel.save() 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); return res.json(message);
} }

View File

@ -8,7 +8,8 @@ import {
GuildRoleDeleteEvent, GuildRoleDeleteEvent,
emitEvent, emitEvent,
Config, Config,
DiscordApiErrors DiscordApiErrors,
handleFile
} from "@fosscord/util"; } from "@fosscord/util";
import { HTTPError } from "lambert-server"; import { HTTPError } from "lambert-server";
import { route } from "@fosscord/api"; import { route } from "@fosscord/api";
@ -22,6 +23,8 @@ export interface RoleModifySchema {
hoist?: boolean; // whether the role should be displayed separately in the sidebar hoist?: boolean; // whether the role should be displayed separately in the sidebar
mentionable?: boolean; // whether the role should be mentionable mentionable?: boolean; // whether the role should be mentionable
position?: number; position?: number;
icon?: string;
unicode_emoji?: string;
} }
export type RolePositionUpdateSchema = { export type RolePositionUpdateSchema = {
@ -58,7 +61,9 @@ router.post("/", route({ body: "RoleModifySchema", permission: "MANAGE_ROLES" })
guild_id: guild_id, guild_id: guild_id,
managed: false, managed: false,
permissions: String(req.permission!.bitfield & BigInt(body.permissions || "0")), permissions: String(req.permission!.bitfield & BigInt(body.permissions || "0")),
tags: undefined tags: undefined,
icon: null,
unicode_emoji: null
}); });
await Promise.all([ await Promise.all([
@ -105,6 +110,8 @@ router.patch("/:role_id", route({ body: "RoleModifySchema", permission: "MANAGE_
const { role_id, guild_id } = req.params; const { role_id, guild_id } = req.params;
const body = req.body as RoleModifySchema; 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({ const role = new Role({
...body, ...body,
id: role_id, 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 { features } = await Guild.findOneOrFail({ id: guild_id});
const { public_flags } = await User.findOneOrFail({ id: req.user_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); const invite = await Invite.joinGuild(req.user_id, code);

View File

@ -2,6 +2,7 @@ import {
Channel, Channel,
Embed, Embed,
emitEvent, emitEvent,
Guild,
Message, Message,
MessageCreateEvent, MessageCreateEvent,
MessageUpdateEvent, MessageUpdateEvent,
@ -17,13 +18,14 @@ import {
User, User,
Application, Application,
Webhook, Webhook,
Attachment Attachment,
Config,
} from "@fosscord/util"; } from "@fosscord/util";
import { HTTPError } from "lambert-server"; import { HTTPError } from "lambert-server";
import fetch from "node-fetch"; import fetch from "node-fetch";
import cheerio from "cheerio"; import cheerio from "cheerio";
import { MessageCreateSchema } from "../../routes/channels/#channel_id/messages"; import { MessageCreateSchema } from "../../routes/channels/#channel_id/messages";
const allow_empty = false;
// TODO: check webhook, application, system author, stickers // TODO: check webhook, application, system author, stickers
// TODO: embed gifs/videos/images // TODO: embed gifs/videos/images
@ -55,6 +57,10 @@ export async function handleMessage(opts: MessageOptions): Promise<Message> {
type: opts.type ?? 0 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? // TODO: are tts messages allowed in dm channels? should permission be checked?
if (opts.author_id) { if (opts.author_id) {
message.author = await User.getPublicUser(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); 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) { if (permission.cache.member) {
message.member = 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.tts) permission.hasThrow("SEND_TTS_MESSAGES");
if (opts.message_reference) { if (opts.message_reference) {
permission.hasThrow("READ_MESSAGE_HISTORY"); 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"); // code below has to be redone when we add custom message routing and cross-channel replies
if (opts.message_reference.channel_id !== opts.channel_id) throw new HTTPError("You can only reference messages from this channel"); 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? // TODO: should be checked if the referenced message exists?
// @ts-ignore // @ts-ignore
message.type = MessageType.REPLY; message.type = MessageType.REPLY;
} }
// TODO: stickers/activity // 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); 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_user_ids = [] as string[];
var mention_everyone = false; var mention_everyone = false;
if (content) { if (content) { // TODO: explicit-only mentions
message.content = content.trim(); message.content = content.trim();
for (const [_, mention] of content.matchAll(CHANNEL_MENTION)) { for (const [_, mention] of content.matchAll(CHANNEL_MENTION)) {
if (!mention_channel_ids.includes(mention)) mention_channel_ids.push(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 }; const data = { ...message };
data.embeds = data.embeds.filter((x) => x.type !== "link"); 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) { for (const link of links) {
try { 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) 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; return message;
} }

View File

@ -46,7 +46,7 @@
"morgan": "^1.10.0", "morgan": "^1.10.0",
"multer": "^1.4.2", "multer": "^1.4.2",
"nanocolors": "^0.2.12", "nanocolors": "^0.2.12",
"node-fetch": "^2.6.1", "node-fetch": "^2.6.7",
"node-os-utils": "^1.3.5", "node-os-utils": "^1.3.5",
"patch-package": "^6.4.7", "patch-package": "^6.4.7",
"pg": "^8.7.1", "pg": "^8.7.1",
@ -100,7 +100,7 @@
"name": "@fosscord/api", "name": "@fosscord/api",
"version": "1.0.0", "version": "1.0.0",
"hasInstallScript": true, "hasInstallScript": true,
"license": "ISC", "license": "GPLV3",
"dependencies": { "dependencies": {
"@babel/preset-env": "^7.15.8", "@babel/preset-env": "^7.15.8",
"@babel/preset-typescript": "^7.15.0", "@babel/preset-typescript": "^7.15.0",
@ -126,7 +126,7 @@
"missing-native-js-functions": "^1.2.18", "missing-native-js-functions": "^1.2.18",
"morgan": "^1.10.0", "morgan": "^1.10.0",
"multer": "^1.4.2", "multer": "^1.4.2",
"node-fetch": "^2.6.1", "node-fetch": "^3.1.1",
"patch-package": "^6.4.7", "patch-package": "^6.4.7",
"picocolors": "^1.0.0", "picocolors": "^1.0.0",
"proxy-agent": "^5.0.0", "proxy-agent": "^5.0.0",
@ -163,7 +163,7 @@
"../cdn": { "../cdn": {
"name": "@fosscord/cdn", "name": "@fosscord/cdn",
"version": "1.0.0", "version": "1.0.0",
"license": "ISC", "license": "GPLV3",
"dependencies": { "dependencies": {
"@aws-sdk/client-s3": "^3.36.1", "@aws-sdk/client-s3": "^3.36.1",
"@aws-sdk/node-http-handler": "^3.36.0", "@aws-sdk/node-http-handler": "^3.36.0",
@ -184,7 +184,7 @@
"missing-native-js-functions": "^1.2.17", "missing-native-js-functions": "^1.2.17",
"multer": "^1.4.2", "multer": "^1.4.2",
"nanocolors": "^0.2.12", "nanocolors": "^0.2.12",
"node-fetch": "^2.6.1", "node-fetch": "^2.6.7",
"supertest": "^6.1.6", "supertest": "^6.1.6",
"typescript": "^4.1.2" "typescript": "^4.1.2"
}, },
@ -207,7 +207,7 @@
"name": "@fosscord/gateway", "name": "@fosscord/gateway",
"version": "1.0.0", "version": "1.0.0",
"hasInstallScript": true, "hasInstallScript": true,
"license": "ISC", "license": "GPLV3",
"dependencies": { "dependencies": {
"@fosscord/util": "file:../util", "@fosscord/util": "file:../util",
"amqplib": "^0.8.0", "amqplib": "^0.8.0",
@ -215,7 +215,7 @@
"jsonwebtoken": "^8.5.1", "jsonwebtoken": "^8.5.1",
"lambert-server": "^1.2.11", "lambert-server": "^1.2.11",
"missing-native-js-functions": "^1.2.18", "missing-native-js-functions": "^1.2.18",
"node-fetch": "^2.6.1", "node-fetch": "^3.1.1",
"proxy-agent": "^5.0.0", "proxy-agent": "^5.0.0",
"typeorm": "^0.2.37", "typeorm": "^0.2.37",
"ws": "^7.4.2" "ws": "^7.4.2"
@ -7909,13 +7909,22 @@
"integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==" "integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A=="
}, },
"node_modules/node-fetch": { "node_modules/node-fetch": {
"version": "2.6.5", "version": "2.6.7",
"integrity": "sha512-mmlIVHJEu5rnIxgEgez6b9GgWXbkZj5YZ7fx+2r94a2E+Uirsp6HsPTPlomfdHtpt/B0cdKviwkoaM6pyvUOpQ==", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
"integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==",
"dependencies": { "dependencies": {
"whatwg-url": "^5.0.0" "whatwg-url": "^5.0.0"
}, },
"engines": { "engines": {
"node": "4.x || >=6.0.0" "node": "4.x || >=6.0.0"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
} }
}, },
"node_modules/node-fetch/node_modules/tr46": { "node_modules/node-fetch/node_modules/tr46": {
@ -13392,7 +13401,7 @@
"missing-native-js-functions": "^1.2.18", "missing-native-js-functions": "^1.2.18",
"morgan": "^1.10.0", "morgan": "^1.10.0",
"multer": "^1.4.2", "multer": "^1.4.2",
"node-fetch": "^2.6.1", "node-fetch": "^3.1.1",
"patch-package": "^6.4.7", "patch-package": "^6.4.7",
"picocolors": "^1.0.0", "picocolors": "^1.0.0",
"proxy-agent": "^5.0.0", "proxy-agent": "^5.0.0",
@ -13438,7 +13447,7 @@
"missing-native-js-functions": "^1.2.17", "missing-native-js-functions": "^1.2.17",
"multer": "^1.4.2", "multer": "^1.4.2",
"nanocolors": "^0.2.12", "nanocolors": "^0.2.12",
"node-fetch": "^2.6.1", "node-fetch": "^2.6.7",
"supertest": "^6.1.6", "supertest": "^6.1.6",
"ts-patch": "^1.4.4", "ts-patch": "^1.4.4",
"typescript": "^4.1.2" "typescript": "^4.1.2"
@ -13460,7 +13469,7 @@
"jsonwebtoken": "^8.5.1", "jsonwebtoken": "^8.5.1",
"lambert-server": "^1.2.11", "lambert-server": "^1.2.11",
"missing-native-js-functions": "^1.2.18", "missing-native-js-functions": "^1.2.18",
"node-fetch": "^2.6.1", "node-fetch": "^3.1.1",
"proxy-agent": "^5.0.0", "proxy-agent": "^5.0.0",
"ts-node-dev": "^1.1.6", "ts-node-dev": "^1.1.6",
"ts-patch": "^1.4.4", "ts-patch": "^1.4.4",
@ -17366,8 +17375,9 @@
"integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==" "integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A=="
}, },
"node-fetch": { "node-fetch": {
"version": "2.6.5", "version": "2.6.7",
"integrity": "sha512-mmlIVHJEu5rnIxgEgez6b9GgWXbkZj5YZ7fx+2r94a2E+Uirsp6HsPTPlomfdHtpt/B0cdKviwkoaM6pyvUOpQ==", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
"integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==",
"requires": { "requires": {
"whatwg-url": "^5.0.0" "whatwg-url": "^5.0.0"
}, },

View File

@ -94,7 +94,7 @@
"morgan": "^1.10.0", "morgan": "^1.10.0",
"multer": "^1.4.2", "multer": "^1.4.2",
"nanocolors": "^0.2.12", "nanocolors": "^0.2.12",
"node-fetch": "^2.6.1", "node-fetch": "^2.6.7",
"node-os-utils": "^1.3.5", "node-os-utils": "^1.3.5",
"patch-package": "^6.4.7", "patch-package": "^6.4.7",
"pg": "^8.7.1", "pg": "^8.7.1",

28
cdn/package-lock.json generated
View File

@ -7,7 +7,7 @@
"": { "": {
"name": "@fosscord/cdn", "name": "@fosscord/cdn",
"version": "1.0.0", "version": "1.0.0",
"license": "ISC", "license": "GPLV3",
"dependencies": { "dependencies": {
"@aws-sdk/client-s3": "^3.36.1", "@aws-sdk/client-s3": "^3.36.1",
"@aws-sdk/node-http-handler": "^3.36.0", "@aws-sdk/node-http-handler": "^3.36.0",
@ -28,7 +28,7 @@
"missing-native-js-functions": "^1.2.17", "missing-native-js-functions": "^1.2.17",
"multer": "^1.4.2", "multer": "^1.4.2",
"nanocolors": "^0.2.12", "nanocolors": "^0.2.12",
"node-fetch": "^2.6.1", "node-fetch": "^2.6.7",
"supertest": "^6.1.6", "supertest": "^6.1.6",
"typescript": "^4.1.2" "typescript": "^4.1.2"
}, },
@ -59,10 +59,10 @@
"lambert-server": "^1.2.12", "lambert-server": "^1.2.12",
"missing-native-js-functions": "^1.2.18", "missing-native-js-functions": "^1.2.18",
"multer": "^1.4.3", "multer": "^1.4.3",
"nanocolors": "^0.2.12",
"node-fetch": "^2.6.1", "node-fetch": "^2.6.1",
"patch-package": "^6.4.7", "patch-package": "^6.4.7",
"pg": "^8.7.1", "pg": "^8.7.1",
"picocolors": "^1.0.0",
"proxy-agent": "^5.0.0", "proxy-agent": "^5.0.0",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"typeorm": "^0.2.38", "typeorm": "^0.2.38",
@ -6101,14 +6101,22 @@
} }
}, },
"node_modules/node-fetch": { "node_modules/node-fetch": {
"version": "2.6.6", "version": "2.6.7",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.6.tgz", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
"integrity": "sha512-Z8/6vRlTUChSdIgMa51jxQ4lrw/Jy5SOW10ObaA47/RElsAN2c5Pn8bTgFGWn/ibwzXTE8qwr1Yzx28vsecXEA==", "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==",
"dependencies": { "dependencies": {
"whatwg-url": "^5.0.0" "whatwg-url": "^5.0.0"
}, },
"engines": { "engines": {
"node": "4.x || >=6.0.0" "node": "4.x || >=6.0.0"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
} }
}, },
"node_modules/node-fetch/node_modules/tr46": { "node_modules/node-fetch/node_modules/tr46": {
@ -9140,10 +9148,10 @@
"lambert-server": "^1.2.12", "lambert-server": "^1.2.12",
"missing-native-js-functions": "^1.2.18", "missing-native-js-functions": "^1.2.18",
"multer": "^1.4.3", "multer": "^1.4.3",
"nanocolors": "^0.2.12",
"node-fetch": "^2.6.1", "node-fetch": "^2.6.1",
"patch-package": "^6.4.7", "patch-package": "^6.4.7",
"pg": "^8.7.1", "pg": "^8.7.1",
"picocolors": "^1.0.0",
"proxy-agent": "^5.0.0", "proxy-agent": "^5.0.0",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"ts-node": "^10.2.1", "ts-node": "^10.2.1",
@ -12574,9 +12582,9 @@
"integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw=="
}, },
"node-fetch": { "node-fetch": {
"version": "2.6.6", "version": "2.6.7",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.6.tgz", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
"integrity": "sha512-Z8/6vRlTUChSdIgMa51jxQ4lrw/Jy5SOW10ObaA47/RElsAN2c5Pn8bTgFGWn/ibwzXTE8qwr1Yzx28vsecXEA==", "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==",
"requires": { "requires": {
"whatwg-url": "^5.0.0" "whatwg-url": "^5.0.0"
}, },

View File

@ -15,7 +15,7 @@
}, },
"keywords": [], "keywords": [],
"author": "", "author": "",
"license": "ISC", "license": "GPLV3",
"bugs": { "bugs": {
"url": "https://github.com/fosscord/fosscord-server/issues" "url": "https://github.com/fosscord/fosscord-server/issues"
}, },
@ -54,7 +54,7 @@
"missing-native-js-functions": "^1.2.17", "missing-native-js-functions": "^1.2.17",
"multer": "^1.4.2", "multer": "^1.4.2",
"nanocolors": "^0.2.12", "nanocolors": "^0.2.12",
"node-fetch": "^2.6.1", "node-fetch": "^2.6.7",
"supertest": "^6.1.6", "supertest": "^6.1.6",
"typescript": "^4.1.2" "typescript": "^4.1.2"
}, },

View File

@ -2,6 +2,7 @@ import { Server, ServerOptions } from "lambert-server";
import { Config, initDatabase, registerRoutes } from "@fosscord/util"; import { Config, initDatabase, registerRoutes } from "@fosscord/util";
import path from "path"; import path from "path";
import avatarsRoute from "./routes/avatars"; import avatarsRoute from "./routes/avatars";
import iconsRoute from "./routes/role-icons";
import bodyParser from "body-parser"; import bodyParser from "body-parser";
export interface CDNServerOptions extends ServerOptions {} export interface CDNServerOptions extends ServerOptions {}
@ -40,6 +41,9 @@ export class CDNServer extends Server {
this.app.use("/icons/", avatarsRoute); this.app.use("/icons/", avatarsRoute);
this.log("verbose", "[Server] Route /icons registered"); 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.app.use("/emojis/", avatarsRoute);
this.log("verbose", "[Server] Route /emojis registered"); 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": [], "keywords": [],
"author": "Fosscord", "author": "Fosscord",
"license": "ISC", "license": "GPLV3",
"devDependencies": { "devDependencies": {
"@types/amqplib": "^0.8.1", "@types/amqplib": "^0.8.1",
"@types/jsonwebtoken": "^8.5.0", "@types/jsonwebtoken": "^8.5.0",
@ -32,7 +32,7 @@
"jsonwebtoken": "^8.5.1", "jsonwebtoken": "^8.5.1",
"lambert-server": "^1.2.11", "lambert-server": "^1.2.11",
"missing-native-js-functions": "^1.2.18", "missing-native-js-functions": "^1.2.18",
"node-fetch": "^2.6.1", "node-fetch": "^3.1.1",
"proxy-agent": "^5.0.0", "proxy-agent": "^5.0.0",
"typeorm": "^0.2.37", "typeorm": "^0.2.37",
"ws": "^7.4.2" "ws": "^7.4.2"

View File

@ -1,7 +1,9 @@
var erlpack: any; var erlpack: any;
try { try {
erlpack = require("@yukikaze-bot/erlpack"); 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"; import { Payload, WebSocket } from "@fosscord/gateway";
export async function Send(socket: WebSocket, data: Payload) { 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", "lambert-server": "^1.2.12",
"missing-native-js-functions": "^1.2.18", "missing-native-js-functions": "^1.2.18",
"multer": "^1.4.3", "multer": "^1.4.3",
"node-fetch": "^2.6.1", "node-fetch": "^2.6.7",
"patch-package": "^6.4.7", "patch-package": "^6.4.7",
"pg": "^8.7.1", "pg": "^8.7.1",
"picocolors": "^1.0.0", "picocolors": "^1.0.0",

View File

@ -1,332 +1,357 @@
import { Column, Entity, JoinColumn, ManyToOne, OneToMany, RelationId } from "typeorm"; import { Column, Entity, JoinColumn, ManyToOne, OneToMany, RelationId } from "typeorm";
import { BaseClass } from "./BaseClass"; import { BaseClass } from "./BaseClass";
import { Guild } from "./Guild"; import { Guild } from "./Guild";
import { PublicUserProjection, User } from "./User"; import { PublicUserProjection, User } from "./User";
import { HTTPError } from "lambert-server"; import { HTTPError } from "lambert-server";
import { containsAll, emitEvent, getPermission, Snowflake, trimSpecial } from "../util"; import { containsAll, emitEvent, getPermission, Snowflake, trimSpecial, InvisibleCharacters } from "../util";
import { ChannelCreateEvent, ChannelRecipientRemoveEvent } from "../interfaces"; import { ChannelCreateEvent, ChannelRecipientRemoveEvent } from "../interfaces";
import { Recipient } from "./Recipient"; import { Recipient } from "./Recipient";
import { Message } from "./Message"; import { Message } from "./Message";
import { ReadState } from "./ReadState"; import { ReadState } from "./ReadState";
import { Invite } from "./Invite"; import { Invite } from "./Invite";
import { VoiceState } from "./VoiceState"; import { VoiceState } from "./VoiceState";
import { Webhook } from "./Webhook"; import { Webhook } from "./Webhook";
import { DmChannelDTO } from "../dtos"; import { DmChannelDTO } from "../dtos";
export enum ChannelType { export enum ChannelType {
GUILD_TEXT = 0, // a text channel within a server GUILD_TEXT = 0, // a text channel within a server
DM = 1, // a direct message between users DM = 1, // a direct message between users
GUILD_VOICE = 2, // a voice channel within a server GUILD_VOICE = 2, // a voice channel within a server
GROUP_DM = 3, // a direct message between multiple users GROUP_DM = 3, // a direct message between multiple users
GUILD_CATEGORY = 4, // an organizational category that contains up to 50 channels 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_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 GUILD_STORE = 6, // a channel in which game developers can sell their game on Discord
// TODO: what are channel types between 7-9? ENCRYPTED = 7, // end-to-end encrypted channel
GUILD_NEWS_THREAD = 10, // a temporary sub-channel within a GUILD_NEWS channel ENCRYPTED_THREAD = 8, // end-to-end encrypted thread channel
GUILD_PUBLIC_THREAD = 11, // a temporary sub-channel within a GUILD_TEXT channel GUILD_NEWS_THREAD = 10, // a temporary sub-channel within a GUILD_NEWS 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_PUBLIC_THREAD = 11, // a temporary sub-channel within a GUILD_TEXT channel
GUILD_STAGE_VOICE = 13, // a voice channel for hosting events with an audience 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
@Entity("channels") UNHANDLED = 255 // unhandled unowned pass-through channel type
export class Channel extends BaseClass { }
@Column()
created_at: Date; @Entity("channels")
export class Channel extends BaseClass {
@Column({ nullable: true }) @Column()
name?: string; created_at: Date;
@Column({ type: "text", nullable: true }) @Column({ nullable: true })
icon?: string | null; name?: string;
@Column({ type: "int" }) @Column({ type: "text", nullable: true })
type: ChannelType; icon?: string | null;
@OneToMany(() => Recipient, (recipient: Recipient) => recipient.channel, { @Column({ type: "int" })
cascade: true, type: ChannelType;
orphanedRowAction: "delete",
}) @OneToMany(() => Recipient, (recipient: Recipient) => recipient.channel, {
recipients?: Recipient[]; cascade: true,
orphanedRowAction: "delete",
@Column({ nullable: true }) })
last_message_id: string; recipients?: Recipient[];
@Column({ nullable: true }) @Column({ nullable: true })
@RelationId((channel: Channel) => channel.guild) last_message_id: string;
guild_id?: string;
@Column({ nullable: true })
@JoinColumn({ name: "guild_id" }) @RelationId((channel: Channel) => channel.guild)
@ManyToOne(() => Guild, { guild_id?: string;
onDelete: "CASCADE",
}) @JoinColumn({ name: "guild_id" })
guild: Guild; @ManyToOne(() => Guild, {
onDelete: "CASCADE",
@Column({ nullable: true }) })
@RelationId((channel: Channel) => channel.parent) guild: Guild;
parent_id: string;
@Column({ nullable: true })
@JoinColumn({ name: "parent_id" }) @RelationId((channel: Channel) => channel.parent)
@ManyToOne(() => Channel) parent_id: string;
parent?: Channel;
@JoinColumn({ name: "parent_id" })
// only for group dms @ManyToOne(() => Channel)
@Column({ nullable: true }) parent?: Channel;
@RelationId((channel: Channel) => channel.owner)
owner_id: string; // only for group dms
@Column({ nullable: true })
@JoinColumn({ name: "owner_id" }) @RelationId((channel: Channel) => channel.owner)
@ManyToOne(() => User) owner_id: string;
owner: User;
@JoinColumn({ name: "owner_id" })
@Column({ nullable: true }) @ManyToOne(() => User)
last_pin_timestamp?: number; owner: User;
@Column({ nullable: true }) @Column({ nullable: true })
default_auto_archive_duration?: number; last_pin_timestamp?: number;
@Column({ nullable: true }) @Column({ nullable: true })
position?: number; default_auto_archive_duration?: number;
@Column({ type: "simple-json", nullable: true }) @Column({ nullable: true })
permission_overwrites?: ChannelPermissionOverwrite[]; position?: number;
@Column({ nullable: true }) @Column({ type: "simple-json", nullable: true })
video_quality_mode?: number; permission_overwrites?: ChannelPermissionOverwrite[];
@Column({ nullable: true }) @Column({ nullable: true })
bitrate?: number; video_quality_mode?: number;
@Column({ nullable: true }) @Column({ nullable: true })
user_limit?: number; bitrate?: number;
@Column({ nullable: true }) @Column({ nullable: true })
nsfw?: boolean; user_limit?: number;
@Column({ nullable: true }) @Column({ nullable: true })
rate_limit_per_user?: number; nsfw?: boolean;
@Column({ nullable: true }) @Column({ nullable: true })
topic?: string; rate_limit_per_user?: number;
@OneToMany(() => Invite, (invite: Invite) => invite.channel, { @Column({ nullable: true })
cascade: true, topic?: string;
orphanedRowAction: "delete",
}) @OneToMany(() => Invite, (invite: Invite) => invite.channel, {
invites?: Invite[]; cascade: true,
orphanedRowAction: "delete",
@OneToMany(() => Message, (message: Message) => message.channel, { })
cascade: true, invites?: Invite[];
orphanedRowAction: "delete",
}) @OneToMany(() => Message, (message: Message) => message.channel, {
messages?: Message[]; cascade: true,
orphanedRowAction: "delete",
@OneToMany(() => VoiceState, (voice_state: VoiceState) => voice_state.channel, { })
cascade: true, messages?: Message[];
orphanedRowAction: "delete",
}) @OneToMany(() => VoiceState, (voice_state: VoiceState) => voice_state.channel, {
voice_states?: VoiceState[]; cascade: true,
orphanedRowAction: "delete",
@OneToMany(() => ReadState, (read_state: ReadState) => read_state.channel, { })
cascade: true, voice_states?: VoiceState[];
orphanedRowAction: "delete",
}) @OneToMany(() => ReadState, (read_state: ReadState) => read_state.channel, {
read_states?: ReadState[]; cascade: true,
orphanedRowAction: "delete",
@OneToMany(() => Webhook, (webhook: Webhook) => webhook.channel, { })
cascade: true, read_states?: ReadState[];
orphanedRowAction: "delete",
}) @OneToMany(() => Webhook, (webhook: Webhook) => webhook.channel, {
webhooks?: Webhook[]; cascade: true,
orphanedRowAction: "delete",
// TODO: DM channel })
static async createChannel( webhooks?: Webhook[];
channel: Partial<Channel>,
user_id: string = "0", // TODO: DM channel
opts?: { static async createChannel(
keepId?: boolean; channel: Partial<Channel>,
skipExistsCheck?: boolean; user_id: string = "0",
skipPermissionCheck?: boolean; opts?: {
skipEventEmit?: boolean; keepId?: boolean;
} skipExistsCheck?: boolean;
) { skipPermissionCheck?: boolean;
if (!opts?.skipPermissionCheck) { skipEventEmit?: boolean;
// Always check if user has permission first skipNameChecks?: boolean;
const permissions = await getPermission(user_id, channel.guild_id); }
permissions.hasThrow("MANAGE_CHANNELS"); ) {
} if (!opts?.skipPermissionCheck) {
// Always check if user has permission first
switch (channel.type) { const permissions = await getPermission(user_id, channel.guild_id);
case ChannelType.GUILD_TEXT: permissions.hasThrow("MANAGE_CHANNELS");
case ChannelType.GUILD_VOICE: }
if (channel.parent_id && !opts?.skipExistsCheck) {
const exists = await Channel.findOneOrFail({ id: channel.parent_id }); if (!opts?.skipNameChecks) {
if (!exists) throw new HTTPError("Parent id channel doesn't exist", 400); const guild = await Guild.findOneOrFail({ id: channel.guild_id });
if (exists.guild_id !== channel.guild_id) if (!guild.features.includes("ALLOW_INVALID_CHANNEL_NAMES") && channel.name) {
throw new HTTPError("The category channel needs to be in the guild"); for (var character of InvisibleCharacters)
} if (channel.name.includes(character))
break; throw new HTTPError("Channel name cannot include invalid characters", 403);
case ChannelType.GUILD_CATEGORY:
break; if (channel.name.match(/\-\-+/g))
case ChannelType.DM: throw new HTTPError("Channel name cannot include multiple adjacent dashes.", 403)
case ChannelType.GROUP_DM:
throw new HTTPError("You can't create a dm channel in a guild"); if (channel.name.charAt(0) === "-" ||
// TODO: check if guild is community server channel.name.charAt(channel.name.length - 1) === "-")
case ChannelType.GUILD_STORE: throw new HTTPError("Channel name cannot start/end with dash.", 403)
case ChannelType.GUILD_NEWS: }
default:
throw new HTTPError("Not yet supported"); if (!guild.features.includes("ALLOW_UNNAMED_CHANNELS")) {
} if (!channel.name)
throw new HTTPError("Channel name cannot be empty.", 403);
if (!channel.permission_overwrites) channel.permission_overwrites = []; }
// TODO: auto generate position }
channel = { switch (channel.type) {
...channel, case ChannelType.GUILD_TEXT:
...(!opts?.keepId && { id: Snowflake.generate() }), case ChannelType.GUILD_VOICE:
created_at: new Date(), if (channel.parent_id && !opts?.skipExistsCheck) {
position: channel.position || 0, 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)
await Promise.all([ throw new HTTPError("The category channel needs to be in the guild");
new Channel(channel).save(), }
!opts?.skipEventEmit break;
? emitEvent({ case ChannelType.GUILD_CATEGORY:
event: "CHANNEL_CREATE", break;
data: channel, case ChannelType.DM:
guild_id: channel.guild_id, case ChannelType.GROUP_DM:
} as ChannelCreateEvent) throw new HTTPError("You can't create a dm channel in a guild");
: Promise.resolve(), // TODO: check if guild is community server
]); case ChannelType.GUILD_STORE:
case ChannelType.GUILD_NEWS:
return channel; default:
} throw new HTTPError("Not yet supported");
}
static async createDMChannel(recipients: string[], creator_user_id: string, name?: string) {
recipients = recipients.unique().filter((x) => x !== creator_user_id); if (!channel.permission_overwrites) channel.permission_overwrites = [];
const otherRecipientsUsers = await User.find({ where: recipients.map((x) => ({ id: x })) }); // TODO: auto generate position
// TODO: check config for max number of recipients channel = {
if (otherRecipientsUsers.length !== recipients.length) { ...channel,
throw new HTTPError("Recipient/s not found"); ...(!opts?.keepId && { id: Snowflake.generate() }),
} created_at: new Date(),
position: channel.position || 0,
const type = recipients.length === 1 ? ChannelType.DM : ChannelType.GROUP_DM; };
let channel = null; await Promise.all([
new Channel(channel).save(),
const channelRecipients = [...recipients, creator_user_id]; !opts?.skipEventEmit
? emitEvent({
const userRecipients = await Recipient.find({ event: "CHANNEL_CREATE",
where: { user_id: creator_user_id }, data: channel,
relations: ["channel", "channel.recipients"], guild_id: channel.guild_id,
}); } as ChannelCreateEvent)
: Promise.resolve(),
for (let ur of userRecipients) { ]);
let re = ur.channel.recipients!.map((r) => r.user_id);
if (re.length === channelRecipients.length) { return channel;
if (containsAll(re, channelRecipients)) { }
if (channel == null) {
channel = ur.channel; static async createDMChannel(recipients: string[], creator_user_id: string, name?: string) {
await ur.assign({ closed: false }).save(); 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");
if (channel == null) { }
name = trimSpecial(name);
const type = recipients.length === 1 ? ChannelType.DM : ChannelType.GROUP_DM;
channel = await new Channel({
name, let channel = null;
type,
owner_id: type === ChannelType.DM ? undefined : creator_user_id, const channelRecipients = [...recipients, creator_user_id];
created_at: new Date(),
last_message_id: null, const userRecipients = await Recipient.find({
recipients: channelRecipients.map( where: { user_id: creator_user_id },
(x) => relations: ["channel", "channel.recipients"],
new Recipient({ user_id: x, closed: !(type === ChannelType.GROUP_DM || x === creator_user_id) }) });
),
}).save(); for (let ur of userRecipients) {
} let re = ur.channel.recipients!.map((r) => r.user_id);
if (re.length === channelRecipients.length) {
const channel_dto = await DmChannelDTO.from(channel); if (containsAll(re, channelRecipients)) {
if (channel == null) {
if (type === ChannelType.GROUP_DM) { channel = ur.channel;
for (let recipient of channel.recipients!) { await ur.assign({ closed: false }).save();
await emitEvent({ }
event: "CHANNEL_CREATE", }
data: channel_dto.excludedRecipients([recipient.user_id]), }
user_id: recipient.user_id, }
});
} if (channel == null) {
} else { name = trimSpecial(name);
await emitEvent({ event: "CHANNEL_CREATE", data: channel_dto, user_id: creator_user_id });
} channel = await new Channel({
name,
return channel_dto.excludedRecipients([creator_user_id]); type,
} owner_id: type === ChannelType.DM ? undefined : null, // 1:1 DMs are ownerless in fosscord-server
created_at: new Date(),
static async removeRecipientFromChannel(channel: Channel, user_id: string) { last_message_id: null,
await Recipient.delete({ channel_id: channel.id, user_id: user_id }); recipients: channelRecipients.map(
channel.recipients = channel.recipients?.filter((r) => r.user_id !== user_id); (x) =>
new Recipient({ user_id: x, closed: !(type === ChannelType.GROUP_DM || x === creator_user_id) })
if (channel.recipients?.length === 0) { ),
await Channel.deleteChannel(channel); }).save();
await emitEvent({ }
event: "CHANNEL_DELETE",
data: await DmChannelDTO.from(channel, [user_id]), const channel_dto = await DmChannelDTO.from(channel);
user_id: user_id,
}); if (type === ChannelType.GROUP_DM) {
return; for (let recipient of channel.recipients!) {
} await emitEvent({
event: "CHANNEL_CREATE",
await emitEvent({ data: channel_dto.excludedRecipients([recipient.user_id]),
event: "CHANNEL_DELETE", user_id: recipient.user_id,
data: await DmChannelDTO.from(channel, [user_id]), });
user_id: user_id, }
}); } else {
await emitEvent({ event: "CHANNEL_CREATE", data: channel_dto, user_id: creator_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? return channel_dto.excludedRecipients([creator_user_id]);
await emitEvent({ }
event: "CHANNEL_UPDATE",
data: await DmChannelDTO.from(channel, [user_id]), static async removeRecipientFromChannel(channel: Channel, user_id: string) {
channel_id: channel.id, 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.save(); await Channel.deleteChannel(channel);
await emitEvent({
await emitEvent({ event: "CHANNEL_DELETE",
event: "CHANNEL_RECIPIENT_REMOVE", data: await DmChannelDTO.from(channel, [user_id]),
data: { user_id: user_id,
channel_id: channel.id, });
user: await User.findOneOrFail({ where: { id: user_id }, select: PublicUserProjection }), return;
}, }
channel_id: channel.id,
} as ChannelRecipientRemoveEvent); await emitEvent({
} event: "CHANNEL_DELETE",
data: await DmChannelDTO.from(channel, [user_id]),
static async deleteChannel(channel: Channel) { user_id: user_id,
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 }); //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
isDm() { await emitEvent({
return this.type === ChannelType.DM || this.type === ChannelType.GROUP_DM; event: "CHANNEL_UPDATE",
} data: await DmChannelDTO.from(channel, [user_id]),
} channel_id: channel.id,
});
export interface ChannelPermissionOverwrite { }
allow: string;
deny: string; await channel.save();
id: string;
type: ChannelPermissionOverwriteType; await emitEvent({
} event: "CHANNEL_RECIPIENT_REMOVE",
data: {
export enum ChannelPermissionOverwriteType { channel_id: channel.id,
role = 0, user: await User.findOneOrFail({ where: { id: user_id }, select: PublicUserProjection }),
member = 1, },
} 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; minUpperCase: number;
minSymbols: number; minSymbols: number;
}; };
incrementingDiscriminators: boolean; // random otherwise
}; };
regions: { regions: {
default: string; default: string;
@ -335,6 +336,7 @@ export const DefaultConfigOptions: ConfigValue = {
minUpperCase: 2, minUpperCase: 2,
minSymbols: 0, minSymbols: 0,
}, },
incrementingDiscriminators: false,
}, },
regions: { regions: {
default: "fosscord", default: "fosscord",

View File

@ -10,7 +10,7 @@ export class Emoji extends BaseClass {
animated: boolean; animated: boolean;
@Column() @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() @Column()
guild_id: string; guild_id: string;
@ -40,4 +40,7 @@ export class Emoji extends BaseClass {
@Column({ type: "simple-array" }) @Column({ type: "simple-array" })
roles: string[]; // roles this emoji is whitelisted to (new discord feature?) 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; owner: User;
@Column({ nullable: true }) @Column({ nullable: true })
preferred_locale?: string; // only community guilds can choose this preferred_locale?: string;
@Column({ nullable: true }) @Column({ nullable: true })
premium_subscription_count?: number; premium_subscription_count?: number;
@ -301,22 +301,22 @@ export class Guild extends BaseClass {
name: body.name || "Fosscord", name: body.name || "Fosscord",
icon: await handleFile(`/icons/${guild_id}`, body.icon as string), icon: await handleFile(`/icons/${guild_id}`, body.icon as string),
region: Config.get().regions.default, 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, 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, explicit_content_filter: 0,
features: [], features: [],
id: guild_id, id: guild_id,
max_members: 250000, max_members: 250000,
max_presences: 250000, max_presences: 250000,
max_video_channel_users: 25, max_video_channel_users: 200,
presence_count: 0, presence_count: 0,
member_count: 0, // will automatically be increased by addMember() member_count: 0, // will automatically be increased by addMember()
mfa_level: 0, mfa_level: 0,
preferred_locale: "en-US", preferred_locale: "en-US",
premium_subscription_count: 0, premium_subscription_count: 0,
premium_tier: 0, premium_tier: 0,
system_channel_flags: 0, system_channel_flags: 4, // defaults effect: suppress the setup tips to save performance
unavailable: false, unavailable: false,
nsfw: false, nsfw: false,
nsfw_level: 0, nsfw_level: 0,
@ -326,20 +326,24 @@ export class Guild extends BaseClass {
description: "No description", description: "No description",
welcome_channels: [], welcome_channels: [],
}, },
widget_enabled: false, widget_enabled: true, // NB: don't set it as false to prevent artificial restrictions
}).save(); }).save();
// we have to create the role _after_ the guild because else we would get a "SQLITE_CONSTRAINT: FOREIGN KEY constraint failed" error // 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({ await new Role({
id: guild_id, id: guild_id,
guild_id: guild_id, guild_id: guild_id,
color: 0, color: 0,
hoist: false, hoist: false,
managed: 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, mentionable: false,
name: "@everyone", name: "@everyone",
permissions: String("2251804225"), permissions: String("2251804225"),
position: 0, position: 0,
icon: null,
unicode_emoji: null
}).save(); }).save();
if (!body.channels || !body.channels.length) body.channels = [{ id: "01", type: 0, name: "general" }]; 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))) { for (const channel of body.channels?.sort((a, b) => (a.parent_id ? 1 : -1))) {
var id = ids.get(channel.id) || Snowflake.generate(); 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); var parent_id = ids.get(channel.parent_id);
await Channel.createChannel({ ...channel, guild_id, id, parent_id }, body.owner_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() @Column()
position: number; position: number;
@Column({ nullable: true })
icon: string;
@Column({ nullable: true })
unicode_emoji: string;
@Column({ type: "simple-json", nullable: true }) @Column({ type: "simple-json", nullable: true })
tags?: { tags?: {
bot_id?: string; bot_id?: string;

View File

@ -64,7 +64,7 @@ export class User extends BaseClass {
setDiscriminator(val: string) { setDiscriminator(val: string) {
const number = Number(val); const number = Number(val);
if (isNaN(number)) throw new Error("invalid discriminator"); 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"); 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({ static async register({
email, email,
username, username,
@ -194,21 +223,9 @@ export class User extends BaseClass {
// trim special uf8 control characters -> Backspace, Newline, ... // trim special uf8 control characters -> Backspace, Newline, ...
username = trimSpecial(username); username = trimSpecial(username);
// discriminator will be randomly generated const discriminator = await User.generateDiscriminator(username);
let discriminator = ""; if (!discriminator) {
// We've failed to generate a valid and unused 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) {
throw FieldErrors({ throw FieldErrors({
username: { username: {
code: "USERNAME_TOO_MANY_USERS", 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 "./String";
export * from "./Array"; export * from "./Array";
export * from "./TraverseDirectory"; 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 This program is distributed in the hope that it will be useful,
of this software and associated documentation files (the "Software"), to deal but WITHOUT ANY WARRANTY; without even the implied warranty of
in the Software without restriction, including without limitation the rights MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell GNU Affero General Public License for more details.
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all You should have received a copy of the GNU Affero General Public License
copies or substantial portions of the Software. along with this program. If not, see <https://www.gnu.org/licenses/>.
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.