diff --git a/Dockerfile b/Dockerfile index d4b423ee..3c8a0b31 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,40 @@ -FROM node:14 -WORKDIR /usr/src/fosscord-server/ -COPY . . -WORKDIR /usr/src/fosscord-server/bundle +FROM node:alpine + +# env vars +ENV WORK_DIR="/srv/fosscord-server" +ENV DEV_MODE=0 +ENV HTTP_PORT=3001 +ENV WS_PORT=3002 +ENV CDN_PORT=3003 +ENV RTC_PORT=3004 +ENV ADMIN_PORT=3005 + +# exposed ports (only for reference, see https://docs.docker.com/engine/reference/builder/#expose) +EXPOSE ${HTTP_PORT}/tcp ${WS_PORT}/tcp ${CDN_PORT}/tcp ${RTC_PORT}/tcp ${ADMIN_PORT}/tcp + +# install required apps +RUN apk add --no-cache --update git python2 py-pip make build-base + +# optionl: packages for debugging/development +RUN apk add --no-cache sqlite + +# download fosscord-server +WORKDIR $WORK_DIR/src +RUN git clone https://github.com/fosscord/fosscord-server.git . + +# setup and run +WORKDIR $WORK_DIR/src/bundle RUN npm run setup -EXPOSE 3001 -CMD [ "npm", "run", "start:bundle" ] +RUN npm install @yukikaze-bot/erlpack +# RUN npm install mysql --save + +# create update script +RUN printf '#!/bin/sh\n\ngit -C $WORK_DIR/src/ checkout master\ngit -C $WORK_DIR/src/ reset --hard HEAD\ngit -C $WORK_DIR/src/ pull\ncd $WORK_DIR/src/bundle/\nnpm run setup\n' > $WORK_DIR/update.sh +RUN chmod +x $WORK_DIR/update.sh + +# configure entrypoint file +RUN printf '#!/bin/sh\n\nDEV_MODE=${DEV_MODE:-0}\n\nif [ "$DEV_MODE" -eq 1 ]; then\n tail -f /dev/null\nelse\n cd $WORK_DIR/src/bundle/\n npm run start:bundle\nfi\n' > $WORK_DIR/entrypoint.sh +RUN chmod +x $WORK_DIR/entrypoint.sh + +WORKDIR $WORK_DIR +ENTRYPOINT ["./entrypoint.sh"] diff --git a/README.md b/README.md index 1bcea1e3..a15a4b64 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,6 @@ This repository contains: - [Contributing](https://docs.fosscord.com/contributing/server/) -## [Setup](https://docs.fosscord.com/setup/server/) +## [Setup](https://docs.fosscord.com/server/setup/) - [Download](https://github.com/fosscord/fosscord-server/releases) diff --git a/api/assets/openapi.json b/api/assets/openapi.json index 1af0600d..a8a657b2 100644 --- a/api/assets/openapi.json +++ b/api/assets/openapi.json @@ -2,7 +2,7 @@ "openapi": "3.0.0", "servers": [ { - "url": "https://api.fosscord.com/v{version}", + "url": "https://api.fosscord.com/api/v{version}", "description": "Official fosscord instance", "variables": { "version": { @@ -2960,7 +2960,7 @@ "type": { "type": "string" }, - "verifie": { + "verified": { "type": "boolean" }, "visibility": { @@ -2980,7 +2980,7 @@ "type", "user", "user_id", - "verifie", + "verified", "visibility" ] }, @@ -3119,7 +3119,7 @@ "type": "boolean" }, "status": { - "enum": ["dnd", "idle", "offline", "online"], + "enum": ["dnd", "idle", "offline", "online", "invisible"], "type": "string" }, "stream_notifications_enabled": { @@ -5677,7 +5677,7 @@ "type": "boolean" }, "status": { - "enum": ["dnd", "idle", "offline", "online"], + "enum": ["dnd", "idle", "offline", "online", "invisible"], "type": "string" }, "stream_notifications_enabled": { diff --git a/api/assets/preload-plugins/fosscord-login.js b/api/assets/preload-plugins/fosscord-login.js new file mode 100644 index 00000000..38f82200 --- /dev/null +++ b/api/assets/preload-plugins/fosscord-login.js @@ -0,0 +1,12 @@ +// Remove `` from header when we're not accessing `/login` or `/register` +// fosscord-login.css replaces discord's TOS tooltip with something more fitting for fosscord, which when included in the main app, causes other tooltips +// to be affected, which is potentially unwanted. +// +// This script removes fosscord-login.css when a user reloads the page. From testing, it appears fosscord already properly removes +// fosscord-login.css after login is successful, but not if you reload the page after logging in. This script is to remove fosscord-login.css in +// that specific case. + +var token = JSON.parse(localStorage.getItem("token")); +if (!token && location.pathname !== "/login" && location.pathname !== "/register") { + document.getElementById("logincss").remove(); +} diff --git a/api/assets/schemas.json b/api/assets/schemas.json index 818c8a61..555129e3 100644 --- a/api/assets/schemas.json +++ b/api/assets/schemas.json @@ -355,11 +355,11 @@ "type": { "type": "string" }, - "verifie": { + "verified": { "type": "boolean" } }, - "required": ["name", "type", "verifie"] + "required": ["name", "type", "verified"] } }, "$schema": "http://json-schema.org/draft-07/schema#" @@ -7900,7 +7900,7 @@ "type": "boolean" }, "status": { - "enum": ["dnd", "idle", "offline", "online"], + "enum": ["dnd", "idle", "offline", "online", "invisible"], "type": "string" }, "stream_notifications_enabled": { diff --git a/api/locales/he/common.json b/api/locales/he/common.json index 8bb9c042..9e72e941 100644 --- a/api/locales/he/common.json +++ b/api/locales/he/common.json @@ -7,12 +7,12 @@ "BASE_TYPE_BOOLEAN": "This field must be a boolean", "BASE_TYPE_CHOICES": "This field must be one of ({{types}})", "BASE_TYPE_CLASS": "This field must be an instance of {{type}}", - "BASE_TYPE_OBJECT": "This field must be an object", - "BASE_TYPE_ARRAY": "This field must be an array", - "UNKOWN_FIELD": "Unknown key: {{key}}", - "BASE_TYPE_CONSTANT": "This field must be {{value}}", - "EMAIL_TYPE_INVALID_EMAIL": "Not a well-formed email address", - "DATE_TYPE_PARSE": "Could not parse {{date}}. Should be ISO8601", - "BASE_TYPE_BAD_LENGTH": "Must be between {{length}} in length" + "BASE_TYPE_OBJECT": "שדה זה חייב להיות אובייקט", + "BASE_TYPE_ARRAY": "שדה זה חייב להיות מערך", + "UNKOWN_FIELD": "מפתח לא ידוע: {{key}}", + "BASE_TYPE_CONSTANT": "שדה זה להיות {{value}}", + "EMAIL_TYPE_INVALID_EMAIL": "כתובת דואר אלקטרוני לא חוקית", + "DATE_TYPE_PARSE": "לא ניתן לנתח {{date}}. צריך להיות ISO8601", + "BASE_TYPE_BAD_LENGTH": "האורך חייב להיות בין {{length}}" } } diff --git a/api/package-lock.json b/api/package-lock.json index e3e29800..de889188 100644 Binary files a/api/package-lock.json and b/api/package-lock.json differ diff --git a/api/package.json b/api/package.json index f4614c90..c586c9fe 100644 --- a/api/package.json +++ b/api/package.json @@ -49,7 +49,7 @@ "@types/morgan": "^1.9.3", "@types/multer": "^1.4.5", "@types/node": "^14.17.9", - "@types/node-fetch": "^2.5.7", + "@types/node-fetch": "^2.5.5", "@types/supertest": "^2.0.11", "@zerollup/ts-transform-paths": "^1.7.18", "jest": "^27.2.5", @@ -86,7 +86,7 @@ "missing-native-js-functions": "^1.2.18", "morgan": "^1.10.0", "multer": "^1.4.2", - "node-fetch": "^3.1.1", + "node-fetch": "^2.6.2", "patch-package": "^6.4.7", "picocolors": "^1.0.0", "proxy-agent": "^5.0.0", diff --git a/api/scripts/droptables.sql b/api/scripts/droptables.sql index 57d1b271..8a852048 100644 --- a/api/scripts/droptables.sql +++ b/api/scripts/droptables.sql @@ -26,6 +26,6 @@ DROP TABLE webhooks; DROP TABLE channels; DROP TABLE members; DROP TABLE guilds; -DROP TABLE client_relase; +DROP TABLE client_release; -- DROP TABLE users; -- DROP TABLE config; \ No newline at end of file diff --git a/api/src/middlewares/Authentication.ts b/api/src/middlewares/Authentication.ts index 429cf11e..5a08caf3 100644 --- a/api/src/middlewares/Authentication.ts +++ b/api/src/middlewares/Authentication.ts @@ -15,6 +15,7 @@ export const NO_AUTHORIZATION_ROUTES = [ "/experiments", "/updates", "/downloads/", + "/scheduled-maintenances/upcoming.json", // Public kubernetes integration "/-/readyz", "/-/healthz", diff --git a/api/src/routes/channels/#channel_id/invites.ts b/api/src/routes/channels/#channel_id/invites.ts index 6d2c625d..9c361164 100644 --- a/api/src/routes/channels/#channel_id/invites.ts +++ b/api/src/routes/channels/#channel_id/invites.ts @@ -19,7 +19,8 @@ export interface InviteCreateSchema { target_user_type?: number; } -router.post("/", route({ body: "InviteCreateSchema", permission: "CREATE_INSTANT_INVITE" }), async (req: Request, res: Response) => { +router.post("/", route({ body: "InviteCreateSchema", permission: "CREATE_INSTANT_INVITE", right: "CREATE_INVITES" }), + async (req: Request, res: Response) => { const { user_id } = req; const { channel_id } = req.params; const channel = await Channel.findOneOrFail({ where: { id: channel_id }, select: ["id", "name", "type", "guild_id"] }); diff --git a/api/src/routes/channels/#channel_id/messages/#message_id/index.ts b/api/src/routes/channels/#channel_id/messages/#message_id/index.ts index 7f7de264..58dfb1cc 100644 --- a/api/src/routes/channels/#channel_id/messages/#message_id/index.ts +++ b/api/src/routes/channels/#channel_id/messages/#message_id/index.ts @@ -1,4 +1,4 @@ -import { Channel, emitEvent, getPermission, MessageDeleteEvent, Message, MessageUpdateEvent } from "@fosscord/util"; +import { Channel, emitEvent, getPermission, getRights, MessageDeleteEvent, Message, MessageUpdateEvent } from "@fosscord/util"; import { Router, Response, Request } from "express"; import { route } from "@fosscord/api"; import { handleMessage, postHandleMessage } from "@fosscord/api"; @@ -7,18 +7,23 @@ import { MessageCreateSchema } from "../index"; const router = Router(); // TODO: message content/embed string length limit -router.patch("/", route({ body: "MessageCreateSchema", permission: "SEND_MESSAGES" }), async (req: Request, res: Response) => { +router.patch("/", route({ body: "MessageCreateSchema", permission: "SEND_MESSAGES", right: "SEND_MESSAGES" }), async (req: Request, res: Response) => { const { message_id, channel_id } = req.params; var body = req.body as MessageCreateSchema; const message = await Message.findOneOrFail({ where: { id: message_id, channel_id }, relations: ["attachments"] }); const permissions = await getPermission(req.user_id, undefined, channel_id); + + const rights = await getRights(req.user_id); - if (req.user_id !== message.author_id) { - permissions.hasThrow("MANAGE_MESSAGES"); - body = { flags: body.flags }; // admins can only suppress embeds of other messages - } + if ((req.user_id !== message.author_id)) { + if (!rights.has("MANAGE_MESSAGES")) { + permissions.hasThrow("MANAGE_MESSAGES"); + body = { flags: body.flags }; +// guild admins can only suppress embeds of other messages, no such restriction imposed to instance-wide admins + } + } else rights.hasThrow("SELF_EDIT_MESSAGES"); const new_message = await handleMessage({ ...message, @@ -46,17 +51,20 @@ router.patch("/", route({ body: "MessageCreateSchema", permission: "SEND_MESSAGE return res.json(message); }); -// permission check only if deletes messagr from other user router.delete("/", route({}), async (req: Request, res: Response) => { const { message_id, channel_id } = req.params; const channel = await Channel.findOneOrFail({ id: channel_id }); const message = await Message.findOneOrFail({ id: message_id }); + + const rights = await getRights(req.user_id); - if (message.author_id !== req.user_id) { - const permission = await getPermission(req.user_id, channel.guild_id, channel_id); - permission.hasThrow("MANAGE_MESSAGES"); - } + if ((message.author_id !== req.user_id)) { + if (!rights.has("MANAGE_MESSAGES")) { + const permission = await getPermission(req.user_id, channel.guild_id, channel_id); + permission.hasThrow("MANAGE_MESSAGES"); + } + } else rights.hasThrow("SELF_DELETE_MESSAGES"); await Message.delete({ id: message_id }); diff --git a/api/src/routes/downloads.ts b/api/src/routes/downloads.ts index ad78b62f..ddfc080c 100644 --- a/api/src/routes/downloads.ts +++ b/api/src/routes/downloads.ts @@ -1,6 +1,6 @@ import { Router, Response, Request } from "express"; import { route } from "@fosscord/api"; -import { Relase, Config } from "@fosscord/util"; +import { Release, Config } from "@fosscord/util"; const router = Router(); @@ -12,9 +12,9 @@ router.get("/:branch", route({}), async (req: Request, res: Response) => { if(!platform || !["linux", "osx", "win"].includes(platform.toString())) return res.status(404) - const relase = await Relase.findOneOrFail({ name: client.relases.upstreamVersion }); + const release = await Release.findOneOrFail({ name: client.releases.upstreamVersion }); - res.redirect(relase[`win_url`]); + res.redirect(release[`win_url`]); }); export default router; diff --git a/api/src/routes/guilds/#guild_id/bans.ts b/api/src/routes/guilds/#guild_id/bans.ts index 7ccf34d7..1ce41936 100644 --- a/api/src/routes/guilds/#guild_id/bans.ts +++ b/api/src/routes/guilds/#guild_id/bans.ts @@ -33,17 +33,32 @@ router.get("/", route({ permission: "BAN_MEMBERS" }), async (req: Request, res: const { guild_id } = req.params; let bans = await Ban.find({ guild_id: guild_id }); + let promisesToAwait: object[] = []; + const bansObj: object[] = []; - /* Filter secret from database registry.*/ + bans.filter((ban) => ban.user_id !== ban.executor_id); // pretend self-bans don't exist to prevent victim chasing - bans.filter(ban => ban.user_id !== ban.executor_id); - // pretend self-bans don't exist to prevent victim chasing - - bans.forEach((registry: BanRegistrySchema) => { - delete registry.ip; + bans.forEach((ban) => { + promisesToAwait.push(User.getPublicUser(ban.user_id)); }); - - return res.json(bans); + + const bannedUsers: object[] = await Promise.all(promisesToAwait); + + bans.forEach((ban, index) => { + const user = bannedUsers[index] as User; + bansObj.push({ + reason: ban.reason, + user: { + username: user.username, + discriminator: user.discriminator, + id: user.id, + avatar: user.avatar, + public_flags: user.public_flags + } + }); + }); + + return res.json(bansObj); }); router.get("/:user", route({ permission: "BAN_MEMBERS" }), async (req: Request, res: Response) => { diff --git a/api/src/routes/guilds/#guild_id/index.ts b/api/src/routes/guilds/#guild_id/index.ts index 991c3f93..4ec3df72 100644 --- a/api/src/routes/guilds/#guild_id/index.ts +++ b/api/src/routes/guilds/#guild_id/index.ts @@ -1,5 +1,5 @@ import { Request, Response, Router } from "express"; -import { emitEvent, getPermission, Guild, GuildUpdateEvent, handleFile, Member } from "@fosscord/util"; +import { DiscordApiErrors, emitEvent, getPermission, getRights, Guild, GuildUpdateEvent, handleFile, Member } from "@fosscord/util"; import { HTTPError } from "lambert-server"; import { route } from "@fosscord/api"; import "missing-native-js-functions"; @@ -37,9 +37,17 @@ router.get("/", route({}), async (req: Request, res: Response) => { return res.send(guild); }); -router.patch("/", route({ body: "GuildUpdateSchema", permission: "MANAGE_GUILD" }), async (req: Request, res: Response) => { +router.patch("/", route({ body: "GuildUpdateSchema"}), async (req: Request, res: Response) => { const body = req.body as GuildUpdateSchema; const { guild_id } = req.params; + + + const rights = await getRights(req.user_id); + const permission = await getPermission(req.user_id, guild_id); + + if (!rights.has("MANAGE_GUILDS")||!permission.has("MANAGE_GUILD")) + throw DiscordApiErrors.MISSING_PERMISSIONS.withParams("MANAGE_GUILD"); + // TODO: guild update check image if (body.icon) body.icon = await handleFile(`/icons/${guild_id}`, body.icon); diff --git a/api/src/routes/guilds/#guild_id/members/#member_id/index.ts b/api/src/routes/guilds/#guild_id/members/#member_id/index.ts index 24c74af7..34836292 100644 --- a/api/src/routes/guilds/#guild_id/members/#member_id/index.ts +++ b/api/src/routes/guilds/#guild_id/members/#member_id/index.ts @@ -25,13 +25,19 @@ router.patch("/", route({ body: "MemberChangeSchema" }), async (req: Request, re const member = await Member.findOneOrFail({ where: { id: member_id, guild_id }, relations: ["roles", "user"] }); const permission = await getPermission(req.user_id, guild_id); + const everyone = await Role.findOneOrFail({ guild_id: guild_id, name: "@everyone", position: 0 }); if (body.roles) { permission.hasThrow("MANAGE_ROLES"); + + if (body.roles.indexOf(everyone.id) === -1) body.roles.push(everyone.id); member.roles = body.roles.map((x) => new Role({ id: x })); // foreign key constraint will fail if role doesn't exist } await member.save(); + + member.roles = member.roles.filter((x) => x.id !== everyone.id); + // do not use promise.all as we have to first write to db before emitting the event to catch errors await emitEvent({ event: "GUILD_MEMBER_UPDATE", diff --git a/api/src/routes/guilds/#guild_id/vanity-url.ts b/api/src/routes/guilds/#guild_id/vanity-url.ts index 63173345..29cd25e2 100644 --- a/api/src/routes/guilds/#guild_id/vanity-url.ts +++ b/api/src/routes/guilds/#guild_id/vanity-url.ts @@ -9,11 +9,19 @@ const InviteRegex = /\W/g; router.get("/", route({ permission: "MANAGE_GUILD" }), async (req: Request, res: Response) => { const { guild_id } = req.params; + const guild = await Guild.findOneOrFail({ id: guild_id }); - const invite = await Invite.findOne({ where: { guild_id: guild_id, vanity_url: true } }); - if (!invite) return res.json({ code: null }); + if (!guild.features.includes("ALIASABLE_NAMES")) { + const invite = await Invite.findOne({ where: { guild_id: guild_id, vanity_url: true } }); + if (!invite) return res.json({ code: null }); - return res.json({ code: invite.code, uses: invite.uses }); + return res.json({ code: invite.code, uses: invite.uses }); + } else { + const invite = await Invite.find({ where: { guild_id: guild_id, vanity_url: true } }); + if (!invite || invite.length == 0) return res.json({ code: null }); + + return res.json(invite.map((x) => ({ code: x.code, uses: x.uses }))); + } }); export interface VanityUrlSchema { @@ -24,18 +32,33 @@ export interface VanityUrlSchema { code?: string; } -// TODO: check if guild is elgible for vanity url router.patch("/", route({ body: "VanityUrlSchema", permission: "MANAGE_GUILD" }), async (req: Request, res: Response) => { const { guild_id } = req.params; const body = req.body as VanityUrlSchema; const code = body.code?.replace(InviteRegex, ""); + const guild = await Guild.findOneOrFail({ id: guild_id }); + if (!guild.features.includes("VANITY_URL")) throw new HTTPError("Your guild doesn't support vanity urls"); + + if (!code || code.length === 0) throw new HTTPError("Code cannot be null or empty"); + const invite = await Invite.findOne({ code }); if (invite) throw new HTTPError("Invite already exists"); const { id } = await Channel.findOneOrFail({ guild_id, type: ChannelType.GUILD_TEXT }); - await Invite.update({ vanity_url: true, guild_id }, { code: code, channel_id: id }); + await new Invite({ + vanity_url: true, + code: code, + temporary: false, + uses: 0, + max_uses: 0, + max_age: 0, + created_at: new Date(), + expires_at: new Date(), + guild_id: guild_id, + channel_id: id + }).save(); return res.json({ code: code }); }); diff --git a/api/src/routes/guilds/index.ts b/api/src/routes/guilds/index.ts index 7b676211..10721413 100644 --- a/api/src/routes/guilds/index.ts +++ b/api/src/routes/guilds/index.ts @@ -1,5 +1,5 @@ import { Router, Request, Response } from "express"; -import { Role, Guild, Snowflake, Config, Member, Channel, DiscordApiErrors, handleFile } from "@fosscord/util"; +import { Role, Guild, Snowflake, Config, getRights, Member, Channel, DiscordApiErrors, handleFile } from "@fosscord/util"; import { route } from "@fosscord/api"; import { ChannelModifySchema } from "../channels/#channel_id"; @@ -20,12 +20,13 @@ export interface GuildCreateSchema { //TODO: create default channel -router.post("/", route({ body: "GuildCreateSchema" }), async (req: Request, res: Response) => { +router.post("/", route({ body: "GuildCreateSchema", right: "CREATE_GUILDS" }), async (req: Request, res: Response) => { const body = req.body as GuildCreateSchema; const { maxGuilds } = Config.get().limits.user; const guild_count = await Member.count({ id: req.user_id }); - if (guild_count >= maxGuilds) { + const rights = await getRights(req.user_id); + if ((guild_count >= maxGuilds)&&!rights.has("MANAGE_GUILDS")) { throw DiscordApiErrors.MAXIMUM_GUILDS.withParams(maxGuilds); } diff --git a/api/src/routes/invites/index.ts b/api/src/routes/invites/index.ts index 37e9e05a..21da2d18 100644 --- a/api/src/routes/invites/index.ts +++ b/api/src/routes/invites/index.ts @@ -13,7 +13,7 @@ router.get("/:code", route({}), async (req: Request, res: Response) => { res.status(200).send(invite); }); -router.post("/:code", route({}), async (req: Request, res: Response) => { +router.post("/:code", route({right: "JOIN_GUILDS"}), async (req: Request, res: Response) => { const { code } = req.params; const { guild_id } = await Invite.findOneOrFail({ code }) const { features } = await Guild.findOneOrFail({ id: guild_id}); diff --git a/api/src/routes/scheduled-maintenances/upcoming_json.ts b/api/src/routes/scheduled-maintenances/upcoming_json.ts new file mode 100644 index 00000000..83092e44 --- /dev/null +++ b/api/src/routes/scheduled-maintenances/upcoming_json.ts @@ -0,0 +1,12 @@ +import { Router, Request, Response } from "express"; +import { route } from "@fosscord/api"; +const router = Router(); + +router.get("/scheduled-maintenances/upcoming.json",route({}), async (req: Request, res: Response) => { + res.json({ + "page": {}, + "scheduled_maintenances": {} + }); +}); + +export default router; diff --git a/api/src/routes/store/published-listings/applications.ts b/api/src/routes/store/published-listings/applications.ts index f06a01e4..060a4c3d 100644 --- a/api/src/routes/store/published-listings/applications.ts +++ b/api/src/routes/store/published-listings/applications.ts @@ -18,7 +18,7 @@ router.get("/:id", route({}), async (req: Request, res: Response) => { access_type: 2, name: "", features: [], - relase_date: "", + release_date: "", premium: false, slug: "", flags: 4, diff --git a/api/src/routes/store/published-listings/skus.ts b/api/src/routes/store/published-listings/skus.ts index f06a01e4..060a4c3d 100644 --- a/api/src/routes/store/published-listings/skus.ts +++ b/api/src/routes/store/published-listings/skus.ts @@ -18,7 +18,7 @@ router.get("/:id", route({}), async (req: Request, res: Response) => { access_type: 2, name: "", features: [], - relase_date: "", + release_date: "", premium: false, slug: "", flags: 4, diff --git a/api/src/routes/updates.ts b/api/src/routes/updates.ts index 4682ce7c..cb4577c8 100644 --- a/api/src/routes/updates.ts +++ b/api/src/routes/updates.ts @@ -1,19 +1,19 @@ import { Router, Response, Request } from "express"; import { route } from "@fosscord/api"; -import { Config, Relase } from "@fosscord/util"; +import { Config, Release } from "@fosscord/util"; const router = Router(); router.get("/", route({}), async (req: Request, res: Response) => { const { client } = Config.get(); - const relase = await Relase.findOneOrFail({ name: client.relases.upstreamVersion}) + const release = await Release.findOneOrFail({ name: client.releases.upstreamVersion}) res.json({ - name: relase.name, - pub_date: relase.pub_date, - url: relase.url, - notes: relase.notes + name: release.name, + pub_date: release.pub_date, + url: release.url, + notes: release.notes }); }); diff --git a/api/src/routes/users/@me/index.ts b/api/src/routes/users/@me/index.ts index bf62e7fc..122080f2 100644 --- a/api/src/routes/users/@me/index.ts +++ b/api/src/routes/users/@me/index.ts @@ -53,8 +53,6 @@ router.patch("/", route({ body: "UserModifySchema" }), async (req: Request, res: throw FieldErrors({ email: { message: req.t("auth:register.EMAIL_INVALID"), code: "EMAIL_INVALID" } }); } - user.assign(body); - if (body.new_password) { if (!body.password && !user.email) { throw FieldErrors({ @@ -64,14 +62,16 @@ router.patch("/", route({ body: "UserModifySchema" }), async (req: Request, res: user.data.hash = await bcrypt.hash(body.new_password, 12); } - var check_username = body?.username?.replace(/\s/g, ''); - - if(!check_username && !body?.avatar && !body?.banner) { - throw FieldErrors({ - username: { code: "BASE_TYPE_REQUIRED", message: req.t("common:field.BASE_TYPE_REQUIRED") } - }); + if (body.username) { + var check_username = body?.username?.replace(/\s/g, ''); + if (!check_username) { + throw FieldErrors({ + username: { code: "BASE_TYPE_REQUIRED", message: req.t("common:field.BASE_TYPE_REQUIRED") } + }); + } } + user.assign(body); await user.save(); // @ts-ignore diff --git a/api/src/routes/users/@me/notes.ts b/api/src/routes/users/@me/notes.ts index 2ef27bc0..4887b191 100644 --- a/api/src/routes/users/@me/notes.ts +++ b/api/src/routes/users/@me/notes.ts @@ -1,14 +1,39 @@ import { Request, Response, Router } from "express"; import { route } from "@fosscord/api"; +import { User, emitEvent } from "@fosscord/util"; const router: Router = Router(); +router.get("/:id", route({}), async (req: Request, res: Response) => { + const { id } = req.params; + const user = await User.findOneOrFail({ where: { id: req.user_id }, select: ["notes"] }); + + const note = user.notes[id]; + return res.json({ + note: note, + note_user_id: id, + user_id: user.id, + }); +}); + router.put("/:id", route({}), async (req: Request, res: Response) => { - //TODO - res.json({ - message: "400: Bad Request", - code: 0 - }).status(400); + const { id } = req.params; + const user = await User.findOneOrFail({ where: { id: req.user_id } }); + const noteUser = await User.findOneOrFail({ where: { id: id }}); //if noted user does not exist throw + const { note } = req.body; + + await User.update({ id: req.user_id }, { notes: { ...user.notes, [noteUser.id]: note } }); + + await emitEvent({ + event: "USER_NOTE_UPDATE", + data: { + note: note, + id: noteUser.id + }, + user_id: user.id, + }) + + return res.status(204); }); export default router; diff --git a/api/src/util/handlers/Message.ts b/api/src/util/handlers/Message.ts index 21664368..5a5ac666 100644 --- a/api/src/util/handlers/Message.ts +++ b/api/src/util/handlers/Message.ts @@ -7,6 +7,7 @@ import { MessageCreateEvent, MessageUpdateEvent, getPermission, + getRights, CHANNEL_MENTION, Snowflake, USER_MENTION, @@ -61,19 +62,20 @@ export async function handleMessage(opts: MessageOptions): Promise { 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); - } + const rights = await getRights(opts.author_id); + rights.hasThrow("SEND_MESSAGES"); + } if (opts.application_id) { message.application = await Application.findOneOrFail({ id: opts.application_id }); } if (opts.webhook_id) { message.webhook = await Webhook.findOneOrFail({ id: opts.webhook_id }); } - + const permission = await getPermission(opts.author_id, channel.guild_id, opts.channel_id); - permission.hasThrow("SEND_MESSAGES"); // TODO: add the rights check + permission.hasThrow("SEND_MESSAGES"); if (permission.cache.member) { message.member = permission.cache.member; } @@ -81,13 +83,15 @@ export async function handleMessage(opts: MessageOptions): Promise { if (opts.tts) permission.hasThrow("SEND_TTS_MESSAGES"); if (opts.message_reference) { permission.hasThrow("READ_MESSAGE_HISTORY"); - // 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"); + // code below has to be redone when we add custom message routing + if (message.guild_id !== null) { + 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? + // Q: should be checked if the referenced message exists? ANSWER: NO // @ts-ignore message.type = MessageType.REPLY; } diff --git a/api/src/util/utility/passwordStrength.ts b/api/src/util/utility/passwordStrength.ts index 047df008..e75e48f6 100644 --- a/api/src/util/utility/passwordStrength.ts +++ b/api/src/util/utility/passwordStrength.ts @@ -13,6 +13,7 @@ const blocklist: string[] = []; // TODO: update ones passwordblocklist is stored * - min numbers * - min symbols * - min uppercase chars + * - shannon entropy divided by password entropy * * Returns: 0 > pw > 1 */ @@ -22,28 +23,38 @@ export function checkPassword(password: string): number { // checks for total password len if (password.length >= minLength - 1) { - strength += 0.25; + strength += 0.05; } // checks for amount of Numbers if (password.count(reNUMBER) >= minNumbers - 1) { - strength += 0.25; + strength += 0.05; } // checks for amount of Uppercase Letters if (password.count(reUPPERCASELETTER) >= minUpperCase - 1) { - strength += 0.25; + strength += 0.05; } // checks for amount of symbols if (password.replace(reSYMBOLS, "").length >= minSymbols - 1) { - strength += 0.25; + strength += 0.05; } // checks if password only consists of numbers or only consists of chars if (password.length == password.count(reNUMBER) || password.length === password.count(reUPPERCASELETTER)) { strength = 0; } - + + let entropyMap; + for (let i = 0; i < password.length; i++) { + if (entropyMap[password[i]]) entropyMap[password[i]]++; + else entropyMap[password[i]] = 1; + } + + let entropies = Array(entropyMap); + + entropies.map(x => (x / entropyMap.length)); + strength += entropies.reduceRight((a, x), a - (x * Math.log2(x))) / Math.log2(password.length); return strength; } diff --git a/bundle/.vscode/launch.json b/bundle/.vscode/launch.json index aa4e743a..d7129ed8 100644 --- a/bundle/.vscode/launch.json +++ b/bundle/.vscode/launch.json @@ -1,18 +1,35 @@ -{ - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "sourceMaps": true, - "type": "node", - "request": "launch", - "name": "Launch Server", - "program": "${workspaceFolder}/dist/bundle/src/start.js", - "preLaunchTask": "tsc: build - tsconfig.json", - "outFiles": ["${workspaceFolder}/dist/**/*.js"], - "envFile": "${workspaceFolder}/.env" - } - ] -} +{ + "version": "0.2.0", + "configurations": [ + { + "sourceMaps": true, + "name": "ts-node", + "type": "node", + "request": "launch", + "args": [ + "${workspaceFolder}/src/start.ts" + ], + "runtimeArgs": [ + "-r", + "ts-node/register" + ], + "protocol": "inspector", + "internalConsoleOptions": "openOnSessionStart", + "env": { + "TS_NODE_PROJECT": "${workspaceFolder}/tsnode.tsconfig.json", + "TS_NODE_COMPILER": "typescript-cached-transpile" + }, + "resolveSourceMapLocations": null, /* allow breakpoints in modules other than bundle */ + }, + { + "sourceMaps": true, + "type": "node", + "request": "launch", + "name": "Launch Server", + "program": "${workspaceFolder}/dist/bundle/src/start.js", + "preLaunchTask": "tsc: build - tsconfig.json", + "outFiles": ["${workspaceFolder}/dist/**/*.js"], + "envFile": "${workspaceFolder}/.env", + } + ] +} diff --git a/bundle/package-lock.json b/bundle/package-lock.json index a9129c24..4742b4a4 100644 Binary files a/bundle/package-lock.json and b/bundle/package-lock.json differ diff --git a/bundle/package.json b/bundle/package.json index 0b3fc817..7d68427f 100644 --- a/bundle/package.json +++ b/bundle/package.json @@ -9,7 +9,8 @@ "start": "node scripts/build.js && node dist/bundle/src/start.js", "start:bundle": "node dist/bundle/src/start.js", "test": "echo \"Error: no test specified\" && exit 1", - "migrate": "cd ../util/ && npm i && node --require ts-node/register node_modules/typeorm/cli.js -f ../util/ormconfig.json migration:run" + "migrate": "cd ../util/ && npm i && node --require ts-node/register node_modules/typeorm/cli.js -f ../util/ormconfig.json migration:run", + "tsnode": "npx ts-node --transpile-only -P tsnode.tsconfig.json src/start.ts" }, "repository": { "type": "git", @@ -51,6 +52,7 @@ "ts-node": "^10.2.1", "ts-node-dev": "^1.1.6", "ts-patch": "^1.4.4", + "tsconfig-paths": "^3.12.0", "typescript": "^4.2.3", "typescript-json-schema": "0.50.1" }, @@ -91,6 +93,7 @@ "missing-native-js-functions": "^1.2.18", "morgan": "^1.10.0", "multer": "^1.4.2", + "nan": "^2.15.0", "nanocolors": "^0.2.12", "node-fetch": "^2.6.2", "node-os-utils": "^1.3.5", @@ -104,8 +107,8 @@ "tslib": "^2.3.1", "typeorm": "^0.2.37", "typescript": "^4.1.2", + "typescript-cached-transpile": "^0.0.6", "typescript-json-schema": "^0.50.1", - "ws": "^7.4.2", - "nan": "^2.15.0" + "ws": "^7.4.2" } -} +} \ No newline at end of file diff --git a/bundle/tsconfig.json b/bundle/tsconfig.json index 2257b4ab..563ff444 100644 --- a/bundle/tsconfig.json +++ b/bundle/tsconfig.json @@ -1,86 +1,85 @@ -{ - "include": ["dist/**/*.ts"], - "exclude": [], - "compilerOptions": { - /* Visit https://aka.ms/tsconfig.json to read more about this file */ - - /* Basic Options */ - "incremental": false /* Enable incremental compilation */, - "target": "ES6" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */, - "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */, - "lib": [ - "ES2021" - ] /* Specify library files to be included in the compilation. */, - "allowJs": true /* Allow javascript files to be compiled. */, - "checkJs": true /* Report errors in .js files. */, - // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ - "declaration": false /* Generates corresponding '.d.ts' file. */, - "declarationMap": false /* Generates a sourcemap for each corresponding '.d.ts' file. */, - "sourceMap": false /* Generates corresponding '.map' file. */, - // "outFile": "./", /* Concatenate and emit output to single file. */ - "outDir": "./dist/" /* Redirect output structure to the directory. */, - "rootDir": "./dist/" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */, - // "composite": true, /* Enable project compilation */ - // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ - // "removeComments": true, /* Do not emit comments to output. */ - // "noEmit": true, /* Do not emit outputs. */ - // "importHelpers": true, /* Import emit helpers from 'tslib'. */ - // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ - // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ - - /* Strict Type-Checking Options */ - "strict": true /* Enable all strict type-checking options. */, - "noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */, - "strictNullChecks": true /* Enable strict null checks. */, - // "strictFunctionTypes": true, /* Enable strict checking of function types. */ - // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ - "strictPropertyInitialization": false /* Enable strict checking of property initialization in classes. */, - // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ - "alwaysStrict": true /* Parse in strict mode and emit "use strict" for each source file. */, - - /* Additional Checks */ - // "noUnusedLocals": true, /* Report errors on unused locals. */ - // "noUnusedParameters": true, /* Report errors on unused parameters. */ - // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ - // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ - - /* Module Resolution Options */ - "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ - // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ - // "typeRoots": [], /* List of folders to include type definitions from. */ - "types": [ - "node" - ] /* Type declaration files to be included in compilation. */, - // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ - "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, - // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ - // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ - - /* Source Map Options */ - // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ - // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ - // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ - // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ - - /* Experimental Options */ - // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ - // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ - - /* Advanced Options */ - "skipLibCheck": true /* Skip type checking of declaration files. */, - "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */, - "emitDecoratorMetadata": true, - "experimentalDecorators": true, - "resolveJsonModule": true, - "baseUrl": "./dist/", - "paths": { - "@fosscord/api": ["api/src/index"], - "@fosscord/gateway": ["gateway/src/index"], - "@fosscord/cdn": ["cdn/src/index"], - "@fosscord/util": ["util/src/index"] - }, - "plugins": [{ "transform": "@zerollup/ts-transform-paths" }], - "noEmitHelpers": true, - "importHelpers": true - } -} +{ + "include": ["dist/**/*.ts"], + "exclude": [], + "compilerOptions": { + + /* Basic Options */ + "incremental": false /* Enable incremental compilation */, + "target": "ES6" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */, + "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */, + "lib": [ + "ES2021" + ] /* Specify library files to be included in the compilation. */, + "allowJs": true /* Allow javascript files to be compiled. */, + "checkJs": true /* Report errors in .js files. */, + // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ + "declaration": false /* Generates corresponding '.d.ts' file. */, + "declarationMap": false /* Generates a sourcemap for each corresponding '.d.ts' file. */, + "sourceMap": true /* Generates corresponding '.map' file. */, + // "outFile": "./", /* Concatenate and emit output to single file. */ + "outDir": "./dist/" /* Redirect output structure to the directory. */, + "rootDir": "./dist/" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */, + // "composite": true, /* Enable project compilation */ + // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ + // "removeComments": true, /* Do not emit comments to output. */ + // "noEmit": true, /* Do not emit outputs. */ + // "importHelpers": true, /* Import emit helpers from 'tslib'. */ + // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ + // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ + + /* Strict Type-Checking Options */ + "strict": true /* Enable all strict type-checking options. */, + "noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */, + "strictNullChecks": true /* Enable strict null checks. */, + // "strictFunctionTypes": true, /* Enable strict checking of function types. */ + // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ + "strictPropertyInitialization": false /* Enable strict checking of property initialization in classes. */, + // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ + "alwaysStrict": true /* Parse in strict mode and emit "use strict" for each source file. */, + + /* Additional Checks */ + // "noUnusedLocals": true, /* Report errors on unused locals. */ + // "noUnusedParameters": true, /* Report errors on unused parameters. */ + // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ + // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ + + /* Module Resolution Options */ + "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ + // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ + // "typeRoots": [], /* List of folders to include type definitions from. */ + "types": [ + "node" + ] /* Type declaration files to be included in compilation. */, + // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ + "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, + // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + + /* Source Map Options */ + // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ + // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ + + /* Experimental Options */ + // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ + // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ + + /* Advanced Options */ + "skipLibCheck": true /* Skip type checking of declaration files. */, + "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "resolveJsonModule": true, + "baseUrl": "./dist/", + "paths": { + "@fosscord/api": ["api/src/index"], + "@fosscord/gateway": ["gateway/src/index"], + "@fosscord/cdn": ["cdn/src/index"], + "@fosscord/util": ["util/src/index"] + }, + "plugins": [{ "transform": "@zerollup/ts-transform-paths" }], + "noEmitHelpers": true, + "importHelpers": true + } +} diff --git a/bundle/tsnode.tsconfig.json b/bundle/tsnode.tsconfig.json new file mode 100644 index 00000000..422d336c --- /dev/null +++ b/bundle/tsnode.tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "./tsconfig.json", + "ts-node": { + "transpileOnly": true, + "preferTsExts": true, + "require": ["tsconfig-paths/register"], + "compiler": "typescript-cached-transpile", + }, + "compilerOptions": { + "rootDir": "../", + "baseUrl": "../", + "sourceRoot": "../", + "sourceMap": true, + } +} \ No newline at end of file diff --git a/cdn/package-lock.json b/cdn/package-lock.json index de3dfc70..e6e9bb1a 100644 Binary files a/cdn/package-lock.json and b/cdn/package-lock.json differ diff --git a/cdn/package.json b/cdn/package.json index aedcc4bf..7a1f43c9 100644 --- a/cdn/package.json +++ b/cdn/package.json @@ -54,7 +54,7 @@ "missing-native-js-functions": "^1.2.17", "multer": "^1.4.2", "nanocolors": "^0.2.12", - "node-fetch": "^2.6.7", + "node-fetch": "^2.6.2", "supertest": "^6.1.6", "typescript": "^4.1.2" }, diff --git a/docker-compose.yml b/docker-compose.yml index 3c03220c..13696f6f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,47 @@ -version: "3" +version: '3.8' + services: - server: - image: fosscord/server + fosscord: + container_name: fosscord + image: fosscord + restart: on-failure:5 + # depends_on: mariadb build: . ports: - - 3001:3001 + - '3001-3005:3001-3005' + volumes: + # - ./data/:${WORK_DIR:-/srv/fosscord-server}/data/ + - data:${WORK_DIR:-/srv/fosscord-server}/ + environment: + WORK_DIR: ${WORK_DIR:-/srv/fosscord-server} + DEV_MODE: ${DEV_MODE:-0} + THREADS: ${THREADS:-1} + DATABASE: ${DATABASE:-../../data/database.db} + STORAGE_LOCATION: ${STORAGE_LOCATION:-../../data/files/} + HTTP_PORT: 3001 + WS_PORT: 3002 + CDN_PORT: 3003 + RTC_PORT: 3004 + ADMIN_PORT: 3005 + + # mariadb: + # image: mariadb:latest + # restart: on-failure:5 + # environment: + # MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-secr3tpassw0rd} + # MYSQL_DATABASE: ${MYSQL_DATABASE:-fosscord} + # MYSQL_USER: ${MYSQL_USER:-fosscord} + # MYSQL_PASSWORD: ${MYSQL_PASSWORD:-password1} + # networks: + # - default + # volumes: + # - mariadb:/var/lib/mysql + +volumes: + data: + # mariadb: + +networks: + default: + name: fosscord + driver: bridge diff --git a/gateway/package-lock.json b/gateway/package-lock.json index 9b3841af..38bdab90 100644 Binary files a/gateway/package-lock.json and b/gateway/package-lock.json differ diff --git a/gateway/src/opcodes/Identify.ts b/gateway/src/opcodes/Identify.ts index 904aa963..eb15c28f 100644 --- a/gateway/src/opcodes/Identify.ts +++ b/gateway/src/opcodes/Identify.ts @@ -240,8 +240,6 @@ export async function onIdentify(this: WebSocket, data: Payload) { x.guild_hashes = {}; // @ts-ignore x.guild_scheduled_events = []; // @ts-ignore x.threads = []; - x.premium_subscription_count = 30; - x.premium_tier = 3; return x; }), guild_experiments: [], // TODO diff --git a/util/package-lock.json b/util/package-lock.json index 82e90b36..b2fa8bbf 100644 Binary files a/util/package-lock.json and b/util/package-lock.json differ diff --git a/util/src/entities/Channel.ts b/util/src/entities/Channel.ts index 1cc4a538..4bf81901 100644 --- a/util/src/entities/Channel.ts +++ b/util/src/entities/Channel.ts @@ -14,19 +14,23 @@ import { Webhook } from "./Webhook"; import { DmChannelDTO } from "../dtos"; export enum ChannelType { - GUILD_TEXT = 0, // a text channel within a server + GUILD_TEXT = 0, // a text channel within a guild DM = 1, // a direct message between users - GUILD_VOICE = 2, // a voice channel within a server + GUILD_VOICE = 2, // a voice channel within a guild 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 + GUILD_CATEGORY = 4, // an organizational category that contains zero or more channels + GUILD_NEWS = 5, // a channel that users can follow and crosspost into a guild or route + GUILD_STORE = 6, // a channel in which game developers can sell their things ENCRYPTED = 7, // end-to-end encrypted channel ENCRYPTED_THREAD = 8, // end-to-end encrypted thread channel + TRANSACTIONAL = 9, // event chain style transactional 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 + TICKET_TRACKER = 33, // ticket tracker, individual ticket items shall have type 12 + KANBAN = 34, // confluence like kanban board + VOICELESS_WHITEBOARD = 35, // whiteboard but without voice (whiteboard + voice is the same as stage) CUSTOM_START = 64, // start custom channel types from here UNHANDLED = 255 // unhandled unowned pass-through channel type } @@ -72,7 +76,7 @@ export class Channel extends BaseClass { @ManyToOne(() => Channel) parent?: Channel; - // only for group dms + // for group DMs and owned custom channel types @Column({ nullable: true }) @RelationId((channel: Channel) => channel.owner) owner_id: string; @@ -117,6 +121,9 @@ export class Channel extends BaseClass { }) invites?: Invite[]; + @Column({ nullable: true }) + retention_policy_id?: string; + @OneToMany(() => Message, (message: Message) => message.channel, { cascade: true, orphanedRowAction: "delete", @@ -140,7 +147,7 @@ export class Channel extends BaseClass { orphanedRowAction: "delete", }) webhooks?: Webhook[]; - + // TODO: DM channel static async createChannel( channel: Partial, @@ -182,6 +189,7 @@ export class Channel extends BaseClass { switch (channel.type) { case ChannelType.GUILD_TEXT: + case ChannelType.GUILD_NEWS: case ChannelType.GUILD_VOICE: if (channel.parent_id && !opts?.skipExistsCheck) { const exists = await Channel.findOneOrFail({ id: channel.parent_id }); @@ -191,25 +199,24 @@ export class Channel extends BaseClass { } break; case ChannelType.GUILD_CATEGORY: + case ChannelType.UNHANDLED: 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 + // TODO: eagerly auto generate position of all guild channels channel = { ...channel, ...(!opts?.keepId && { id: Snowflake.generate() }), created_at: new Date(), - position: channel.position || 0, + position: (channel.type === ChannelType.UNHANDLED ? 0 : channel.position) || 0, }; await Promise.all([ @@ -231,11 +238,13 @@ export class Channel extends BaseClass { const otherRecipientsUsers = await User.find({ where: recipients.map((x) => ({ id: x })) }); // TODO: check config for max number of recipients + /** if you want to disallow note to self channels, uncomment the conditional below if (otherRecipientsUsers.length !== recipients.length) { throw new HTTPError("Recipient/s not found"); } + **/ - const type = recipients.length === 1 ? ChannelType.DM : ChannelType.GROUP_DM; + const type = recipients.length > 1 ? ChannelType.DM : ChannelType.GROUP_DM; let channel = null; @@ -288,7 +297,8 @@ export class Channel extends BaseClass { await emitEvent({ event: "CHANNEL_CREATE", data: channel_dto, user_id: creator_user_id }); } - return channel_dto.excludedRecipients([creator_user_id]); + if (recipients.length === 1) return channel_dto; + else return channel_dto.excludedRecipients([creator_user_id]); } static async removeRecipientFromChannel(channel: Channel, user_id: string) { @@ -354,4 +364,5 @@ export interface ChannelPermissionOverwrite { export enum ChannelPermissionOverwriteType { role = 0, member = 1, + group = 2, } diff --git a/util/src/entities/ClientRelase.ts b/util/src/entities/ClientRelease.ts similarity index 81% rename from util/src/entities/ClientRelase.ts rename to util/src/entities/ClientRelease.ts index e021b82b..c5afd307 100644 --- a/util/src/entities/ClientRelase.ts +++ b/util/src/entities/ClientRelease.ts @@ -1,8 +1,8 @@ import { Column, Entity} from "typeorm"; import { BaseClass } from "./BaseClass"; -@Entity("client_relase") -export class Relase extends BaseClass { +@Entity("client_release") +export class Release extends BaseClass { @Column() name: string; diff --git a/util/src/entities/Config.ts b/util/src/entities/Config.ts index f4a266dc..8d29b387 100644 --- a/util/src/entities/Config.ts +++ b/util/src/entities/Config.ts @@ -188,8 +188,8 @@ export interface ConfigValue { }, client: { useTestClient: Boolean; - relases: { - useLocalRelases: Boolean; //TODO + releases: { + useLocalRelease: Boolean; //TODO upstreamVersion: string; } }, @@ -222,7 +222,7 @@ export const DefaultConfigOptions: ConfigValue = { }, general: { instanceName: "Fosscord Instance", - instanceDescription: "This is a Fosscord instance made in pre-relase days", + instanceDescription: "This is a Fosscord instance made in pre-release days", frontPage: null, tosPage: null, correspondenceEmail: "noreply@localhost.local", @@ -389,8 +389,8 @@ export const DefaultConfigOptions: ConfigValue = { }, client: { useTestClient: true, - relases: { - useLocalRelases: true, + releases: { + useLocalRelease: true, upstreamVersion: "0.0.264" } }, diff --git a/util/src/entities/ConnectedAccount.ts b/util/src/entities/ConnectedAccount.ts index b8aa2889..09ae30ab 100644 --- a/util/src/entities/ConnectedAccount.ts +++ b/util/src/entities/ConnectedAccount.ts @@ -2,7 +2,7 @@ import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm"; import { BaseClass } from "./BaseClass"; import { User } from "./User"; -export interface PublicConnectedAccount extends Pick {} +export interface PublicConnectedAccount extends Pick {} @Entity("connected_accounts") export class ConnectedAccount extends BaseClass { @@ -35,7 +35,7 @@ export class ConnectedAccount extends BaseClass { type: string; @Column() - verifie: boolean; + verified: boolean; @Column({ select: false }) visibility: number; diff --git a/util/src/entities/Encryption.ts b/util/src/entities/Encryption.ts new file mode 100644 index 00000000..3b82ff84 --- /dev/null +++ b/util/src/entities/Encryption.ts @@ -0,0 +1,35 @@ +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 { BitField, BitFieldResolvable, BitFlag } from "../util/BitField"; +import { Recipient } from "./Recipient"; +import { Message } from "./Message"; +import { ReadState } from "./ReadState"; +import { Invite } from "./Invite"; +import { DmChannelDTO } from "../dtos"; + +@Entity("security_settings") +export class SecuritySettings extends BaseClass { + + @Column({nullable: true}) + guild_id: Snowflake; + + @Column({nullable: true}) + channel_id: Snowflake; + + @Column() + encryption_permission_mask: BitField; + + @Column() + allowed_algorithms: string[]; + + @Column() + current_algorithm: string; + + @Column({nullable: true}) + used_since_message: Snowflake; + +} diff --git a/util/src/entities/Guild.ts b/util/src/entities/Guild.ts index 9ac148ee..70bb41c5 100644 --- a/util/src/entities/Guild.ts +++ b/util/src/entities/Guild.ts @@ -187,11 +187,11 @@ export class Guild extends BaseClass { @Column({ nullable: true }) @RelationId((guild: Guild) => guild.owner) - owner_id: string; + owner_id?: string; // optional to allow for ownerless guilds @JoinColumn({ name: "owner_id", referencedColumnName: "id" }) @ManyToOne(() => User) - owner: User; + owner?: User; // optional to allow for ownerless guilds @Column({ nullable: true }) preferred_locale?: string; @@ -200,7 +200,7 @@ export class Guild extends BaseClass { premium_subscription_count?: number; @Column({ nullable: true }) - premium_tier?: number; // nitro boost level + premium_tier?: number; // crowd premium level @Column({ nullable: true }) @RelationId((guild: Guild) => guild.public_updates_channel) @@ -269,6 +269,10 @@ export class Guild extends BaseClass { @Column({ nullable: true }) nsfw?: boolean; + + // TODO: nested guilds + @Column({ nullable: true }) + parent?: string; // only for developer portal permissions?: number; @@ -308,7 +312,7 @@ export class Guild extends BaseClass { verification_level: 0, welcome_screen: { enabled: false, - description: "No description", + description: "Fill in your description", welcome_channels: [], }, widget_enabled: true, // NB: don't set it as false to prevent artificial restrictions diff --git a/util/src/entities/Member.ts b/util/src/entities/Member.ts index 3c5f9db0..fe2d5590 100644 --- a/util/src/entities/Member.ts +++ b/util/src/entities/Member.ts @@ -70,7 +70,7 @@ export class Member extends BaseClassWithoutId { @Column({ nullable: true }) nick?: string; - + @JoinTable({ name: "member_roles", joinColumn: { name: "index", referencedColumnName: "index" }, @@ -85,8 +85,8 @@ export class Member extends BaseClassWithoutId { @Column() joined_at: Date; - @Column({ nullable: true }) - premium_since?: Date; + @Column({ type: "bigint", nullable: true }) + premium_since?: number; @Column() deaf: boolean; @@ -102,8 +102,17 @@ export class Member extends BaseClassWithoutId { @Column({ nullable: true }) last_message_id?: string; + + /** + @JoinColumn({ name: "id" }) + @ManyToOne(() => User, { + onDelete: "DO NOTHING", + // do not auto-kick force-joined members just because their joiners left the server + }) **/ + @Column({ nullable: true}) + joined_by?: string; - // TODO: update + // TODO: add this when we have proper read receipts // @Column({ type: "simple-json" }) // read_state: ReadState; @@ -245,7 +254,7 @@ export class Member extends BaseClassWithoutId { nick: undefined, roles: [guild_id], // @everyone role joined_at: new Date(), - premium_since: new Date(), + premium_since: (new Date()).getTime(), deaf: false, mute: false, pending: false, diff --git a/util/src/entities/Message.ts b/util/src/entities/Message.ts index e577d5df..b32bbd94 100644 --- a/util/src/entities/Message.ts +++ b/util/src/entities/Message.ts @@ -41,8 +41,14 @@ export enum MessageType { CHANNEL_FOLLOW_ADD = 12, GUILD_DISCOVERY_DISQUALIFIED = 14, GUILD_DISCOVERY_REQUALIFIED = 15, + ENCRYPTED = 16, REPLY = 19, APPLICATION_COMMAND = 20, + ROUTE_ADDED = 41, // custom message routing: new route affecting that channel + ROUTE_DISABLED = 42, // custom message routing: given route no longer affecting that channel + ENCRYPTION = 50, + CUSTOM_START = 63, + UNHANDLED = 255 } @Entity("messages") @@ -84,7 +90,7 @@ export class Message extends BaseClass { @RelationId((message: Message) => message.member) member_id: string; - @JoinColumn({ name: "author_id", referencedColumnName: "id" }) + @JoinColumn({ name: "member_id", referencedColumnName: "id" }) @ManyToOne(() => User, { onDelete: "CASCADE", }) @@ -203,6 +209,7 @@ export interface MessageComponent { } export enum MessageComponentType { + Script = 0, // self command script ActionRow = 1, Button = 2, } diff --git a/util/src/entities/ReadState.ts b/util/src/entities/ReadState.ts index e6d73105..b915573b 100644 --- a/util/src/entities/ReadState.ts +++ b/util/src/entities/ReadState.ts @@ -49,6 +49,7 @@ export class ReadState extends BaseClass { @Column({ nullable: true }) mention_count: number; - @Column({ nullable: true }) + // @Column({ nullable: true }) + // TODO: derive this from (last_message_id=notifications_cursor=public_ack)=true manual: boolean; } diff --git a/util/src/entities/User.ts b/util/src/entities/User.ts index 1d18c838..a5c4c136 100644 --- a/util/src/entities/User.ts +++ b/util/src/entities/User.ts @@ -60,7 +60,7 @@ export class User extends BaseClass { username: string; // username max length 32, min 2 (should be configurable) @Column() - discriminator: string; // #0001 4 digit long string from #0001 - #9999 + discriminator: string; // opaque string: 4 digits on discord.com setDiscriminator(val: string) { const number = Number(val); @@ -88,10 +88,10 @@ export class User extends BaseClass { mobile: boolean; // if the user has mobile app installed @Column() - premium: boolean; // if user bought nitro - + premium: boolean; // if user bought individual premium + @Column() - premium_type: number; // nitro level + premium_type: number; // individual premium level @Column() bot: boolean; // if user is bot @@ -100,11 +100,11 @@ export class User extends BaseClass { bio: string; // short description of the user (max 190 chars -> should be configurable) @Column() - system: boolean; // shouldn't be used, the api sents this field type true, if the generated message comes from a system generated author + system: boolean; // shouldn't be used, the api sends this field type true, if the generated message comes from a system generated author @Column({ select: false }) - nsfw_allowed: boolean; // if the user is older than 18 (resp. Config) - + nsfw_allowed: boolean; // if the user can do age-restricted actions (NSFW channels/guilds/commands) + @Column({ select: false }) mfa_enabled: boolean; // if multi factor authentication is enabled @@ -132,7 +132,7 @@ export class User extends BaseClass { @Column() public_flags: number; - @Column() + @Column({ type: "bigint" }) rights: string; // Rights @OneToMany(() => Session, (session: Session) => session.user) @@ -164,6 +164,9 @@ export class User extends BaseClass { @Column({ type: "simple-json", select: false }) settings: UserSettings; + @Column({ type: "simple-json" }) + notes: { [key: string]: string }; //key is ID of user + toPublicUser() { const user: any = {}; PublicUserProjection.forEach((x) => { @@ -271,6 +274,7 @@ export class User extends BaseClass { }, settings: { ...defaultSettings, locale: language }, fingerprints: [], + notes: {}, }); await user.save(); @@ -360,7 +364,7 @@ export interface UserSettings { render_reactions: boolean; restricted_guilds: string[]; show_current_game: boolean; - status: "online" | "offline" | "dnd" | "idle"; + status: "online" | "offline" | "dnd" | "idle" | "invisible"; stream_notifications_enabled: boolean; theme: "dark" | "white"; // dark timezone_offset: number; // e.g -60 diff --git a/util/src/entities/index.ts b/util/src/entities/index.ts index fc18d422..f023d5a6 100644 --- a/util/src/entities/index.ts +++ b/util/src/entities/index.ts @@ -27,4 +27,4 @@ export * from "./Template"; export * from "./User"; export * from "./VoiceState"; export * from "./Webhook"; -export * from "./ClientRelase"; \ No newline at end of file +export * from "./ClientRelease"; \ No newline at end of file diff --git a/util/src/interfaces/Event.ts b/util/src/interfaces/Event.ts index a5253c09..416082ed 100644 --- a/util/src/interfaces/Event.ts +++ b/util/src/interfaces/Event.ts @@ -623,6 +623,7 @@ export type EVENT = | "PRESENCE_UPDATE" | "TYPING_START" | "USER_UPDATE" + | "USER_NOTE_UPDATE" | "WEBHOOKS_UPDATE" | "INTERACTION_CREATE" | "VOICE_STATE_UPDATE" diff --git a/util/src/interfaces/Interaction.ts b/util/src/interfaces/Interaction.ts index 3cafb2d5..5d3aae24 100644 --- a/util/src/interfaces/Interaction.ts +++ b/util/src/interfaces/Interaction.ts @@ -12,11 +12,13 @@ export interface Interaction { } export enum InteractionType { + SelfCommand = 0, Ping = 1, ApplicationCommand = 2, } export enum InteractionResponseType { + SelfCommandResponse = 0, Pong = 1, Acknowledge = 2, ChannelMessage = 3, diff --git a/util/src/interfaces/Status.ts b/util/src/interfaces/Status.ts index c4dab586..5d2e1bba 100644 --- a/util/src/interfaces/Status.ts +++ b/util/src/interfaces/Status.ts @@ -1,4 +1,4 @@ -export type Status = "idle" | "dnd" | "online" | "offline"; +export type Status = "idle" | "dnd" | "online" | "offline" | "invisible"; export interface ClientStatus { desktop?: string; // e.g. Windows/Linux/Mac diff --git a/util/src/migrations/1648643945733-ReleaseTypo.ts b/util/src/migrations/1648643945733-ReleaseTypo.ts new file mode 100644 index 00000000..944b9dd9 --- /dev/null +++ b/util/src/migrations/1648643945733-ReleaseTypo.ts @@ -0,0 +1,16 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class ReleaseTypo1648643945733 implements MigrationInterface { + name = "ReleaseTypo1648643945733"; + + public async up(queryRunner: QueryRunner): Promise { + //drop table first because typeorm creates it before migrations run + await queryRunner.dropTable("client_release", true); + await queryRunner.renameTable("client_relase", "client_release"); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.dropTable("client_relase", true); + await queryRunner.renameTable("client_release", "client_relase"); + } +} diff --git a/util/src/util/Rights.ts b/util/src/util/Rights.ts index 9a99d393..35ad9514 100644 --- a/util/src/util/Rights.ts +++ b/util/src/util/Rights.ts @@ -1,6 +1,7 @@ import { BitField } from "./BitField"; import "missing-native-js-functions"; import { BitFieldResolvable, BitFlag } from "./BitField"; +import { User } from "../entities"; var HTTPError: any; @@ -65,6 +66,8 @@ export class Rights extends BitField { // inverts the presence confidentiality default (OPERATOR's presence is not routed by default, others' are) for a given user SELF_ADD_DISCOVERABLE: BitFlag(36), // can mark discoverable guilds that they have permissions to mark as discoverable MANAGE_GUILD_DIRECTORY: BitFlag(37), // can change anything in the primary guild directory + POGGERS: BitFlag(38), // can send confetti, screenshake, random user mention (@someone) + USE_ACHIEVEMENTS: BitFlag(39), // can use achievements and cheers INITIATE_INTERACTIONS: BitFlag(40), // can initiate interactions RESPOND_TO_INTERACTIONS: BitFlag(41), // can respond to interactions SEND_BACKDATED_EVENTS: BitFlag(42), // can send backdated events @@ -83,6 +86,15 @@ export class Rights extends BitField { // @ts-ignore throw new HTTPError(`You are missing the following rights ${permission}`, 403); } + } const ALL_RIGHTS = Object.values(Rights.FLAGS).reduce((total, val) => total | val, BigInt(0)); + +export async function getRights( user_id: string + /**, opts: { + in_behalf?: (keyof User)[]; + } = {} **/) { + let user = await User.findOneOrFail({ where: { id: user_id } }); + return new Rights(user.rights); +} diff --git a/util/src/util/TraverseDirectory.ts b/util/src/util/TraverseDirectory.ts index 275b7dcc..3d0d6279 100644 --- a/util/src/util/TraverseDirectory.ts +++ b/util/src/util/TraverseDirectory.ts @@ -1,6 +1,9 @@ import { Server, traverseDirectory } from "lambert-server"; -const DEFAULT_FILTER = /^([^\.].*)(?