summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/api/routes/applications/#id/bot/index.ts3
-rw-r--r--src/api/routes/auth/mfa/totp.ts3
-rw-r--r--src/api/routes/auth/verify/view-backup-codes-challenge.ts30
-rw-r--r--src/api/routes/guilds/#guild_id/member-verification.ts14
-rw-r--r--src/api/routes/guilds/#guild_id/members/#member_id/index.ts7
-rw-r--r--src/api/routes/guilds/#guild_id/profile/index.ts30
-rw-r--r--src/api/routes/guilds/#guild_id/welcome_screen.ts2
-rw-r--r--src/api/routes/policies/stats.ts22
-rw-r--r--src/api/routes/users/#id/profile.ts63
-rw-r--r--src/api/routes/users/@me/mfa/codes-verification.ts41
-rw-r--r--src/api/routes/users/@me/mfa/totp/disable.ts3
-rw-r--r--src/api/routes/users/@me/mfa/totp/enable.ts3
-rw-r--r--src/cdn/Server.ts7
-rw-r--r--src/cdn/routes/guild-profiles.ts84
-rw-r--r--src/util/config/types/SecurityConfiguration.ts1
-rw-r--r--src/util/entities/Guild.ts2
-rw-r--r--src/util/entities/Member.ts23
-rw-r--r--src/util/interfaces/Event.ts1
-rw-r--r--src/util/migrations/mariadb/1661273147273-test.ts71
-rw-r--r--src/util/migrations/mariadb/1661273179287-test2.ts15
-rw-r--r--src/util/migrations/mariadb/1661885910534-guild-member-profiles.ts40
-rw-r--r--src/util/migrations/postgres/1661885830688-guild-member-profiles.ts40
-rw-r--r--src/util/migrations/sqlite/1661885742207-guild-member-profiles.ts136
-rw-r--r--src/util/schemas/BackupCodesChallengeSchema.ts3
-rw-r--r--src/util/schemas/CodesVerificationSchema.ts5
-rw-r--r--src/util/schemas/MemberChangeProfileSchema.ts5
-rw-r--r--src/util/schemas/MemberChangeSchema.ts2
-rw-r--r--src/util/schemas/UserProfileModifySchema.ts5
-rw-r--r--src/util/schemas/index.ts6
-rw-r--r--src/util/util/Database.ts2
-rw-r--r--src/util/util/Rights.ts6
31 files changed, 609 insertions, 66 deletions
diff --git a/src/api/routes/applications/#id/bot/index.ts b/src/api/routes/applications/#id/bot/index.ts

index e663059e..2ac3523b 100644 --- a/src/api/routes/applications/#id/bot/index.ts +++ b/src/api/routes/applications/#id/bot/index.ts
@@ -1,7 +1,6 @@ import { route } from "@fosscord/api"; -import { Application, Config, FieldErrors, generateToken, handleFile, OrmUtils, trimSpecial, User } from "@fosscord/util"; +import { Application, Config, FieldErrors, generateToken, handleFile, OrmUtils, trimSpecial, User, HTTPError } from "@fosscord/util"; import { Request, Response, Router } from "express"; -import { HTTPError } from "lambert-server"; import { verifyToken } from "node-2fa"; const router: Router = Router(); diff --git a/src/api/routes/auth/mfa/totp.ts b/src/api/routes/auth/mfa/totp.ts
index 9938569e..4b080af6 100644 --- a/src/api/routes/auth/mfa/totp.ts +++ b/src/api/routes/auth/mfa/totp.ts
@@ -1,7 +1,6 @@ import { route } from "@fosscord/api"; -import { BackupCode, generateToken, TotpSchema, User } from "@fosscord/util"; +import { BackupCode, generateToken, TotpSchema, User, HTTPError } from "@fosscord/util"; import { Request, Response, Router } from "express"; -import { HTTPError } from "lambert-server"; import { verifyToken } from "node-2fa"; const router = Router(); diff --git a/src/api/routes/auth/verify/view-backup-codes-challenge.ts b/src/api/routes/auth/verify/view-backup-codes-challenge.ts new file mode 100644
index 00000000..d524e0f7 --- /dev/null +++ b/src/api/routes/auth/verify/view-backup-codes-challenge.ts
@@ -0,0 +1,30 @@ +import { Router, Request, Response } from "express"; +import { route } from "@fosscord/api"; +import { FieldErrors, User, BackupCodesChallengeSchema } from "@fosscord/util"; + +let bcrypt: any; +try { + bcrypt = require("bcrypt"); +} catch { + bcrypt = require("bcryptjs"); + console.log("Warning: using bcryptjs because bcrypt is not installed! Performance will be affected."); +} + +const router = Router(); + +router.post("/", route({ body: "BackupCodesChallengeSchema" }), async (req: Request, res: Response) => { + const { password } = req.body as BackupCodesChallengeSchema; + + const user = await User.findOneOrFail({ where: { id: req.user_id }, select: ["data"] }); + + if (!await bcrypt.compare(password, user.data.hash || "")) { + throw FieldErrors({ password: { message: req.t("auth:login.INVALID_PASSWORD"), code: "INVALID_PASSWORD" } }); + } + + return res.json({ + nonce: "NoncePlaceholder", + regenerate_nonce: "RegenNoncePlaceholder", + }); +}); + +export default router; diff --git a/src/api/routes/guilds/#guild_id/member-verification.ts b/src/api/routes/guilds/#guild_id/member-verification.ts new file mode 100644
index 00000000..265a1b35 --- /dev/null +++ b/src/api/routes/guilds/#guild_id/member-verification.ts
@@ -0,0 +1,14 @@ +import { Router, Request, Response } from "express"; +import { route } from "@fosscord/api"; +const router = Router(); + +router.get("/",route({}), async (req: Request, res: Response) => { + // TODO: member verification + + res.status(404).json({ + message: "Unknown Guild Member Verification Form", + code: 10068 + }); +}); + +export default router; diff --git a/src/api/routes/guilds/#guild_id/members/#member_id/index.ts b/src/api/routes/guilds/#guild_id/members/#member_id/index.ts
index 57152f9a..06474f3e 100644 --- a/src/api/routes/guilds/#guild_id/members/#member_id/index.ts +++ b/src/api/routes/guilds/#guild_id/members/#member_id/index.ts
@@ -6,6 +6,7 @@ import { getRights, Guild, GuildMemberUpdateEvent, + handleFile, Member, MemberChangeSchema, OrmUtils, @@ -30,7 +31,7 @@ router.patch("/", route({ body: "MemberChangeSchema" }), async (req: Request, re if (member_id === "@me") member_id = req.user_id; const body = req.body as MemberChangeSchema; - const member = await Member.findOneOrFail({ where: { id: member_id, guild_id }, relations: ["roles", "user"] }); + let 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({ where: { guild_id: guild_id, name: "@everyone", position: 0 } }); @@ -41,6 +42,10 @@ router.patch("/", route({ body: "MemberChangeSchema" }), async (req: Request, re member.roles = body.roles.map((x) => OrmUtils.mergeDeep(new Role(), { id: x })); // foreign key constraint will fail if role doesn't exist } + if (body.avatar) body.avatar = await handleFile(`/guilds/${guild_id}/users/${member_id}/avatars`, body.avatar as string); + + member = await OrmUtils.mergeDeep(member, body); + await member.save(); member.roles = member.roles.filter((x) => x.id !== everyone.id); diff --git a/src/api/routes/guilds/#guild_id/profile/index.ts b/src/api/routes/guilds/#guild_id/profile/index.ts new file mode 100644
index 00000000..ddc30943 --- /dev/null +++ b/src/api/routes/guilds/#guild_id/profile/index.ts
@@ -0,0 +1,30 @@ +import { route } from "@fosscord/api"; +import { emitEvent, GuildMemberUpdateEvent, handleFile, Member, MemberChangeProfileSchema, OrmUtils } from "@fosscord/util"; +import { Request, Response, Router } from "express"; + +const router = Router(); + +router.patch("/:member_id", route({ body: "MemberChangeProfileSchema" }), async (req: Request, res: Response) => { + let { guild_id, member_id } = req.params; + if (member_id === "@me") member_id = req.user_id; + const body = req.body as MemberChangeProfileSchema; + + let member = await Member.findOneOrFail({ where: { id: req.user_id, guild_id }, relations: ["roles", "user"] }); + + if (body.banner) body.banner = await handleFile(`/guilds/${guild_id}/users/${req.user_id}/avatars`, body.banner as string); + + member = await OrmUtils.mergeDeep(member, body); + + await member.save(); + + // 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", + guild_id, + data: { ...member, roles: member.roles.map((x) => x.id) } + } as GuildMemberUpdateEvent); + + res.json(member); +}); + +export default router; diff --git a/src/api/routes/guilds/#guild_id/welcome_screen.ts b/src/api/routes/guilds/#guild_id/welcome_screen.ts
index 85c22a19..7e955c56 100644 --- a/src/api/routes/guilds/#guild_id/welcome_screen.ts +++ b/src/api/routes/guilds/#guild_id/welcome_screen.ts
@@ -24,6 +24,8 @@ router.patch("/", route({ body: "GuildUpdateWelcomeScreenSchema", permission: "M if (body.description) guild.welcome_screen.description = body.description; if (body.enabled != null) guild.welcome_screen.enabled = body.enabled; + await guild.save(); + res.sendStatus(204); }); diff --git a/src/api/routes/policies/stats.ts b/src/api/routes/policies/stats.ts new file mode 100644
index 00000000..d3aad2ec --- /dev/null +++ b/src/api/routes/policies/stats.ts
@@ -0,0 +1,22 @@ +import { route } from "@fosscord/api"; +import { Config, getRights, Guild, Member, Message, User } from "@fosscord/util"; +import { Request, Response, Router } from "express"; +const router = Router(); + +router.get("/", route({}), async (req: Request, res: Response) => { + let users, guilds, msgs, memberships; + // needs to be let otherwise we can't for + + let config = Config.get(); + if (!config.security.statsWorldReadable) { + let rights = await getRights(req.user_id); + rights.hasThrow("VIEW_SERVER_STATS"); + } + users = await User.count(); + guilds = await Guild.count(); + msgs = await Message.count(); + memberships = await Member.count(); + res.json({ user_count: users, guild_count: guilds, msg_count: msgs, membership_rels: memberships }); +}); + +export default router; diff --git a/src/api/routes/users/#id/profile.ts b/src/api/routes/users/#id/profile.ts
index 766c9880..541bb66a 100644 --- a/src/api/routes/users/#id/profile.ts +++ b/src/api/routes/users/#id/profile.ts
@@ -1,5 +1,16 @@ import { route } from "@fosscord/api"; -import { Member, PublicConnectedAccount, User, UserPublic } from "@fosscord/util"; +import { + emitEvent, + handleFile, + Member, + OrmUtils, + PrivateUserProjection, + PublicConnectedAccount, + User, + UserProfileModifySchema, + UserPublic, + UserUpdateEvent +} from "@fosscord/util"; import { Request, Response, Router } from "express"; const router: Router = Router(); @@ -62,12 +73,18 @@ router.get("/", route({ test: { response: { body: "UserProfileResponse" } } }), bot: user.bot }; + const userProfile = { + bio: req.user_bot ? null : user.bio, + accent_color: user.accent_color, + banner: user.banner + }; + const guildMemberDto = guild_member ? { - avatar: user.avatar, // TODO - banner: user.banner, // TODO - bio: req.user_bot ? null : user.bio, // TODO - communication_disabled_until: null, // TODO + avatar: guild_member.avatar, + banner: guild_member.banner, + bio: req.user_bot ? null : guild_member.bio, + communication_disabled_until: guild_member.communication_disabled_until, deaf: guild_member.deaf, flags: user.flags, is_pending: guild_member.pending, @@ -81,13 +98,47 @@ router.get("/", route({ test: { response: { body: "UserProfileResponse" } } }), } : undefined; + const guildMemberProfile = { + accent_color: null, + banner: guild_member?.banner || null, + bio: guild_member?.bio || "", + guild_id + }; res.json({ connected_accounts: user.connected_accounts, premium_guild_since: premium_guild_since, // TODO premium_since: user.premium_since, // TODO mutual_guilds: mutual_guilds, // TODO {id: "", nick: null} when ?with_mutual_guilds=true user: userDto, - guild_member: guildMemberDto + user_profile: userProfile, + guild_member: guild_id && guildMemberDto, + guild_member_profile: guild_id && guildMemberProfile + }); +}); + +router.patch("/", route({ body: "UserProfileModifySchema" }), async (req: Request, res: Response) => { + const body = req.body as UserProfileModifySchema; + + if (body.banner) body.banner = await handleFile(`/banners/${req.user_id}`, body.banner as string); + let user = await User.findOneOrFail({ where: { id: req.user_id }, select: [...PrivateUserProjection, "data"] }); + + user = OrmUtils.mergeDeep(user, body); + await user.save(); + + // @ts-ignore + delete user.data; + + // TODO: send update member list event in gateway + await emitEvent({ + event: "USER_UPDATE", + user_id: req.user_id, + data: user + } as UserUpdateEvent); + + res.json({ + accent_color: user.accent_color, + bio: user.bio, + banner: user.banner }); }); diff --git a/src/api/routes/users/@me/mfa/codes-verification.ts b/src/api/routes/users/@me/mfa/codes-verification.ts new file mode 100644
index 00000000..071c71fa --- /dev/null +++ b/src/api/routes/users/@me/mfa/codes-verification.ts
@@ -0,0 +1,41 @@ +import { Router, Request, Response } from "express"; +import { route } from "@fosscord/api"; +import { BackupCode, generateMfaBackupCodes, User, CodesVerificationSchema } from "@fosscord/util"; + +const router = Router(); + +router.post("/", route({ body: "CodesVerificationSchema" }), async (req: Request, res: Response) => { + const { key, nonce, regenerate } = req.body as CodesVerificationSchema; + + // TODO: We don't have email/etc etc, so can't send a verification code. + // Once that's done, this route can verify `key` + + const user = await User.findOneOrFail({ where: { id: req.user_id } }); + + var codes: BackupCode[]; + if (regenerate) { + await BackupCode.update( + { user: { id: req.user_id } }, + { expired: true } + ); + + codes = generateMfaBackupCodes(req.user_id); + await Promise.all(codes.map(x => x.save())); + } + else { + codes = await BackupCode.find({ + where: { + user: { + id: req.user_id, + }, + expired: false, + } + }); + } + + return res.json({ + backup_codes: codes.map(x => ({ ...x, expired: undefined })), + }); +}); + +export default router; diff --git a/src/api/routes/users/@me/mfa/totp/disable.ts b/src/api/routes/users/@me/mfa/totp/disable.ts
index 6bc9a5c7..07fdbb05 100644 --- a/src/api/routes/users/@me/mfa/totp/disable.ts +++ b/src/api/routes/users/@me/mfa/totp/disable.ts
@@ -1,7 +1,6 @@ import { route } from "@fosscord/api"; -import { BackupCode, generateToken, TotpDisableSchema, User } from "@fosscord/util"; +import { BackupCode, generateToken, TotpDisableSchema, User, HTTPError } from "@fosscord/util"; import { Request, Response, Router } from "express"; -import { HTTPError } from "lambert-server"; import { verifyToken } from "node-2fa"; const router = Router(); diff --git a/src/api/routes/users/@me/mfa/totp/enable.ts b/src/api/routes/users/@me/mfa/totp/enable.ts
index f3a73c28..adf51d6e 100644 --- a/src/api/routes/users/@me/mfa/totp/enable.ts +++ b/src/api/routes/users/@me/mfa/totp/enable.ts
@@ -1,7 +1,6 @@ import { route } from "@fosscord/api"; -import { BackupCode, Config, generateMfaBackupCodes, generateToken, TotpEnableSchema, User } from "@fosscord/util"; +import { BackupCode, Config, generateMfaBackupCodes, generateToken, TotpEnableSchema, User, HTTPError } from "@fosscord/util"; import { Request, Response, Router } from "express"; -import { HTTPError } from "lambert-server"; import { verifyToken } from "node-2fa"; let bcrypt: any; diff --git a/src/cdn/Server.ts b/src/cdn/Server.ts
index ec5edc68..9cedaa02 100644 --- a/src/cdn/Server.ts +++ b/src/cdn/Server.ts
@@ -3,6 +3,7 @@ import bodyParser from "body-parser"; import { Server, ServerOptions } from "lambert-server"; import path from "path"; import avatarsRoute from "./routes/avatars"; +import guildProfilesRoute from "./routes/guild-profiles"; import iconsRoute from "./routes/role-icons"; export interface CDNServerOptions extends ServerOptions {} @@ -65,6 +66,12 @@ export class CDNServer extends Server { this.app.use("/channel-icons/", avatarsRoute); this.log("verbose", "[Server] Route /channel-icons registered"); + this.app.use("/guilds/:guild_id/users/:user_id/avatars", guildProfilesRoute); + this.log("verbose", "[Server] Route /guilds/avatars registered"); + + this.app.use("/guilds/:guild_id/users/:user_id/banners", guildProfilesRoute); + this.log("verbose", "[Server] Route /guilds/banners registered"); + return super.start(); } diff --git a/src/cdn/routes/guild-profiles.ts b/src/cdn/routes/guild-profiles.ts new file mode 100644
index 00000000..32c05ad9 --- /dev/null +++ b/src/cdn/routes/guild-profiles.ts
@@ -0,0 +1,84 @@ +import { Config, HTTPError, Snowflake } from "@fosscord/util"; +import crypto from "crypto"; +import { Request, Response, Router } from "express"; +import FileType from "file-type"; +import { multer } from "../util/multer"; +import { storage } from "../util/Storage"; + +// TODO: check premium and animated pfp are allowed in the config +// TODO: generate different sizes of icon +// TODO: generate different image types of icon +// TODO: delete old icons + +const ANIMATED_MIME_TYPES = ["image/apng", "image/gif", "image/gifv"]; +const STATIC_MIME_TYPES = ["image/png", "image/jpeg", "image/webp", "image/svg+xml", "image/svg"]; +const ALLOWED_MIME_TYPES = [...ANIMATED_MIME_TYPES, ...STATIC_MIME_TYPES]; + +const router = Router(); + +router.post("/", 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 { guild_id, user_id } = req.params; + + let 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"); + if (ANIMATED_MIME_TYPES.includes(type.mime)) hash = `a_${hash}`; // animated icons have a_ infront of the hash + + const path = `guilds/${guild_id}/users/${user_id}/avatars/${hash}`; + 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}/${user_id}/${hash}` + }); +}); + +router.get("/", async (req: Request, res: Response) => { + let { guild_id, user_id } = req.params; + user_id = user_id.split(".")[0]; // remove .file extension + const path = `guilds/${guild_id}/users/${user_id}/avatars`; + + 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"); + + return res.send(file); +}); + +router.get("/:hash", async (req: Request, res: Response) => { + let { guild_id, user_id, hash } = req.params; + hash = hash.split(".")[0]; // remove .file extension + const path = `guilds/${guild_id}/users/${user_id}/avatars/${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"); + + return res.send(file); +}); + +router.delete("/:id", async (req: Request, res: Response) => { + if (req.headers.signature !== Config.get().security.requestSignature) throw new HTTPError("Invalid request signature"); + const { guild_id, user_id, id } = req.params; + const path = `guilds/${guild_id}/users/${user_id}/avatars/${id}`; + + await storage.delete(path); + + return res.send({ success: true }); +}); + +export default router; diff --git a/src/util/config/types/SecurityConfiguration.ts b/src/util/config/types/SecurityConfiguration.ts
index a2cebbd3..5a3d5aa6 100644 --- a/src/util/config/types/SecurityConfiguration.ts +++ b/src/util/config/types/SecurityConfiguration.ts
@@ -16,4 +16,5 @@ export class SecurityConfiguration { ipdataApiKey: string | null = "eca677b284b3bac29eb72f5e496aa9047f26543605efe99ff2ce35c9"; mfaBackupCodeCount: number = 10; mfaBackupCodeBytes: number = 4; + statsWorldReadable: boolean = true; } diff --git a/src/util/entities/Guild.ts b/src/util/entities/Guild.ts
index 015c6d04..cd7fa561 100644 --- a/src/util/entities/Guild.ts +++ b/src/util/entities/Guild.ts
@@ -241,7 +241,7 @@ export class Guild extends BaseClass { welcome_channels: { description: string; emoji_id?: string; - emoji_name: string; + emoji_name?: string; channel_id: string; }[]; }; diff --git a/src/util/entities/Member.ts b/src/util/entities/Member.ts
index f67ec02c..9e9131fe 100644 --- a/src/util/entities/Member.ts +++ b/src/util/entities/Member.ts
@@ -94,7 +94,19 @@ export class Member extends BaseClassWithoutId { // do not auto-kick force-joined members just because their joiners left the server }) **/ @Column({ nullable: true }) - joined_by?: string; + joined_by: string; + + @Column({ nullable: true }) + avatar: string; + + @Column({ nullable: true }) + banner: string; + + @Column() + bio: string; + + @Column({ nullable: true }) + communication_disabled_until: Date; // TODO: add this when we have proper read receipts // @Column({ type: "simple-json" }) @@ -243,7 +255,11 @@ export class Member extends BaseClassWithoutId { premium_since: null, deaf: false, mute: false, - pending: false + pending: false, + avatar: null, + banner: null, + bio: "", + communication_disabled_until: null }; //TODO: check for bugs if (guild.member_count) guild.member_count++; @@ -284,7 +300,8 @@ export class Member extends BaseClassWithoutId { joined_at: member.joined_at, presences: [], stage_instances: [], - threads: [] + threads: [], + embedded_activities: [], }, user_id } as GuildCreateEvent) diff --git a/src/util/interfaces/Event.ts b/src/util/interfaces/Event.ts
index f97f4615..2631ef13 100644 --- a/src/util/interfaces/Event.ts +++ b/src/util/interfaces/Event.ts
@@ -155,6 +155,7 @@ export interface GuildCreateEvent extends Event { presences: never[]; stage_instances: never[]; threads: never[]; + embedded_activities: never[]; }; } diff --git a/src/util/migrations/mariadb/1661273147273-test.ts b/src/util/migrations/mariadb/1661273147273-test.ts
index 4e077a11..0090e2aa 100644 --- a/src/util/migrations/mariadb/1661273147273-test.ts +++ b/src/util/migrations/mariadb/1661273147273-test.ts
@@ -1,23 +1,23 @@ import { MigrationInterface, QueryRunner } from "typeorm"; export class test1661273147273 implements MigrationInterface { - name = 'test1661273147273' + name = "test1661273147273"; - public async up(queryRunner: QueryRunner): Promise<void> { - await queryRunner.query(` + public async up(queryRunner: QueryRunner): Promise<void> { + await queryRunner.query(` ALTER TABLE \`invites\` DROP FOREIGN KEY \`FK_15c35422032e0b22b4ada95f48f\` `); - await queryRunner.query(` + await queryRunner.query(` DROP INDEX \`IDX_2ce5a55796fe4c2f77ece57a64\` ON \`applications\` `); - await queryRunner.query(` + await queryRunner.query(` CREATE TABLE \`plugin_config\` ( \`key\` varchar(255) NOT NULL, \`value\` text NULL, PRIMARY KEY (\`key\`) ) ENGINE = InnoDB `); - await queryRunner.query(` + await queryRunner.query(` CREATE TABLE \`user_settings\` ( \`id\` varchar(255) NOT NULL, \`afk_timeout\` int NULL, @@ -54,96 +54,95 @@ export class test1661273147273 implements MigrationInterface { PRIMARY KEY (\`id\`) ) ENGINE = InnoDB `); - await queryRunner.query(` + await queryRunner.query(` ALTER TABLE \`users\` DROP COLUMN \`settings\` `); - await queryRunner.query(` + await queryRunner.query(` ALTER TABLE \`users\` ADD \`settingsId\` varchar(255) NULL `); - await queryRunner.query(` + await queryRunner.query(` ALTER TABLE \`users\` ADD UNIQUE INDEX \`IDX_76ba283779c8441fd5ff819c8c\` (\`settingsId\`) `); - await queryRunner.query(` + await queryRunner.query(` ALTER TABLE \`channels\` ADD \`flags\` int NULL `); - await queryRunner.query(` + await queryRunner.query(` ALTER TABLE \`channels\` ADD \`default_thread_rate_limit_per_user\` int NULL `); - await queryRunner.query(` + await queryRunner.query(` ALTER TABLE \`guilds\` ADD \`premium_progress_bar_enabled\` tinyint NULL `); - await queryRunner.query(` + await queryRunner.query(` ALTER TABLE \`users\` CHANGE \`bio\` \`bio\` varchar(255) NULL `); - await queryRunner.query(` + await queryRunner.query(` ALTER TABLE \`users\` CHANGE \`mfa_enabled\` \`mfa_enabled\` tinyint NULL `); - await queryRunner.query(` + await queryRunner.query(` CREATE UNIQUE INDEX \`REL_76ba283779c8441fd5ff819c8c\` ON \`users\` (\`settingsId\`) `); - await queryRunner.query(` + await queryRunner.query(` ALTER TABLE \`users\` ADD CONSTRAINT \`FK_76ba283779c8441fd5ff819c8cf\` FOREIGN KEY (\`settingsId\`) REFERENCES \`user_settings\`(\`id\`) ON DELETE NO ACTION ON UPDATE NO ACTION `); - await queryRunner.query(` + await queryRunner.query(` ALTER TABLE \`invites\` ADD CONSTRAINT \`FK_15c35422032e0b22b4ada95f48f\` FOREIGN KEY (\`inviter_id\`) REFERENCES \`users\`(\`id\`) ON DELETE CASCADE ON UPDATE NO ACTION `); - } + } - public async down(queryRunner: QueryRunner): Promise<void> { - await queryRunner.query(` + public async down(queryRunner: QueryRunner): Promise<void> { + await queryRunner.query(` ALTER TABLE \`invites\` DROP FOREIGN KEY \`FK_15c35422032e0b22b4ada95f48f\` `); - await queryRunner.query(` + await queryRunner.query(` ALTER TABLE \`users\` DROP FOREIGN KEY \`FK_76ba283779c8441fd5ff819c8cf\` `); - await queryRunner.query(` + await queryRunner.query(` DROP INDEX \`REL_76ba283779c8441fd5ff819c8c\` ON \`users\` `); - await queryRunner.query(` + await queryRunner.query(` ALTER TABLE \`users\` CHANGE \`mfa_enabled\` \`mfa_enabled\` tinyint NOT NULL `); - await queryRunner.query(` + await queryRunner.query(` ALTER TABLE \`users\` CHANGE \`bio\` \`bio\` varchar(255) NOT NULL `); - await queryRunner.query(` + await queryRunner.query(` ALTER TABLE \`guilds\` DROP COLUMN \`premium_progress_bar_enabled\` `); - await queryRunner.query(` + await queryRunner.query(` ALTER TABLE \`channels\` DROP COLUMN \`default_thread_rate_limit_per_user\` `); - await queryRunner.query(` + await queryRunner.query(` ALTER TABLE \`channels\` DROP COLUMN \`flags\` `); - await queryRunner.query(` + await queryRunner.query(` ALTER TABLE \`users\` DROP INDEX \`IDX_76ba283779c8441fd5ff819c8c\` `); - await queryRunner.query(` + await queryRunner.query(` ALTER TABLE \`users\` DROP COLUMN \`settingsId\` `); - await queryRunner.query(` + await queryRunner.query(` ALTER TABLE \`users\` ADD \`settings\` text NOT NULL `); - await queryRunner.query(` + await queryRunner.query(` DROP TABLE \`user_settings\` `); - await queryRunner.query(` + await queryRunner.query(` DROP TABLE \`plugin_config\` `); - await queryRunner.query(` + await queryRunner.query(` CREATE UNIQUE INDEX \`IDX_2ce5a55796fe4c2f77ece57a64\` ON \`applications\` (\`bot_user_id\`) `); - await queryRunner.query(` + await queryRunner.query(` ALTER TABLE \`invites\` ADD CONSTRAINT \`FK_15c35422032e0b22b4ada95f48f\` FOREIGN KEY (\`inviter_id\`) REFERENCES \`users\`(\`id\`) ON DELETE NO ACTION ON UPDATE NO ACTION `); - } - + } } diff --git a/src/util/migrations/mariadb/1661273179287-test2.ts b/src/util/migrations/mariadb/1661273179287-test2.ts
index 0f77f284..973d8124 100644 --- a/src/util/migrations/mariadb/1661273179287-test2.ts +++ b/src/util/migrations/mariadb/1661273179287-test2.ts
@@ -1,18 +1,17 @@ import { MigrationInterface, QueryRunner } from "typeorm"; export class test21661273179287 implements MigrationInterface { - name = 'test21661273179287' + name = "test21661273179287"; - public async up(queryRunner: QueryRunner): Promise<void> { - await queryRunner.query(` + public async up(queryRunner: QueryRunner): Promise<void> { + await queryRunner.query(` DROP INDEX \`IDX_76ba283779c8441fd5ff819c8c\` ON \`users\` `); - } + } - public async down(queryRunner: QueryRunner): Promise<void> { - await queryRunner.query(` + public async down(queryRunner: QueryRunner): Promise<void> { + await queryRunner.query(` CREATE UNIQUE INDEX \`IDX_76ba283779c8441fd5ff819c8c\` ON \`users\` (\`settingsId\`) `); - } - + } } diff --git a/src/util/migrations/mariadb/1661885910534-guild-member-profiles.ts b/src/util/migrations/mariadb/1661885910534-guild-member-profiles.ts new file mode 100644
index 00000000..6e1ac3f8 --- /dev/null +++ b/src/util/migrations/mariadb/1661885910534-guild-member-profiles.ts
@@ -0,0 +1,40 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class guildMemberProfiles1661885910534 implements MigrationInterface { + name = 'guildMemberProfiles1661885910534' + + public async up(queryRunner: QueryRunner): Promise<void> { + await queryRunner.query(` + ALTER TABLE \`members\` + ADD \`avatar\` varchar(255) NULL + `); + await queryRunner.query(` + ALTER TABLE \`members\` + ADD \`banner\` varchar(255) NULL + `); + await queryRunner.query(` + ALTER TABLE \`members\` + ADD \`bio\` varchar(255) NOT NULL + `); + await queryRunner.query(` + ALTER TABLE \`members\` + ADD \`communication_disabled_until\` datetime NULL + `); + } + + public async down(queryRunner: QueryRunner): Promise<void> { + await queryRunner.query(` + ALTER TABLE \`members\` DROP COLUMN \`communication_disabled_until\` + `); + await queryRunner.query(` + ALTER TABLE \`members\` DROP COLUMN \`bio\` + `); + await queryRunner.query(` + ALTER TABLE \`members\` DROP COLUMN \`banner\` + `); + await queryRunner.query(` + ALTER TABLE \`members\` DROP COLUMN \`avatar\` + `); + } + +} diff --git a/src/util/migrations/postgres/1661885830688-guild-member-profiles.ts b/src/util/migrations/postgres/1661885830688-guild-member-profiles.ts new file mode 100644
index 00000000..81671250 --- /dev/null +++ b/src/util/migrations/postgres/1661885830688-guild-member-profiles.ts
@@ -0,0 +1,40 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class guildMemberProfiles1661885830688 implements MigrationInterface { + name = 'guildMemberProfiles1661885830688' + + public async up(queryRunner: QueryRunner): Promise<void> { + await queryRunner.query(` + ALTER TABLE "members" + ADD "avatar" character varying + `); + await queryRunner.query(` + ALTER TABLE "members" + ADD "banner" character varying + `); + await queryRunner.query(` + ALTER TABLE "members" + ADD "bio" character varying NOT NULL default '' + `); + await queryRunner.query(` + ALTER TABLE "members" + ADD "communication_disabled_until" TIMESTAMP + `); + } + + public async down(queryRunner: QueryRunner): Promise<void> { + await queryRunner.query(` + ALTER TABLE "members" DROP COLUMN "communication_disabled_until" + `); + await queryRunner.query(` + ALTER TABLE "members" DROP COLUMN "bio" + `); + await queryRunner.query(` + ALTER TABLE "members" DROP COLUMN "banner" + `); + await queryRunner.query(` + ALTER TABLE "members" DROP COLUMN "avatar" + `); + } + +} diff --git a/src/util/migrations/sqlite/1661885742207-guild-member-profiles.ts b/src/util/migrations/sqlite/1661885742207-guild-member-profiles.ts new file mode 100644
index 00000000..24ec9c72 --- /dev/null +++ b/src/util/migrations/sqlite/1661885742207-guild-member-profiles.ts
@@ -0,0 +1,136 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class guildMemberProfiles1661885742207 implements MigrationInterface { + name = 'guildMemberProfiles1661885742207' + + public async up(queryRunner: QueryRunner): Promise<void> { + await queryRunner.query(` + DROP INDEX "IDX_bb2bf9386ac443afbbbf9f12d3" + `); + await queryRunner.query(` + CREATE TABLE "temporary_members" ( + "index" integer PRIMARY KEY AUTOINCREMENT NOT NULL, + "id" varchar NOT NULL, + "guild_id" varchar NOT NULL, + "nick" varchar, + "joined_at" datetime NOT NULL, + "premium_since" datetime, + "deaf" boolean NOT NULL, + "mute" boolean NOT NULL, + "pending" boolean NOT NULL, + "settings" text NOT NULL, + "last_message_id" varchar, + "joined_by" varchar, + "avatar" varchar, + "banner" varchar, + "bio" varchar NOT NULL, + "communication_disabled_until" datetime, + CONSTRAINT "FK_28b53062261b996d9c99fa12404" FOREIGN KEY ("id") REFERENCES "users" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, + CONSTRAINT "FK_16aceddd5b89825b8ed6029ad1c" FOREIGN KEY ("guild_id") REFERENCES "guilds" ("id") ON DELETE CASCADE ON UPDATE NO ACTION + ) + `); + await queryRunner.query(` + INSERT INTO "temporary_members"( + "index", + "id", + "guild_id", + "nick", + "joined_at", + "premium_since", + "deaf", + "mute", + "pending", + "settings", + "last_message_id", + "joined_by" + ) + SELECT "index", + "id", + "guild_id", + "nick", + "joined_at", + "premium_since", + "deaf", + "mute", + "pending", + "settings", + "last_message_id", + "joined_by" + FROM "members" + `); + await queryRunner.query(` + DROP TABLE "members" + `); + await queryRunner.query(` + ALTER TABLE "temporary_members" + RENAME TO "members" + `); + await queryRunner.query(` + CREATE UNIQUE INDEX "IDX_bb2bf9386ac443afbbbf9f12d3" ON "members" ("id", "guild_id") + `); + } + + public async down(queryRunner: QueryRunner): Promise<void> { + await queryRunner.query(` + DROP INDEX "IDX_bb2bf9386ac443afbbbf9f12d3" + `); + await queryRunner.query(` + ALTER TABLE "members" + RENAME TO "temporary_members" + `); + await queryRunner.query(` + CREATE TABLE "members" ( + "index" integer PRIMARY KEY AUTOINCREMENT NOT NULL, + "id" varchar NOT NULL, + "guild_id" varchar NOT NULL, + "nick" varchar, + "joined_at" datetime NOT NULL, + "premium_since" datetime, + "deaf" boolean NOT NULL, + "mute" boolean NOT NULL, + "pending" boolean NOT NULL, + "settings" text NOT NULL, + "last_message_id" varchar, + "joined_by" varchar, + CONSTRAINT "FK_28b53062261b996d9c99fa12404" FOREIGN KEY ("id") REFERENCES "users" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, + CONSTRAINT "FK_16aceddd5b89825b8ed6029ad1c" FOREIGN KEY ("guild_id") REFERENCES "guilds" ("id") ON DELETE CASCADE ON UPDATE NO ACTION + ) + `); + await queryRunner.query(` + INSERT INTO "members"( + "index", + "id", + "guild_id", + "nick", + "joined_at", + "premium_since", + "deaf", + "mute", + "pending", + "settings", + "last_message_id", + "joined_by" + ) + SELECT "index", + "id", + "guild_id", + "nick", + "joined_at", + "premium_since", + "deaf", + "mute", + "pending", + "settings", + "last_message_id", + "joined_by" + FROM "temporary_members" + `); + await queryRunner.query(` + DROP TABLE "temporary_members" + `); + await queryRunner.query(` + CREATE UNIQUE INDEX "IDX_bb2bf9386ac443afbbbf9f12d3" ON "members" ("id", "guild_id") + `); + } + +} diff --git a/src/util/schemas/BackupCodesChallengeSchema.ts b/src/util/schemas/BackupCodesChallengeSchema.ts new file mode 100644
index 00000000..d6b519b7 --- /dev/null +++ b/src/util/schemas/BackupCodesChallengeSchema.ts
@@ -0,0 +1,3 @@ +export interface BackupCodesChallengeSchema { + password: string; +} \ No newline at end of file diff --git a/src/util/schemas/CodesVerificationSchema.ts b/src/util/schemas/CodesVerificationSchema.ts new file mode 100644
index 00000000..e8e2e7b4 --- /dev/null +++ b/src/util/schemas/CodesVerificationSchema.ts
@@ -0,0 +1,5 @@ +export interface CodesVerificationSchema { + key: string; + nonce: string; + regenerate?: boolean; +} \ No newline at end of file diff --git a/src/util/schemas/MemberChangeProfileSchema.ts b/src/util/schemas/MemberChangeProfileSchema.ts new file mode 100644
index 00000000..3e85174d --- /dev/null +++ b/src/util/schemas/MemberChangeProfileSchema.ts
@@ -0,0 +1,5 @@ +export interface MemberChangeProfileSchema { + banner?: string | null; + nick?: string; + bio?: string; +} diff --git a/src/util/schemas/MemberChangeSchema.ts b/src/util/schemas/MemberChangeSchema.ts
index db434538..0cbab4a3 100644 --- a/src/util/schemas/MemberChangeSchema.ts +++ b/src/util/schemas/MemberChangeSchema.ts
@@ -1,3 +1,5 @@ export interface MemberChangeSchema { roles?: string[]; + nick?: string; + avatar?: string | null; } diff --git a/src/util/schemas/UserProfileModifySchema.ts b/src/util/schemas/UserProfileModifySchema.ts new file mode 100644
index 00000000..33a372c9 --- /dev/null +++ b/src/util/schemas/UserProfileModifySchema.ts
@@ -0,0 +1,5 @@ +export interface UserProfileModifySchema { + bio?: string; + accent_color?: number | null; + banner?: string | null; +} diff --git a/src/util/schemas/index.ts b/src/util/schemas/index.ts
index a15ab4b0..320593ec 100644 --- a/src/util/schemas/index.ts +++ b/src/util/schemas/index.ts
@@ -1,4 +1,5 @@ export * from "./ActivitySchema"; +export * from "./BackupCodesChallengeSchema"; export * from "./BanCreateSchema"; export * from "./BanModeratorSchema"; export * from "./BanRegistrySchema"; @@ -6,6 +7,7 @@ export * from "./BulkDeleteSchema"; export * from "./ChannelModifySchema"; export * from "./ChannelPermissionOverwriteSchema"; export * from "./ChannelReorderSchema"; +export * from "./CodesVerificationSchema"; export * from "./DmChannelCreateSchema"; export * from "./EmojiCreateSchema"; export * from "./EmojiModifySchema"; @@ -17,6 +19,7 @@ export * from "./IdentifySchema"; export * from "./InviteCreateSchema"; export * from "./LazyRequestSchema"; export * from "./LoginSchema"; +export * from "./MemberChangeProfileSchema"; export * from "./MemberChangeSchema"; export * from "./MemberNickChangeSchema"; export * from "./MessageAcknowledgeSchema"; @@ -36,8 +39,9 @@ export * from "./TotpDisableSchema"; export * from "./TotpEnableSchema"; export * from "./TotpSchema"; export * from "./UserModifySchema"; +export * from "./UserProfileModifySchema"; export * from "./UserSettingsSchema"; export * from "./VanityUrlSchema"; export * from "./VoiceStateUpdateSchema"; export * from "./WebhookCreateSchema"; -export * from "./WidgetModifySchema"; +export * from "./WidgetModifySchema"; \ No newline at end of file diff --git a/src/util/util/Database.ts b/src/util/util/Database.ts
index b9f8365e..647de26a 100644 --- a/src/util/util/Database.ts +++ b/src/util/util/Database.ts
@@ -96,7 +96,7 @@ function getDataSourceOptions(): DataSourceOptions { name: "default", migrations: synchronizeInsteadOfMigrations ? [] : [path.join(__dirname, "..", "migrations", type, "*.js")], migrationsRun: !synchronizeInsteadOfMigrations, - applicationName: `Fosscord Server`, + applicationName: `Fosscord Server` } as DataSourceOptions; } diff --git a/src/util/util/Rights.ts b/src/util/util/Rights.ts
index 51bb098c..236bfea7 100644 --- a/src/util/util/Rights.ts +++ b/src/util/util/Rights.ts
@@ -63,7 +63,11 @@ export class Rights extends BitField { RESPOND_TO_INTERACTIONS: BitFlag(41), // can respond to interactions SEND_BACKDATED_EVENTS: BitFlag(42), // can send backdated events USE_MASS_INVITES: BitFlag(43), // added per @xnacly's request — can accept mass invites - ACCEPT_INVITES: BitFlag(44) // added per @xnacly's request — can accept user-specific invites and DM requests + ACCEPT_INVITES: BitFlag(44), // added per @xnacly's request — can accept user-specific invites and DM requests + SELF_EDIT_FLAGS: BitFlag(45), // can modify own flags + EDIT_FLAGS: BitFlag(46), // can set others' flags + MANAGE_GROUPS: BitFlag(47), // can manage others' groups + VIEW_SERVER_STATS: BitFlag(48) // added per @chrischrome's request — can view server stats) }; any(permission: RightResolvable, checkOperator = true) {