diff --git a/src/api/routes/users/#id/profile.ts b/src/api/routes/users/#id/profile.ts
index eecec0f3..db0922d6 100644
--- a/src/api/routes/users/#id/profile.ts
+++ b/src/api/routes/users/#id/profile.ts
@@ -18,6 +18,7 @@
import { route } from "@spacebar/api";
import {
+ Badge,
Member,
PrivateUserProjection,
User,
@@ -98,6 +99,9 @@ router.get(
bio: guild_member?.bio || "",
guild_id,
};
+
+ const badges = await Badge.find();
+
res.json({
connected_accounts: user.connected_accounts.filter(
(x) => x.visibility != 0,
@@ -111,6 +115,7 @@ router.get(
user_profile: userProfile,
guild_member: guild_member?.toPublicMember(),
guild_member_profile: guild_id && guildMemberProfile,
+ badges: badges.filter((x) => user.badge_ids?.includes(x.id)),
});
},
);
diff --git a/src/util/dtos/UserDTO.ts b/src/util/dtos/UserDTO.ts
index a24c8d96..17e4435f 100644
--- a/src/util/dtos/UserDTO.ts
+++ b/src/util/dtos/UserDTO.ts
@@ -24,6 +24,7 @@ export class MinimalPublicUserDTO {
id: string;
public_flags: number;
username: string;
+ badge_ids?: string[] | null;
constructor(user: User) {
this.avatar = user.avatar;
@@ -31,5 +32,6 @@ export class MinimalPublicUserDTO {
this.id = user.id;
this.public_flags = user.public_flags;
this.username = user.username;
+ this.badge_ids = user.badge_ids;
}
}
diff --git a/src/util/entities/Badge.ts b/src/util/entities/Badge.ts
new file mode 100644
index 00000000..9535e207
--- /dev/null
+++ b/src/util/entities/Badge.ts
@@ -0,0 +1,35 @@
+/*
+ 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 .
+*/
+
+import { Column, Entity } from "typeorm";
+import { BaseClassWithoutId } from "./BaseClass";
+
+@Entity("badges")
+export class Badge extends BaseClassWithoutId {
+ @Column({ primary: true })
+ id: string;
+
+ @Column()
+ description: string;
+
+ @Column()
+ icon: string;
+
+ @Column({ nullable: true })
+ link?: string;
+}
diff --git a/src/util/entities/User.ts b/src/util/entities/User.ts
index c6582b00..c929039e 100644
--- a/src/util/entities/User.ts
+++ b/src/util/entities/User.ts
@@ -49,6 +49,7 @@ export enum PublicUserEnum {
premium_type,
theme_colors,
pronouns,
+ badge_ids,
}
export type PublicUserKeys = keyof typeof PublicUserEnum;
@@ -231,6 +232,9 @@ export class User extends BaseClass {
@OneToMany(() => SecurityKey, (key: SecurityKey) => key.user)
security_keys: SecurityKey[];
+ @Column({ type: "simple-array", nullable: true })
+ badge_ids?: string[];
+
// TODO: I don't like this method?
validate() {
if (this.discriminator) {
diff --git a/src/util/entities/index.ts b/src/util/entities/index.ts
index aa943dca..b2356aa7 100644
--- a/src/util/entities/index.ts
+++ b/src/util/entities/index.ts
@@ -20,6 +20,7 @@ export * from "./Application";
export * from "./Attachment";
export * from "./AuditLog";
export * from "./BackupCodes";
+export * from "./Badge";
export * from "./Ban";
export * from "./BaseClass";
export * from "./Categories";
diff --git a/src/util/migration/mariadb/1720628601997-badges.ts b/src/util/migration/mariadb/1720628601997-badges.ts
new file mode 100644
index 00000000..af298e42
--- /dev/null
+++ b/src/util/migration/mariadb/1720628601997-badges.ts
@@ -0,0 +1,21 @@
+import { MigrationInterface, QueryRunner } from "typeorm";
+
+export class Badges1720628601997 implements MigrationInterface {
+ name = "Badges1720628601997";
+
+ public async up(queryRunner: QueryRunner): Promise {
+ await queryRunner.query(
+ `CREATE TABLE \`badges\` (\`id\` varchar(255) NOT NULL, \`description\` varchar(255) NOT NULL, \`icon\` varchar(255) NOT NULL, \`link\` varchar(255) NULL, PRIMARY KEY (\`id\`)) ENGINE=InnoDB`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE \`users\` ADD \`badge_ids\` text NULL`,
+ );
+ }
+
+ public async down(queryRunner: QueryRunner): Promise {
+ await queryRunner.query(
+ `ALTER TABLE \`users\` DROP COLUMN \`badge_ids\``,
+ );
+ await queryRunner.query(`DROP TABLE \`badges\``);
+ }
+}
diff --git a/src/util/migration/mysql/1720628601997-badges.ts b/src/util/migration/mysql/1720628601997-badges.ts
new file mode 100644
index 00000000..af298e42
--- /dev/null
+++ b/src/util/migration/mysql/1720628601997-badges.ts
@@ -0,0 +1,21 @@
+import { MigrationInterface, QueryRunner } from "typeorm";
+
+export class Badges1720628601997 implements MigrationInterface {
+ name = "Badges1720628601997";
+
+ public async up(queryRunner: QueryRunner): Promise {
+ await queryRunner.query(
+ `CREATE TABLE \`badges\` (\`id\` varchar(255) NOT NULL, \`description\` varchar(255) NOT NULL, \`icon\` varchar(255) NOT NULL, \`link\` varchar(255) NULL, PRIMARY KEY (\`id\`)) ENGINE=InnoDB`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE \`users\` ADD \`badge_ids\` text NULL`,
+ );
+ }
+
+ public async down(queryRunner: QueryRunner): Promise {
+ await queryRunner.query(
+ `ALTER TABLE \`users\` DROP COLUMN \`badge_ids\``,
+ );
+ await queryRunner.query(`DROP TABLE \`badges\``);
+ }
+}
diff --git a/src/util/migration/postgres/1720628601997-badges.ts b/src/util/migration/postgres/1720628601997-badges.ts
new file mode 100644
index 00000000..f7b9958b
--- /dev/null
+++ b/src/util/migration/postgres/1720628601997-badges.ts
@@ -0,0 +1,16 @@
+import { MigrationInterface, QueryRunner } from "typeorm";
+
+export class Badges1720628601997 implements MigrationInterface {
+ name = "Badges1720628601997";
+
+ public async up(queryRunner: QueryRunner): Promise {
+ await queryRunner.query(
+ `CREATE TABLE "badges" ("id" character varying NOT NULL, "description" character varying NOT NULL, "icon" character varying NOT NULL, "link" character varying, CONSTRAINT "PK_8a651318b8de577e8e217676466" PRIMARY KEY ("id"))`,
+ );
+ await queryRunner.query(`ALTER TABLE "users" ADD "badge_ids" text`);
+ }
+
+ public async down(queryRunner: QueryRunner): Promise {
+ await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "badge_ids"`);
+ }
+}
diff --git a/src/util/schemas/responses/UserProfileResponse.ts b/src/util/schemas/responses/UserProfileResponse.ts
index 26e7e3bc..7b63542e 100644
--- a/src/util/schemas/responses/UserProfileResponse.ts
+++ b/src/util/schemas/responses/UserProfileResponse.ts
@@ -17,6 +17,7 @@
*/
import {
+ Badge,
Member,
PublicConnectedAccount,
PublicMember,
@@ -52,4 +53,5 @@ export interface UserProfileResponse {
user_profile: UserProfile;
guild_member?: PublicMember;
guild_member_profile?: PublicMemberProfile;
+ badges: Badge[];
}