mirror of
https://github.com/spacebarchat/server.git
synced 2026-09-25 19:55:03 +00:00
Some more avatar decorations work
This commit is contained in:
Binary file not shown.
Binary file not shown.
@@ -19,8 +19,8 @@
|
||||
import bcrypt from "bcrypt";
|
||||
import { Request, Response, Router } from "express";
|
||||
import { route } from "@spacebar/api/middlewares";
|
||||
import { User } from "@spacebar/database";
|
||||
import { Config, emitEvent, FieldErrors, generateToken, handleFile, UserUpdateEvent } from "@spacebar/util";
|
||||
import { AvatarDecoration, User } from "@spacebar/database";
|
||||
import { ApiError, Config, DiscordApiErrors, emitEvent, FieldErrors, generateToken, handleFile, UserUpdateEvent } from "@spacebar/util";
|
||||
import { DisplayNameStyle, PrivateUserProjection, UserModifySchema } from "@spacebar/schemas";
|
||||
|
||||
const router: Router = Router({ mergeParams: true });
|
||||
@@ -216,6 +216,21 @@ router.patch(
|
||||
}
|
||||
}
|
||||
|
||||
if ("avatar_decoration_sku_id" in body) {
|
||||
if (!body.avatar_decoration_sku_id) {
|
||||
user.avatar_decoration_data = undefined;
|
||||
user.avatar_decoration_id = undefined;
|
||||
} else {
|
||||
const avatarDecoration = await AvatarDecoration.findOne({ where: { id: body.avatar_decoration_sku_id } });
|
||||
if (!avatarDecoration) throw FieldErrors({ avatar_decoration_sku_id: { code: "50057", message: "Invalid SKU" } });
|
||||
|
||||
if (!(await avatarDecoration.canUseAvatarDecoration(req.user_id)))
|
||||
throw FieldErrors({ avatar_decoration_sku_id: { code: "40018", message: "You do not have access to this avatar decoration" } }); // TODO: find a better code
|
||||
|
||||
user.avatar_decoration_id = body.avatar_decoration_sku_id;
|
||||
}
|
||||
}
|
||||
|
||||
user.assign(body);
|
||||
user.validate();
|
||||
await user.save();
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
import { Router, Response, Request } from "express";
|
||||
import { route } from "@spacebar/api/middlewares";
|
||||
import { AvatarDecorations } from "@spacebar/database";
|
||||
import { AvatarDecoration } from "@spacebar/database";
|
||||
import { PublicAvatarDecorationResponse, UpdateAvatarDecorationSchema } from "@spacebar/schemas/api/spacebar/AvatarDecorations";
|
||||
import { ApiError } from "@spacebar/util";
|
||||
|
||||
@@ -38,7 +38,7 @@ router.patch(
|
||||
}),
|
||||
async (req: Request, res: Response) => {
|
||||
const changes = req.body as UpdateAvatarDecorationSchema;
|
||||
const deco = await AvatarDecorations.findOneOrFail({ where: { id: req.params.id as string } });
|
||||
const deco = await AvatarDecoration.findOneOrFail({ where: { id: req.params.id as string } });
|
||||
|
||||
if (deco.uploader_id !== req.user_id) throw new ApiError("You do not have permission to update this avatar decoration", 0, 403);
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
import { Router, Response, Request } from "express";
|
||||
import { Raw } from "typeorm";
|
||||
import { route } from "@spacebar/api/middlewares";
|
||||
import { AvatarDecorations, Member } from "@spacebar/database";
|
||||
import { AvatarDecoration, Member } from "@spacebar/database";
|
||||
import { PublicAvatarDecorationListResponse } from "@spacebar/schemas/api/spacebar/AvatarDecorations";
|
||||
import { arrayDistinctBy } from "@spacebar/extensions";
|
||||
|
||||
@@ -40,7 +40,7 @@ router.get(
|
||||
const memberships = await Member.find({ select: { guild_id: true, roles: { id: true } }, relations: { roles: true }, where: { id: req.user_id } });
|
||||
|
||||
const decos = (
|
||||
await AvatarDecorations.find({
|
||||
await AvatarDecoration.find({
|
||||
where: [
|
||||
{ approved: true, public: true },
|
||||
{ approved: true, uploader_id: req.user_id },
|
||||
|
||||
@@ -20,11 +20,12 @@ import { Column, Entity, Index, JoinColumn, ManyToOne, RelationId } from "typeor
|
||||
import { AvatarDecorationData, PublicAvatarDecorationResponse } from "@spacebar/schemas";
|
||||
import { BaseClass } from "./BaseClass";
|
||||
import { User } from "./User";
|
||||
import { Member } from "./Member";
|
||||
|
||||
@Entity({
|
||||
name: "avatar_decorations",
|
||||
})
|
||||
export class AvatarDecorations extends BaseClass {
|
||||
export class AvatarDecoration extends BaseClass {
|
||||
@Column({})
|
||||
asset: string;
|
||||
|
||||
@@ -32,7 +33,7 @@ export class AvatarDecorations extends BaseClass {
|
||||
approved: boolean;
|
||||
|
||||
@Column({ nullable: true })
|
||||
@RelationId((deco: AvatarDecorations) => deco.uploader)
|
||||
@RelationId((deco: AvatarDecoration) => deco.uploader)
|
||||
@Index("IDX_avatar_decoration_uploader_id")
|
||||
uploader_id: string;
|
||||
|
||||
@@ -70,4 +71,25 @@ export class AvatarDecorations extends BaseClass {
|
||||
available: opts?.available ?? this.public,
|
||||
} satisfies PublicAvatarDecorationResponse;
|
||||
}
|
||||
|
||||
async canUseAvatarDecoration(user_id: string): Promise<boolean> {
|
||||
if (!this.approved) return false;
|
||||
if (this.uploader_id == user_id) return true;
|
||||
if (this.allowed_user_ids.includes(user_id)) return true;
|
||||
|
||||
let memberships: Member[];
|
||||
if (this.allowed_guild_ids.length > 0) {
|
||||
memberships ??= await Member.find({ select: { guild_id: true, roles: { id: true } }, where: { id: user_id }, relations: { roles: true } });
|
||||
const guildIds = memberships.map((x) => x.guild_id);
|
||||
for (const allowedGuildId of this.allowed_guild_ids) if (guildIds.includes(allowedGuildId)) return true;
|
||||
}
|
||||
|
||||
if (this.allowed_role_ids.length > 0) {
|
||||
memberships ??= await Member.find({ select: { guild_id: true, roles: true }, where: { id: user_id } });
|
||||
const roleIds = memberships.flatMap((x) => x.roles.map((x) => x.id));
|
||||
for (const allowedRoleId of this.allowed_role_ids) if (roleIds.includes(allowedRoleId)) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,10 +17,11 @@
|
||||
*/
|
||||
|
||||
import { Request } from "express";
|
||||
import { Column, Entity, JoinColumn, OneToMany, OneToOne } from "typeorm";
|
||||
import { Column, Entity, Index, JoinColumn, ManyToOne, OneToMany, OneToOne, RelationId } from "typeorm";
|
||||
import { Config, Email, FieldErrors, Snowflake } from "@spacebar/util";
|
||||
import { Stopwatch, trimSpecial, Random } from "@spacebar/extensions";
|
||||
import { BaseClass } from "./BaseClass";
|
||||
import { AvatarDecoration } from "./AvatarDecoration";
|
||||
import { Channel } from "./Channel";
|
||||
import { ConnectedAccount } from "./ConnectedAccount";
|
||||
import { Member } from "./Member";
|
||||
@@ -197,6 +198,14 @@ export class User extends BaseClass {
|
||||
@Column({ type: "jsonb", nullable: true })
|
||||
primary_guild?: PrimaryGuild;
|
||||
|
||||
@JoinColumn({ name: "avatar_decoration_id", foreignKeyConstraintName: "FK_user_avatar_decoration_id" })
|
||||
@OneToOne(() => AvatarDecoration, { onDelete: "SET NULL", nullable: true })
|
||||
avatar_decoration?: AvatarDecoration;
|
||||
|
||||
@Column({ type: "int8", nullable: true })
|
||||
@RelationId((user: User) => user.avatar_decoration)
|
||||
avatar_decoration_id?: string;
|
||||
|
||||
// TODO: I don't like this method?
|
||||
validate() {
|
||||
if (this.discriminator) {
|
||||
@@ -220,6 +229,9 @@ export class User extends BaseClass {
|
||||
PublicUserProjection.forEach((x) => {
|
||||
user[x] = this[x];
|
||||
});
|
||||
|
||||
if (this.avatar_decoration) (<PublicUser>user).avatar_decoration_data = this.avatar_decoration.toJSON();
|
||||
|
||||
return user as PublicUser;
|
||||
}
|
||||
|
||||
@@ -230,7 +242,12 @@ export class User extends BaseClass {
|
||||
discriminator: this.discriminator,
|
||||
global_name: undefined, // TODO when pomelo
|
||||
avatar: this.avatar ?? null,
|
||||
avatar_decoration_data: this.avatar_decoration_data,
|
||||
avatar_decoration_data: this.avatar_decoration
|
||||
? {
|
||||
...this.avatar_decoration?.toJSON(),
|
||||
...this.avatar_decoration_data,
|
||||
}
|
||||
: null,
|
||||
bot: this.bot,
|
||||
system: this.system,
|
||||
banner: this.banner,
|
||||
@@ -246,6 +263,9 @@ export class User extends BaseClass {
|
||||
[...PrivateUserProjection, ...extraFields].forEach((x) => {
|
||||
user[x] = this[x];
|
||||
});
|
||||
|
||||
if (this.avatar_decoration) (<UserPrivate>user).avatar_decoration_data = this.avatar_decoration.toJSON();
|
||||
|
||||
return user as UserPrivate;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class AvatarDecorationsRelation1790152636577 implements MigrationInterface {
|
||||
name = "AvatarDecorationsRelation1790152636577";
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "users" ADD "avatar_decoration_id" bigint`);
|
||||
await queryRunner.query(`ALTER TABLE "users" ADD CONSTRAINT "UQ_119117c066ad70abbe777d34f40" UNIQUE ("avatar_decoration_id")`);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "users" ADD CONSTRAINT "FK_user_avatar_decoration_id" FOREIGN KEY ("avatar_decoration_id") REFERENCES "avatar_decorations"("id") ON DELETE SET NULL ON UPDATE NO ACTION`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "users" DROP CONSTRAINT "FK_user_avatar_decoration_id"`);
|
||||
await queryRunner.query(`ALTER TABLE "users" DROP CONSTRAINT "UQ_119117c066ad70abbe777d34f40"`);
|
||||
await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "avatar_decoration_id"`);
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { ConnectedAccountSchema, Snowflake, UserSettingsSchema } from "@spacebar/schemas";
|
||||
import { base64str, ConnectedAccountSchema, Snowflake, UserSettingsSchema } from "@spacebar/schemas";
|
||||
// TODO: maybe move the one thing this depends on to
|
||||
import { BitField } from "@spacebar/util/util";
|
||||
// TODO: remove entity import
|
||||
@@ -212,3 +212,27 @@ export class UserFlags extends BitField {
|
||||
RESTRICTED_COLLABORATOR: 1n << 51n,
|
||||
};
|
||||
}
|
||||
|
||||
// OAuth2 only (account.global_name.update) - why can this be null?
|
||||
export class UserAccountModifySchema {
|
||||
global_name?: string | null;
|
||||
}
|
||||
|
||||
export class UserProfileModifySchema {
|
||||
pronouns?: string | null;
|
||||
bio?: string | null;
|
||||
banner?: base64str | null;
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
accent_color?: number | null;
|
||||
/**
|
||||
* @items.type integer
|
||||
*/
|
||||
theme_colors?: [number, number] | null;
|
||||
//@deprecated - what even was this
|
||||
popout_animation_particle_type?: Snowflake | null;
|
||||
//@deprecated - what even was this
|
||||
emoji_id?: Snowflake | null;
|
||||
profile_effect_id?: Snowflake | null;
|
||||
}
|
||||
|
||||
@@ -57,4 +57,6 @@ export interface UserModifySchema {
|
||||
display_name_colors?: number[];
|
||||
display_name_effect_id?: User_DisplayNameEffect;
|
||||
display_name_font_id?: User_DisplayNameFont;
|
||||
|
||||
avatar_decoration_sku_id?: string | null;
|
||||
}
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
/*
|
||||
Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
|
||||
Copyright (C) 2023 Spacebar and Spacebar Contributors
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
export interface UserProfileModifySchema {
|
||||
bio?: string;
|
||||
accent_color?: number | null;
|
||||
banner?: string | null;
|
||||
pronouns?: string;
|
||||
/**
|
||||
* @items.type integer
|
||||
*/
|
||||
theme_colors?: [number, number];
|
||||
}
|
||||
@@ -79,7 +79,6 @@ export * from "./UserDeleteSchema";
|
||||
export * from "./UserGuildSettingsSchema";
|
||||
export * from "./UserModifySchema";
|
||||
export * from "./UserNoteUpdateSchema";
|
||||
export * from "./UserProfileModifySchema";
|
||||
export * from "./VanityUrlSchema";
|
||||
export * from "./VerifyEmailSchema";
|
||||
export * from "./VoiceStateUpdateSchema";
|
||||
|
||||
@@ -864,6 +864,9 @@ export const DiscordApiErrors = {
|
||||
get CANNOT_SELF_REDEEM_GIFT() {
|
||||
return new ApiError("Cannot self-redeem this gift", 50054);
|
||||
},
|
||||
get INVALID_SKU() {
|
||||
return new ApiError("Invalid SKU", 50057);
|
||||
},
|
||||
get PAYMENT_SOURCE_REQUIRED() {
|
||||
return new ApiError("Payment source required to redeem gift", 50070);
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user