mirror of
https://github.com/versia-pub/server.git
synced 2025-12-06 08:28:19 +01:00
refactor(federation): ♻️ Refactor User federation code
This commit is contained in:
parent
d638610361
commit
9ff9b90f6b
|
|
@ -353,8 +353,7 @@ export default apiRoute((app) =>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await User.fromDataLocal({
|
await User.register(username, {
|
||||||
username,
|
|
||||||
password,
|
password,
|
||||||
email,
|
email,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -176,6 +176,15 @@ export default apiRoute((app) =>
|
||||||
} = context.req.valid("json");
|
} = context.req.valid("json");
|
||||||
|
|
||||||
const self = user.data;
|
const self = user.data;
|
||||||
|
if (!self.source) {
|
||||||
|
self.source = {
|
||||||
|
fields: [],
|
||||||
|
privacy: "public",
|
||||||
|
language: "en",
|
||||||
|
sensitive: false,
|
||||||
|
note: "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const sanitizedDisplayName = await sanitizedHtmlStrip(
|
const sanitizedDisplayName = await sanitizedHtmlStrip(
|
||||||
display_name ?? "",
|
display_name ?? "",
|
||||||
|
|
@ -185,7 +194,7 @@ export default apiRoute((app) =>
|
||||||
self.displayName = sanitizedDisplayName;
|
self.displayName = sanitizedDisplayName;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (note && self.source) {
|
if (note) {
|
||||||
self.source.note = note;
|
self.source.note = note;
|
||||||
self.note = await contentToHtml(
|
self.note = await contentToHtml(
|
||||||
new VersiaEntities.TextContentFormat({
|
new VersiaEntities.TextContentFormat({
|
||||||
|
|
@ -197,16 +206,13 @@ export default apiRoute((app) =>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (source?.privacy) {
|
if (source) {
|
||||||
self.source.privacy = source.privacy;
|
self.source = {
|
||||||
}
|
...self.source,
|
||||||
|
privacy: source.privacy ?? self.source.privacy,
|
||||||
if (source?.sensitive) {
|
sensitive: source.sensitive ?? self.source.sensitive,
|
||||||
self.source.sensitive = source.sensitive;
|
language: source.language ?? self.source.language,
|
||||||
}
|
};
|
||||||
|
|
||||||
if (source?.language) {
|
|
||||||
self.source.language = source.language;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (username) {
|
if (username) {
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ describe("/api/v1/accounts/verify_credentials", () => {
|
||||||
expect(data.id).toBe(users[0].id);
|
expect(data.id).toBe(users[0].id);
|
||||||
expect(data.username).toBe(users[0].data.username);
|
expect(data.username).toBe(users[0].data.username);
|
||||||
expect(data.acct).toBe(users[0].data.username);
|
expect(data.acct).toBe(users[0].data.username);
|
||||||
expect(data.display_name).toBe(users[0].data.displayName);
|
expect(data.display_name).toBe(users[0].data.displayName ?? "");
|
||||||
expect(data.note).toBe(users[0].data.note);
|
expect(data.note).toBe(users[0].data.note);
|
||||||
expect(data.url).toBe(
|
expect(data.url).toBe(
|
||||||
new URL(
|
new URL(
|
||||||
|
|
|
||||||
|
|
@ -679,120 +679,106 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async fromVersia(user: VersiaEntities.User): Promise<User> {
|
/**
|
||||||
const instance = await Instance.resolve(user.data.uri);
|
* Takes a Versia User representation, and serializes it to the database.
|
||||||
|
*
|
||||||
|
* If the user already exists, it will update it.
|
||||||
|
* @param user
|
||||||
|
*/
|
||||||
|
public static async fromVersia(
|
||||||
|
versiaUser: VersiaEntities.User,
|
||||||
|
): Promise<User> {
|
||||||
|
const {
|
||||||
|
username,
|
||||||
|
inbox,
|
||||||
|
avatar,
|
||||||
|
header,
|
||||||
|
display_name,
|
||||||
|
fields,
|
||||||
|
collections,
|
||||||
|
created_at,
|
||||||
|
bio,
|
||||||
|
public_key,
|
||||||
|
uri,
|
||||||
|
extensions,
|
||||||
|
} = versiaUser.data;
|
||||||
|
const instance = await Instance.resolve(versiaUser.data.uri);
|
||||||
|
const existingUser = await User.fromSql(
|
||||||
|
eq(Users.uri, versiaUser.data.uri.href),
|
||||||
|
);
|
||||||
|
|
||||||
const data = {
|
const user =
|
||||||
username: user.data.username,
|
existingUser ??
|
||||||
uri: user.data.uri.href,
|
(await User.insert({
|
||||||
createdAt: new Date(user.data.created_at).toISOString(),
|
username,
|
||||||
endpoints: {
|
id: randomUUIDv7(),
|
||||||
dislikes:
|
publicKey: public_key.key,
|
||||||
user.data.collections["pub.versia:likes/Dislikes"]?.href ??
|
uri: uri.href,
|
||||||
undefined,
|
|
||||||
featured: user.data.collections.featured.href,
|
|
||||||
likes:
|
|
||||||
user.data.collections["pub.versia:likes/Likes"]?.href ??
|
|
||||||
undefined,
|
|
||||||
followers: user.data.collections.followers.href,
|
|
||||||
following: user.data.collections.following.href,
|
|
||||||
inbox: user.data.inbox.href,
|
|
||||||
outbox: user.data.collections.outbox.href,
|
|
||||||
},
|
|
||||||
fields: user.data.fields ?? [],
|
|
||||||
updatedAt: new Date(user.data.created_at).toISOString(),
|
|
||||||
instanceId: instance.id,
|
instanceId: instance.id,
|
||||||
displayName: user.data.display_name ?? "",
|
}));
|
||||||
note: getBestContentType(user.data.bio).content,
|
|
||||||
publicKey: user.data.public_key.key,
|
|
||||||
source: {
|
|
||||||
language: "en",
|
|
||||||
note: "",
|
|
||||||
privacy: "public",
|
|
||||||
sensitive: false,
|
|
||||||
fields: [],
|
|
||||||
} as z.infer<typeof Source>,
|
|
||||||
};
|
|
||||||
|
|
||||||
const userEmojis =
|
// Avatars and headers are stored in a separate table, so we need to update them separately
|
||||||
user.data.extensions?.["pub.versia:custom_emojis"]?.emojis ?? [];
|
let userAvatar: Media | null = null;
|
||||||
|
let userHeader: Media | null = null;
|
||||||
|
|
||||||
|
if (avatar) {
|
||||||
|
if (user.avatar) {
|
||||||
|
userAvatar = new Media(
|
||||||
|
await user.avatar.update({
|
||||||
|
content: avatar,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
userAvatar = await Media.insert({
|
||||||
|
id: randomUUIDv7(),
|
||||||
|
content: avatar,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (header) {
|
||||||
|
if (user.header) {
|
||||||
|
userHeader = new Media(
|
||||||
|
await user.header.update({
|
||||||
|
content: header,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
userHeader = await Media.insert({
|
||||||
|
id: randomUUIDv7(),
|
||||||
|
content: header,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await user.update({
|
||||||
|
createdAt: new Date(created_at).toISOString(),
|
||||||
|
endpoints: {
|
||||||
|
inbox: inbox.href,
|
||||||
|
outbox: collections.outbox.href,
|
||||||
|
followers: collections.followers.href,
|
||||||
|
following: collections.following.href,
|
||||||
|
featured: collections.featured.href,
|
||||||
|
likes: collections["pub.versia:likes/Likes"]?.href,
|
||||||
|
dislikes: collections["pub.versia:likes/Dislikes"]?.href,
|
||||||
|
},
|
||||||
|
avatarId: userAvatar?.id,
|
||||||
|
headerId: userHeader?.id,
|
||||||
|
fields: fields ?? [],
|
||||||
|
displayName: display_name,
|
||||||
|
note: getBestContentType(bio).content,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Emojis are stored in a separate table, so we need to update them separately
|
||||||
const emojis = await Promise.all(
|
const emojis = await Promise.all(
|
||||||
userEmojis.map((emoji) => Emoji.fromVersia(emoji, instance)),
|
extensions?.["pub.versia:custom_emojis"]?.emojis.map((e) =>
|
||||||
|
Emoji.fromVersia(e, instance),
|
||||||
|
) ?? [],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Check if new user already exists
|
await user.updateEmojis(emojis);
|
||||||
const foundUser = await User.fromSql(eq(Users.uri, user.data.uri.href));
|
|
||||||
|
|
||||||
// If it exists, simply update it
|
return user;
|
||||||
if (foundUser) {
|
|
||||||
let avatar: Media | null = null;
|
|
||||||
let header: Media | null = null;
|
|
||||||
|
|
||||||
if (user.data.avatar) {
|
|
||||||
if (foundUser.avatar) {
|
|
||||||
avatar = new Media(
|
|
||||||
await foundUser.avatar.update({
|
|
||||||
content: user.data.avatar,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
avatar = await Media.insert({
|
|
||||||
id: randomUUIDv7(),
|
|
||||||
content: user.data.avatar,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (user.data.header) {
|
|
||||||
if (foundUser.header) {
|
|
||||||
header = new Media(
|
|
||||||
await foundUser.header.update({
|
|
||||||
content: user.data.header,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
header = await Media.insert({
|
|
||||||
id: randomUUIDv7(),
|
|
||||||
content: user.data.header,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await foundUser.update({
|
|
||||||
...data,
|
|
||||||
avatarId: avatar?.id,
|
|
||||||
headerId: header?.id,
|
|
||||||
});
|
|
||||||
await foundUser.updateEmojis(emojis);
|
|
||||||
|
|
||||||
return foundUser;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Else, create a new user
|
|
||||||
const avatar = user.data.avatar
|
|
||||||
? await Media.insert({
|
|
||||||
id: randomUUIDv7(),
|
|
||||||
content: user.data.avatar,
|
|
||||||
})
|
|
||||||
: null;
|
|
||||||
|
|
||||||
const header = user.data.header
|
|
||||||
? await Media.insert({
|
|
||||||
id: randomUUIDv7(),
|
|
||||||
content: user.data.header,
|
|
||||||
})
|
|
||||||
: null;
|
|
||||||
|
|
||||||
const newUser = await User.insert({
|
|
||||||
id: randomUUIDv7(),
|
|
||||||
...data,
|
|
||||||
avatarId: avatar?.id,
|
|
||||||
headerId: header?.id,
|
|
||||||
});
|
|
||||||
await newUser.updateEmojis(emojis);
|
|
||||||
|
|
||||||
return newUser;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async insert(
|
public static async insert(
|
||||||
|
|
@ -879,35 +865,28 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async fromDataLocal(data: {
|
public static async register(
|
||||||
username: string;
|
username: string,
|
||||||
display_name?: string;
|
options?: Partial<{
|
||||||
password: string | undefined;
|
email: string;
|
||||||
email: string | undefined;
|
password: string;
|
||||||
bio?: string;
|
avatar: Media;
|
||||||
avatar?: Media;
|
isAdmin: boolean;
|
||||||
header?: Media;
|
}>,
|
||||||
admin?: boolean;
|
): Promise<User> {
|
||||||
skipPasswordHash?: boolean;
|
|
||||||
}): Promise<User> {
|
|
||||||
const keys = await User.generateKeys();
|
const keys = await User.generateKeys();
|
||||||
|
|
||||||
const newUser = (
|
const user = await User.insert({
|
||||||
await db
|
|
||||||
.insert(Users)
|
|
||||||
.values({
|
|
||||||
id: randomUUIDv7(),
|
id: randomUUIDv7(),
|
||||||
username: data.username,
|
username: username,
|
||||||
displayName: data.display_name ?? data.username,
|
displayName: username,
|
||||||
password:
|
password: options?.password
|
||||||
data.skipPasswordHash || !data.password
|
? await bunPassword.hash(options.password)
|
||||||
? data.password
|
: null,
|
||||||
: await bunPassword.hash(data.password),
|
email: options?.email,
|
||||||
email: data.email,
|
note: "",
|
||||||
note: data.bio ?? "",
|
avatarId: options?.avatar?.id,
|
||||||
avatarId: data.avatar?.id,
|
isAdmin: options?.isAdmin,
|
||||||
headerId: data.header?.id,
|
|
||||||
isAdmin: data.admin ?? false,
|
|
||||||
publicKey: keys.public_key,
|
publicKey: keys.public_key,
|
||||||
fields: [],
|
fields: [],
|
||||||
privateKey: keys.private_key,
|
privateKey: keys.private_key,
|
||||||
|
|
@ -919,20 +898,12 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
||||||
sensitive: false,
|
sensitive: false,
|
||||||
fields: [],
|
fields: [],
|
||||||
} as z.infer<typeof Source>,
|
} as z.infer<typeof Source>,
|
||||||
})
|
});
|
||||||
.returning()
|
|
||||||
)[0];
|
|
||||||
|
|
||||||
const finalUser = await User.fromId(newUser.id);
|
|
||||||
|
|
||||||
if (!finalUser) {
|
|
||||||
throw new Error("Failed to create user");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add to search index
|
// Add to search index
|
||||||
await searchManager.addUser(finalUser);
|
await searchManager.addUser(user);
|
||||||
|
|
||||||
return finalUser;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -1093,7 +1064,7 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
||||||
return {
|
return {
|
||||||
id: user.id,
|
id: user.id,
|
||||||
username: user.username,
|
username: user.username,
|
||||||
display_name: user.displayName,
|
display_name: user.displayName || user.username,
|
||||||
note: user.note,
|
note: user.note,
|
||||||
uri: this.getUri().toString(),
|
uri: this.getUri().toString(),
|
||||||
url:
|
url:
|
||||||
|
|
@ -1119,7 +1090,7 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
||||||
verified_at: null,
|
verified_at: null,
|
||||||
})),
|
})),
|
||||||
bot: user.isBot,
|
bot: user.isBot,
|
||||||
source: isOwnAccount ? user.source : undefined,
|
source: isOwnAccount ? (user.source ?? undefined) : undefined,
|
||||||
// TODO: Add static avatar and header
|
// TODO: Add static avatar and header
|
||||||
avatar_static: this.getAvatarUrl().proxied,
|
avatar_static: this.getAvatarUrl().proxied,
|
||||||
header_static: this.getHeaderUrl()?.proxied ?? "",
|
header_static: this.getHeaderUrl()?.proxied ?? "",
|
||||||
|
|
|
||||||
|
|
@ -155,7 +155,7 @@ export class SonicSearchManager {
|
||||||
private static getNthDatabaseAccountBatch(
|
private static getNthDatabaseAccountBatch(
|
||||||
n: number,
|
n: number,
|
||||||
batchSize = 1000,
|
batchSize = 1000,
|
||||||
): Promise<Record<string, string | Date>[]> {
|
): Promise<Record<string, string | null | Date>[]> {
|
||||||
return db.query.Users.findMany({
|
return db.query.Users.findMany({
|
||||||
offset: n * batchSize,
|
offset: n * batchSize,
|
||||||
limit: batchSize,
|
limit: batchSize,
|
||||||
|
|
|
||||||
|
|
@ -48,11 +48,10 @@ export const createUserCommand = defineCommand(
|
||||||
throw new Error(`User ${chalk.gray(username)} is taken.`);
|
throw new Error(`User ${chalk.gray(username)} is taken.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await User.fromDataLocal({
|
const user = await User.register(username, {
|
||||||
email,
|
email,
|
||||||
password,
|
password,
|
||||||
username,
|
isAdmin: admin,
|
||||||
admin,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
|
|
|
||||||
|
|
@ -556,7 +556,7 @@ export const Users = pgTable(
|
||||||
id: id(),
|
id: id(),
|
||||||
uri: uri(),
|
uri: uri(),
|
||||||
username: text("username").notNull(),
|
username: text("username").notNull(),
|
||||||
displayName: text("display_name").notNull(),
|
displayName: text("display_name"),
|
||||||
password: text("password"),
|
password: text("password"),
|
||||||
email: text("email"),
|
email: text("email"),
|
||||||
note: text("note").default("").notNull(),
|
note: text("note").default("").notNull(),
|
||||||
|
|
@ -578,7 +578,7 @@ export const Users = pgTable(
|
||||||
inbox: string;
|
inbox: string;
|
||||||
outbox: string;
|
outbox: string;
|
||||||
}> | null>(),
|
}> | null>(),
|
||||||
source: jsonb("source").notNull().$type<z.infer<typeof Source>>(),
|
source: jsonb("source").$type<z.infer<typeof Source>>(),
|
||||||
avatarId: uuid("avatarId").references(() => Medias.id, {
|
avatarId: uuid("avatarId").references(() => Medias.id, {
|
||||||
onDelete: "set null",
|
onDelete: "set null",
|
||||||
onUpdate: "cascade",
|
onUpdate: "cascade",
|
||||||
|
|
|
||||||
|
|
@ -235,11 +235,9 @@ export default (plugin: PluginType): void => {
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
// Create new user
|
// Create new user
|
||||||
const user = await User.fromDataLocal({
|
const user = await User.register(username, {
|
||||||
email: doesEmailExist ? undefined : email,
|
email: doesEmailExist ? undefined : email,
|
||||||
username,
|
|
||||||
avatar: avatar ?? undefined,
|
avatar: avatar ?? undefined,
|
||||||
password: undefined,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Link account
|
// Link account
|
||||||
|
|
|
||||||
|
|
@ -103,8 +103,7 @@ export const getTestUsers = async (
|
||||||
for (let i = 0; i < count; i++) {
|
for (let i = 0; i < count; i++) {
|
||||||
const password = randomString(32, "hex");
|
const password = randomString(32, "hex");
|
||||||
|
|
||||||
const user = await User.fromDataLocal({
|
const user = await User.register(`test-${randomString(8, "hex")}`, {
|
||||||
username: `test-${randomString(8, "hex")}`,
|
|
||||||
email: `${randomString(16, "hex")}@test.com`,
|
email: `${randomString(16, "hex")}@test.com`,
|
||||||
password,
|
password,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue