diff --git a/src/api/routes/safety-hub/@me/index.ts b/src/api/routes/safety-hub/@me/index.ts
index 75c9638b..4bd14eb4 100644
--- a/src/api/routes/safety-hub/@me/index.ts
+++ b/src/api/routes/safety-hub/@me/index.ts
@@ -17,7 +17,7 @@
*/
import { route } from "@spacebar/api";
-import { User, AccountStandingResponse } from "@spacebar/util";
+import { AccountStandingResponse, AccountStandingState, AppealEligibility, User } from "@spacebar/util";
import { Request, Response, Router } from "express";
const router = Router({ mergeParams: true });
@@ -44,13 +44,13 @@ router.get(
classifications: [],
guild_classifications: [],
account_standing: {
- state: 100,
+ state: AccountStandingState.ALL_GOOD,
},
is_dsa_eligible: true,
username: user.username,
discriminator: user.discriminator,
is_appeal_eligible: true,
- appeal_eligibility: [1, 2],
+ appeal_eligibility: [AppealEligibility.DSA_ELIGIBLE, AppealEligibility.IN_APP_ELIGIBLE, AppealEligibility.AGE_VERIFY_ELIGIBLE],
} as AccountStandingResponse);
},
);
diff --git a/src/api/routes/safety-hub/suspended/@me.ts b/src/api/routes/safety-hub/suspended/@me.ts
new file mode 100644
index 00000000..4bd14eb4
--- /dev/null
+++ b/src/api/routes/safety-hub/suspended/@me.ts
@@ -0,0 +1,58 @@
+/*
+ Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
+ Copyright (C) 2025 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 { route } from "@spacebar/api";
+import { AccountStandingResponse, AccountStandingState, AppealEligibility, User } from "@spacebar/util";
+import { Request, Response, Router } from "express";
+
+const router = Router({ mergeParams: true });
+
+router.get(
+ "/",
+ route({
+ responses: {
+ 200: {
+ body: "AccountStandingResponse",
+ },
+ 401: {
+ body: "APIErrorResponse",
+ },
+ },
+ }),
+ async (req: Request, res: Response) => {
+ const user = await User.findOneOrFail({
+ where: { id: req.user_id },
+ select: ["data"],
+ });
+
+ res.send({
+ classifications: [],
+ guild_classifications: [],
+ account_standing: {
+ state: AccountStandingState.ALL_GOOD,
+ },
+ is_dsa_eligible: true,
+ username: user.username,
+ discriminator: user.discriminator,
+ is_appeal_eligible: true,
+ appeal_eligibility: [AppealEligibility.DSA_ELIGIBLE, AppealEligibility.IN_APP_ELIGIBLE, AppealEligibility.AGE_VERIFY_ELIGIBLE],
+ } as AccountStandingResponse);
+ },
+);
+
+export default router;
diff --git a/src/util/schemas/responses/AccountStandingResponse.ts b/src/util/schemas/responses/AccountStandingResponse.ts
index cee00741..c64986bd 100644
--- a/src/util/schemas/responses/AccountStandingResponse.ts
+++ b/src/util/schemas/responses/AccountStandingResponse.ts
@@ -16,23 +16,145 @@
along with this program. If not, see .
*/
+import { MemberNameType } from "@typescript-eslint/eslint-plugin/dist/util";
+import { Attachment } from "../../entities";
+
+export enum AccountStandingState {
+ ALL_GOOD = 100,
+ LIMITED = 200,
+ VERY_LIMITED = 300,
+ AT_RISK = 400,
+ SUSPENDED = 500,
+}
+
+export enum AppealEligibility {
+ DSA_ELIGIBLE = 1,
+ IN_APP_ELIGIBLE = 2,
+ AGE_VERIFY_ELIGIBLE = 3,
+}
+
+export enum ClassificationType {
+ UNKNOWN = 1,
+ UNSOLICITED_PORNOGRAPHY = 100,
+ NONCONSENSUAL_PORNOGRAPHY = 200,
+ GLORIFYING_VIOLENCE = 210,
+ HATE_SPEECH = 220,
+ CRACKED_ACCOUNTS = 230,
+ ILLICIT_GOODS = 240,
+ SOCIAL_ENGINEERING = 250,
+ CHILD_SAFETY = 280,
+ HARRASMENT_AND_BULLYING = 290,
+ HARRASMENT_AND_BULLYING_2 = 310,
+ HATEFUL_CONDUCT = 320,
+ HARRASMENT_AND_BULLYING_3 = 390,
+ CHILD_SAFETY_2 = 600,
+ CHILD_SAFETY_3 = 650,
+ IMPERSONATION = 711,
+ BAN_EVASION = 720,
+ MALICIOUS_CONDUCT = 3010,
+ SPAM = 3030,
+ NONCONSENSUAL_ADULT_CONTENT = 4000,
+ FRAUD = 4010,
+ DOXXING_GUILD_OWNER = 4130,
+ COPYRIGHT_INFRINGEMENT_GUILD_OWNER = 4140,
+ CHILD_SAFETY_4 = 5010,
+ CHILD_SELF_ENDANGERMENT = 5090,
+ DOXXING_GUILD_MEMBER = 5305,
+ UNDERAGE = 5411,
+ COPYRIGHT_INFRINGEMENT_GUILD_MEMBER = 5440,
+ COPYRIGHT_INFRINGEMENT_3 = 5485,
+}
+
+export enum AppealIngestionType {
+ WEBFORM = 0,
+ AGE_VERIFY = 1,
+ IN_APP = 2,
+}
+
+export enum ClassificationActionType {
+ BAN = 0,
+ TEMP_BAN = 1,
+ GLOBAL_QUARANTINE = 2,
+ REQUIRE_VERIFICATION = 3,
+ USER_WARNING = 4,
+ USER_SPAMMER = 5,
+ CHANNEL_SPAM = 6,
+ MESSAGE_SPAM = 7,
+ DISABLE_SUSPICIOUS_ACTIVITY = 8,
+ LIMITED_ACCESS = 9,
+ CHANNEL_SCHEDULE_DELETE = 10,
+ MESSAGE_CONTENT_REMOVAL = 11,
+ GUILD_DISABLE_INVITE = 12,
+ USER_CONTENT_REMOVAL = 13,
+ USER_USERNAME_MANGLED = 14,
+ GUILD_LIMITED_ACCESS = 15,
+ USER_MESSAGE_REMOVAL = 16,
+ GUILD_DELETE = 20,
+ USER_PROFILE_MANGLED = 22,
+}
+
+export interface ClassificationAction {
+ id: string;
+ action_type: ClassificationActionType;
+ descriptions: string[];
+}
+
+export enum AppealStatusValue {
+ REVIEW_PENDING = 1,
+ CLASSIFICATION_UPHELD = 2,
+ CLASSIFICATION_INVALIDATED = 3,
+}
+
+export interface AppealStatus {
+ status: AppealStatusValue;
+}
+
+// why is this just a reduced message?
+export interface FlaggedContent {
+ type: "message";
+ id: string;
+ content: string;
+ attachments: Attachment[];
+}
+
+export interface Classification {
+ id: string;
+ classification_type: ClassificationType;
+ description: string;
+ explainer_link: string;
+ actions: ClassificationAction[];
+ max_expiration_time: string; // ISO 8601 timestamp
+ flagged_content: unknown[]; // TODO
+ appeal_status: AppealStatus;
+ is_coppa: boolean;
+ is_spam: boolean;
+ appeal_ingestion_type: AppealIngestionType;
+}
+
+export enum GuildMemberType {
+ OWNER = 1,
+ MEMBER = 2
+}
+
+export interface GuildMetadata {
+ name: string;
+ icon?: string;
+ member_type: GuildMemberType;
+}
+
+export interface GuildClassification extends Classification {
+ guild_metadata: GuildMetadata;
+}
+
export interface AccountStandingResponse {
- classifications: unknown[]; // TODO: We don't know what this is yet
- guild_classifications: unknown[]; // TODO: We don't know what this is yet
+ classifications: Classification[];
+ guild_classifications: GuildClassification[];
account_standing: {
- /**
- * @defaultValue 100
- * @minimum 0
- * @maximum 100
- */
- state: number;
+ state: AccountStandingState;
};
is_dsa_eligible: boolean;
username: string;
discriminator: string; // Not sure if this is even valid, we don't have any examples of pre-pomelo users
is_appeal_eligible: boolean;
- /**
- * @description We don't yet know what these mean, but Discord appears to return "1" and "2".
- */
- appeal_eligibility: number[];
+ appeal_eligibility: AppealEligibility[];
}