initial pomelo implementation

This commit is contained in:
Puyodead1
2026-01-21 07:10:10 +01:00
committed by Rory&
parent 00b2a18cdb
commit cca6cb3210
18 changed files with 149 additions and 57 deletions
+11 -5
View File
@@ -20,6 +20,7 @@ import { route, verifyCaptcha } from "@spacebar/api";
import { Config, Email, User } from "@spacebar/util";
import { Request, Response, Router } from "express";
import { ForgotPasswordSchema } from "@spacebar/schemas";
import { HTTPError } from "lambert-server*";
const router = Router({ mergeParams: true });
router.post(
@@ -66,11 +67,16 @@ router.post(
select: { username: true, id: true, email: true },
}).catch(() => {});
if (user && user.email) {
Email.sendResetPassword(user, user.email).catch((e) => {
console.error(`Failed to send password reset email to ${user.username}#${user.discriminator} (${user.id}): ${e}`);
});
}
if (user && user.email)
return await Email.sendResetPassword(user, user.email)
.then(() => {
return res.sendStatus(204);
})
.catch((e) => {
console.error(`Failed to send password reset email to ${user.handle}: ${e}`);
throw new HTTPError("Failed to send password reset email", 500);
});
else throw new HTTPError("No user or email", 500);
},
);
@@ -43,6 +43,7 @@ router.post(
username: "",
avatar: "",
discriminator: "",
global_name: "",
public_flags: 64,
},
attachments: [],
@@ -192,11 +192,14 @@ router.get(
if ((y.user_ids || []).includes(req.user_id)) y.me = true;
delete y.user_ids;
});
const { pomeloEnabled } = Config.get().general;
if (!x.author)
x.author = User.create({
id: "4",
discriminator: "0000",
discriminator: pomeloEnabled ? "0" : "0000",
username: "Spacebar Ghost",
global_name: "spacebarghost",
display_name: "Spacebar Ghost",
public_flags: 0,
});
x.attachments?.forEach((y: Attachment) => {
+2
View File
@@ -61,6 +61,8 @@ router.get(
user: {
username: user.username,
discriminator: user.discriminator,
global_name: user.global_name,
display_name: user.display_name,
id: user.id,
avatar: user.avatar ?? null,
public_flags: user.public_flags,
@@ -123,6 +123,8 @@ router.get(
avatar: x.author?.avatar,
avatar_decoration: null,
discriminator: x.author?.discriminator,
global_name: x.author?.global_name,
display_name: x.author?.display_name,
public_flags: x.author?.public_flags,
},
attachments: x.attachments,
+6 -2
View File
@@ -75,7 +75,7 @@ router.get(
id: req.user_id,
bot: false,
},
select: { id: true, username: true, avatar: true, discriminator: true, public_flags: true },
select: { id: true, username: true, avatar: true, discriminator: true, public_flags: true, global_name: true, display_name: true, public_flags: true },
});
const guilds = await Member.find({
@@ -122,6 +122,8 @@ router.get(
avatar: user.avatar,
avatar_decoration: null, // TODO
discriminator: user.discriminator,
global_name: user.global_name,
display_name: user.display_name,
public_flags: user.public_flags,
},
application: {
@@ -144,6 +146,8 @@ router.get(
avatar: bot.avatar,
avatar_decoration: null, // TODO
discriminator: bot.discriminator,
global_name: bot.global_name,
display_name: bot.display_name,
public_flags: bot.public_flags,
bot: true,
approximated_guild_count: 0, // TODO
@@ -220,4 +224,4 @@ router.post(
},
);
export default router;
export default router;
@@ -55,6 +55,8 @@ router.get(
username: relation_user.username,
avatar: relation_user.avatar,
discriminator: relation_user.discriminator,
global_name: relation_user.global_name,
display_name: relation_user.display_name,
public_flags: relation_user.public_flags,
});
}
+2
View File
@@ -120,6 +120,7 @@ router.patch(
newToken = (await generateToken(user.id)) as string;
}
// TODO: pomelo: disallow if pomelo is enabled
if (body.username) {
const check_username = body?.username?.replace(/\s/g, "").trim();
if (!check_username) {
@@ -151,6 +152,7 @@ router.patch(
}
}
// TODO: pomelo: disallow if pomelo is enabled
if (body.discriminator) {
if (
await User.findOne({
+14 -4
View File
@@ -131,16 +131,26 @@ router.post(
},
}),
async (req: Request, res: Response) => {
const { pomeloEnabled } = Config.get().general;
const where = pomeloEnabled
? {
// TODO: pomelo: should we use username or add global_name property to the request?
global_name: req.body.username,
}
: {
discriminator: String(req.body.discriminator).padStart(
4,
"0",
), //Discord send the discriminator as integer, we need to add leading zeroes
username: req.body.username,
};
return await updateRelationship(
req,
res,
await User.findOneOrFail({
relations: { relationships: { to: true } },
select: userProjection,
where: {
discriminator: String(req.body.discriminator).padStart(4, "0"), //Discord send the discriminator as integer, we need to add leading zeroes
username: req.body.username,
},
where,
}),
req.body.type,
);
+3
View File
@@ -25,6 +25,8 @@ interface UserResponse {
id: string;
username: string;
discriminator: string;
global_name: string;
display_name?: string;
avatar_url: string | null;
}
@@ -113,6 +115,7 @@ export default class DiscordConnection extends Connection {
if (exists) return null;
// TODO: pomelo
return await this.createConnection({
user_id: userId,
external_id: userInfo.id,
@@ -21,6 +21,8 @@ export interface GuildBansResponse {
user: {
username: string;
discriminator: string;
global_name: string;
display_name: string | null;
id: string;
avatar: string | null;
public_flags: number;
@@ -19,21 +19,23 @@
import { ClientStatus } from "@spacebar/util";
export interface GuildWidgetJsonResponse {
id: string;
name: string;
instant_invite: string;
channels: {
id: string;
name: string;
position: number;
}[];
members: {
id: string;
username: string;
discriminator: string;
avatar: string | null;
status: ClientStatus;
avatar_url: string;
}[];
presence_count: number;
id: string;
name: string;
instant_invite: string;
channels: {
id: string;
name: string;
position: number;
}[];
members: {
id: string;
username: string;
global_name: string;
display_name: string | null;
discriminator: string;
avatar: string | null;
status: ClientStatus;
avatar_url: string;
}[];
presence_count: number;
}
@@ -17,4 +17,10 @@
*/
import { User } from "@spacebar/util";
export type UserRelationsResponse = (Pick<User, "id"> & Pick<User, "username"> & Pick<User, "discriminator"> & Pick<User, "avatar"> & Pick<User, "public_flags">)[];
export type UserRelationsResponse = (Pick<User, "id"> &
Pick<User, "username"> &
Pick<User, "global_name"> &
Pick<User, "display_name"> &
Pick<User, "discriminator"> &
Pick<User, "avatar"> &
Pick<User, "public_flags">)[];
@@ -16,7 +16,8 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
// TODO: pomelo?
export interface RelationshipPostSchema {
discriminator: string;
username: string;
discriminator: string;
username: string;
}
@@ -29,4 +29,5 @@ export class GeneralConfiguration {
image: string | null = null;
instanceId: string = Snowflake.generate();
autoCreateBotUsers: boolean = false;
pomeloEnabled: boolean = false;
}
+16 -14
View File
@@ -19,19 +19,21 @@
import { User } from "../entities";
export class MinimalPublicUserDTO {
avatar?: string | null;
discriminator: string;
id: string;
public_flags: number;
username: string;
badge_ids?: string[] | null;
avatar?: string | null;
discriminator: string;
id: string;
public_flags: number;
username: string;
global_name: string;
display_name?: string;
constructor(user: User) {
this.avatar = user.avatar;
this.discriminator = user.discriminator;
this.id = user.id;
this.public_flags = user.public_flags;
this.username = user.username;
this.badge_ids = user.badge_ids;
}
constructor(user: User) {
this.avatar = user.avatar;
this.discriminator = user.discriminator;
this.id = user.id;
this.public_flags = user.public_flags;
this.username = user.username;
this.global_name = user.global_name;
this.display_name = user.display_name;
}
}
+51 -11
View File
@@ -29,6 +29,25 @@ import { Session } from "./Session";
import { UserSettings } from "./UserSettings";
import { ChannelType, PrivateUserProjection, PublicUser, PublicUserProjection, UserPrivate } from "@spacebar/schemas";
export enum PublicUserEnum {
username,
global_name,
display_name,
discriminator,
id,
public_flags,
avatar,
accent_color,
banner,
bio,
bot,
premium_since,
premium_type,
theme_colors,
pronouns,
}
export type PublicUserKeys = keyof typeof PublicUserEnum;
@Entity({
name: "users",
})
@@ -36,8 +55,14 @@ export class User extends BaseClass {
@Column()
username: string; // username max length 32, min 2 (should be configurable)
@Column({nullable: true})
global_name: string; // puyo: pomelo
@Column({nullable: true})
display_name?: string; // puyo: pomelo
@Column()
discriminator: string; // opaque string: 4 digits on discord.com
discriminator: string; // opaque string: 4 digits on discord.com, 0 for pomelo
@Column({ nullable: true })
avatar?: string; // hash of the user avatar
@@ -245,6 +270,13 @@ export class User extends BaseClass {
}
}
public get handle(): string {
const {pomeloEnabled} = Config.get().general;
// if pomelo is enabled, global_name should be set
return pomeloEnabled ? this.global_name as string : `${this.username}#${this.discriminator}`;
}
static async register({
email,
username,
@@ -261,19 +293,25 @@ export class User extends BaseClass {
req?: Request;
bot?: boolean;
}) {
const {pomeloEnabled} = Config.get().general;
// trim special uf8 control characters -> Backspace, Newline, ...
username = trimSpecial(username);
const discriminator = await User.generateDiscriminator(username);
if (!discriminator) {
// We've failed to generate a valid and unused discriminator
throw FieldErrors({
username: {
code: "USERNAME_TOO_MANY_USERS",
let discriminator: string | undefined;
if(pomeloEnabled) discriminator = "0";
else {
discriminator = await User.generateDiscriminator(username);
if (!discriminator) {
// We've failed to generate a valid and unused discriminator
throw FieldErrors({
username: {
code: "USERNAME_TOO_MANY_USERS",
message: req?.t("auth:register.USERNAME_TOO_MANY_USERS") || "",
},
});
}
},
});
}
}
// TODO: save date_of_birth
// apparently discord doesn't save the date of birth and just calculate if nsfw is allowed
@@ -286,6 +324,8 @@ export class User extends BaseClass {
const user = User.create({
username: username,
global_name: username, // TODO: convert to lowercase, strip special characters,etc???
// display_name: username, // TODO: how should we do this?
discriminator,
id: id || Snowflake.generate(),
email: email,
@@ -310,7 +350,7 @@ export class User extends BaseClass {
// send verification email if users aren't verified by default and we have an email
if (!Config.get().defaults.user.verified && email) {
await Email.sendVerifyEmail(user, email).catch((e) => {
console.error(`Failed to send verification email to ${user.username}#${user.discriminator}: ${e}`);
console.error(`Failed to send verification email to ${user.handle}: ${e}`);
});
}
+3
View File
@@ -100,9 +100,12 @@ export const Email: {
) {
const { instanceName } = Config.get().general;
// TODO: pomelo: display_name should take precedence over username if pomelo is enabled. maybe we should use global_name as the username?
const replacements = [
["{instanceName}", instanceName],
["{userUsername}", user.username],
["{userGlobalName}", user.global_name],
["{userDisplayName}", user.display_name],
["{userDiscriminator}", user.discriminator],
["{userId}", user.id],
["{phoneNumber}", user.phone?.slice(-4)],