mirror of
https://github.com/versia-pub/server.git
synced 2026-03-13 05:49:16 +01:00
refactor(api): 🎨 Move User methods into their own class similar to Note
This commit is contained in:
parent
abc8f1ae16
commit
9d70778abd
81 changed files with 2055 additions and 1603 deletions
|
|
@ -5,6 +5,7 @@ import {
|
|||
desc,
|
||||
eq,
|
||||
inArray,
|
||||
isNotNull,
|
||||
} from "drizzle-orm";
|
||||
import { htmlToText } from "html-to-text";
|
||||
import type * as Lysand from "lysand-types";
|
||||
|
|
@ -30,17 +31,7 @@ import {
|
|||
contentToHtml,
|
||||
findFirstNote,
|
||||
findManyNotes,
|
||||
getStatusUri,
|
||||
} from "~database/entities/Status";
|
||||
import {
|
||||
type User,
|
||||
type UserWithRelations,
|
||||
type UserWithRelationsAndRelationships,
|
||||
findManyUsers,
|
||||
getUserUri,
|
||||
userToAPI,
|
||||
userToMention,
|
||||
} from "~database/entities/User";
|
||||
import { db } from "~drizzle/db";
|
||||
import {
|
||||
Attachments,
|
||||
|
|
@ -48,13 +39,12 @@ import {
|
|||
NoteToMentions,
|
||||
Notes,
|
||||
Notifications,
|
||||
UserToPinnedNotes,
|
||||
Users,
|
||||
UsersRelations,
|
||||
} from "~drizzle/schema";
|
||||
import { config } from "~packages/config-manager";
|
||||
import type { Attachment as APIAttachment } from "~types/mastodon/attachment";
|
||||
import type { Status as APIStatus } from "~types/mastodon/status";
|
||||
import { User } from "./user";
|
||||
|
||||
/**
|
||||
* Gives helpers to fetch notes from database in a nice format
|
||||
|
|
@ -101,36 +91,44 @@ export class Note {
|
|||
return found.map((s) => new Note(s));
|
||||
}
|
||||
|
||||
get id() {
|
||||
return this.status.id;
|
||||
}
|
||||
|
||||
async getUsersToFederateTo() {
|
||||
// Mentioned users
|
||||
const mentionedUsers =
|
||||
this.getStatus().mentions.length > 0
|
||||
? await findManyUsers({
|
||||
where: (user, { and, isNotNull, inArray }) =>
|
||||
and(
|
||||
isNotNull(user.instanceId),
|
||||
inArray(
|
||||
user.id,
|
||||
this.getStatus().mentions.map(
|
||||
(mention) => mention.id,
|
||||
),
|
||||
? await User.manyFromSql(
|
||||
and(
|
||||
isNotNull(Users.instanceId),
|
||||
inArray(
|
||||
Users.id,
|
||||
this.getStatus().mentions.map(
|
||||
(mention) => mention.id,
|
||||
),
|
||||
),
|
||||
})
|
||||
),
|
||||
)
|
||||
: [];
|
||||
|
||||
const usersThatCanSeePost = await findManyUsers({
|
||||
where: (user, { isNotNull }) => isNotNull(user.instanceId),
|
||||
with: {
|
||||
relationships: {
|
||||
where: (relationship, { eq, and }) =>
|
||||
and(
|
||||
eq(relationship.subjectId, Users.id),
|
||||
eq(relationship.following, true),
|
||||
),
|
||||
const usersThatCanSeePost = await User.manyFromSql(
|
||||
isNotNull(Users.instanceId),
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
{
|
||||
with: {
|
||||
relationships: {
|
||||
where: (relationship, { eq, and }) =>
|
||||
and(
|
||||
eq(relationship.subjectId, Users.id),
|
||||
eq(relationship.following, true),
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
);
|
||||
|
||||
const fusedUsers = [...mentionedUsers, ...usersThatCanSeePost];
|
||||
|
||||
|
|
@ -159,39 +157,13 @@ export class Note {
|
|||
}
|
||||
|
||||
getAuthor() {
|
||||
return this.status.author;
|
||||
return new User(this.status.author);
|
||||
}
|
||||
|
||||
async getReplyChildren() {
|
||||
return await Note.manyFromSql(eq(Notes.replyId, this.status.id));
|
||||
}
|
||||
|
||||
async pin(pinner: User) {
|
||||
return (
|
||||
await db
|
||||
.insert(UserToPinnedNotes)
|
||||
.values({
|
||||
noteId: this.status.id,
|
||||
userId: pinner.id,
|
||||
})
|
||||
.returning()
|
||||
)[0];
|
||||
}
|
||||
|
||||
async unpin(unpinner: User) {
|
||||
return (
|
||||
await db
|
||||
.delete(UserToPinnedNotes)
|
||||
.where(
|
||||
and(
|
||||
eq(NoteToMentions.noteId, this.status.id),
|
||||
eq(NoteToMentions.userId, unpinner.id),
|
||||
),
|
||||
)
|
||||
.returning()
|
||||
)[0];
|
||||
}
|
||||
|
||||
static async insert(values: InferInsertModel<typeof Notes>) {
|
||||
return (await db.insert(Notes).values(values).returning())[0];
|
||||
}
|
||||
|
|
@ -204,7 +176,7 @@ export class Note {
|
|||
spoiler_text: string,
|
||||
emojis: EmojiWithInstance[],
|
||||
uri?: string,
|
||||
mentions?: UserWithRelations[],
|
||||
mentions?: User[],
|
||||
/** List of IDs of database Attachment objects */
|
||||
media_attachments?: string[],
|
||||
replyId?: string,
|
||||
|
|
@ -216,7 +188,7 @@ export class Note {
|
|||
// Parse emojis and fuse with existing emojis
|
||||
let foundEmojis = emojis;
|
||||
|
||||
if (author.instanceId === null) {
|
||||
if (author.isLocal()) {
|
||||
const parsedEmojis = await parseEmojis(htmlContent);
|
||||
// Fuse and deduplicate
|
||||
foundEmojis = [...emojis, ...parsedEmojis].filter(
|
||||
|
|
@ -277,7 +249,7 @@ export class Note {
|
|||
|
||||
// Send notifications for mentioned local users
|
||||
for (const mention of mentions ?? []) {
|
||||
if (mention.instanceId === null) {
|
||||
if (mention.isLocal()) {
|
||||
await db.insert(Notifications).values({
|
||||
accountId: author.id,
|
||||
notifiedId: mention.id,
|
||||
|
|
@ -296,7 +268,7 @@ export class Note {
|
|||
is_sensitive?: boolean,
|
||||
spoiler_text?: string,
|
||||
emojis: EmojiWithInstance[] = [],
|
||||
mentions: UserWithRelations[] = [],
|
||||
mentions: User[] = [],
|
||||
/** List of IDs of database Attachment objects */
|
||||
media_attachments: string[] = [],
|
||||
) {
|
||||
|
|
@ -307,7 +279,7 @@ export class Note {
|
|||
// Parse emojis and fuse with existing emojis
|
||||
let foundEmojis = emojis;
|
||||
|
||||
if (this.getAuthor().instanceId === null && htmlContent) {
|
||||
if (this.getAuthor().isLocal() && htmlContent) {
|
||||
const parsedEmojis = await parseEmojis(htmlContent);
|
||||
// Fuse and deduplicate
|
||||
foundEmojis = [...emojis, ...parsedEmojis].filter(
|
||||
|
|
@ -401,7 +373,7 @@ export class Note {
|
|||
* @param user The user to check.
|
||||
* @returns Whether this status is viewable by the user.
|
||||
*/
|
||||
async isViewableByUser(user: UserWithRelations | null) {
|
||||
async isViewableByUser(user: User | null) {
|
||||
if (this.getAuthor().id === user?.id) return true;
|
||||
if (this.getStatus().visibility === "public") return true;
|
||||
if (this.getStatus().visibility === "unlisted") return true;
|
||||
|
|
@ -423,7 +395,7 @@ export class Note {
|
|||
);
|
||||
}
|
||||
|
||||
async toAPI(userFetching?: UserWithRelations | null): Promise<APIStatus> {
|
||||
async toAPI(userFetching?: User | null): Promise<APIStatus> {
|
||||
const data = this.getStatus();
|
||||
const wasPinnedByUser = userFetching
|
||||
? !!(await db.query.UserToPinnedNotes.findFirst({
|
||||
|
|
@ -480,7 +452,7 @@ export class Note {
|
|||
id: data.id,
|
||||
in_reply_to_id: data.replyId || null,
|
||||
in_reply_to_account_id: data.reply?.authorId || null,
|
||||
account: userToAPI(data.author),
|
||||
account: this.getAuthor().toAPI(userFetching?.id === data.authorId),
|
||||
created_at: new Date(data.createdAt).toISOString(),
|
||||
application: data.application
|
||||
? applicationToAPI(data.application)
|
||||
|
|
@ -495,7 +467,16 @@ export class Note {
|
|||
media_attachments: (data.attachments ?? []).map(
|
||||
(a) => attachmentToAPI(a) as APIAttachment,
|
||||
),
|
||||
mentions: data.mentions.map((mention) => userToMention(mention)),
|
||||
mentions: data.mentions.map((mention) => ({
|
||||
id: mention.id,
|
||||
acct: User.getAcct(
|
||||
mention.instanceId === null,
|
||||
mention.username,
|
||||
mention.instance?.baseUrl,
|
||||
),
|
||||
url: User.getUri(mention.id, mention.uri, config.http.base_url),
|
||||
username: mention.username,
|
||||
})),
|
||||
language: null,
|
||||
muted: wasMutedByUser,
|
||||
pinned: wasPinnedByUser,
|
||||
|
|
@ -531,8 +512,13 @@ export class Note {
|
|||
return localObjectURI(this.getStatus().id);
|
||||
}
|
||||
|
||||
static getURI(id?: string | null) {
|
||||
if (!id) return null;
|
||||
return localObjectURI(id);
|
||||
}
|
||||
|
||||
getMastoURI() {
|
||||
return `/@${this.getAuthor().username}/${this.getStatus().id}`;
|
||||
return `/@${this.getAuthor().getUser().username}/${this.id}`;
|
||||
}
|
||||
|
||||
toLysand(): Lysand.Note {
|
||||
|
|
@ -541,7 +527,7 @@ export class Note {
|
|||
type: "Note",
|
||||
created_at: new Date(status.createdAt).toISOString(),
|
||||
id: status.id,
|
||||
author: getUserUri(status.author),
|
||||
author: this.getAuthor().getUri(),
|
||||
uri: this.getURI(),
|
||||
content: {
|
||||
"text/html": {
|
||||
|
|
@ -556,8 +542,8 @@ export class Note {
|
|||
),
|
||||
is_sensitive: status.sensitive,
|
||||
mentions: status.mentions.map((mention) => mention.uri || ""),
|
||||
quotes: getStatusUri(status.quote) ?? undefined,
|
||||
replies_to: getStatusUri(status.reply) ?? undefined,
|
||||
quotes: Note.getURI(status.quotingId) ?? undefined,
|
||||
replies_to: Note.getURI(status.replyId) ?? undefined,
|
||||
subject: status.spoilerText,
|
||||
visibility: status.visibility as Lysand.Visibility,
|
||||
extensions: {
|
||||
|
|
@ -572,7 +558,7 @@ export class Note {
|
|||
/**
|
||||
* Return all the ancestors of this post,
|
||||
*/
|
||||
async getAncestors(fetcher: UserWithRelationsAndRelationships | null) {
|
||||
async getAncestors(fetcher: User | null) {
|
||||
const ancestors: Note[] = [];
|
||||
|
||||
let currentStatus: Note = this;
|
||||
|
|
@ -599,10 +585,7 @@ export class Note {
|
|||
* Return all the descendants of this post (recursive)
|
||||
* Temporary implementation, will be replaced with a recursive SQL query when I get to it
|
||||
*/
|
||||
async getDescendants(
|
||||
fetcher: UserWithRelationsAndRelationships | null,
|
||||
depth = 0,
|
||||
) {
|
||||
async getDescendants(fetcher: User | null, depth = 0) {
|
||||
const descendants: Note[] = [];
|
||||
for (const child of await this.getReplyChildren()) {
|
||||
descendants.push(child);
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
import { type SQL, gt } from "drizzle-orm";
|
||||
import { Notes } from "~drizzle/schema";
|
||||
import { Notes, Users } from "~drizzle/schema";
|
||||
import { config } from "~packages/config-manager";
|
||||
import { Note } from "./note";
|
||||
import { User } from "./user";
|
||||
|
||||
enum TimelineType {
|
||||
NOTE = "Note",
|
||||
USER = "User",
|
||||
}
|
||||
|
||||
export class Timeline {
|
||||
|
|
@ -15,7 +17,23 @@ export class Timeline {
|
|||
limit: number,
|
||||
url: string,
|
||||
) {
|
||||
return new Timeline(TimelineType.NOTE).fetchTimeline(sql, limit, url);
|
||||
return new Timeline(TimelineType.NOTE).fetchTimeline<Note>(
|
||||
sql,
|
||||
limit,
|
||||
url,
|
||||
);
|
||||
}
|
||||
|
||||
static async getUserTimeline(
|
||||
sql: SQL<unknown> | undefined,
|
||||
limit: number,
|
||||
url: string,
|
||||
) {
|
||||
return new Timeline(TimelineType.USER).fetchTimeline<User>(
|
||||
sql,
|
||||
limit,
|
||||
url,
|
||||
);
|
||||
}
|
||||
|
||||
private async fetchTimeline<T>(
|
||||
|
|
@ -23,13 +41,15 @@ export class Timeline {
|
|||
limit: number,
|
||||
url: string,
|
||||
) {
|
||||
const objects: Note[] = [];
|
||||
const notes: Note[] = [];
|
||||
const users: User[] = [];
|
||||
|
||||
switch (this.type) {
|
||||
case TimelineType.NOTE:
|
||||
objects.push(
|
||||
...(await Note.manyFromSql(sql, undefined, limit)),
|
||||
);
|
||||
notes.push(...(await Note.manyFromSql(sql, undefined, limit)));
|
||||
break;
|
||||
case TimelineType.USER:
|
||||
users.push(...(await User.manyFromSql(sql, undefined, limit)));
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
@ -39,26 +59,26 @@ export class Timeline {
|
|||
config.http.base_url,
|
||||
).toString();
|
||||
|
||||
if (objects.length > 0) {
|
||||
if (notes.length > 0) {
|
||||
switch (this.type) {
|
||||
case TimelineType.NOTE: {
|
||||
const objectBefore = await Note.fromSql(
|
||||
gt(Notes.id, objects[0].getStatus().id),
|
||||
gt(Notes.id, notes[0].getStatus().id),
|
||||
);
|
||||
|
||||
if (objectBefore) {
|
||||
linkHeader.push(
|
||||
`<${urlWithoutQuery}?limit=${limit ?? 20}&min_id=${
|
||||
objects[0].getStatus().id
|
||||
notes[0].getStatus().id
|
||||
}>; rel="prev"`,
|
||||
);
|
||||
}
|
||||
|
||||
if (objects.length >= (limit ?? 20)) {
|
||||
if (notes.length >= (limit ?? 20)) {
|
||||
const objectAfter = await Note.fromSql(
|
||||
gt(
|
||||
Notes.id,
|
||||
objects[objects.length - 1].getStatus().id,
|
||||
notes[notes.length - 1].getStatus().id,
|
||||
),
|
||||
);
|
||||
|
||||
|
|
@ -67,7 +87,37 @@ export class Timeline {
|
|||
`<${urlWithoutQuery}?limit=${
|
||||
limit ?? 20
|
||||
}&max_id=${
|
||||
objects[objects.length - 1].getStatus().id
|
||||
notes[notes.length - 1].getStatus().id
|
||||
}>; rel="next"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case TimelineType.USER: {
|
||||
const objectBefore = await User.fromSql(
|
||||
gt(Users.id, users[0].id),
|
||||
);
|
||||
|
||||
if (objectBefore) {
|
||||
linkHeader.push(
|
||||
`<${urlWithoutQuery}?limit=${limit ?? 20}&min_id=${
|
||||
users[0].id
|
||||
}>; rel="prev"`,
|
||||
);
|
||||
}
|
||||
|
||||
if (users.length >= (limit ?? 20)) {
|
||||
const objectAfter = await User.fromSql(
|
||||
gt(Users.id, users[users.length - 1].id),
|
||||
);
|
||||
|
||||
if (objectAfter) {
|
||||
linkHeader.push(
|
||||
`<${urlWithoutQuery}?limit=${
|
||||
limit ?? 20
|
||||
}&max_id=${
|
||||
users[users.length - 1].id
|
||||
}>; rel="next"`,
|
||||
);
|
||||
}
|
||||
|
|
@ -77,9 +127,17 @@ export class Timeline {
|
|||
}
|
||||
}
|
||||
|
||||
return {
|
||||
link: linkHeader.join(", "),
|
||||
objects,
|
||||
};
|
||||
switch (this.type) {
|
||||
case TimelineType.NOTE:
|
||||
return {
|
||||
link: linkHeader.join(", "),
|
||||
objects: notes as T[],
|
||||
};
|
||||
case TimelineType.USER:
|
||||
return {
|
||||
link: linkHeader.join(", "),
|
||||
objects: users as T[],
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
421
packages/database-interface/user.ts
Normal file
421
packages/database-interface/user.ts
Normal file
|
|
@ -0,0 +1,421 @@
|
|||
import { idValidator } from "@api";
|
||||
import { getBestContentType, urlToContentFormat } from "@content_types";
|
||||
import { addUserToMeilisearch } from "@meilisearch";
|
||||
import { type SQL, and, desc, eq, inArray } from "drizzle-orm";
|
||||
import { htmlToText } from "html-to-text";
|
||||
import type * as Lysand from "lysand-types";
|
||||
import {
|
||||
emojiToAPI,
|
||||
emojiToLysand,
|
||||
fetchEmoji,
|
||||
} from "~database/entities/Emoji";
|
||||
import { addInstanceIfNotExists } from "~database/entities/Instance";
|
||||
import {
|
||||
type UserWithRelations,
|
||||
findFirstUser,
|
||||
findManyUsers,
|
||||
} from "~database/entities/User";
|
||||
import { db } from "~drizzle/db";
|
||||
import {
|
||||
EmojiToUser,
|
||||
NoteToMentions,
|
||||
UserToPinnedNotes,
|
||||
Users,
|
||||
} from "~drizzle/schema";
|
||||
import { type Config, config } from "~packages/config-manager";
|
||||
import type { Account as APIAccount } from "~types/mastodon/account";
|
||||
import type { Mention as APIMention } from "~types/mastodon/mention";
|
||||
import type { Note } from "./note";
|
||||
|
||||
/**
|
||||
* Gives helpers to fetch users from database in a nice format
|
||||
*/
|
||||
export class User {
|
||||
constructor(private user: UserWithRelations) {}
|
||||
|
||||
static async fromId(id: string | null): Promise<User | null> {
|
||||
if (!id) return null;
|
||||
|
||||
return await User.fromSql(eq(Users.id, id));
|
||||
}
|
||||
|
||||
static async fromIds(ids: string[]): Promise<User[]> {
|
||||
return await User.manyFromSql(inArray(Users.id, ids));
|
||||
}
|
||||
|
||||
static async fromSql(
|
||||
sql: SQL<unknown> | undefined,
|
||||
orderBy: SQL<unknown> | undefined = desc(Users.id),
|
||||
) {
|
||||
const found = await findFirstUser({
|
||||
where: sql,
|
||||
orderBy,
|
||||
});
|
||||
|
||||
if (!found) return null;
|
||||
return new User(found);
|
||||
}
|
||||
|
||||
static async manyFromSql(
|
||||
sql: SQL<unknown> | undefined,
|
||||
orderBy: SQL<unknown> | undefined = desc(Users.id),
|
||||
limit?: number,
|
||||
offset?: number,
|
||||
extra?: Parameters<typeof db.query.Users.findMany>[0],
|
||||
) {
|
||||
const found = await findManyUsers({
|
||||
where: sql,
|
||||
orderBy,
|
||||
limit,
|
||||
offset,
|
||||
with: extra?.with,
|
||||
});
|
||||
|
||||
return found.map((s) => new User(s));
|
||||
}
|
||||
|
||||
get id() {
|
||||
return this.user.id;
|
||||
}
|
||||
|
||||
getUser() {
|
||||
return this.user;
|
||||
}
|
||||
|
||||
isLocal() {
|
||||
return this.user.instanceId === null;
|
||||
}
|
||||
|
||||
isRemote() {
|
||||
return !this.isLocal();
|
||||
}
|
||||
|
||||
getUri() {
|
||||
return (
|
||||
this.user.uri ||
|
||||
new URL(`/users/${this.user.id}`, config.http.base_url).toString()
|
||||
);
|
||||
}
|
||||
|
||||
static getUri(id: string, uri: string | null, baseUrl: string) {
|
||||
return uri || new URL(`/users/${id}`, baseUrl).toString();
|
||||
}
|
||||
|
||||
async pin(note: Note) {
|
||||
return (
|
||||
await db
|
||||
.insert(UserToPinnedNotes)
|
||||
.values({
|
||||
noteId: note.id,
|
||||
userId: this.id,
|
||||
})
|
||||
.returning()
|
||||
)[0];
|
||||
}
|
||||
|
||||
async unpin(note: Note) {
|
||||
return (
|
||||
await db
|
||||
.delete(UserToPinnedNotes)
|
||||
.where(
|
||||
and(
|
||||
eq(NoteToMentions.noteId, note.id),
|
||||
eq(NoteToMentions.userId, this.id),
|
||||
),
|
||||
)
|
||||
.returning()
|
||||
)[0];
|
||||
}
|
||||
|
||||
static async resolve(uri: string): Promise<User | null> {
|
||||
// Check if user not already in database
|
||||
const foundUser = await User.fromSql(eq(Users.uri, uri));
|
||||
|
||||
if (foundUser) return foundUser;
|
||||
|
||||
// Check if URI is of a local user
|
||||
if (uri.startsWith(config.http.base_url)) {
|
||||
const uuid = uri.match(idValidator);
|
||||
|
||||
if (!uuid || !uuid[0]) {
|
||||
throw new Error(
|
||||
`URI ${uri} is of a local user, but it could not be parsed`,
|
||||
);
|
||||
}
|
||||
|
||||
return await User.fromId(uuid[0]);
|
||||
}
|
||||
|
||||
if (!URL.canParse(uri)) {
|
||||
throw new Error(`Invalid URI to parse ${uri}`);
|
||||
}
|
||||
|
||||
const response = await fetch(uri, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
const data = (await response.json()) as Partial<Lysand.User>;
|
||||
|
||||
if (
|
||||
!(
|
||||
data.id &&
|
||||
data.username &&
|
||||
data.uri &&
|
||||
data.created_at &&
|
||||
data.dislikes &&
|
||||
data.featured &&
|
||||
data.likes &&
|
||||
data.followers &&
|
||||
data.following &&
|
||||
data.inbox &&
|
||||
data.outbox &&
|
||||
data.public_key
|
||||
)
|
||||
) {
|
||||
throw new Error("Invalid user data");
|
||||
}
|
||||
|
||||
// Parse emojis and add them to database
|
||||
const userEmojis =
|
||||
data.extensions?.["org.lysand:custom_emojis"]?.emojis ?? [];
|
||||
|
||||
const instance = await addInstanceIfNotExists(data.uri);
|
||||
|
||||
const emojis = [];
|
||||
|
||||
for (const emoji of userEmojis) {
|
||||
emojis.push(await fetchEmoji(emoji));
|
||||
}
|
||||
|
||||
const newUser = (
|
||||
await db
|
||||
.insert(Users)
|
||||
.values({
|
||||
username: data.username,
|
||||
uri: data.uri,
|
||||
createdAt: new Date(data.created_at).toISOString(),
|
||||
endpoints: {
|
||||
dislikes: data.dislikes,
|
||||
featured: data.featured,
|
||||
likes: data.likes,
|
||||
followers: data.followers,
|
||||
following: data.following,
|
||||
inbox: data.inbox,
|
||||
outbox: data.outbox,
|
||||
},
|
||||
updatedAt: new Date(data.created_at).toISOString(),
|
||||
instanceId: instance.id,
|
||||
avatar: data.avatar
|
||||
? Object.entries(data.avatar)[0][1].content
|
||||
: "",
|
||||
header: data.header
|
||||
? Object.entries(data.header)[0][1].content
|
||||
: "",
|
||||
displayName: data.display_name ?? "",
|
||||
note: getBestContentType(data.bio).content,
|
||||
publicKey: data.public_key.public_key,
|
||||
source: {
|
||||
language: null,
|
||||
note: "",
|
||||
privacy: "public",
|
||||
sensitive: false,
|
||||
fields: [],
|
||||
},
|
||||
})
|
||||
.returning()
|
||||
)[0];
|
||||
|
||||
// Add emojis to user
|
||||
if (emojis.length > 0) {
|
||||
await db.insert(EmojiToUser).values(
|
||||
emojis.map((emoji) => ({
|
||||
emojiId: emoji.id,
|
||||
userId: newUser.id,
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
const finalUser = await User.fromId(newUser.id);
|
||||
|
||||
if (!finalUser) return null;
|
||||
|
||||
// Add to Meilisearch
|
||||
await addUserToMeilisearch(finalUser);
|
||||
|
||||
return finalUser;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user's avatar in raw URL format
|
||||
* @param config The config to use
|
||||
* @returns The raw URL for the user's avatar
|
||||
*/
|
||||
getAvatarUrl(config: Config) {
|
||||
if (!this.user.avatar)
|
||||
return (
|
||||
config.defaults.avatar ||
|
||||
`https://api.dicebear.com/8.x/${config.defaults.placeholder_style}/svg?seed=${this.user.username}`
|
||||
);
|
||||
return this.user.avatar;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user's header in raw URL format
|
||||
* @param config The config to use
|
||||
* @returns The raw URL for the user's header
|
||||
*/
|
||||
getHeaderUrl(config: Config) {
|
||||
if (!this.user.header) return config.defaults.header;
|
||||
return this.user.header;
|
||||
}
|
||||
|
||||
getAcct() {
|
||||
return this.isLocal()
|
||||
? this.user.username
|
||||
: `${this.user.username}@${this.user.instance?.baseUrl}`;
|
||||
}
|
||||
|
||||
static getAcct(isLocal: boolean, username: string, baseUrl?: string) {
|
||||
return isLocal ? username : `${username}@${baseUrl}`;
|
||||
}
|
||||
|
||||
toAPI(isOwnAccount = false): APIAccount {
|
||||
const user = this.getUser();
|
||||
return {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
display_name: user.displayName,
|
||||
note: user.note,
|
||||
url:
|
||||
user.uri ||
|
||||
new URL(`/@${user.username}`, config.http.base_url).toString(),
|
||||
avatar: this.getAvatarUrl(config),
|
||||
header: this.getHeaderUrl(config),
|
||||
locked: user.isLocked,
|
||||
created_at: new Date(user.createdAt).toISOString(),
|
||||
followers_count: user.followerCount,
|
||||
following_count: user.followingCount,
|
||||
statuses_count: user.statusCount,
|
||||
emojis: user.emojis.map((emoji) => emojiToAPI(emoji)),
|
||||
// TODO: Add fields
|
||||
fields: [],
|
||||
bot: user.isBot,
|
||||
source: isOwnAccount ? user.source : undefined,
|
||||
// TODO: Add static avatar and header
|
||||
avatar_static: this.getAvatarUrl(config),
|
||||
header_static: this.getHeaderUrl(config),
|
||||
acct: this.getAcct(),
|
||||
// TODO: Add these fields
|
||||
limited: false,
|
||||
moved: null,
|
||||
noindex: false,
|
||||
suspended: false,
|
||||
discoverable: undefined,
|
||||
mute_expires_at: undefined,
|
||||
group: false,
|
||||
// @ts-expect-error Pleroma extension
|
||||
pleroma: {
|
||||
is_admin: user.isAdmin,
|
||||
is_moderator: user.isAdmin,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
toLysand(): Lysand.User {
|
||||
if (this.isRemote()) {
|
||||
throw new Error("Cannot convert remote user to Lysand format");
|
||||
}
|
||||
|
||||
const user = this.getUser();
|
||||
|
||||
return {
|
||||
id: user.id,
|
||||
type: "User",
|
||||
uri: this.getUri(),
|
||||
bio: {
|
||||
"text/html": {
|
||||
content: user.note,
|
||||
},
|
||||
"text/plain": {
|
||||
content: htmlToText(user.note),
|
||||
},
|
||||
},
|
||||
created_at: new Date(user.createdAt).toISOString(),
|
||||
dislikes: new URL(
|
||||
`/users/${user.id}/dislikes`,
|
||||
config.http.base_url,
|
||||
).toString(),
|
||||
featured: new URL(
|
||||
`/users/${user.id}/featured`,
|
||||
config.http.base_url,
|
||||
).toString(),
|
||||
likes: new URL(
|
||||
`/users/${user.id}/likes`,
|
||||
config.http.base_url,
|
||||
).toString(),
|
||||
followers: new URL(
|
||||
`/users/${user.id}/followers`,
|
||||
config.http.base_url,
|
||||
).toString(),
|
||||
following: new URL(
|
||||
`/users/${user.id}/following`,
|
||||
config.http.base_url,
|
||||
).toString(),
|
||||
inbox: new URL(
|
||||
`/users/${user.id}/inbox`,
|
||||
config.http.base_url,
|
||||
).toString(),
|
||||
outbox: new URL(
|
||||
`/users/${user.id}/outbox`,
|
||||
config.http.base_url,
|
||||
).toString(),
|
||||
indexable: false,
|
||||
username: user.username,
|
||||
avatar: urlToContentFormat(this.getAvatarUrl(config)) ?? undefined,
|
||||
header: urlToContentFormat(this.getHeaderUrl(config)) ?? undefined,
|
||||
display_name: user.displayName,
|
||||
fields: user.source.fields.map((field) => ({
|
||||
key: {
|
||||
"text/html": {
|
||||
content: field.name,
|
||||
},
|
||||
"text/plain": {
|
||||
content: htmlToText(field.name),
|
||||
},
|
||||
},
|
||||
value: {
|
||||
"text/html": {
|
||||
content: field.value,
|
||||
},
|
||||
"text/plain": {
|
||||
content: htmlToText(field.value),
|
||||
},
|
||||
},
|
||||
})),
|
||||
public_key: {
|
||||
actor: new URL(
|
||||
`/users/${user.id}`,
|
||||
config.http.base_url,
|
||||
).toString(),
|
||||
public_key: user.publicKey,
|
||||
},
|
||||
extensions: {
|
||||
"org.lysand:custom_emojis": {
|
||||
emojis: user.emojis.map((emoji) => emojiToLysand(emoji)),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
toMention(): APIMention {
|
||||
return {
|
||||
url: this.getUri(),
|
||||
username: this.getUser().username,
|
||||
acct: this.getAcct(),
|
||||
id: this.id,
|
||||
};
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue