mirror of
https://github.com/versia-pub/server.git
synced 2025-12-06 08:28:19 +01:00
refactor(api): 🎨 Move User methods into their own class similar to Note
This commit is contained in:
parent
abc8f1ae16
commit
9d70778abd
76
cli.ts
76
cli.ts
|
|
@ -13,15 +13,11 @@ import extract from "extract-zip";
|
||||||
import { MediaBackend } from "media-manager";
|
import { MediaBackend } from "media-manager";
|
||||||
import { lookup } from "mime-types";
|
import { lookup } from "mime-types";
|
||||||
import { getUrl } from "~database/entities/Attachment";
|
import { getUrl } from "~database/entities/Attachment";
|
||||||
import {
|
import { type UserType, createNewLocalUser } from "~database/entities/User";
|
||||||
type User,
|
|
||||||
createNewLocalUser,
|
|
||||||
findFirstUser,
|
|
||||||
findManyUsers,
|
|
||||||
} from "~database/entities/User";
|
|
||||||
import { client, db } from "~drizzle/db";
|
import { client, db } from "~drizzle/db";
|
||||||
import { Emojis, Notes, OpenIdAccounts, Users } from "~drizzle/schema";
|
import { Emojis, Notes, OpenIdAccounts, Users } from "~drizzle/schema";
|
||||||
import { Note } from "~packages/database-interface/note";
|
import { Note } from "~packages/database-interface/note";
|
||||||
|
import { User } from "~packages/database-interface/user";
|
||||||
|
|
||||||
await client.connect();
|
await client.connect();
|
||||||
const args = process.argv;
|
const args = process.argv;
|
||||||
|
|
@ -111,13 +107,12 @@ const cliBuilder = new CliBuilder([
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if user already exists
|
// Check if user already exists
|
||||||
const user = await findFirstUser({
|
const user = await User.fromSql(
|
||||||
where: (user, { or, eq }) =>
|
or(eq(Users.username, username), eq(Users.email, email)),
|
||||||
or(eq(user.username, username), eq(user.email, email)),
|
);
|
||||||
});
|
|
||||||
|
|
||||||
if (user) {
|
if (user) {
|
||||||
if (user.username === username) {
|
if (user.getUser().username === username) {
|
||||||
console.log(
|
console.log(
|
||||||
`${chalk.red("✗")} User with username ${chalk.blue(
|
`${chalk.red("✗")} User with username ${chalk.blue(
|
||||||
username,
|
username,
|
||||||
|
|
@ -143,7 +138,7 @@ const cliBuilder = new CliBuilder([
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
`${chalk.green("✓")} Created user ${chalk.blue(
|
`${chalk.green("✓")} Created user ${chalk.blue(
|
||||||
newUser?.username,
|
newUser?.getUser().username,
|
||||||
)}${admin ? chalk.green(" (admin)") : ""}`,
|
)}${admin ? chalk.green(" (admin)") : ""}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -196,9 +191,7 @@ const cliBuilder = new CliBuilder([
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
const foundUser = await findFirstUser({
|
const foundUser = await User.fromSql(eq(Users.username, username));
|
||||||
where: (user, { eq }) => eq(user.username, username),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!foundUser) {
|
if (!foundUser) {
|
||||||
console.log(`${chalk.red("✗")} User not found`);
|
console.log(`${chalk.red("✗")} User not found`);
|
||||||
|
|
@ -208,7 +201,7 @@ const cliBuilder = new CliBuilder([
|
||||||
if (!args.noconfirm) {
|
if (!args.noconfirm) {
|
||||||
process.stdout.write(
|
process.stdout.write(
|
||||||
`Are you sure you want to delete user ${chalk.blue(
|
`Are you sure you want to delete user ${chalk.blue(
|
||||||
foundUser.username,
|
foundUser.getUser().username,
|
||||||
)}?\n${chalk.red(
|
)}?\n${chalk.red(
|
||||||
chalk.bold(
|
chalk.bold(
|
||||||
"This is a destructive action and cannot be undone!",
|
"This is a destructive action and cannot be undone!",
|
||||||
|
|
@ -229,7 +222,7 @@ const cliBuilder = new CliBuilder([
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
`${chalk.green("✓")} Deleted user ${chalk.blue(
|
`${chalk.green("✓")} Deleted user ${chalk.blue(
|
||||||
foundUser.username,
|
foundUser.getUser().username,
|
||||||
)}`,
|
)}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -312,22 +305,19 @@ const cliBuilder = new CliBuilder([
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// @ts-ignore
|
let users = (
|
||||||
let users: (User & {
|
await User.manyFromSql(
|
||||||
instance?: {
|
admins ? eq(Users.isAdmin, true) : undefined,
|
||||||
baseUrl: string;
|
undefined,
|
||||||
};
|
args.limit ?? 200,
|
||||||
})[] = await findManyUsers({
|
)
|
||||||
where: (user, { eq }) =>
|
).map((u) => u.getUser());
|
||||||
admins ? eq(user.isAdmin, true) : undefined,
|
|
||||||
limit: args.limit ?? 200,
|
|
||||||
});
|
|
||||||
|
|
||||||
// If instance is not in fields, remove them
|
// If instance is not in fields, remove them
|
||||||
if (fields.length > 0 && !fields.includes("instance")) {
|
if (fields.length > 0 && !fields.includes("instance")) {
|
||||||
users = users.map((user) => ({
|
users = users.map((user) => ({
|
||||||
...user,
|
...user,
|
||||||
instance: undefined,
|
instance: null,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -505,14 +495,14 @@ const cliBuilder = new CliBuilder([
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
const users = await findManyUsers({
|
const users: User["user"][] = (
|
||||||
where: (user, { or, eq }) =>
|
await User.manyFromSql(
|
||||||
or(
|
// @ts-ignore
|
||||||
// @ts-expect-error
|
or(...fields.map((field) => eq(users[field], query))),
|
||||||
...fields.map((field) => eq(user[field], query)),
|
undefined,
|
||||||
),
|
Number(limit),
|
||||||
limit: Number(limit),
|
)
|
||||||
});
|
).map((u) => u.getUser());
|
||||||
|
|
||||||
if (redact) {
|
if (redact) {
|
||||||
for (const user of users) {
|
for (const user of users) {
|
||||||
|
|
@ -631,9 +621,7 @@ const cliBuilder = new CliBuilder([
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await findFirstUser({
|
const user = await User.fromSql(eq(Users.username, username));
|
||||||
where: (user, { eq }) => eq(user.username, username),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
console.log(`${chalk.red("✗")} User not found`);
|
console.log(`${chalk.red("✗")} User not found`);
|
||||||
|
|
@ -653,7 +641,7 @@ const cliBuilder = new CliBuilder([
|
||||||
if (linkedOpenIdAccounts.find((a) => a.issuerId === issuerId)) {
|
if (linkedOpenIdAccounts.find((a) => a.issuerId === issuerId)) {
|
||||||
console.log(
|
console.log(
|
||||||
`${chalk.red("✗")} User ${chalk.blue(
|
`${chalk.red("✗")} User ${chalk.blue(
|
||||||
user.username,
|
user.getUser().username,
|
||||||
)} is already connected to this OpenID Connect issuer with another account`,
|
)} is already connected to this OpenID Connect issuer with another account`,
|
||||||
);
|
);
|
||||||
return 1;
|
return 1;
|
||||||
|
|
@ -670,7 +658,7 @@ const cliBuilder = new CliBuilder([
|
||||||
`${chalk.green(
|
`${chalk.green(
|
||||||
"✓",
|
"✓",
|
||||||
)} Connected OpenID Connect account to user ${chalk.blue(
|
)} Connected OpenID Connect account to user ${chalk.blue(
|
||||||
user.username,
|
user.getUser().username,
|
||||||
)}`,
|
)}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -732,9 +720,7 @@ const cliBuilder = new CliBuilder([
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await findFirstUser({
|
const user = await User.fromId(account.userId);
|
||||||
where: (user, { eq }) => eq(user.id, account.userId ?? ""),
|
|
||||||
});
|
|
||||||
|
|
||||||
await db
|
await db
|
||||||
.delete(OpenIdAccounts)
|
.delete(OpenIdAccounts)
|
||||||
|
|
@ -744,7 +730,7 @@ const cliBuilder = new CliBuilder([
|
||||||
`${chalk.green(
|
`${chalk.green(
|
||||||
"✓",
|
"✓",
|
||||||
)} Disconnected OpenID account from user ${chalk.blue(
|
)} Disconnected OpenID account from user ${chalk.blue(
|
||||||
user?.username,
|
user?.getUser().username,
|
||||||
)}`,
|
)}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { config } from "config-manager";
|
import { config } from "config-manager";
|
||||||
import type * as Lysand from "lysand-types";
|
import type * as Lysand from "lysand-types";
|
||||||
import { type User, getUserUri } from "./User";
|
import type { User } from "~packages/database-interface/user";
|
||||||
|
|
||||||
export const localObjectURI = (id: string) => `/objects/${id}`;
|
export const localObjectURI = (id: string) => `/objects/${id}`;
|
||||||
|
|
||||||
|
|
@ -9,17 +9,17 @@ export const objectToInboxRequest = async (
|
||||||
author: User,
|
author: User,
|
||||||
userToSendTo: User,
|
userToSendTo: User,
|
||||||
): Promise<Request> => {
|
): Promise<Request> => {
|
||||||
if (!userToSendTo.instanceId || !userToSendTo.endpoints?.inbox) {
|
if (userToSendTo.isLocal() || !userToSendTo.getUser().endpoints?.inbox) {
|
||||||
throw new Error("UserToSendTo has no inbox or is a local user");
|
throw new Error("UserToSendTo has no inbox or is a local user");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (author.instanceId) {
|
if (author.isRemote()) {
|
||||||
throw new Error("Author is a remote user");
|
throw new Error("Author is a remote user");
|
||||||
}
|
}
|
||||||
|
|
||||||
const privateKey = await crypto.subtle.importKey(
|
const privateKey = await crypto.subtle.importKey(
|
||||||
"pkcs8",
|
"pkcs8",
|
||||||
Buffer.from(author.privateKey ?? "", "base64"),
|
Buffer.from(author.getUser().privateKey ?? "", "base64"),
|
||||||
"Ed25519",
|
"Ed25519",
|
||||||
false,
|
false,
|
||||||
["sign"],
|
["sign"],
|
||||||
|
|
@ -30,7 +30,7 @@ export const objectToInboxRequest = async (
|
||||||
new TextEncoder().encode(JSON.stringify(object)),
|
new TextEncoder().encode(JSON.stringify(object)),
|
||||||
);
|
);
|
||||||
|
|
||||||
const userInbox = new URL(userToSendTo.endpoints.inbox);
|
const userInbox = new URL(userToSendTo.getUser().endpoints?.inbox ?? "");
|
||||||
|
|
||||||
const date = new Date();
|
const date = new Date();
|
||||||
|
|
||||||
|
|
@ -41,14 +41,14 @@ export const objectToInboxRequest = async (
|
||||||
`(request-target): post ${userInbox.pathname}\n` +
|
`(request-target): post ${userInbox.pathname}\n` +
|
||||||
`host: ${userInbox.host}\n` +
|
`host: ${userInbox.host}\n` +
|
||||||
`date: ${date.toISOString()}\n` +
|
`date: ${date.toISOString()}\n` +
|
||||||
`digest: SHA-256=${btoa(
|
`digest: SHA-256=${Buffer.from(new Uint8Array(digest)).toString(
|
||||||
String.fromCharCode(...new Uint8Array(digest)),
|
"base64",
|
||||||
)}\n`,
|
)}\n`,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
const signatureBase64 = btoa(
|
const signatureBase64 = Buffer.from(new Uint8Array(signature)).toString(
|
||||||
String.fromCharCode(...new Uint8Array(signature)),
|
"base64",
|
||||||
);
|
);
|
||||||
|
|
||||||
return new Request(userInbox, {
|
return new Request(userInbox, {
|
||||||
|
|
@ -57,9 +57,7 @@ export const objectToInboxRequest = async (
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
Date: date.toISOString(),
|
Date: date.toISOString(),
|
||||||
Origin: new URL(config.http.base_url).host,
|
Origin: new URL(config.http.base_url).host,
|
||||||
Signature: `keyId="${getUserUri(
|
Signature: `keyId="${author.getUri()}",algorithm="ed25519",headers="(request-target) host date digest",signature="${signatureBase64}"`,
|
||||||
author,
|
|
||||||
)}",algorithm="ed25519",headers="(request-target) host date digest",signature="${signatureBase64}"`,
|
|
||||||
},
|
},
|
||||||
body: JSON.stringify(object),
|
body: JSON.stringify(object),
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ export const addInstanceIfNotExists = async (url: string) => {
|
||||||
const origin = new URL(url).origin;
|
const origin = new URL(url).origin;
|
||||||
const host = new URL(url).host;
|
const host = new URL(url).host;
|
||||||
|
|
||||||
const found = await db.query.instance.findFirst({
|
const found = await db.query.Instances.findFirst({
|
||||||
where: (instance, { eq }) => eq(instance.baseUrl, host),
|
where: (instance, { eq }) => eq(instance.baseUrl, host),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,8 @@ import { type InferSelectModel, and, eq } from "drizzle-orm";
|
||||||
import type * as Lysand from "lysand-types";
|
import type * as Lysand from "lysand-types";
|
||||||
import { db } from "~drizzle/db";
|
import { db } from "~drizzle/db";
|
||||||
import { Likes, Notifications } from "~drizzle/schema";
|
import { Likes, Notifications } from "~drizzle/schema";
|
||||||
import type { StatusWithRelations } from "./Status";
|
import type { Note } from "~packages/database-interface/note";
|
||||||
import type { UserWithRelations } from "./User";
|
import type { User } from "~packages/database-interface/user";
|
||||||
|
|
||||||
export type Like = InferSelectModel<typeof Likes>;
|
export type Like = InferSelectModel<typeof Likes>;
|
||||||
|
|
||||||
|
|
@ -27,24 +27,21 @@ export const likeToLysand = (like: Like): Lysand.Like => {
|
||||||
/**
|
/**
|
||||||
* Create a like
|
* Create a like
|
||||||
* @param user User liking the status
|
* @param user User liking the status
|
||||||
* @param status Status being liked
|
* @param note Status being liked
|
||||||
*/
|
*/
|
||||||
export const createLike = async (
|
export const createLike = async (user: User, note: Note) => {
|
||||||
user: UserWithRelations,
|
|
||||||
status: StatusWithRelations,
|
|
||||||
) => {
|
|
||||||
await db.insert(Likes).values({
|
await db.insert(Likes).values({
|
||||||
likedId: status.id,
|
likedId: note.id,
|
||||||
likerId: user.id,
|
likerId: user.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (status.author.instanceId === user.instanceId) {
|
if (note.getAuthor().getUser().instanceId === user.getUser().instanceId) {
|
||||||
// Notify the user that their post has been favourited
|
// Notify the user that their post has been favourited
|
||||||
await db.insert(Notifications).values({
|
await db.insert(Notifications).values({
|
||||||
accountId: user.id,
|
accountId: user.id,
|
||||||
type: "favourite",
|
type: "favourite",
|
||||||
notifiedId: status.authorId,
|
notifiedId: note.getAuthor().id,
|
||||||
noteId: status.id,
|
noteId: note.id,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// TODO: Add database jobs for federating this
|
// TODO: Add database jobs for federating this
|
||||||
|
|
@ -54,15 +51,12 @@ export const createLike = async (
|
||||||
/**
|
/**
|
||||||
* Delete a like
|
* Delete a like
|
||||||
* @param user User deleting their like
|
* @param user User deleting their like
|
||||||
* @param status Status being unliked
|
* @param note Status being unliked
|
||||||
*/
|
*/
|
||||||
export const deleteLike = async (
|
export const deleteLike = async (user: User, note: Note) => {
|
||||||
user: UserWithRelations,
|
|
||||||
status: StatusWithRelations,
|
|
||||||
) => {
|
|
||||||
await db
|
await db
|
||||||
.delete(Likes)
|
.delete(Likes)
|
||||||
.where(and(eq(Likes.likedId, status.id), eq(Likes.likerId, user.id)));
|
.where(and(eq(Likes.likedId, note.id), eq(Likes.likerId, user.id)));
|
||||||
|
|
||||||
// Notify the user that their post has been favourited
|
// Notify the user that their post has been favourited
|
||||||
await db
|
await db
|
||||||
|
|
@ -71,12 +65,12 @@ export const deleteLike = async (
|
||||||
and(
|
and(
|
||||||
eq(Notifications.accountId, user.id),
|
eq(Notifications.accountId, user.id),
|
||||||
eq(Notifications.type, "favourite"),
|
eq(Notifications.type, "favourite"),
|
||||||
eq(Notifications.notifiedId, status.authorId),
|
eq(Notifications.notifiedId, note.getAuthor().id),
|
||||||
eq(Notifications.noteId, status.id),
|
eq(Notifications.noteId, note.id),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (user.instanceId === null && status.author.instanceId !== null) {
|
if (user.isLocal() && note.getAuthor().isRemote()) {
|
||||||
// User is local, federate the delete
|
// User is local, federate the delete
|
||||||
// TODO: Federate this
|
// TODO: Federate this
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import type { InferSelectModel } from "drizzle-orm";
|
||||||
import { db } from "~drizzle/db";
|
import { db } from "~drizzle/db";
|
||||||
import type { Notifications } from "~drizzle/schema";
|
import type { Notifications } from "~drizzle/schema";
|
||||||
import { Note } from "~packages/database-interface/note";
|
import { Note } from "~packages/database-interface/note";
|
||||||
|
import { User } from "~packages/database-interface/user";
|
||||||
import type { Notification as APINotification } from "~types/mastodon/notification";
|
import type { Notification as APINotification } from "~types/mastodon/notification";
|
||||||
import type { StatusWithRelations } from "./Status";
|
import type { StatusWithRelations } from "./Status";
|
||||||
import {
|
import {
|
||||||
|
|
@ -9,7 +10,6 @@ import {
|
||||||
transformOutputToUserWithRelations,
|
transformOutputToUserWithRelations,
|
||||||
userExtrasTemplate,
|
userExtrasTemplate,
|
||||||
userRelations,
|
userRelations,
|
||||||
userToAPI,
|
|
||||||
} from "./User";
|
} from "./User";
|
||||||
|
|
||||||
export type Notification = InferSelectModel<typeof Notifications>;
|
export type Notification = InferSelectModel<typeof Notifications>;
|
||||||
|
|
@ -50,15 +50,14 @@ export const findManyNotifications = async (
|
||||||
export const notificationToAPI = async (
|
export const notificationToAPI = async (
|
||||||
notification: NotificationWithRelations,
|
notification: NotificationWithRelations,
|
||||||
): Promise<APINotification> => {
|
): Promise<APINotification> => {
|
||||||
|
const account = new User(notification.account);
|
||||||
return {
|
return {
|
||||||
account: userToAPI(notification.account),
|
account: account.toAPI(),
|
||||||
created_at: new Date(notification.createdAt).toISOString(),
|
created_at: new Date(notification.createdAt).toISOString(),
|
||||||
id: notification.id,
|
id: notification.id,
|
||||||
type: notification.type,
|
type: notification.type,
|
||||||
status: notification.status
|
status: notification.status
|
||||||
? await Note.fromStatus(notification.status).toAPI(
|
? await Note.fromStatus(notification.status).toAPI(account)
|
||||||
notification.account,
|
|
||||||
)
|
|
||||||
: undefined,
|
: undefined,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ export const createFromObject = async (
|
||||||
object: Lysand.Entity,
|
object: Lysand.Entity,
|
||||||
authorUri: string,
|
authorUri: string,
|
||||||
) => {
|
) => {
|
||||||
const foundObject = await db.query.lysandObject.findFirst({
|
const foundObject = await db.query.LysandObjects.findFirst({
|
||||||
where: (o, { eq }) => eq(o.remoteId, object.id),
|
where: (o, { eq }) => eq(o.remoteId, object.id),
|
||||||
with: {
|
with: {
|
||||||
author: true,
|
author: true,
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
import type { InferSelectModel } from "drizzle-orm";
|
import type { InferSelectModel } from "drizzle-orm";
|
||||||
import { db } from "~drizzle/db";
|
import { db } from "~drizzle/db";
|
||||||
import { Relationships } from "~drizzle/schema";
|
import { Relationships } from "~drizzle/schema";
|
||||||
|
import type { User } from "~packages/database-interface/user";
|
||||||
import type { Relationship as APIRelationship } from "~types/mastodon/relationship";
|
import type { Relationship as APIRelationship } from "~types/mastodon/relationship";
|
||||||
import type { User } from "./User";
|
import type { UserType } from "./User";
|
||||||
|
|
||||||
export type Relationship = InferSelectModel<typeof Relationships>;
|
export type Relationship = InferSelectModel<typeof Relationships>;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import markdownItTaskLists from "@hackmd/markdown-it-task-lists";
|
||||||
import { dualLogger } from "@loggers";
|
import { dualLogger } from "@loggers";
|
||||||
import { sanitizeHtml } from "@sanitization";
|
import { sanitizeHtml } from "@sanitization";
|
||||||
import { config } from "config-manager";
|
import { config } from "config-manager";
|
||||||
|
|
@ -10,7 +11,6 @@ import {
|
||||||
or,
|
or,
|
||||||
sql,
|
sql,
|
||||||
} from "drizzle-orm";
|
} from "drizzle-orm";
|
||||||
import { htmlToText } from "html-to-text";
|
|
||||||
import linkifyHtml from "linkify-html";
|
import linkifyHtml from "linkify-html";
|
||||||
import type * as Lysand from "lysand-types";
|
import type * as Lysand from "lysand-types";
|
||||||
import {
|
import {
|
||||||
|
|
@ -24,46 +24,30 @@ import {
|
||||||
maybe,
|
maybe,
|
||||||
oneOrMore,
|
oneOrMore,
|
||||||
} from "magic-regexp";
|
} from "magic-regexp";
|
||||||
|
import MarkdownIt from "markdown-it";
|
||||||
|
import markdownItAnchor from "markdown-it-anchor";
|
||||||
|
import markdownItContainer from "markdown-it-container";
|
||||||
|
import markdownItTocDoneRight from "markdown-it-toc-done-right";
|
||||||
import { db } from "~drizzle/db";
|
import { db } from "~drizzle/db";
|
||||||
import {
|
import { type Attachments, Instances, Notes, Users } from "~drizzle/schema";
|
||||||
Attachments,
|
|
||||||
EmojiToNote,
|
|
||||||
Instances,
|
|
||||||
NoteToMentions,
|
|
||||||
Notes,
|
|
||||||
Notifications,
|
|
||||||
Users,
|
|
||||||
} from "~drizzle/schema";
|
|
||||||
import { Note } from "~packages/database-interface/note";
|
import { Note } from "~packages/database-interface/note";
|
||||||
|
import { User } from "~packages/database-interface/user";
|
||||||
import { LogLevel } from "~packages/log-manager";
|
import { LogLevel } from "~packages/log-manager";
|
||||||
import type { Status as APIStatus } from "~types/mastodon/status";
|
import type { Status as APIStatus } from "~types/mastodon/status";
|
||||||
import type { Application } from "./Application";
|
import type { Application } from "./Application";
|
||||||
import { attachmentFromLysand, attachmentToLysand } from "./Attachment";
|
import { attachmentFromLysand } from "./Attachment";
|
||||||
import {
|
import { type EmojiWithInstance, fetchEmoji } from "./Emoji";
|
||||||
type EmojiWithInstance,
|
|
||||||
emojiToLysand,
|
|
||||||
fetchEmoji,
|
|
||||||
parseEmojis,
|
|
||||||
} from "./Emoji";
|
|
||||||
import { objectToInboxRequest } from "./Federation";
|
import { objectToInboxRequest } from "./Federation";
|
||||||
import type { Like } from "./Like";
|
import type { Like } from "./Like";
|
||||||
import {
|
import {
|
||||||
type User,
|
type UserType,
|
||||||
type UserWithInstance,
|
type UserWithInstance,
|
||||||
type UserWithRelations,
|
type UserWithRelations,
|
||||||
findManyUsers,
|
|
||||||
getUserUri,
|
|
||||||
resolveUser,
|
|
||||||
resolveWebFinger,
|
resolveWebFinger,
|
||||||
transformOutputToUserWithRelations,
|
transformOutputToUserWithRelations,
|
||||||
userExtrasTemplate,
|
userExtrasTemplate,
|
||||||
userRelations,
|
userRelations,
|
||||||
} from "./User";
|
} from "./User";
|
||||||
import MarkdownIt from "markdown-it";
|
|
||||||
import markdownItTocDoneRight from "markdown-it-toc-done-right";
|
|
||||||
import markdownItContainer from "markdown-it-container";
|
|
||||||
import markdownItAnchor from "markdown-it-anchor";
|
|
||||||
import markdownItTaskLists from "@hackmd/markdown-it-task-lists";
|
|
||||||
|
|
||||||
export type Status = InferSelectModel<typeof Notes>;
|
export type Status = InferSelectModel<typeof Notes>;
|
||||||
|
|
||||||
|
|
@ -362,7 +346,7 @@ export const resolveNote = async (
|
||||||
throw new Error("Invalid object author");
|
throw new Error("Invalid object author");
|
||||||
}
|
}
|
||||||
|
|
||||||
const author = await resolveUser(note.author);
|
const author = await User.resolve(note.author);
|
||||||
|
|
||||||
if (!author) {
|
if (!author) {
|
||||||
throw new Error("Invalid object author");
|
throw new Error("Invalid object author");
|
||||||
|
|
@ -415,10 +399,8 @@ export const resolveNote = async (
|
||||||
note.uri,
|
note.uri,
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
(note.mentions ?? [])
|
(note.mentions ?? [])
|
||||||
.map((mention) => resolveUser(mention))
|
.map((mention) => User.resolve(mention))
|
||||||
.filter(
|
.filter((mention) => mention !== null) as Promise<User>[],
|
||||||
(mention) => mention !== null,
|
|
||||||
) as Promise<UserWithRelations>[],
|
|
||||||
),
|
),
|
||||||
attachments.map((a) => a.id),
|
attachments.map((a) => a.id),
|
||||||
note.replies_to
|
note.replies_to
|
||||||
|
|
@ -454,9 +436,7 @@ export const createMentionRegExp = () =>
|
||||||
* @param text The text to parse mentions from.
|
* @param text The text to parse mentions from.
|
||||||
* @returns An array of users mentioned in the text.
|
* @returns An array of users mentioned in the text.
|
||||||
*/
|
*/
|
||||||
export const parseTextMentions = async (
|
export const parseTextMentions = async (text: string): Promise<User[]> => {
|
||||||
text: string,
|
|
||||||
): Promise<UserWithRelations[]> => {
|
|
||||||
const mentionedPeople = [...text.matchAll(createMentionRegExp())] ?? [];
|
const mentionedPeople = [...text.matchAll(createMentionRegExp())] ?? [];
|
||||||
if (mentionedPeople.length === 0) return [];
|
if (mentionedPeople.length === 0) return [];
|
||||||
|
|
||||||
|
|
@ -497,13 +477,12 @@ export const parseTextMentions = async (
|
||||||
|
|
||||||
const finalList =
|
const finalList =
|
||||||
foundUsers.length > 0
|
foundUsers.length > 0
|
||||||
? await findManyUsers({
|
? await User.manyFromSql(
|
||||||
where: (user, { inArray }) =>
|
|
||||||
inArray(
|
inArray(
|
||||||
user.id,
|
Users.id,
|
||||||
foundUsers.map((u) => u.id),
|
foundUsers.map((u) => u.id),
|
||||||
),
|
),
|
||||||
})
|
)
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
// Attempt to resolve mentions that were not found
|
// Attempt to resolve mentions that were not found
|
||||||
|
|
@ -521,49 +500,47 @@ export const parseTextMentions = async (
|
||||||
return finalList;
|
return finalList;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const replaceTextMentions = async (
|
export const replaceTextMentions = async (text: string, mentions: User[]) => {
|
||||||
text: string,
|
|
||||||
mentions: UserWithRelations[],
|
|
||||||
) => {
|
|
||||||
let finalText = text;
|
let finalText = text;
|
||||||
for (const mention of mentions) {
|
for (const mention of mentions) {
|
||||||
|
const user = mention.getUser();
|
||||||
// Replace @username and @username@domain
|
// Replace @username and @username@domain
|
||||||
if (mention.instance) {
|
if (user.instance) {
|
||||||
finalText = finalText.replace(
|
finalText = finalText.replace(
|
||||||
createRegExp(
|
createRegExp(
|
||||||
exactly(`@${mention.username}@${mention.instance.baseUrl}`),
|
exactly(`@${user.username}@${user.instance.baseUrl}`),
|
||||||
[global],
|
[global],
|
||||||
),
|
),
|
||||||
`<a class="u-url mention" rel="nofollow noopener noreferrer" target="_blank" href="${getUserUri(
|
`<a class="u-url mention" rel="nofollow noopener noreferrer" target="_blank" href="${mention.getUri()}">@${
|
||||||
mention,
|
user.username
|
||||||
)}">@${mention.username}@${mention.instance.baseUrl}</a>`,
|
}@${user.instance.baseUrl}</a>`,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
finalText = finalText.replace(
|
finalText = finalText.replace(
|
||||||
// Only replace @username if it doesn't have another @ right after
|
// Only replace @username if it doesn't have another @ right after
|
||||||
createRegExp(
|
createRegExp(
|
||||||
exactly(`@${mention.username}`)
|
exactly(`@${user.username}`)
|
||||||
.notBefore(anyOf(letter, digit, charIn("@")))
|
.notBefore(anyOf(letter, digit, charIn("@")))
|
||||||
.notAfter(anyOf(letter, digit, charIn("@"))),
|
.notAfter(anyOf(letter, digit, charIn("@"))),
|
||||||
[global],
|
[global],
|
||||||
),
|
),
|
||||||
`<a class="u-url mention" rel="nofollow noopener noreferrer" target="_blank" href="${getUserUri(
|
`<a class="u-url mention" rel="nofollow noopener noreferrer" target="_blank" href="${mention.getUri()}">@${
|
||||||
mention,
|
user.username
|
||||||
)}">@${mention.username}</a>`,
|
}</a>`,
|
||||||
);
|
);
|
||||||
|
|
||||||
finalText = finalText.replace(
|
finalText = finalText.replace(
|
||||||
createRegExp(
|
createRegExp(
|
||||||
exactly(
|
exactly(
|
||||||
`@${mention.username}@${
|
`@${user.username}@${
|
||||||
new URL(config.http.base_url).host
|
new URL(config.http.base_url).host
|
||||||
}`,
|
}`,
|
||||||
),
|
),
|
||||||
[global],
|
[global],
|
||||||
),
|
),
|
||||||
`<a class="u-url mention" rel="nofollow noopener noreferrer" target="_blank" href="${getUserUri(
|
`<a class="u-url mention" rel="nofollow noopener noreferrer" target="_blank" href="${mention.getUri()}">@${
|
||||||
mention,
|
user.username
|
||||||
)}">@${mention.username}</a>`,
|
}</a>`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -573,7 +550,7 @@ export const replaceTextMentions = async (
|
||||||
|
|
||||||
export const contentToHtml = async (
|
export const contentToHtml = async (
|
||||||
content: Lysand.ContentFormat,
|
content: Lysand.ContentFormat,
|
||||||
mentions: UserWithRelations[] = [],
|
mentions: User[] = [],
|
||||||
): Promise<string> => {
|
): Promise<string> => {
|
||||||
let htmlContent: string;
|
let htmlContent: string;
|
||||||
|
|
||||||
|
|
@ -663,152 +640,17 @@ export const federateNote = async (note: Note) => {
|
||||||
dualLogger.log(
|
dualLogger.log(
|
||||||
LogLevel.ERROR,
|
LogLevel.ERROR,
|
||||||
"Federation.Status",
|
"Federation.Status",
|
||||||
`Failed to federate status ${note.getStatus().id} to ${
|
`Failed to federate status ${
|
||||||
user.uri
|
note.getStatus().id
|
||||||
}`,
|
} to ${user.getUri()}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const editStatus = async (
|
export const isFavouritedBy = async (status: Status, user: UserType) => {
|
||||||
statusToEdit: StatusWithRelations,
|
|
||||||
data: {
|
|
||||||
content: string;
|
|
||||||
visibility?: APIStatus["visibility"];
|
|
||||||
sensitive: boolean;
|
|
||||||
spoiler_text: string;
|
|
||||||
emojis?: EmojiWithInstance[];
|
|
||||||
content_type?: string;
|
|
||||||
uri?: string;
|
|
||||||
mentions?: User[];
|
|
||||||
media_attachments?: string[];
|
|
||||||
},
|
|
||||||
): Promise<Note | null> => {
|
|
||||||
const mentions = await parseTextMentions(data.content);
|
|
||||||
|
|
||||||
// Parse emojis
|
|
||||||
const emojis = await parseEmojis(data.content);
|
|
||||||
|
|
||||||
// Fuse and deduplicate emojis
|
|
||||||
data.emojis = data.emojis
|
|
||||||
? [...data.emojis, ...emojis].filter(
|
|
||||||
(emoji, index, self) =>
|
|
||||||
index === self.findIndex((t) => t.id === emoji.id),
|
|
||||||
)
|
|
||||||
: emojis;
|
|
||||||
|
|
||||||
const htmlContent = await contentToHtml({
|
|
||||||
[data.content_type ?? "text/plain"]: {
|
|
||||||
content: data.content,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const note = await Note.fromId(statusToEdit.id);
|
|
||||||
|
|
||||||
if (!note) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const updated = await note.update({
|
|
||||||
content: htmlContent,
|
|
||||||
contentSource: data.content,
|
|
||||||
contentType: data.content_type,
|
|
||||||
visibility: data.visibility,
|
|
||||||
sensitive: data.sensitive,
|
|
||||||
spoilerText: data.spoiler_text,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Connect emojis
|
|
||||||
for (const emoji of data.emojis) {
|
|
||||||
await db
|
|
||||||
.insert(EmojiToNote)
|
|
||||||
.values({
|
|
||||||
emojiId: emoji.id,
|
|
||||||
noteId: updated.id,
|
|
||||||
})
|
|
||||||
.execute();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Connect mentions
|
|
||||||
for (const mention of mentions) {
|
|
||||||
await db
|
|
||||||
.insert(NoteToMentions)
|
|
||||||
.values({
|
|
||||||
noteId: updated.id,
|
|
||||||
userId: mention.id,
|
|
||||||
})
|
|
||||||
.execute();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send notifications for mentioned local users
|
|
||||||
for (const mention of mentions ?? []) {
|
|
||||||
if (mention.instanceId === null) {
|
|
||||||
await db.insert(Notifications).values({
|
|
||||||
accountId: statusToEdit.authorId,
|
|
||||||
notifiedId: mention.id,
|
|
||||||
type: "mention",
|
|
||||||
noteId: updated.id,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set attachment parents
|
|
||||||
await db
|
|
||||||
.update(Attachments)
|
|
||||||
.set({
|
|
||||||
noteId: updated.id,
|
|
||||||
})
|
|
||||||
.where(inArray(Attachments.id, data.media_attachments ?? []));
|
|
||||||
|
|
||||||
return await Note.fromId(updated.id);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const isFavouritedBy = async (status: Status, user: User) => {
|
|
||||||
return !!(await db.query.Likes.findFirst({
|
return !!(await db.query.Likes.findFirst({
|
||||||
where: (like, { and, eq }) =>
|
where: (like, { and, eq }) =>
|
||||||
and(eq(like.likerId, user.id), eq(like.likedId, status.id)),
|
and(eq(like.likerId, user.id), eq(like.likedId, status.id)),
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getStatusUri = (status?: Status | null) => {
|
|
||||||
if (!status) return undefined;
|
|
||||||
|
|
||||||
return (
|
|
||||||
status.uri ||
|
|
||||||
new URL(`/objects/${status.id}`, config.http.base_url).toString()
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const statusToLysand = (status: StatusWithRelations): Lysand.Note => {
|
|
||||||
return {
|
|
||||||
type: "Note",
|
|
||||||
created_at: new Date(status.createdAt).toISOString(),
|
|
||||||
id: status.id,
|
|
||||||
author: getUserUri(status.author),
|
|
||||||
uri: getStatusUri(status) ?? "",
|
|
||||||
content: {
|
|
||||||
"text/html": {
|
|
||||||
content: status.content,
|
|
||||||
},
|
|
||||||
"text/plain": {
|
|
||||||
content: htmlToText(status.content),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
attachments: (status.attachments ?? []).map((attachment) =>
|
|
||||||
attachmentToLysand(attachment),
|
|
||||||
),
|
|
||||||
is_sensitive: status.sensitive,
|
|
||||||
mentions: status.mentions.map((mention) => mention.uri || ""),
|
|
||||||
quotes: getStatusUri(status.quote) ?? undefined,
|
|
||||||
replies_to: getStatusUri(status.reply) ?? undefined,
|
|
||||||
subject: status.spoilerText,
|
|
||||||
visibility: status.visibility as Lysand.Visibility,
|
|
||||||
extensions: {
|
|
||||||
"org.lysand:custom_emojis": {
|
|
||||||
emojis: status.emojis.map((emoji) => emojiToLysand(emoji)),
|
|
||||||
},
|
|
||||||
// TODO: Add polls and reactions
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
|
||||||
|
|
@ -1,43 +1,32 @@
|
||||||
import { getBestContentType, urlToContentFormat } from "@content_types";
|
|
||||||
import { dualLogger } from "@loggers";
|
import { dualLogger } from "@loggers";
|
||||||
import { addUserToMeilisearch } from "@meilisearch";
|
import { addUserToMeilisearch } from "@meilisearch";
|
||||||
import { type Config, config } from "config-manager";
|
import { config } from "config-manager";
|
||||||
import { type InferSelectModel, and, eq, sql } from "drizzle-orm";
|
import { type InferSelectModel, and, eq, inArray, sql } from "drizzle-orm";
|
||||||
import { htmlToText } from "html-to-text";
|
|
||||||
import type * as Lysand from "lysand-types";
|
import type * as Lysand from "lysand-types";
|
||||||
import { db } from "~drizzle/db";
|
import { db } from "~drizzle/db";
|
||||||
import {
|
import {
|
||||||
Applications,
|
Applications,
|
||||||
EmojiToUser,
|
|
||||||
Instances,
|
Instances,
|
||||||
Notifications,
|
Notifications,
|
||||||
Relationships,
|
Relationships,
|
||||||
Tokens,
|
Tokens,
|
||||||
Users,
|
Users,
|
||||||
} from "~drizzle/schema";
|
} from "~drizzle/schema";
|
||||||
|
import { User } from "~packages/database-interface/user";
|
||||||
import { LogLevel } from "~packages/log-manager";
|
import { LogLevel } from "~packages/log-manager";
|
||||||
import type { Account as APIAccount } from "~types/mastodon/account";
|
|
||||||
import type { Mention as APIMention } from "~types/mastodon/mention";
|
|
||||||
import type { Source as APISource } from "~types/mastodon/source";
|
|
||||||
import type { Application } from "./Application";
|
import type { Application } from "./Application";
|
||||||
import {
|
import type { EmojiWithInstance } from "./Emoji";
|
||||||
type EmojiWithInstance,
|
|
||||||
emojiToAPI,
|
|
||||||
emojiToLysand,
|
|
||||||
fetchEmoji,
|
|
||||||
} from "./Emoji";
|
|
||||||
import { objectToInboxRequest } from "./Federation";
|
import { objectToInboxRequest } from "./Federation";
|
||||||
import { addInstanceIfNotExists } from "./Instance";
|
|
||||||
import { createNewRelationship } from "./Relationship";
|
import { createNewRelationship } from "./Relationship";
|
||||||
import type { Token } from "./Token";
|
import type { Token } from "./Token";
|
||||||
|
|
||||||
export type User = InferSelectModel<typeof Users>;
|
export type UserType = InferSelectModel<typeof Users>;
|
||||||
|
|
||||||
export type UserWithInstance = User & {
|
export type UserWithInstance = UserType & {
|
||||||
instance: InferSelectModel<typeof Instances> | null;
|
instance: InferSelectModel<typeof Instances> | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type UserWithRelations = User & {
|
export type UserWithRelations = UserType & {
|
||||||
instance: InferSelectModel<typeof Instances> | null;
|
instance: InferSelectModel<typeof Instances> | null;
|
||||||
emojis: EmojiWithInstance[];
|
emojis: EmojiWithInstance[];
|
||||||
followerCount: number;
|
followerCount: number;
|
||||||
|
|
@ -105,35 +94,11 @@ export const userExtrasTemplate = (name: string) => ({
|
||||||
});
|
});
|
||||||
|
|
||||||
export interface AuthData {
|
export interface AuthData {
|
||||||
user: UserWithRelations | null;
|
user: User | null;
|
||||||
token: string;
|
token: string;
|
||||||
application: Application | null;
|
application: Application | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the user's avatar in raw URL format
|
|
||||||
* @param config The config to use
|
|
||||||
* @returns The raw URL for the user's avatar
|
|
||||||
*/
|
|
||||||
export const getAvatarUrl = (user: User, config: Config) => {
|
|
||||||
if (!user.avatar)
|
|
||||||
return (
|
|
||||||
config.defaults.avatar ||
|
|
||||||
`https://api.dicebear.com/8.x/${config.defaults.placeholder_style}/svg?seed=${user.username}`
|
|
||||||
);
|
|
||||||
return 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
|
|
||||||
*/
|
|
||||||
export const getHeaderUrl = (user: User, config: Config) => {
|
|
||||||
if (!user.header) return config.defaults.header;
|
|
||||||
return user.header;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getFromRequest = async (req: Request): Promise<AuthData> => {
|
export const getFromRequest = async (req: Request): Promise<AuthData> => {
|
||||||
// Check auth token
|
// Check auth token
|
||||||
const token = req.headers.get("Authorization")?.split(" ")[1] || "";
|
const token = req.headers.get("Authorization")?.split(" ")[1] || "";
|
||||||
|
|
@ -152,14 +117,14 @@ export const followRequestUser = async (
|
||||||
notify = false,
|
notify = false,
|
||||||
languages: string[] = [],
|
languages: string[] = [],
|
||||||
): Promise<InferSelectModel<typeof Relationships>> => {
|
): Promise<InferSelectModel<typeof Relationships>> => {
|
||||||
const isRemote = followee.instanceId !== null;
|
const isRemote = followee.isRemote();
|
||||||
|
|
||||||
const updatedRelationship = (
|
const updatedRelationship = (
|
||||||
await db
|
await db
|
||||||
.update(Relationships)
|
.update(Relationships)
|
||||||
.set({
|
.set({
|
||||||
following: isRemote ? false : !followee.isLocked,
|
following: isRemote ? false : !followee.getUser().isLocked,
|
||||||
requested: isRemote ? true : followee.isLocked,
|
requested: isRemote ? true : followee.getUser().isLocked,
|
||||||
showingReblogs: reblogs,
|
showingReblogs: reblogs,
|
||||||
notifying: notify,
|
notifying: notify,
|
||||||
languages: languages,
|
languages: languages,
|
||||||
|
|
@ -190,7 +155,9 @@ export const followRequestUser = async (
|
||||||
dualLogger.log(
|
dualLogger.log(
|
||||||
LogLevel.ERROR,
|
LogLevel.ERROR,
|
||||||
"Federation.FollowRequest",
|
"Federation.FollowRequest",
|
||||||
`Failed to federate follow request from ${follower.id} to ${followee.uri}`,
|
`Failed to federate follow request from ${
|
||||||
|
follower.id
|
||||||
|
} to ${followee.getUri()}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -207,7 +174,7 @@ export const followRequestUser = async (
|
||||||
} else {
|
} else {
|
||||||
await db.insert(Notifications).values({
|
await db.insert(Notifications).values({
|
||||||
accountId: follower.id,
|
accountId: follower.id,
|
||||||
type: followee.isLocked ? "follow_request" : "follow",
|
type: followee.getUser().isLocked ? "follow_request" : "follow",
|
||||||
notifiedId: followee.id,
|
notifiedId: followee.id,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -236,7 +203,9 @@ export const sendFollowAccept = async (follower: User, followee: User) => {
|
||||||
dualLogger.log(
|
dualLogger.log(
|
||||||
LogLevel.ERROR,
|
LogLevel.ERROR,
|
||||||
"Federation.FollowAccept",
|
"Federation.FollowAccept",
|
||||||
`Failed to federate follow accept from ${followee.id} to ${follower.uri}`,
|
`Failed to federate follow accept from ${
|
||||||
|
followee.id
|
||||||
|
} to ${follower.getUri()}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -262,13 +231,15 @@ export const sendFollowReject = async (follower: User, followee: User) => {
|
||||||
dualLogger.log(
|
dualLogger.log(
|
||||||
LogLevel.ERROR,
|
LogLevel.ERROR,
|
||||||
"Federation.FollowReject",
|
"Federation.FollowReject",
|
||||||
`Failed to federate follow reject from ${followee.id} to ${follower.uri}`,
|
`Failed to federate follow reject from ${
|
||||||
|
followee.id
|
||||||
|
} to ${follower.getUri()}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const transformOutputToUserWithRelations = (
|
export const transformOutputToUserWithRelations = (
|
||||||
user: Omit<User, "endpoints"> & {
|
user: Omit<UserType, "endpoints"> & {
|
||||||
followerCount: unknown;
|
followerCount: unknown;
|
||||||
followingCount: unknown;
|
followingCount: unknown;
|
||||||
statusCount: unknown;
|
statusCount: unknown;
|
||||||
|
|
@ -343,146 +314,6 @@ export const findFirstUser = async (
|
||||||
return transformOutputToUserWithRelations(output);
|
return transformOutputToUserWithRelations(output);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const resolveUser = async (
|
|
||||||
uri: string,
|
|
||||||
): Promise<UserWithRelations | null> => {
|
|
||||||
// Check if user not already in database
|
|
||||||
const foundUser = await findFirstUser({
|
|
||||||
where: (user, { eq }) => eq(user.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(
|
|
||||||
/[0-9A-F]{8}-[0-9A-F]{4}-[7][0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!uuid) {
|
|
||||||
throw new Error(
|
|
||||||
`URI ${uri} is of a local user, but it could not be parsed`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const foundLocalUser = await findFirstUser({
|
|
||||||
where: (user, { eq }) => eq(user.id, uuid[0]),
|
|
||||||
});
|
|
||||||
|
|
||||||
return foundLocalUser || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 findFirstUser({
|
|
||||||
where: (user, { eq }) => eq(user.id, newUser.id),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!finalUser) return null;
|
|
||||||
|
|
||||||
// Add to Meilisearch
|
|
||||||
await addUserToMeilisearch(finalUser);
|
|
||||||
|
|
||||||
return finalUser;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getUserUri = (user: User) => {
|
|
||||||
return (
|
|
||||||
user.uri ||
|
|
||||||
new URL(`/users/${user.id}`, config.http.base_url).toString()
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolves a WebFinger identifier to a user.
|
* Resolves a WebFinger identifier to a user.
|
||||||
* @param identifier Either a UUID or a username
|
* @param identifier Either a UUID or a username
|
||||||
|
|
@ -490,7 +321,7 @@ export const getUserUri = (user: User) => {
|
||||||
export const resolveWebFinger = async (
|
export const resolveWebFinger = async (
|
||||||
identifier: string,
|
identifier: string,
|
||||||
host: string,
|
host: string,
|
||||||
): Promise<UserWithRelations | null> => {
|
): Promise<User | null> => {
|
||||||
// Check if user not already in database
|
// Check if user not already in database
|
||||||
const foundUser = await db
|
const foundUser = await db
|
||||||
.select()
|
.select()
|
||||||
|
|
@ -499,12 +330,7 @@ export const resolveWebFinger = async (
|
||||||
.where(and(eq(Users.username, identifier), eq(Instances.baseUrl, host)))
|
.where(and(eq(Users.username, identifier), eq(Instances.baseUrl, host)))
|
||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
if (foundUser[0])
|
if (foundUser[0]) return await User.fromId(foundUser[0].Users.id);
|
||||||
return (
|
|
||||||
(await findFirstUser({
|
|
||||||
where: (user, { eq }) => eq(user.id, foundUser[0].Users.id),
|
|
||||||
})) || null
|
|
||||||
);
|
|
||||||
|
|
||||||
const hostWithProtocol = host.startsWith("http") ? host : `https://${host}`;
|
const hostWithProtocol = host.startsWith("http") ? host : `https://${host}`;
|
||||||
|
|
||||||
|
|
@ -550,7 +376,7 @@ export const resolveWebFinger = async (
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return resolveUser(relevantLink.href);
|
return User.resolve(relevantLink.href);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -575,7 +401,7 @@ export const createNewLocalUser = async (data: {
|
||||||
header?: string;
|
header?: string;
|
||||||
admin?: boolean;
|
admin?: boolean;
|
||||||
skipPasswordHash?: boolean;
|
skipPasswordHash?: boolean;
|
||||||
}): Promise<UserWithRelations | null> => {
|
}): Promise<User | null> => {
|
||||||
const keys = await generateUserKeys();
|
const keys = await generateUserKeys();
|
||||||
|
|
||||||
const newUser = (
|
const newUser = (
|
||||||
|
|
@ -606,9 +432,7 @@ export const createNewLocalUser = async (data: {
|
||||||
.returning()
|
.returning()
|
||||||
)[0];
|
)[0];
|
||||||
|
|
||||||
const finalUser = await findFirstUser({
|
const finalUser = await User.fromId(newUser.id);
|
||||||
where: (user, { eq }) => eq(user.id, newUser.id),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!finalUser) return null;
|
if (!finalUser) return null;
|
||||||
|
|
||||||
|
|
@ -623,11 +447,8 @@ export const createNewLocalUser = async (data: {
|
||||||
*/
|
*/
|
||||||
export const parseMentionsUris = async (
|
export const parseMentionsUris = async (
|
||||||
mentions: string[],
|
mentions: string[],
|
||||||
): Promise<UserWithRelations[]> => {
|
): Promise<User[]> => {
|
||||||
return await findManyUsers({
|
return await User.manyFromSql(inArray(Users.uri, mentions));
|
||||||
where: (user, { inArray }) => inArray(user.uri, mentions),
|
|
||||||
with: userRelations,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -637,16 +458,14 @@ export const parseMentionsUris = async (
|
||||||
*/
|
*/
|
||||||
export const retrieveUserFromToken = async (
|
export const retrieveUserFromToken = async (
|
||||||
access_token: string,
|
access_token: string,
|
||||||
): Promise<UserWithRelations | null> => {
|
): Promise<User | null> => {
|
||||||
if (!access_token) return null;
|
if (!access_token) return null;
|
||||||
|
|
||||||
const token = await retrieveToken(access_token);
|
const token = await retrieveToken(access_token);
|
||||||
|
|
||||||
if (!token || !token.userId) return null;
|
if (!token || !token.userId) return null;
|
||||||
|
|
||||||
const user = await findFirstUser({
|
const user = await User.fromId(token.userId);
|
||||||
where: (user, { eq }) => eq(user.id, token.userId ?? ""),
|
|
||||||
});
|
|
||||||
|
|
||||||
return user;
|
return user;
|
||||||
};
|
};
|
||||||
|
|
@ -654,7 +473,7 @@ export const retrieveUserFromToken = async (
|
||||||
export const retrieveUserAndApplicationFromToken = async (
|
export const retrieveUserAndApplicationFromToken = async (
|
||||||
access_token: string,
|
access_token: string,
|
||||||
): Promise<{
|
): Promise<{
|
||||||
user: UserWithRelations | null;
|
user: User | null;
|
||||||
application: Application | null;
|
application: Application | null;
|
||||||
}> => {
|
}> => {
|
||||||
if (!access_token) return { user: null, application: null };
|
if (!access_token) return { user: null, application: null };
|
||||||
|
|
@ -673,9 +492,7 @@ export const retrieveUserAndApplicationFromToken = async (
|
||||||
|
|
||||||
if (!output?.token.userId) return { user: null, application: null };
|
if (!output?.token.userId) return { user: null, application: null };
|
||||||
|
|
||||||
const user = await findFirstUser({
|
const user = await User.fromId(output.token.userId);
|
||||||
where: (user, { eq }) => eq(user.id, output.token.userId ?? ""),
|
|
||||||
});
|
|
||||||
|
|
||||||
return { user, application: output.application ?? null };
|
return { user, application: output.application ?? null };
|
||||||
};
|
};
|
||||||
|
|
@ -698,7 +515,7 @@ export const retrieveToken = async (
|
||||||
* @returns The relationship to the other user.
|
* @returns The relationship to the other user.
|
||||||
*/
|
*/
|
||||||
export const getRelationshipToOtherUser = async (
|
export const getRelationshipToOtherUser = async (
|
||||||
user: UserWithRelations,
|
user: User,
|
||||||
other: User,
|
other: User,
|
||||||
): Promise<InferSelectModel<typeof Relationships>> => {
|
): Promise<InferSelectModel<typeof Relationships>> => {
|
||||||
const foundRelationship = await db.query.Relationships.findFirst({
|
const foundRelationship = await db.query.Relationships.findFirst({
|
||||||
|
|
@ -745,169 +562,19 @@ export const generateUserKeys = async () => {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const userToMention = (user: UserWithInstance): APIMention => ({
|
|
||||||
url: getUserUri(user),
|
|
||||||
username: user.username,
|
|
||||||
acct:
|
|
||||||
user.instance === null
|
|
||||||
? user.username
|
|
||||||
: `${user.username}@${user.instance.baseUrl}`,
|
|
||||||
id: user.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const userToAPI = (
|
|
||||||
userToConvert: UserWithRelations,
|
|
||||||
isOwnAccount = false,
|
|
||||||
): APIAccount => {
|
|
||||||
return {
|
|
||||||
id: userToConvert.id,
|
|
||||||
username: userToConvert.username,
|
|
||||||
display_name: userToConvert.displayName,
|
|
||||||
note: userToConvert.note,
|
|
||||||
url:
|
|
||||||
userToConvert.uri ||
|
|
||||||
new URL(
|
|
||||||
`/@${userToConvert.username}`,
|
|
||||||
config.http.base_url,
|
|
||||||
).toString(),
|
|
||||||
avatar: getAvatarUrl(userToConvert, config),
|
|
||||||
header: getHeaderUrl(userToConvert, config),
|
|
||||||
locked: userToConvert.isLocked,
|
|
||||||
created_at: new Date(userToConvert.createdAt).toISOString(),
|
|
||||||
followers_count: userToConvert.followerCount,
|
|
||||||
following_count: userToConvert.followingCount,
|
|
||||||
statuses_count: userToConvert.statusCount,
|
|
||||||
emojis: userToConvert.emojis.map((emoji) => emojiToAPI(emoji)),
|
|
||||||
// TODO: Add fields
|
|
||||||
fields: [],
|
|
||||||
bot: userToConvert.isBot,
|
|
||||||
source:
|
|
||||||
isOwnAccount && userToConvert.source
|
|
||||||
? (userToConvert.source as APISource)
|
|
||||||
: undefined,
|
|
||||||
// TODO: Add static avatar and header
|
|
||||||
avatar_static: getAvatarUrl(userToConvert, config),
|
|
||||||
header_static: getHeaderUrl(userToConvert, config),
|
|
||||||
acct:
|
|
||||||
userToConvert.instance === null
|
|
||||||
? userToConvert.username
|
|
||||||
: `${userToConvert.username}@${userToConvert.instance.baseUrl}`,
|
|
||||||
// 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: userToConvert.isAdmin,
|
|
||||||
is_moderator: userToConvert.isAdmin,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Should only return local users
|
|
||||||
*/
|
|
||||||
export const userToLysand = (user: UserWithRelations): Lysand.User => {
|
|
||||||
if (user.instanceId !== null) {
|
|
||||||
throw new Error("Cannot convert remote user to Lysand format");
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: user.id,
|
|
||||||
type: "User",
|
|
||||||
uri: getUserUri(user),
|
|
||||||
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(getAvatarUrl(user, config)) ?? undefined,
|
|
||||||
header: urlToContentFormat(getHeaderUrl(user, config)) ?? undefined,
|
|
||||||
display_name: user.displayName,
|
|
||||||
fields: (user.source as APISource).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)),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const followRequestToLysand = (
|
export const followRequestToLysand = (
|
||||||
follower: User,
|
follower: User,
|
||||||
followee: User,
|
followee: User,
|
||||||
): Lysand.Follow => {
|
): Lysand.Follow => {
|
||||||
if (follower.instanceId) {
|
if (follower.isRemote()) {
|
||||||
throw new Error("Follower must be a local user");
|
throw new Error("Follower must be a local user");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!followee.instanceId) {
|
if (!followee.isRemote()) {
|
||||||
throw new Error("Followee must be a remote user");
|
throw new Error("Followee must be a remote user");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!followee.uri) {
|
if (!followee.getUser().uri) {
|
||||||
throw new Error("Followee must have a URI in database");
|
throw new Error("Followee must have a URI in database");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -916,8 +583,8 @@ export const followRequestToLysand = (
|
||||||
return {
|
return {
|
||||||
type: "Follow",
|
type: "Follow",
|
||||||
id: id,
|
id: id,
|
||||||
author: getUserUri(follower),
|
author: follower.getUri(),
|
||||||
followee: followee.uri,
|
followee: followee.getUri(),
|
||||||
created_at: new Date().toISOString(),
|
created_at: new Date().toISOString(),
|
||||||
uri: new URL(`/follows/${id}`, config.http.base_url).toString(),
|
uri: new URL(`/follows/${id}`, config.http.base_url).toString(),
|
||||||
};
|
};
|
||||||
|
|
@ -927,15 +594,15 @@ export const followAcceptToLysand = (
|
||||||
follower: User,
|
follower: User,
|
||||||
followee: User,
|
followee: User,
|
||||||
): Lysand.FollowAccept => {
|
): Lysand.FollowAccept => {
|
||||||
if (!follower.instanceId) {
|
if (!follower.isRemote()) {
|
||||||
throw new Error("Follower must be a remote user");
|
throw new Error("Follower must be a remote user");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (followee.instanceId) {
|
if (followee.isRemote()) {
|
||||||
throw new Error("Followee must be a local user");
|
throw new Error("Followee must be a local user");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!follower.uri) {
|
if (!follower.getUser().uri) {
|
||||||
throw new Error("Follower must have a URI in database");
|
throw new Error("Follower must have a URI in database");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -944,9 +611,9 @@ export const followAcceptToLysand = (
|
||||||
return {
|
return {
|
||||||
type: "FollowAccept",
|
type: "FollowAccept",
|
||||||
id: id,
|
id: id,
|
||||||
author: getUserUri(followee),
|
author: followee.getUri(),
|
||||||
created_at: new Date().toISOString(),
|
created_at: new Date().toISOString(),
|
||||||
follower: follower.uri,
|
follower: follower.getUri(),
|
||||||
uri: new URL(`/follows/${id}`, config.http.base_url).toString(),
|
uri: new URL(`/follows/${id}`, config.http.base_url).toString(),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -70,6 +70,7 @@
|
||||||
"@hackmd/markdown-it-task-lists": "^2.1.4",
|
"@hackmd/markdown-it-task-lists": "^2.1.4",
|
||||||
"@json2csv/plainjs": "^7.0.6",
|
"@json2csv/plainjs": "^7.0.6",
|
||||||
"@shikijs/markdown-it": "^1.3.0",
|
"@shikijs/markdown-it": "^1.3.0",
|
||||||
|
"@tufjs/canonical-json": "^2.0.0",
|
||||||
"blurhash": "^2.0.5",
|
"blurhash": "^2.0.5",
|
||||||
"bullmq": "^5.7.1",
|
"bullmq": "^5.7.1",
|
||||||
"chalk": "^5.3.0",
|
"chalk": "^5.3.0",
|
||||||
|
|
@ -101,6 +102,7 @@
|
||||||
"pg": "^8.11.5",
|
"pg": "^8.11.5",
|
||||||
"request-parser": "workspace:*",
|
"request-parser": "workspace:*",
|
||||||
"sharp": "^0.33.3",
|
"sharp": "^0.33.3",
|
||||||
"zod": "^3.22.4"
|
"zod": "^3.22.4",
|
||||||
|
"zod-validation-error": "^3.2.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import {
|
||||||
desc,
|
desc,
|
||||||
eq,
|
eq,
|
||||||
inArray,
|
inArray,
|
||||||
|
isNotNull,
|
||||||
} from "drizzle-orm";
|
} from "drizzle-orm";
|
||||||
import { htmlToText } from "html-to-text";
|
import { htmlToText } from "html-to-text";
|
||||||
import type * as Lysand from "lysand-types";
|
import type * as Lysand from "lysand-types";
|
||||||
|
|
@ -30,17 +31,7 @@ import {
|
||||||
contentToHtml,
|
contentToHtml,
|
||||||
findFirstNote,
|
findFirstNote,
|
||||||
findManyNotes,
|
findManyNotes,
|
||||||
getStatusUri,
|
|
||||||
} from "~database/entities/Status";
|
} 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 { db } from "~drizzle/db";
|
||||||
import {
|
import {
|
||||||
Attachments,
|
Attachments,
|
||||||
|
|
@ -48,13 +39,12 @@ import {
|
||||||
NoteToMentions,
|
NoteToMentions,
|
||||||
Notes,
|
Notes,
|
||||||
Notifications,
|
Notifications,
|
||||||
UserToPinnedNotes,
|
|
||||||
Users,
|
Users,
|
||||||
UsersRelations,
|
|
||||||
} from "~drizzle/schema";
|
} from "~drizzle/schema";
|
||||||
import { config } from "~packages/config-manager";
|
import { config } from "~packages/config-manager";
|
||||||
import type { Attachment as APIAttachment } from "~types/mastodon/attachment";
|
import type { Attachment as APIAttachment } from "~types/mastodon/attachment";
|
||||||
import type { Status as APIStatus } from "~types/mastodon/status";
|
import type { Status as APIStatus } from "~types/mastodon/status";
|
||||||
|
import { User } from "./user";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gives helpers to fetch notes from database in a nice format
|
* Gives helpers to fetch notes from database in a nice format
|
||||||
|
|
@ -101,26 +91,33 @@ export class Note {
|
||||||
return found.map((s) => new Note(s));
|
return found.map((s) => new Note(s));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get id() {
|
||||||
|
return this.status.id;
|
||||||
|
}
|
||||||
|
|
||||||
async getUsersToFederateTo() {
|
async getUsersToFederateTo() {
|
||||||
// Mentioned users
|
// Mentioned users
|
||||||
const mentionedUsers =
|
const mentionedUsers =
|
||||||
this.getStatus().mentions.length > 0
|
this.getStatus().mentions.length > 0
|
||||||
? await findManyUsers({
|
? await User.manyFromSql(
|
||||||
where: (user, { and, isNotNull, inArray }) =>
|
|
||||||
and(
|
and(
|
||||||
isNotNull(user.instanceId),
|
isNotNull(Users.instanceId),
|
||||||
inArray(
|
inArray(
|
||||||
user.id,
|
Users.id,
|
||||||
this.getStatus().mentions.map(
|
this.getStatus().mentions.map(
|
||||||
(mention) => mention.id,
|
(mention) => mention.id,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
})
|
)
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
const usersThatCanSeePost = await findManyUsers({
|
const usersThatCanSeePost = await User.manyFromSql(
|
||||||
where: (user, { isNotNull }) => isNotNull(user.instanceId),
|
isNotNull(Users.instanceId),
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
with: {
|
with: {
|
||||||
relationships: {
|
relationships: {
|
||||||
where: (relationship, { eq, and }) =>
|
where: (relationship, { eq, and }) =>
|
||||||
|
|
@ -130,7 +127,8 @@ export class Note {
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const fusedUsers = [...mentionedUsers, ...usersThatCanSeePost];
|
const fusedUsers = [...mentionedUsers, ...usersThatCanSeePost];
|
||||||
|
|
||||||
|
|
@ -159,39 +157,13 @@ export class Note {
|
||||||
}
|
}
|
||||||
|
|
||||||
getAuthor() {
|
getAuthor() {
|
||||||
return this.status.author;
|
return new User(this.status.author);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getReplyChildren() {
|
async getReplyChildren() {
|
||||||
return await Note.manyFromSql(eq(Notes.replyId, this.status.id));
|
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>) {
|
static async insert(values: InferInsertModel<typeof Notes>) {
|
||||||
return (await db.insert(Notes).values(values).returning())[0];
|
return (await db.insert(Notes).values(values).returning())[0];
|
||||||
}
|
}
|
||||||
|
|
@ -204,7 +176,7 @@ export class Note {
|
||||||
spoiler_text: string,
|
spoiler_text: string,
|
||||||
emojis: EmojiWithInstance[],
|
emojis: EmojiWithInstance[],
|
||||||
uri?: string,
|
uri?: string,
|
||||||
mentions?: UserWithRelations[],
|
mentions?: User[],
|
||||||
/** List of IDs of database Attachment objects */
|
/** List of IDs of database Attachment objects */
|
||||||
media_attachments?: string[],
|
media_attachments?: string[],
|
||||||
replyId?: string,
|
replyId?: string,
|
||||||
|
|
@ -216,7 +188,7 @@ export class Note {
|
||||||
// Parse emojis and fuse with existing emojis
|
// Parse emojis and fuse with existing emojis
|
||||||
let foundEmojis = emojis;
|
let foundEmojis = emojis;
|
||||||
|
|
||||||
if (author.instanceId === null) {
|
if (author.isLocal()) {
|
||||||
const parsedEmojis = await parseEmojis(htmlContent);
|
const parsedEmojis = await parseEmojis(htmlContent);
|
||||||
// Fuse and deduplicate
|
// Fuse and deduplicate
|
||||||
foundEmojis = [...emojis, ...parsedEmojis].filter(
|
foundEmojis = [...emojis, ...parsedEmojis].filter(
|
||||||
|
|
@ -277,7 +249,7 @@ export class Note {
|
||||||
|
|
||||||
// Send notifications for mentioned local users
|
// Send notifications for mentioned local users
|
||||||
for (const mention of mentions ?? []) {
|
for (const mention of mentions ?? []) {
|
||||||
if (mention.instanceId === null) {
|
if (mention.isLocal()) {
|
||||||
await db.insert(Notifications).values({
|
await db.insert(Notifications).values({
|
||||||
accountId: author.id,
|
accountId: author.id,
|
||||||
notifiedId: mention.id,
|
notifiedId: mention.id,
|
||||||
|
|
@ -296,7 +268,7 @@ export class Note {
|
||||||
is_sensitive?: boolean,
|
is_sensitive?: boolean,
|
||||||
spoiler_text?: string,
|
spoiler_text?: string,
|
||||||
emojis: EmojiWithInstance[] = [],
|
emojis: EmojiWithInstance[] = [],
|
||||||
mentions: UserWithRelations[] = [],
|
mentions: User[] = [],
|
||||||
/** List of IDs of database Attachment objects */
|
/** List of IDs of database Attachment objects */
|
||||||
media_attachments: string[] = [],
|
media_attachments: string[] = [],
|
||||||
) {
|
) {
|
||||||
|
|
@ -307,7 +279,7 @@ export class Note {
|
||||||
// Parse emojis and fuse with existing emojis
|
// Parse emojis and fuse with existing emojis
|
||||||
let foundEmojis = emojis;
|
let foundEmojis = emojis;
|
||||||
|
|
||||||
if (this.getAuthor().instanceId === null && htmlContent) {
|
if (this.getAuthor().isLocal() && htmlContent) {
|
||||||
const parsedEmojis = await parseEmojis(htmlContent);
|
const parsedEmojis = await parseEmojis(htmlContent);
|
||||||
// Fuse and deduplicate
|
// Fuse and deduplicate
|
||||||
foundEmojis = [...emojis, ...parsedEmojis].filter(
|
foundEmojis = [...emojis, ...parsedEmojis].filter(
|
||||||
|
|
@ -401,7 +373,7 @@ export class Note {
|
||||||
* @param user The user to check.
|
* @param user The user to check.
|
||||||
* @returns Whether this status is viewable by the user.
|
* @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.getAuthor().id === user?.id) return true;
|
||||||
if (this.getStatus().visibility === "public") return true;
|
if (this.getStatus().visibility === "public") return true;
|
||||||
if (this.getStatus().visibility === "unlisted") 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 data = this.getStatus();
|
||||||
const wasPinnedByUser = userFetching
|
const wasPinnedByUser = userFetching
|
||||||
? !!(await db.query.UserToPinnedNotes.findFirst({
|
? !!(await db.query.UserToPinnedNotes.findFirst({
|
||||||
|
|
@ -480,7 +452,7 @@ export class Note {
|
||||||
id: data.id,
|
id: data.id,
|
||||||
in_reply_to_id: data.replyId || null,
|
in_reply_to_id: data.replyId || null,
|
||||||
in_reply_to_account_id: data.reply?.authorId || 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(),
|
created_at: new Date(data.createdAt).toISOString(),
|
||||||
application: data.application
|
application: data.application
|
||||||
? applicationToAPI(data.application)
|
? applicationToAPI(data.application)
|
||||||
|
|
@ -495,7 +467,16 @@ export class Note {
|
||||||
media_attachments: (data.attachments ?? []).map(
|
media_attachments: (data.attachments ?? []).map(
|
||||||
(a) => attachmentToAPI(a) as APIAttachment,
|
(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,
|
language: null,
|
||||||
muted: wasMutedByUser,
|
muted: wasMutedByUser,
|
||||||
pinned: wasPinnedByUser,
|
pinned: wasPinnedByUser,
|
||||||
|
|
@ -531,8 +512,13 @@ export class Note {
|
||||||
return localObjectURI(this.getStatus().id);
|
return localObjectURI(this.getStatus().id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static getURI(id?: string | null) {
|
||||||
|
if (!id) return null;
|
||||||
|
return localObjectURI(id);
|
||||||
|
}
|
||||||
|
|
||||||
getMastoURI() {
|
getMastoURI() {
|
||||||
return `/@${this.getAuthor().username}/${this.getStatus().id}`;
|
return `/@${this.getAuthor().getUser().username}/${this.id}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
toLysand(): Lysand.Note {
|
toLysand(): Lysand.Note {
|
||||||
|
|
@ -541,7 +527,7 @@ export class Note {
|
||||||
type: "Note",
|
type: "Note",
|
||||||
created_at: new Date(status.createdAt).toISOString(),
|
created_at: new Date(status.createdAt).toISOString(),
|
||||||
id: status.id,
|
id: status.id,
|
||||||
author: getUserUri(status.author),
|
author: this.getAuthor().getUri(),
|
||||||
uri: this.getURI(),
|
uri: this.getURI(),
|
||||||
content: {
|
content: {
|
||||||
"text/html": {
|
"text/html": {
|
||||||
|
|
@ -556,8 +542,8 @@ export class Note {
|
||||||
),
|
),
|
||||||
is_sensitive: status.sensitive,
|
is_sensitive: status.sensitive,
|
||||||
mentions: status.mentions.map((mention) => mention.uri || ""),
|
mentions: status.mentions.map((mention) => mention.uri || ""),
|
||||||
quotes: getStatusUri(status.quote) ?? undefined,
|
quotes: Note.getURI(status.quotingId) ?? undefined,
|
||||||
replies_to: getStatusUri(status.reply) ?? undefined,
|
replies_to: Note.getURI(status.replyId) ?? undefined,
|
||||||
subject: status.spoilerText,
|
subject: status.spoilerText,
|
||||||
visibility: status.visibility as Lysand.Visibility,
|
visibility: status.visibility as Lysand.Visibility,
|
||||||
extensions: {
|
extensions: {
|
||||||
|
|
@ -572,7 +558,7 @@ export class Note {
|
||||||
/**
|
/**
|
||||||
* Return all the ancestors of this post,
|
* Return all the ancestors of this post,
|
||||||
*/
|
*/
|
||||||
async getAncestors(fetcher: UserWithRelationsAndRelationships | null) {
|
async getAncestors(fetcher: User | null) {
|
||||||
const ancestors: Note[] = [];
|
const ancestors: Note[] = [];
|
||||||
|
|
||||||
let currentStatus: Note = this;
|
let currentStatus: Note = this;
|
||||||
|
|
@ -599,10 +585,7 @@ export class Note {
|
||||||
* Return all the descendants of this post (recursive)
|
* Return all the descendants of this post (recursive)
|
||||||
* Temporary implementation, will be replaced with a recursive SQL query when I get to it
|
* Temporary implementation, will be replaced with a recursive SQL query when I get to it
|
||||||
*/
|
*/
|
||||||
async getDescendants(
|
async getDescendants(fetcher: User | null, depth = 0) {
|
||||||
fetcher: UserWithRelationsAndRelationships | null,
|
|
||||||
depth = 0,
|
|
||||||
) {
|
|
||||||
const descendants: Note[] = [];
|
const descendants: Note[] = [];
|
||||||
for (const child of await this.getReplyChildren()) {
|
for (const child of await this.getReplyChildren()) {
|
||||||
descendants.push(child);
|
descendants.push(child);
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
import { type SQL, gt } from "drizzle-orm";
|
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 { config } from "~packages/config-manager";
|
||||||
import { Note } from "./note";
|
import { Note } from "./note";
|
||||||
|
import { User } from "./user";
|
||||||
|
|
||||||
enum TimelineType {
|
enum TimelineType {
|
||||||
NOTE = "Note",
|
NOTE = "Note",
|
||||||
|
USER = "User",
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Timeline {
|
export class Timeline {
|
||||||
|
|
@ -15,7 +17,23 @@ export class Timeline {
|
||||||
limit: number,
|
limit: number,
|
||||||
url: string,
|
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>(
|
private async fetchTimeline<T>(
|
||||||
|
|
@ -23,13 +41,15 @@ export class Timeline {
|
||||||
limit: number,
|
limit: number,
|
||||||
url: string,
|
url: string,
|
||||||
) {
|
) {
|
||||||
const objects: Note[] = [];
|
const notes: Note[] = [];
|
||||||
|
const users: User[] = [];
|
||||||
|
|
||||||
switch (this.type) {
|
switch (this.type) {
|
||||||
case TimelineType.NOTE:
|
case TimelineType.NOTE:
|
||||||
objects.push(
|
notes.push(...(await Note.manyFromSql(sql, undefined, limit)));
|
||||||
...(await Note.manyFromSql(sql, undefined, limit)),
|
break;
|
||||||
);
|
case TimelineType.USER:
|
||||||
|
users.push(...(await User.manyFromSql(sql, undefined, limit)));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -39,26 +59,26 @@ export class Timeline {
|
||||||
config.http.base_url,
|
config.http.base_url,
|
||||||
).toString();
|
).toString();
|
||||||
|
|
||||||
if (objects.length > 0) {
|
if (notes.length > 0) {
|
||||||
switch (this.type) {
|
switch (this.type) {
|
||||||
case TimelineType.NOTE: {
|
case TimelineType.NOTE: {
|
||||||
const objectBefore = await Note.fromSql(
|
const objectBefore = await Note.fromSql(
|
||||||
gt(Notes.id, objects[0].getStatus().id),
|
gt(Notes.id, notes[0].getStatus().id),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (objectBefore) {
|
if (objectBefore) {
|
||||||
linkHeader.push(
|
linkHeader.push(
|
||||||
`<${urlWithoutQuery}?limit=${limit ?? 20}&min_id=${
|
`<${urlWithoutQuery}?limit=${limit ?? 20}&min_id=${
|
||||||
objects[0].getStatus().id
|
notes[0].getStatus().id
|
||||||
}>; rel="prev"`,
|
}>; rel="prev"`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (objects.length >= (limit ?? 20)) {
|
if (notes.length >= (limit ?? 20)) {
|
||||||
const objectAfter = await Note.fromSql(
|
const objectAfter = await Note.fromSql(
|
||||||
gt(
|
gt(
|
||||||
Notes.id,
|
Notes.id,
|
||||||
objects[objects.length - 1].getStatus().id,
|
notes[notes.length - 1].getStatus().id,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -67,7 +87,37 @@ export class Timeline {
|
||||||
`<${urlWithoutQuery}?limit=${
|
`<${urlWithoutQuery}?limit=${
|
||||||
limit ?? 20
|
limit ?? 20
|
||||||
}&max_id=${
|
}&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"`,
|
}>; rel="next"`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -77,9 +127,17 @@ export class Timeline {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
switch (this.type) {
|
||||||
|
case TimelineType.NOTE:
|
||||||
return {
|
return {
|
||||||
link: linkHeader.join(", "),
|
link: linkHeader.join(", "),
|
||||||
objects,
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,11 +2,8 @@ import { join } from "node:path";
|
||||||
import { redirect } from "@response";
|
import { redirect } from "@response";
|
||||||
import type { BunFile } from "bun";
|
import type { BunFile } from "bun";
|
||||||
import { config } from "config-manager";
|
import { config } from "config-manager";
|
||||||
import {
|
import { retrieveUserFromToken } from "~database/entities/User";
|
||||||
type UserWithRelations,
|
import type { User } from "~packages/database-interface/user";
|
||||||
retrieveUserFromToken,
|
|
||||||
userToAPI,
|
|
||||||
} from "~database/entities/User";
|
|
||||||
import type { LogManager, MultiLogManager } from "~packages/log-manager";
|
import type { LogManager, MultiLogManager } from "~packages/log-manager";
|
||||||
import { languages } from "./glitch-languages";
|
import { languages } from "./glitch-languages";
|
||||||
|
|
||||||
|
|
@ -104,7 +101,7 @@ const handleSignInRequest = async (
|
||||||
req: Request,
|
req: Request,
|
||||||
path: string,
|
path: string,
|
||||||
url: URL,
|
url: URL,
|
||||||
user: UserWithRelations | null,
|
user: User | null,
|
||||||
accessToken: string,
|
accessToken: string,
|
||||||
) => {
|
) => {
|
||||||
if (req.method === "POST") {
|
if (req.method === "POST") {
|
||||||
|
|
@ -181,7 +178,7 @@ const returnFile = async (file: BunFile, content?: string) => {
|
||||||
const handleDefaultRequest = async (
|
const handleDefaultRequest = async (
|
||||||
req: Request,
|
req: Request,
|
||||||
path: string,
|
path: string,
|
||||||
user: UserWithRelations | null,
|
user: User | null,
|
||||||
accessToken: string,
|
accessToken: string,
|
||||||
) => {
|
) => {
|
||||||
const file = Bun.file(join(config.frontend.glitch.assets, path));
|
const file = Bun.file(join(config.frontend.glitch.assets, path));
|
||||||
|
|
@ -204,7 +201,7 @@ const handleDefaultRequest = async (
|
||||||
const brandingTransforms = async (
|
const brandingTransforms = async (
|
||||||
fileContents: string,
|
fileContents: string,
|
||||||
accessToken: string,
|
accessToken: string,
|
||||||
user: UserWithRelations | null,
|
user: User | null,
|
||||||
) => {
|
) => {
|
||||||
let newFileContents = fileContents;
|
let newFileContents = fileContents;
|
||||||
for (const server of config.frontend.glitch.server) {
|
for (const server of config.frontend.glitch.server) {
|
||||||
|
|
@ -239,7 +236,7 @@ const brandingTransforms = async (
|
||||||
const htmlTransforms = async (
|
const htmlTransforms = async (
|
||||||
fileContents: string,
|
fileContents: string,
|
||||||
accessToken: string,
|
accessToken: string,
|
||||||
user: UserWithRelations | null,
|
user: User | null,
|
||||||
) => {
|
) => {
|
||||||
// Find script id="initial-state" and replace its contents with custom json
|
// Find script id="initial-state" and replace its contents with custom json
|
||||||
const rewriter = new HTMLRewriter()
|
const rewriter = new HTMLRewriter()
|
||||||
|
|
@ -290,7 +287,7 @@ const htmlTransforms = async (
|
||||||
},
|
},
|
||||||
accounts: user
|
accounts: user
|
||||||
? {
|
? {
|
||||||
[user.id]: userToAPI(user, true),
|
[user.id]: user.toAPI(true),
|
||||||
}
|
}
|
||||||
: {},
|
: {},
|
||||||
media_attachments: {
|
media_attachments: {
|
||||||
|
|
|
||||||
262
packages/lysand-utils/index.ts
Normal file
262
packages/lysand-utils/index.ts
Normal file
|
|
@ -0,0 +1,262 @@
|
||||||
|
import type * as Lysand from "lysand-types";
|
||||||
|
import { fromZodError } from "zod-validation-error";
|
||||||
|
import { schemas } from "./schemas";
|
||||||
|
|
||||||
|
const types = [
|
||||||
|
"Note",
|
||||||
|
"User",
|
||||||
|
"Reaction",
|
||||||
|
"Poll",
|
||||||
|
"Vote",
|
||||||
|
"VoteResult",
|
||||||
|
"Report",
|
||||||
|
"ServerMetadata",
|
||||||
|
"Like",
|
||||||
|
"Dislike",
|
||||||
|
"Follow",
|
||||||
|
"FollowAccept",
|
||||||
|
"FollowReject",
|
||||||
|
"Announce",
|
||||||
|
"Undo",
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates an incoming Lysand object using Zod, and returns the object if it is valid.
|
||||||
|
*/
|
||||||
|
export class EntityValidator {
|
||||||
|
constructor(private entity: Lysand.Entity) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates the entity.
|
||||||
|
*/
|
||||||
|
validate<ExpectedType>() {
|
||||||
|
// Check if type is valid
|
||||||
|
if (!this.entity.type) {
|
||||||
|
throw new Error("Entity type is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
const schema = this.matchSchema(this.getType());
|
||||||
|
|
||||||
|
const output = schema.safeParse(this.entity);
|
||||||
|
|
||||||
|
if (!output.success) {
|
||||||
|
throw fromZodError(output.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return output.data as ExpectedType;
|
||||||
|
}
|
||||||
|
|
||||||
|
getType() {
|
||||||
|
// Check if type is valid, return TypeScript type
|
||||||
|
if (!this.entity.type) {
|
||||||
|
throw new Error("Entity type is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!types.includes(this.entity.type)) {
|
||||||
|
throw new Error(`Unknown entity type: ${this.entity.type}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.entity.type as (typeof types)[number];
|
||||||
|
}
|
||||||
|
|
||||||
|
matchSchema(type: string) {
|
||||||
|
switch (type) {
|
||||||
|
case "Note":
|
||||||
|
return schemas.Note;
|
||||||
|
case "User":
|
||||||
|
return schemas.User;
|
||||||
|
case "Reaction":
|
||||||
|
return schemas.Reaction;
|
||||||
|
case "Poll":
|
||||||
|
return schemas.Poll;
|
||||||
|
case "Vote":
|
||||||
|
return schemas.Vote;
|
||||||
|
case "VoteResult":
|
||||||
|
return schemas.VoteResult;
|
||||||
|
case "Report":
|
||||||
|
return schemas.Report;
|
||||||
|
case "ServerMetadata":
|
||||||
|
return schemas.ServerMetadata;
|
||||||
|
case "Like":
|
||||||
|
return schemas.Like;
|
||||||
|
case "Dislike":
|
||||||
|
return schemas.Dislike;
|
||||||
|
case "Follow":
|
||||||
|
return schemas.Follow;
|
||||||
|
case "FollowAccept":
|
||||||
|
return schemas.FollowAccept;
|
||||||
|
case "FollowReject":
|
||||||
|
return schemas.FollowReject;
|
||||||
|
case "Announce":
|
||||||
|
return schemas.Announce;
|
||||||
|
case "Undo":
|
||||||
|
return schemas.Undo;
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown entity type: ${type}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SignatureValidator {
|
||||||
|
constructor(
|
||||||
|
private public_key: CryptoKey,
|
||||||
|
private signature: string,
|
||||||
|
private date: string,
|
||||||
|
private method: string,
|
||||||
|
private url: URL,
|
||||||
|
private body: string,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
static async fromStringKey(
|
||||||
|
public_key: string,
|
||||||
|
signature: string,
|
||||||
|
date: string,
|
||||||
|
method: string,
|
||||||
|
url: URL,
|
||||||
|
body: string,
|
||||||
|
) {
|
||||||
|
return new SignatureValidator(
|
||||||
|
await crypto.subtle.importKey(
|
||||||
|
"spki",
|
||||||
|
Buffer.from(public_key, "base64"),
|
||||||
|
"Ed25519",
|
||||||
|
false,
|
||||||
|
["verify"],
|
||||||
|
),
|
||||||
|
signature,
|
||||||
|
date,
|
||||||
|
method,
|
||||||
|
url,
|
||||||
|
body,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
async validate() {
|
||||||
|
const signature = this.signature
|
||||||
|
.split("signature=")[1]
|
||||||
|
.replace(/"/g, "");
|
||||||
|
|
||||||
|
const digest = await crypto.subtle.digest(
|
||||||
|
"SHA-256",
|
||||||
|
new TextEncoder().encode(this.body),
|
||||||
|
);
|
||||||
|
|
||||||
|
const expectedSignedString =
|
||||||
|
`(request-target): ${this.method.toLowerCase()} ${
|
||||||
|
this.url.pathname
|
||||||
|
}\n` +
|
||||||
|
`host: ${this.url.host}\n` +
|
||||||
|
`date: ${this.date}\n` +
|
||||||
|
`digest: SHA-256=${Buffer.from(new Uint8Array(digest)).toString(
|
||||||
|
"base64",
|
||||||
|
)}\n`;
|
||||||
|
|
||||||
|
// Check if signed string is valid
|
||||||
|
const isValid = await crypto.subtle.verify(
|
||||||
|
"Ed25519",
|
||||||
|
this.public_key,
|
||||||
|
Buffer.from(signature, "base64"),
|
||||||
|
new TextEncoder().encode(expectedSignedString),
|
||||||
|
);
|
||||||
|
|
||||||
|
return isValid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SignatureConstructor {
|
||||||
|
constructor(
|
||||||
|
private private_key: CryptoKey,
|
||||||
|
private url: URL,
|
||||||
|
private authorUri: URL,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
static async fromStringKey(private_key: string, url: URL, authorUri: URL) {
|
||||||
|
return new SignatureConstructor(
|
||||||
|
await crypto.subtle.importKey(
|
||||||
|
"pkcs8",
|
||||||
|
Buffer.from(private_key, "base64"),
|
||||||
|
"Ed25519",
|
||||||
|
false,
|
||||||
|
["sign"],
|
||||||
|
),
|
||||||
|
url,
|
||||||
|
authorUri,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async sign(method: string, body: string) {
|
||||||
|
const digest = await crypto.subtle.digest(
|
||||||
|
"SHA-256",
|
||||||
|
new TextEncoder().encode(body),
|
||||||
|
);
|
||||||
|
|
||||||
|
const date = new Date();
|
||||||
|
|
||||||
|
const signature = await crypto.subtle.sign(
|
||||||
|
"Ed25519",
|
||||||
|
this.private_key,
|
||||||
|
new TextEncoder().encode(
|
||||||
|
`(request-target): ${method.toLowerCase()} ${
|
||||||
|
this.url.pathname
|
||||||
|
}\n` +
|
||||||
|
`host: ${this.url.host}\n` +
|
||||||
|
`date: ${date.toISOString()}\n` +
|
||||||
|
`digest: SHA-256=${Buffer.from(
|
||||||
|
new Uint8Array(digest),
|
||||||
|
).toString("base64")}\n`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const signatureBase64 = Buffer.from(new Uint8Array(signature)).toString(
|
||||||
|
"base64",
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
date: date.toISOString(),
|
||||||
|
signature: `keyId="${this.authorUri.toString()}",algorithm="ed25519",headers="(request-target) host date digest",signature="${signatureBase64}"`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extends native fetch with object signing
|
||||||
|
* Make sure to format your JSON in Canonical JSON format!
|
||||||
|
* @param url URL to fetch
|
||||||
|
* @param options Standard Web Fetch API options
|
||||||
|
* @param privateKey Author private key in base64
|
||||||
|
* @param authorUri Author URI
|
||||||
|
* @param baseUrl Base URL of this server
|
||||||
|
* @returns Fetch response
|
||||||
|
*/
|
||||||
|
export const signedFetch = async (
|
||||||
|
url: string | URL,
|
||||||
|
options: RequestInit,
|
||||||
|
privateKey: string,
|
||||||
|
authorUri: string | URL,
|
||||||
|
baseUrl: string | URL,
|
||||||
|
) => {
|
||||||
|
const urlObj = new URL(url);
|
||||||
|
const authorUriObj = new URL(authorUri);
|
||||||
|
|
||||||
|
const signature = await SignatureConstructor.fromStringKey(
|
||||||
|
privateKey,
|
||||||
|
urlObj,
|
||||||
|
authorUriObj,
|
||||||
|
);
|
||||||
|
|
||||||
|
const { date, signature: signatureHeader } = await signature.sign(
|
||||||
|
options.method ?? "GET",
|
||||||
|
options.body?.toString() || "",
|
||||||
|
);
|
||||||
|
|
||||||
|
return fetch(url, {
|
||||||
|
...options,
|
||||||
|
headers: {
|
||||||
|
Date: date,
|
||||||
|
Origin: new URL(baseUrl).origin,
|
||||||
|
Signature: signatureHeader,
|
||||||
|
"Content-Type": "application/json; charset=utf-8",
|
||||||
|
Accept: "application/json",
|
||||||
|
...options.headers,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
6
packages/lysand-utils/package.json
Normal file
6
packages/lysand-utils/package.json
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"name": "lysand-utils",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"main": "index.ts",
|
||||||
|
"dependencies": { "zod": "^3.22.4", "zod-validation-error": "^3.1.0" }
|
||||||
|
}
|
||||||
261
packages/lysand-utils/schemas.ts
Normal file
261
packages/lysand-utils/schemas.ts
Normal file
|
|
@ -0,0 +1,261 @@
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const Entity = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
created_at: z.string(),
|
||||||
|
uri: z.string().url(),
|
||||||
|
type: z.string(),
|
||||||
|
extensions: z.object({
|
||||||
|
"org.lysand:custom_emojis": z.object({
|
||||||
|
emojis: z.array(
|
||||||
|
z.object({
|
||||||
|
shortcode: z.string(),
|
||||||
|
url: z.string(),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const ContentFormat = z.record(
|
||||||
|
z.string(),
|
||||||
|
z.object({
|
||||||
|
content: z.string(),
|
||||||
|
description: z.string().optional(),
|
||||||
|
size: z.number().optional(),
|
||||||
|
hash: z.record(z.string().optional()).optional(),
|
||||||
|
blurhash: z.string().optional(),
|
||||||
|
fps: z.number().optional(),
|
||||||
|
width: z.number().optional(),
|
||||||
|
height: z.number().optional(),
|
||||||
|
duration: z.number().optional(),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const Visibility = z.enum(["public", "unlisted", "private", "direct"]);
|
||||||
|
|
||||||
|
const Publication = Entity.extend({
|
||||||
|
type: z.union([z.literal("Note"), z.literal("Patch")]),
|
||||||
|
author: z.string().url(),
|
||||||
|
content: ContentFormat.optional(),
|
||||||
|
attachments: z.array(ContentFormat).optional(),
|
||||||
|
replies_to: z.string().url().optional(),
|
||||||
|
quotes: z.string().url().optional(),
|
||||||
|
mentions: z.array(z.string().url()).optional(),
|
||||||
|
subject: z.string().optional(),
|
||||||
|
is_sensitive: z.boolean().optional(),
|
||||||
|
visibility: Visibility,
|
||||||
|
extensions: Entity.shape.extensions.extend({
|
||||||
|
"org.lysand:reactions": z
|
||||||
|
.object({
|
||||||
|
reactions: z.string(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
"org.lysand:polls": z
|
||||||
|
.object({
|
||||||
|
poll: z.object({
|
||||||
|
options: z.array(ContentFormat),
|
||||||
|
votes: z.array(z.number()),
|
||||||
|
multiple_choice: z.boolean().optional(),
|
||||||
|
expires_at: z.string(),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const Note = Publication.extend({
|
||||||
|
type: z.literal("Note"),
|
||||||
|
});
|
||||||
|
|
||||||
|
const Patch = Publication.extend({
|
||||||
|
type: z.literal("Patch"),
|
||||||
|
patched_id: z.string().uuid(),
|
||||||
|
patched_at: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const ActorPublicKeyData = z.object({
|
||||||
|
public_key: z.string(),
|
||||||
|
actor: z.string().url(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const VanityExtension = z.object({
|
||||||
|
avatar_overlay: ContentFormat.optional(),
|
||||||
|
avatar_mask: ContentFormat.optional(),
|
||||||
|
background: ContentFormat.optional(),
|
||||||
|
audio: ContentFormat.optional(),
|
||||||
|
pronouns: z.record(
|
||||||
|
z.string(),
|
||||||
|
z.array(
|
||||||
|
z.union([
|
||||||
|
z.object({
|
||||||
|
subject: z.string(),
|
||||||
|
object: z.string(),
|
||||||
|
dependent_possessive: z.string(),
|
||||||
|
independent_possessive: z.string(),
|
||||||
|
reflexive: z.string(),
|
||||||
|
}),
|
||||||
|
z.string(),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
birthday: z.string().optional(),
|
||||||
|
location: z.string().optional(),
|
||||||
|
activitypub: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const User = Entity.extend({
|
||||||
|
type: z.literal("User"),
|
||||||
|
display_name: z.string().optional(),
|
||||||
|
username: z.string(),
|
||||||
|
avatar: ContentFormat.optional(),
|
||||||
|
header: ContentFormat.optional(),
|
||||||
|
indexable: z.boolean(),
|
||||||
|
public_key: ActorPublicKeyData,
|
||||||
|
bio: ContentFormat.optional(),
|
||||||
|
fields: z
|
||||||
|
.array(
|
||||||
|
z.object({
|
||||||
|
name: ContentFormat,
|
||||||
|
value: ContentFormat,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.optional(),
|
||||||
|
featured: z.string().url(),
|
||||||
|
followers: z.string().url(),
|
||||||
|
following: z.string().url(),
|
||||||
|
likes: z.string().url(),
|
||||||
|
dislikes: z.string().url(),
|
||||||
|
inbox: z.string().url(),
|
||||||
|
outbox: z.string().url(),
|
||||||
|
extensions: Entity.shape.extensions.extend({
|
||||||
|
"org.lysand:vanity": VanityExtension.optional(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const Action = Entity.extend({
|
||||||
|
type: z.union([
|
||||||
|
z.literal("Like"),
|
||||||
|
z.literal("Dislike"),
|
||||||
|
z.literal("Follow"),
|
||||||
|
z.literal("FollowAccept"),
|
||||||
|
z.literal("FollowReject"),
|
||||||
|
z.literal("Announce"),
|
||||||
|
z.literal("Undo"),
|
||||||
|
]),
|
||||||
|
author: z.string().url(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const Like = Action.extend({
|
||||||
|
type: z.literal("Like"),
|
||||||
|
object: z.string().url(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const Undo = Action.extend({
|
||||||
|
type: z.literal("Undo"),
|
||||||
|
object: z.string().url(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const Dislike = Action.extend({
|
||||||
|
type: z.literal("Dislike"),
|
||||||
|
object: z.string().url(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const Follow = Action.extend({
|
||||||
|
type: z.literal("Follow"),
|
||||||
|
followee: z.string().url(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const FollowAccept = Action.extend({
|
||||||
|
type: z.literal("FollowAccept"),
|
||||||
|
follower: z.string().url(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const FollowReject = Action.extend({
|
||||||
|
type: z.literal("FollowReject"),
|
||||||
|
follower: z.string().url(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const Announce = Action.extend({
|
||||||
|
type: z.literal("Announce"),
|
||||||
|
object: z.string().url(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const Extension = Entity.extend({
|
||||||
|
type: z.literal("Extension"),
|
||||||
|
extension_type: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const Reaction = Extension.extend({
|
||||||
|
extension_type: z.literal("org.lysand:reactions/Reaction"),
|
||||||
|
object: z.string().url(),
|
||||||
|
content: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const Poll = Extension.extend({
|
||||||
|
extension_type: z.literal("org.lysand:polls/Poll"),
|
||||||
|
options: z.array(ContentFormat),
|
||||||
|
votes: z.array(z.number()),
|
||||||
|
multiple_choice: z.boolean().optional(),
|
||||||
|
expires_at: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const Vote = Extension.extend({
|
||||||
|
extension_type: z.literal("org.lysand:polls/Vote"),
|
||||||
|
poll: z.string().url(),
|
||||||
|
option: z.number(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const VoteResult = Extension.extend({
|
||||||
|
extension_type: z.literal("org.lysand:polls/VoteResult"),
|
||||||
|
poll: z.string().url(),
|
||||||
|
votes: z.array(z.number()),
|
||||||
|
});
|
||||||
|
|
||||||
|
const Report = Extension.extend({
|
||||||
|
extension_type: z.literal("org.lysand:reports/Report"),
|
||||||
|
objects: z.array(z.string().url()),
|
||||||
|
reason: z.string(),
|
||||||
|
comment: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const ServerMetadata = Entity.extend({
|
||||||
|
type: z.literal("ServerMetadata"),
|
||||||
|
name: z.string(),
|
||||||
|
version: z.string(),
|
||||||
|
description: z.string().optional(),
|
||||||
|
website: z.string().optional(),
|
||||||
|
moderators: z.array(z.string()).optional(),
|
||||||
|
admins: z.array(z.string()).optional(),
|
||||||
|
logo: ContentFormat.optional(),
|
||||||
|
banner: ContentFormat.optional(),
|
||||||
|
supported_extensions: z.array(z.string()),
|
||||||
|
extensions: z.record(z.string(), z.any()).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const schemas = {
|
||||||
|
Entity,
|
||||||
|
ContentFormat,
|
||||||
|
Visibility,
|
||||||
|
Publication,
|
||||||
|
Note,
|
||||||
|
Patch,
|
||||||
|
ActorPublicKeyData,
|
||||||
|
VanityExtension,
|
||||||
|
User,
|
||||||
|
Action,
|
||||||
|
Like,
|
||||||
|
Undo,
|
||||||
|
Dislike,
|
||||||
|
Follow,
|
||||||
|
FollowAccept,
|
||||||
|
FollowReject,
|
||||||
|
Announce,
|
||||||
|
Extension,
|
||||||
|
Reaction,
|
||||||
|
Poll,
|
||||||
|
Vote,
|
||||||
|
VoteResult,
|
||||||
|
Report,
|
||||||
|
ServerMetadata,
|
||||||
|
};
|
||||||
|
|
@ -1,51 +0,0 @@
|
||||||
import type { APActor, APNote } from "activitypub-types";
|
|
||||||
import { ActivityPubTranslator } from "./protocols/activitypub";
|
|
||||||
|
|
||||||
export enum SupportedProtocols {
|
|
||||||
ACTIVITYPUB = "activitypub",
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ProtocolTranslator
|
|
||||||
* @summary Translates between federation protocols such as ActivityPub to Lysand and back
|
|
||||||
* @description This class is responsible for translating between federation protocols such as ActivityPub to Lysand and back.
|
|
||||||
* This class is not meant to be instantiated directly, but rather for its children to be used.
|
|
||||||
*/
|
|
||||||
export class ProtocolTranslator {
|
|
||||||
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
|
|
||||||
static auto(object: any) {
|
|
||||||
const protocol = ProtocolTranslator.recognizeProtocol(object);
|
|
||||||
switch (protocol) {
|
|
||||||
case SupportedProtocols.ACTIVITYPUB:
|
|
||||||
return new ActivityPubTranslator();
|
|
||||||
default:
|
|
||||||
throw new Error("Unknown protocol");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Translates an ActivityPub actor to a Lysand user
|
|
||||||
* @param data Raw JSON-LD data from an ActivityPub actor
|
|
||||||
*/
|
|
||||||
user(data: APActor) {
|
|
||||||
//
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Translates an ActivityPub note to a Lysand status
|
|
||||||
* @param data Raw JSON-LD data from an ActivityPub note
|
|
||||||
*/
|
|
||||||
status(data: APNote) {
|
|
||||||
//
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Automatically recognizes the protocol of a given object
|
|
||||||
*/
|
|
||||||
|
|
||||||
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
|
|
||||||
private static recognizeProtocol(object: any) {
|
|
||||||
// Temporary stub
|
|
||||||
return SupportedProtocols.ACTIVITYPUB;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
{
|
|
||||||
"name": "protocol-translator",
|
|
||||||
"version": "0.0.0",
|
|
||||||
"main": "index.ts",
|
|
||||||
"dependencies": {},
|
|
||||||
"devDependencies": {
|
|
||||||
"activitypub-types": "^1.1.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
import { ProtocolTranslator } from "..";
|
|
||||||
|
|
||||||
export class ActivityPubTranslator extends ProtocolTranslator {
|
|
||||||
user() {}
|
|
||||||
}
|
|
||||||
|
|
@ -7,11 +7,8 @@ import { RequestParser } from "request-parser";
|
||||||
import type { ZodType, z } from "zod";
|
import type { ZodType, z } from "zod";
|
||||||
import { fromZodError } from "zod-validation-error";
|
import { fromZodError } from "zod-validation-error";
|
||||||
import type { Application } from "~database/entities/Application";
|
import type { Application } from "~database/entities/Application";
|
||||||
import {
|
import { type AuthData, getFromRequest } from "~database/entities/User";
|
||||||
type AuthData,
|
import type { User } from "~packages/database-interface/user";
|
||||||
type UserWithRelations,
|
|
||||||
getFromRequest,
|
|
||||||
} from "~database/entities/User";
|
|
||||||
|
|
||||||
type MaybePromise<T> = T | Promise<T>;
|
type MaybePromise<T> = T | Promise<T>;
|
||||||
type HttpVerb = "GET" | "POST" | "PUT" | "DELETE" | "PATCH" | "OPTIONS";
|
type HttpVerb = "GET" | "POST" | "PUT" | "DELETE" | "PATCH" | "OPTIONS";
|
||||||
|
|
@ -24,11 +21,11 @@ export type RouteHandler<
|
||||||
matchedRoute: MatchedRoute,
|
matchedRoute: MatchedRoute,
|
||||||
extraData: {
|
extraData: {
|
||||||
auth: {
|
auth: {
|
||||||
// If the route doesn't require authentication, set the type to UserWithRelations | null
|
// If the route doesn't require authentication, set the type to User | null
|
||||||
// Otherwise set to UserWithRelations
|
// Otherwise set to User
|
||||||
user: RouteMeta["auth"]["required"] extends true
|
user: RouteMeta["auth"]["required"] extends true
|
||||||
? UserWithRelations
|
? User
|
||||||
: UserWithRelations | null;
|
: User | null;
|
||||||
token: RouteMeta["auth"]["required"] extends true
|
token: RouteMeta["auth"]["required"] extends true
|
||||||
? string
|
? string
|
||||||
: string | null;
|
: string | null;
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,14 @@
|
||||||
import { apiRoute, applyConfig } from "@api";
|
import { apiRoute, applyConfig } from "@api";
|
||||||
import { errorResponse, response } from "@response";
|
import { errorResponse, response } from "@response";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
import { SignJWT } from "jose";
|
import { SignJWT } from "jose";
|
||||||
import { stringify } from "qs";
|
import { stringify } from "qs";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { fromZodError } from "zod-validation-error";
|
import { fromZodError } from "zod-validation-error";
|
||||||
import { findFirstUser } from "~database/entities/User";
|
|
||||||
import { db } from "~drizzle/db";
|
import { db } from "~drizzle/db";
|
||||||
|
import { Users } from "~drizzle/schema";
|
||||||
import { config } from "~packages/config-manager";
|
import { config } from "~packages/config-manager";
|
||||||
|
import { User } from "~packages/database-interface/user";
|
||||||
import { RequestParser } from "~packages/request-parser";
|
import { RequestParser } from "~packages/request-parser";
|
||||||
|
|
||||||
export const meta = applyConfig({
|
export const meta = applyConfig({
|
||||||
|
|
@ -77,11 +79,12 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
|
||||||
);
|
);
|
||||||
|
|
||||||
// Find user
|
// Find user
|
||||||
const user = await findFirstUser({
|
const user = await User.fromSql(eq(Users.email, email));
|
||||||
where: (user, { eq }) => eq(user.email, email),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!user || !(await Bun.password.verify(password, user.password || "")))
|
if (
|
||||||
|
!user ||
|
||||||
|
!(await Bun.password.verify(password, user.getUser().password || ""))
|
||||||
|
)
|
||||||
return returnError(
|
return returnError(
|
||||||
extraData.parsedRequest,
|
extraData.parsedRequest,
|
||||||
"invalid_request",
|
"invalid_request",
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,11 @@ import { randomBytes } from "node:crypto";
|
||||||
import { apiRoute, applyConfig } from "@api";
|
import { apiRoute, applyConfig } from "@api";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { TokenType } from "~database/entities/Token";
|
import { TokenType } from "~database/entities/Token";
|
||||||
import { findFirstUser } from "~database/entities/User";
|
|
||||||
import { db } from "~drizzle/db";
|
import { db } from "~drizzle/db";
|
||||||
import { Tokens } from "~drizzle/schema";
|
import { Tokens, Users } from "~drizzle/schema";
|
||||||
import { config } from "~packages/config-manager";
|
import { config } from "~packages/config-manager";
|
||||||
|
import { User } from "~packages/database-interface/user";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
|
||||||
export const meta = applyConfig({
|
export const meta = applyConfig({
|
||||||
allowedMethods: ["POST"],
|
allowedMethods: ["POST"],
|
||||||
|
|
@ -44,13 +45,14 @@ export default apiRoute<typeof meta, typeof schema>(
|
||||||
302,
|
302,
|
||||||
);
|
);
|
||||||
|
|
||||||
const user = await findFirstUser({
|
const user = await User.fromSql(eq(Users.email, email));
|
||||||
where: (user, { eq }) => eq(user.email, email),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!user ||
|
!user ||
|
||||||
!(await Bun.password.verify(password, user.password || ""))
|
!(await Bun.password.verify(
|
||||||
|
password,
|
||||||
|
user.getUser().password || "",
|
||||||
|
))
|
||||||
)
|
)
|
||||||
return redirectToLogin("Invalid email or password");
|
return redirectToLogin("Invalid email or password");
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,10 @@ import { apiRoute, applyConfig, idValidator } from "@api";
|
||||||
import { errorResponse, jsonResponse } from "@response";
|
import { errorResponse, jsonResponse } from "@response";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { relationshipToAPI } from "~database/entities/Relationship";
|
import { relationshipToAPI } from "~database/entities/Relationship";
|
||||||
import {
|
import { getRelationshipToOtherUser } from "~database/entities/User";
|
||||||
findFirstUser,
|
|
||||||
getRelationshipToOtherUser,
|
|
||||||
} from "~database/entities/User";
|
|
||||||
import { db } from "~drizzle/db";
|
import { db } from "~drizzle/db";
|
||||||
import { Relationships } from "~drizzle/schema";
|
import { Relationships } from "~drizzle/schema";
|
||||||
|
import { User } from "~packages/database-interface/user";
|
||||||
|
|
||||||
export const meta = applyConfig({
|
export const meta = applyConfig({
|
||||||
allowedMethods: ["POST"],
|
allowedMethods: ["POST"],
|
||||||
|
|
@ -35,9 +33,7 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
|
||||||
|
|
||||||
if (!self) return errorResponse("Unauthorized", 401);
|
if (!self) return errorResponse("Unauthorized", 401);
|
||||||
|
|
||||||
const otherUser = await findFirstUser({
|
const otherUser = await User.fromId(id);
|
||||||
where: (user, { eq }) => eq(user.id, id),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!otherUser) return errorResponse("User not found", 404);
|
if (!otherUser) return errorResponse("User not found", 404);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,10 @@ import ISO6391 from "iso-639-1";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { relationshipToAPI } from "~database/entities/Relationship";
|
import { relationshipToAPI } from "~database/entities/Relationship";
|
||||||
import {
|
import {
|
||||||
findFirstUser,
|
|
||||||
followRequestUser,
|
followRequestUser,
|
||||||
getRelationshipToOtherUser,
|
getRelationshipToOtherUser,
|
||||||
} from "~database/entities/User";
|
} from "~database/entities/User";
|
||||||
|
import { User } from "~packages/database-interface/user";
|
||||||
|
|
||||||
export const meta = applyConfig({
|
export const meta = applyConfig({
|
||||||
allowedMethods: ["POST"],
|
allowedMethods: ["POST"],
|
||||||
|
|
@ -46,9 +46,7 @@ export default apiRoute<typeof meta, typeof schema>(
|
||||||
|
|
||||||
const { languages, notify, reblogs } = extraData.parsedRequest;
|
const { languages, notify, reblogs } = extraData.parsedRequest;
|
||||||
|
|
||||||
const otherUser = await findFirstUser({
|
const otherUser = await User.fromId(id);
|
||||||
where: (user, { eq }) => eq(user.id, id),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!otherUser) return errorResponse("User not found", 404);
|
if (!otherUser) return errorResponse("User not found", 404);
|
||||||
|
|
||||||
|
|
|
||||||
105
server/api/api/v1/accounts/[id]/followers.test.ts
Normal file
105
server/api/api/v1/accounts/[id]/followers.test.ts
Normal file
|
|
@ -0,0 +1,105 @@
|
||||||
|
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
||||||
|
import { config } from "config-manager";
|
||||||
|
import {
|
||||||
|
deleteOldTestUsers,
|
||||||
|
getTestUsers,
|
||||||
|
sendTestRequest,
|
||||||
|
} from "~tests/utils";
|
||||||
|
import type { Account as APIAccount } from "~types/mastodon/account";
|
||||||
|
import { meta } from "./followers";
|
||||||
|
|
||||||
|
await deleteOldTestUsers();
|
||||||
|
|
||||||
|
const { users, tokens, deleteUsers } = await getTestUsers(5);
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await deleteUsers();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
// Follow user
|
||||||
|
const response = await sendTestRequest(
|
||||||
|
new Request(
|
||||||
|
new URL(
|
||||||
|
`/api/v1/accounts/${users[1].id}/follow`,
|
||||||
|
config.http.base_url,
|
||||||
|
),
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${tokens[0].accessToken}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
// /api/v1/accounts/:id/followers
|
||||||
|
describe(meta.route, () => {
|
||||||
|
test("should return 200 with followers", async () => {
|
||||||
|
const response = await sendTestRequest(
|
||||||
|
new Request(
|
||||||
|
new URL(
|
||||||
|
meta.route.replace(":id", users[1].id),
|
||||||
|
config.http.base_url,
|
||||||
|
),
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${tokens[0].accessToken}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
|
||||||
|
const data = (await response.json()) as APIAccount[];
|
||||||
|
|
||||||
|
expect(data).toBeInstanceOf(Array);
|
||||||
|
expect(data.length).toBe(1);
|
||||||
|
expect(data[0].id).toBe(users[0].id);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should return no followers after unfollowing", async () => {
|
||||||
|
// Unfollow user
|
||||||
|
const response = await sendTestRequest(
|
||||||
|
new Request(
|
||||||
|
new URL(
|
||||||
|
`/api/v1/accounts/${users[1].id}/unfollow`,
|
||||||
|
config.http.base_url,
|
||||||
|
),
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${tokens[0].accessToken}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
|
||||||
|
const response2 = await sendTestRequest(
|
||||||
|
new Request(
|
||||||
|
new URL(
|
||||||
|
meta.route.replace(":id", users[1].id),
|
||||||
|
config.http.base_url,
|
||||||
|
),
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${tokens[0].accessToken}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(response2.status).toBe(200);
|
||||||
|
|
||||||
|
const data = (await response2.json()) as APIAccount[];
|
||||||
|
|
||||||
|
expect(data).toBeInstanceOf(Array);
|
||||||
|
expect(data.length).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -1,13 +1,10 @@
|
||||||
import { apiRoute, applyConfig, idValidator } from "@api";
|
import { apiRoute, applyConfig, idValidator } from "@api";
|
||||||
import { errorResponse, jsonResponse } from "@response";
|
import { errorResponse, jsonResponse } from "@response";
|
||||||
import { fetchTimeline } from "@timelines";
|
import { and, gt, gte, lt, sql } from "drizzle-orm";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import {
|
import { Users } from "~drizzle/schema";
|
||||||
type UserWithRelations,
|
import { Timeline } from "~packages/database-interface/timeline";
|
||||||
findFirstUser,
|
import { User } from "~packages/database-interface/user";
|
||||||
findManyUsers,
|
|
||||||
userToAPI,
|
|
||||||
} from "~database/entities/User";
|
|
||||||
|
|
||||||
export const meta = applyConfig({
|
export const meta = applyConfig({
|
||||||
allowedMethods: ["GET"],
|
allowedMethods: ["GET"],
|
||||||
|
|
@ -42,32 +39,23 @@ export default apiRoute<typeof meta, typeof schema>(
|
||||||
// TODO: Add pinned
|
// TODO: Add pinned
|
||||||
const { max_id, min_id, since_id, limit } = extraData.parsedRequest;
|
const { max_id, min_id, since_id, limit } = extraData.parsedRequest;
|
||||||
|
|
||||||
const otherUser = await findFirstUser({
|
const otherUser = await User.fromId(id);
|
||||||
where: (user, { eq }) => eq(user.id, id),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!otherUser) return errorResponse("User not found", 404);
|
if (!otherUser) return errorResponse("User not found", 404);
|
||||||
|
|
||||||
const { objects, link } = await fetchTimeline<UserWithRelations>(
|
const { objects, link } = await Timeline.getUserTimeline(
|
||||||
findManyUsers,
|
|
||||||
{
|
|
||||||
// @ts-ignore
|
|
||||||
where: (follower, { and, lt, gt, gte, eq, sql }) =>
|
|
||||||
and(
|
and(
|
||||||
max_id ? lt(follower.id, max_id) : undefined,
|
max_id ? lt(Users.id, max_id) : undefined,
|
||||||
since_id ? gte(follower.id, since_id) : undefined,
|
since_id ? gte(Users.id, since_id) : undefined,
|
||||||
min_id ? gt(follower.id, min_id) : undefined,
|
min_id ? gt(Users.id, min_id) : undefined,
|
||||||
sql`EXISTS (SELECT 1 FROM "Relationships" WHERE "Relationships"."subjectId" = ${otherUser.id} AND "Relationships"."ownerId" = ${follower.id} AND "Relationships"."following" = true)`,
|
sql`EXISTS (SELECT 1 FROM "Relationships" WHERE "Relationships"."subjectId" = ${otherUser.id} AND "Relationships"."ownerId" = ${Users.id} AND "Relationships"."following" = true)`,
|
||||||
),
|
),
|
||||||
// @ts-expect-error Yes I KNOW the types are wrong
|
|
||||||
orderBy: (liker, { desc }) => desc(liker.id),
|
|
||||||
limit,
|
limit,
|
||||||
},
|
req.url,
|
||||||
req,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return jsonResponse(
|
return jsonResponse(
|
||||||
await Promise.all(objects.map((object) => userToAPI(object))),
|
await Promise.all(objects.map((object) => object.toAPI())),
|
||||||
200,
|
200,
|
||||||
{
|
{
|
||||||
Link: link,
|
Link: link,
|
||||||
|
|
|
||||||
105
server/api/api/v1/accounts/[id]/following.test.ts
Normal file
105
server/api/api/v1/accounts/[id]/following.test.ts
Normal file
|
|
@ -0,0 +1,105 @@
|
||||||
|
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
||||||
|
import { config } from "config-manager";
|
||||||
|
import {
|
||||||
|
deleteOldTestUsers,
|
||||||
|
getTestUsers,
|
||||||
|
sendTestRequest,
|
||||||
|
} from "~tests/utils";
|
||||||
|
import type { Account as APIAccount } from "~types/mastodon/account";
|
||||||
|
import { meta } from "./following";
|
||||||
|
|
||||||
|
await deleteOldTestUsers();
|
||||||
|
|
||||||
|
const { users, tokens, deleteUsers } = await getTestUsers(5);
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await deleteUsers();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
// Follow user
|
||||||
|
const response = await sendTestRequest(
|
||||||
|
new Request(
|
||||||
|
new URL(
|
||||||
|
`/api/v1/accounts/${users[1].id}/follow`,
|
||||||
|
config.http.base_url,
|
||||||
|
),
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${tokens[0].accessToken}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
// /api/v1/accounts/:id/following
|
||||||
|
describe(meta.route, () => {
|
||||||
|
test("should return 200 with following", async () => {
|
||||||
|
const response = await sendTestRequest(
|
||||||
|
new Request(
|
||||||
|
new URL(
|
||||||
|
meta.route.replace(":id", users[0].id),
|
||||||
|
config.http.base_url,
|
||||||
|
),
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${tokens[1].accessToken}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
|
||||||
|
const data = (await response.json()) as APIAccount[];
|
||||||
|
|
||||||
|
expect(data).toBeInstanceOf(Array);
|
||||||
|
expect(data.length).toBe(1);
|
||||||
|
expect(data[0].id).toBe(users[1].id);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should return no following after unfollowing", async () => {
|
||||||
|
// Unfollow user
|
||||||
|
const response = await sendTestRequest(
|
||||||
|
new Request(
|
||||||
|
new URL(
|
||||||
|
`/api/v1/accounts/${users[1].id}/unfollow`,
|
||||||
|
config.http.base_url,
|
||||||
|
),
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${tokens[0].accessToken}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
|
||||||
|
const response2 = await sendTestRequest(
|
||||||
|
new Request(
|
||||||
|
new URL(
|
||||||
|
meta.route.replace(":id", users[0].id),
|
||||||
|
config.http.base_url,
|
||||||
|
),
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${tokens[1].accessToken}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(response2.status).toBe(200);
|
||||||
|
|
||||||
|
const data = (await response2.json()) as APIAccount[];
|
||||||
|
|
||||||
|
expect(data).toBeInstanceOf(Array);
|
||||||
|
expect(data.length).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -1,13 +1,10 @@
|
||||||
import { apiRoute, applyConfig, idValidator } from "@api";
|
import { apiRoute, applyConfig, idValidator } from "@api";
|
||||||
import { errorResponse, jsonResponse } from "@response";
|
import { errorResponse, jsonResponse } from "@response";
|
||||||
import { fetchTimeline } from "@timelines";
|
import { and, gt, gte, lt, sql } from "drizzle-orm";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import {
|
import { Users } from "~drizzle/schema";
|
||||||
type UserWithRelations,
|
import { Timeline } from "~packages/database-interface/timeline";
|
||||||
findFirstUser,
|
import { User } from "~packages/database-interface/user";
|
||||||
findManyUsers,
|
|
||||||
userToAPI,
|
|
||||||
} from "~database/entities/User";
|
|
||||||
|
|
||||||
export const meta = applyConfig({
|
export const meta = applyConfig({
|
||||||
allowedMethods: ["GET"],
|
allowedMethods: ["GET"],
|
||||||
|
|
@ -42,32 +39,23 @@ export default apiRoute<typeof meta, typeof schema>(
|
||||||
// TODO: Add pinned
|
// TODO: Add pinned
|
||||||
const { max_id, min_id, since_id, limit } = extraData.parsedRequest;
|
const { max_id, min_id, since_id, limit } = extraData.parsedRequest;
|
||||||
|
|
||||||
const otherUser = await findFirstUser({
|
const otherUser = await User.fromId(id);
|
||||||
where: (user, { eq }) => eq(user.id, id),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!otherUser) return errorResponse("User not found", 404);
|
if (!otherUser) return errorResponse("User not found", 404);
|
||||||
|
|
||||||
const { objects, link } = await fetchTimeline<UserWithRelations>(
|
const { objects, link } = await Timeline.getUserTimeline(
|
||||||
findManyUsers,
|
|
||||||
{
|
|
||||||
// @ts-ignore
|
|
||||||
where: (following, { and, lt, gt, gte, eq, sql }) =>
|
|
||||||
and(
|
and(
|
||||||
max_id ? lt(following.id, max_id) : undefined,
|
max_id ? lt(Users.id, max_id) : undefined,
|
||||||
since_id ? gte(following.id, since_id) : undefined,
|
since_id ? gte(Users.id, since_id) : undefined,
|
||||||
min_id ? gt(following.id, min_id) : undefined,
|
min_id ? gt(Users.id, min_id) : undefined,
|
||||||
sql`EXISTS (SELECT 1 FROM "Relationships" WHERE "Relationships"."subjectId" = ${following.id} AND "Relationships"."ownerId" = ${otherUser.id} AND "Relationships"."following" = true)`,
|
sql`EXISTS (SELECT 1 FROM "Relationships" WHERE "Relationships"."subjectId" = ${Users.id} AND "Relationships"."ownerId" = ${otherUser.id} AND "Relationships"."following" = true)`,
|
||||||
),
|
),
|
||||||
// @ts-expect-error Yes I KNOW the types are wrong
|
|
||||||
orderBy: (liker, { desc }) => desc(liker.id),
|
|
||||||
limit,
|
limit,
|
||||||
},
|
req.url,
|
||||||
req,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return jsonResponse(
|
return jsonResponse(
|
||||||
await Promise.all(objects.map((object) => userToAPI(object))),
|
await Promise.all(objects.map((object) => object.toAPI())),
|
||||||
200,
|
200,
|
||||||
{
|
{
|
||||||
Link: link,
|
Link: link,
|
||||||
|
|
|
||||||
|
|
@ -64,17 +64,17 @@ describe(meta.route, () => {
|
||||||
const data = (await response.json()) as APIAccount;
|
const data = (await response.json()) as APIAccount;
|
||||||
expect(data).toMatchObject({
|
expect(data).toMatchObject({
|
||||||
id: users[0].id,
|
id: users[0].id,
|
||||||
username: users[0].username,
|
username: users[0].getUser().username,
|
||||||
display_name: users[0].displayName,
|
display_name: users[0].getUser().displayName,
|
||||||
avatar: expect.any(String),
|
avatar: expect.any(String),
|
||||||
header: expect.any(String),
|
header: expect.any(String),
|
||||||
locked: users[0].isLocked,
|
locked: users[0].getUser().isLocked,
|
||||||
created_at: new Date(users[0].createdAt).toISOString(),
|
created_at: new Date(users[0].getUser().createdAt).toISOString(),
|
||||||
followers_count: 0,
|
followers_count: 0,
|
||||||
following_count: 0,
|
following_count: 0,
|
||||||
statuses_count: 40,
|
statuses_count: 40,
|
||||||
note: users[0].note,
|
note: users[0].getUser().note,
|
||||||
acct: users[0].username,
|
acct: users[0].getUser().username,
|
||||||
url: expect.any(String),
|
url: expect.any(String),
|
||||||
avatar_static: expect.any(String),
|
avatar_static: expect.any(String),
|
||||||
header_static: expect.any(String),
|
header_static: expect.any(String),
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { apiRoute, applyConfig, idValidator } from "@api";
|
import { apiRoute, applyConfig, idValidator } from "@api";
|
||||||
import { errorResponse, jsonResponse } from "@response";
|
import { errorResponse, jsonResponse } from "@response";
|
||||||
import { findFirstUser, userToAPI } from "~database/entities/User";
|
import { User } from "~packages/database-interface/user";
|
||||||
|
|
||||||
export const meta = applyConfig({
|
export const meta = applyConfig({
|
||||||
allowedMethods: ["GET"],
|
allowedMethods: ["GET"],
|
||||||
|
|
@ -26,11 +26,9 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
|
||||||
|
|
||||||
const { user } = extraData.auth;
|
const { user } = extraData.auth;
|
||||||
|
|
||||||
const foundUser = await findFirstUser({
|
const foundUser = await User.fromId(id);
|
||||||
where: (user, { eq }) => eq(user.id, id),
|
|
||||||
}).catch(() => null);
|
|
||||||
|
|
||||||
if (!foundUser) return errorResponse("User not found", 404);
|
if (!foundUser) return errorResponse("User not found", 404);
|
||||||
|
|
||||||
return jsonResponse(userToAPI(foundUser, user?.id === foundUser.id));
|
return jsonResponse(foundUser.toAPI(user?.id === foundUser.id));
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,10 @@ import { errorResponse, jsonResponse } from "@response";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { relationshipToAPI } from "~database/entities/Relationship";
|
import { relationshipToAPI } from "~database/entities/Relationship";
|
||||||
import {
|
import { getRelationshipToOtherUser } from "~database/entities/User";
|
||||||
findFirstUser,
|
|
||||||
getRelationshipToOtherUser,
|
|
||||||
} from "~database/entities/User";
|
|
||||||
import { db } from "~drizzle/db";
|
import { db } from "~drizzle/db";
|
||||||
import { Relationships } from "~drizzle/schema";
|
import { Relationships } from "~drizzle/schema";
|
||||||
|
import { User } from "~packages/database-interface/user";
|
||||||
|
|
||||||
export const meta = applyConfig({
|
export const meta = applyConfig({
|
||||||
allowedMethods: ["POST"],
|
allowedMethods: ["POST"],
|
||||||
|
|
@ -49,9 +47,7 @@ export default apiRoute<typeof meta, typeof schema>(
|
||||||
|
|
||||||
const { notifications, duration } = extraData.parsedRequest;
|
const { notifications, duration } = extraData.parsedRequest;
|
||||||
|
|
||||||
const user = await findFirstUser({
|
const user = await User.fromId(id);
|
||||||
where: (user, { eq }) => eq(user.id, id),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!user) return errorResponse("User not found", 404);
|
if (!user) return errorResponse("User not found", 404);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,10 @@ import { errorResponse, jsonResponse } from "@response";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { relationshipToAPI } from "~database/entities/Relationship";
|
import { relationshipToAPI } from "~database/entities/Relationship";
|
||||||
import {
|
import { getRelationshipToOtherUser } from "~database/entities/User";
|
||||||
findFirstUser,
|
|
||||||
getRelationshipToOtherUser,
|
|
||||||
} from "~database/entities/User";
|
|
||||||
import { db } from "~drizzle/db";
|
import { db } from "~drizzle/db";
|
||||||
import { Relationships } from "~drizzle/schema";
|
import { Relationships } from "~drizzle/schema";
|
||||||
|
import { User } from "~packages/database-interface/user";
|
||||||
|
|
||||||
export const meta = applyConfig({
|
export const meta = applyConfig({
|
||||||
allowedMethods: ["POST"],
|
allowedMethods: ["POST"],
|
||||||
|
|
@ -43,9 +41,7 @@ export default apiRoute<typeof meta, typeof schema>(
|
||||||
|
|
||||||
const { comment } = extraData.parsedRequest;
|
const { comment } = extraData.parsedRequest;
|
||||||
|
|
||||||
const otherUser = await findFirstUser({
|
const otherUser = await User.fromId(id);
|
||||||
where: (user, { eq }) => eq(user.id, id),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!otherUser) return errorResponse("User not found", 404);
|
if (!otherUser) return errorResponse("User not found", 404);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,10 @@ import { apiRoute, applyConfig, idValidator } from "@api";
|
||||||
import { errorResponse, jsonResponse } from "@response";
|
import { errorResponse, jsonResponse } from "@response";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { relationshipToAPI } from "~database/entities/Relationship";
|
import { relationshipToAPI } from "~database/entities/Relationship";
|
||||||
import {
|
import { getRelationshipToOtherUser } from "~database/entities/User";
|
||||||
findFirstUser,
|
|
||||||
getRelationshipToOtherUser,
|
|
||||||
} from "~database/entities/User";
|
|
||||||
import { db } from "~drizzle/db";
|
import { db } from "~drizzle/db";
|
||||||
import { Relationships } from "~drizzle/schema";
|
import { Relationships } from "~drizzle/schema";
|
||||||
|
import { User } from "~packages/database-interface/user";
|
||||||
|
|
||||||
export const meta = applyConfig({
|
export const meta = applyConfig({
|
||||||
allowedMethods: ["POST"],
|
allowedMethods: ["POST"],
|
||||||
|
|
@ -35,9 +33,7 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
|
||||||
|
|
||||||
if (!self) return errorResponse("Unauthorized", 401);
|
if (!self) return errorResponse("Unauthorized", 401);
|
||||||
|
|
||||||
const otherUser = await findFirstUser({
|
const otherUser = await User.fromId(id);
|
||||||
where: (user, { eq }) => eq(user.id, id),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!otherUser) return errorResponse("User not found", 404);
|
if (!otherUser) return errorResponse("User not found", 404);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,10 @@ import { apiRoute, applyConfig, idValidator } from "@api";
|
||||||
import { errorResponse, jsonResponse } from "@response";
|
import { errorResponse, jsonResponse } from "@response";
|
||||||
import { and, eq } from "drizzle-orm";
|
import { and, eq } from "drizzle-orm";
|
||||||
import { relationshipToAPI } from "~database/entities/Relationship";
|
import { relationshipToAPI } from "~database/entities/Relationship";
|
||||||
import {
|
import { getRelationshipToOtherUser } from "~database/entities/User";
|
||||||
findFirstUser,
|
|
||||||
getRelationshipToOtherUser,
|
|
||||||
} from "~database/entities/User";
|
|
||||||
import { db } from "~drizzle/db";
|
import { db } from "~drizzle/db";
|
||||||
import { Relationships } from "~drizzle/schema";
|
import { Relationships } from "~drizzle/schema";
|
||||||
|
import { User } from "~packages/database-interface/user";
|
||||||
|
|
||||||
export const meta = applyConfig({
|
export const meta = applyConfig({
|
||||||
allowedMethods: ["POST"],
|
allowedMethods: ["POST"],
|
||||||
|
|
@ -35,9 +33,7 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
|
||||||
|
|
||||||
if (!self) return errorResponse("Unauthorized", 401);
|
if (!self) return errorResponse("Unauthorized", 401);
|
||||||
|
|
||||||
const otherUser = await findFirstUser({
|
const otherUser = await User.fromId(id);
|
||||||
where: (user, { eq }) => eq(user.id, id),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!otherUser) return errorResponse("User not found", 404);
|
if (!otherUser) return errorResponse("User not found", 404);
|
||||||
|
|
||||||
|
|
@ -54,7 +50,7 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
|
||||||
})
|
})
|
||||||
.where(eq(Relationships.id, foundRelationship.id));
|
.where(eq(Relationships.id, foundRelationship.id));
|
||||||
|
|
||||||
if (otherUser.instanceId === null) {
|
if (otherUser.isLocal()) {
|
||||||
// Also remove from followers list
|
// Also remove from followers list
|
||||||
await db
|
await db
|
||||||
.update(Relationships)
|
.update(Relationships)
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,9 @@ import { apiRoute, applyConfig, idValidator } from "@api";
|
||||||
import { errorResponse, jsonResponse } from "@response";
|
import { errorResponse, jsonResponse } from "@response";
|
||||||
import { and, eq, gt, gte, isNull, lt, sql } from "drizzle-orm";
|
import { and, eq, gt, gte, isNull, lt, sql } from "drizzle-orm";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { findFirstUser } from "~database/entities/User";
|
|
||||||
import { Notes } from "~drizzle/schema";
|
import { Notes } from "~drizzle/schema";
|
||||||
import { Timeline } from "~packages/database-interface/timeline";
|
import { Timeline } from "~packages/database-interface/timeline";
|
||||||
|
import { User } from "~packages/database-interface/user";
|
||||||
|
|
||||||
export const meta = applyConfig({
|
export const meta = applyConfig({
|
||||||
allowedMethods: ["GET"],
|
allowedMethods: ["GET"],
|
||||||
|
|
@ -52,9 +52,7 @@ export default apiRoute<typeof meta, typeof schema>(
|
||||||
pinned,
|
pinned,
|
||||||
} = extraData.parsedRequest;
|
} = extraData.parsedRequest;
|
||||||
|
|
||||||
const user = await findFirstUser({
|
const user = await User.fromId(id);
|
||||||
where: (user, { eq }) => eq(user.id, id),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!user) return errorResponse("User not found", 404);
|
if (!user) return errorResponse("User not found", 404);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,10 @@ import { apiRoute, applyConfig, idValidator } from "@api";
|
||||||
import { errorResponse, jsonResponse } from "@response";
|
import { errorResponse, jsonResponse } from "@response";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { relationshipToAPI } from "~database/entities/Relationship";
|
import { relationshipToAPI } from "~database/entities/Relationship";
|
||||||
import {
|
import { getRelationshipToOtherUser } from "~database/entities/User";
|
||||||
findFirstUser,
|
|
||||||
getRelationshipToOtherUser,
|
|
||||||
} from "~database/entities/User";
|
|
||||||
import { db } from "~drizzle/db";
|
import { db } from "~drizzle/db";
|
||||||
import { Relationships } from "~drizzle/schema";
|
import { Relationships } from "~drizzle/schema";
|
||||||
|
import { User } from "~packages/database-interface/user";
|
||||||
|
|
||||||
export const meta = applyConfig({
|
export const meta = applyConfig({
|
||||||
allowedMethods: ["POST"],
|
allowedMethods: ["POST"],
|
||||||
|
|
@ -32,9 +30,7 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
|
||||||
|
|
||||||
if (!self) return errorResponse("Unauthorized", 401);
|
if (!self) return errorResponse("Unauthorized", 401);
|
||||||
|
|
||||||
const otherUser = await findFirstUser({
|
const otherUser = await User.fromId(id);
|
||||||
where: (user, { eq }) => eq(user.id, id),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!otherUser) return errorResponse("User not found", 404);
|
if (!otherUser) return errorResponse("User not found", 404);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,10 @@ import { apiRoute, applyConfig, idValidator } from "@api";
|
||||||
import { errorResponse, jsonResponse } from "@response";
|
import { errorResponse, jsonResponse } from "@response";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { relationshipToAPI } from "~database/entities/Relationship";
|
import { relationshipToAPI } from "~database/entities/Relationship";
|
||||||
import {
|
import { getRelationshipToOtherUser } from "~database/entities/User";
|
||||||
findFirstUser,
|
|
||||||
getRelationshipToOtherUser,
|
|
||||||
} from "~database/entities/User";
|
|
||||||
import { db } from "~drizzle/db";
|
import { db } from "~drizzle/db";
|
||||||
import { Relationships } from "~drizzle/schema";
|
import { Relationships } from "~drizzle/schema";
|
||||||
|
import { User } from "~packages/database-interface/user";
|
||||||
|
|
||||||
export const meta = applyConfig({
|
export const meta = applyConfig({
|
||||||
allowedMethods: ["POST"],
|
allowedMethods: ["POST"],
|
||||||
|
|
@ -35,9 +33,7 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
|
||||||
|
|
||||||
if (!self) return errorResponse("Unauthorized", 401);
|
if (!self) return errorResponse("Unauthorized", 401);
|
||||||
|
|
||||||
const otherUser = await findFirstUser({
|
const otherUser = await User.fromId(id);
|
||||||
where: (user, { eq }) => eq(user.id, id),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!otherUser) return errorResponse("User not found", 404);
|
if (!otherUser) return errorResponse("User not found", 404);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,10 @@ import { apiRoute, applyConfig, idValidator } from "@api";
|
||||||
import { errorResponse, jsonResponse } from "@response";
|
import { errorResponse, jsonResponse } from "@response";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { relationshipToAPI } from "~database/entities/Relationship";
|
import { relationshipToAPI } from "~database/entities/Relationship";
|
||||||
import {
|
import { getRelationshipToOtherUser } from "~database/entities/User";
|
||||||
findFirstUser,
|
|
||||||
getRelationshipToOtherUser,
|
|
||||||
} from "~database/entities/User";
|
|
||||||
import { db } from "~drizzle/db";
|
import { db } from "~drizzle/db";
|
||||||
import { Relationships } from "~drizzle/schema";
|
import { Relationships } from "~drizzle/schema";
|
||||||
|
import { User } from "~packages/database-interface/user";
|
||||||
|
|
||||||
export const meta = applyConfig({
|
export const meta = applyConfig({
|
||||||
allowedMethods: ["POST"],
|
allowedMethods: ["POST"],
|
||||||
|
|
@ -35,9 +33,7 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
|
||||||
|
|
||||||
if (!self) return errorResponse("Unauthorized", 401);
|
if (!self) return errorResponse("Unauthorized", 401);
|
||||||
|
|
||||||
const user = await findFirstUser({
|
const user = await User.fromId(id);
|
||||||
where: (user, { eq }) => eq(user.id, id),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!user) return errorResponse("User not found", 404);
|
if (!user) return errorResponse("User not found", 404);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,10 @@ import { apiRoute, applyConfig, idValidator } from "@api";
|
||||||
import { errorResponse, jsonResponse } from "@response";
|
import { errorResponse, jsonResponse } from "@response";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { relationshipToAPI } from "~database/entities/Relationship";
|
import { relationshipToAPI } from "~database/entities/Relationship";
|
||||||
import {
|
import { getRelationshipToOtherUser } from "~database/entities/User";
|
||||||
findFirstUser,
|
|
||||||
getRelationshipToOtherUser,
|
|
||||||
} from "~database/entities/User";
|
|
||||||
import { db } from "~drizzle/db";
|
import { db } from "~drizzle/db";
|
||||||
import { Relationships } from "~drizzle/schema";
|
import { Relationships } from "~drizzle/schema";
|
||||||
|
import { User } from "~packages/database-interface/user";
|
||||||
|
|
||||||
export const meta = applyConfig({
|
export const meta = applyConfig({
|
||||||
allowedMethods: ["POST"],
|
allowedMethods: ["POST"],
|
||||||
|
|
@ -35,9 +33,7 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
|
||||||
|
|
||||||
if (!self) return errorResponse("Unauthorized", 401);
|
if (!self) return errorResponse("Unauthorized", 401);
|
||||||
|
|
||||||
const otherUser = await findFirstUser({
|
const otherUser = await User.fromId(id);
|
||||||
where: (user, { eq }) => eq(user.id, id),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!otherUser) return errorResponse("User not found", 404);
|
if (!otherUser) return errorResponse("User not found", 404);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
import { apiRoute, applyConfig, idValidator } from "@api";
|
import { apiRoute, applyConfig, idValidator } from "@api";
|
||||||
import { errorResponse, jsonResponse } from "@response";
|
import { errorResponse, jsonResponse } from "@response";
|
||||||
|
import { inArray } from "drizzle-orm";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { findManyUsers, userToAPI } from "~database/entities/User";
|
|
||||||
import { db } from "~drizzle/db";
|
import { db } from "~drizzle/db";
|
||||||
|
import { Users } from "~drizzle/schema";
|
||||||
|
import { User } from "~packages/database-interface/user";
|
||||||
|
|
||||||
export const meta = applyConfig({
|
export const meta = applyConfig({
|
||||||
allowedMethods: ["GET"],
|
allowedMethods: ["GET"],
|
||||||
|
|
@ -67,18 +69,13 @@ export default apiRoute<typeof meta, typeof schema>(
|
||||||
return jsonResponse([]);
|
return jsonResponse([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const finalUsers = await findManyUsers({
|
const finalUsers = await User.manyFromSql(
|
||||||
where: (user, { inArray }) =>
|
|
||||||
inArray(
|
inArray(
|
||||||
user.id,
|
Users.id,
|
||||||
relevantRelationships.map((r) => r.subjectId),
|
relevantRelationships.map((r) => r.subjectId),
|
||||||
),
|
),
|
||||||
});
|
);
|
||||||
|
|
||||||
if (finalUsers.length === 0) {
|
return jsonResponse(finalUsers.map((o) => o.toAPI()));
|
||||||
return jsonResponse([]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return jsonResponse(finalUsers.map((o) => userToAPI(o)));
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,12 @@
|
||||||
import { apiRoute, applyConfig } from "@api";
|
import { apiRoute, applyConfig } from "@api";
|
||||||
import { jsonResponse, response } from "@response";
|
import { jsonResponse, response } from "@response";
|
||||||
import { tempmailDomains } from "@tempmail";
|
import { tempmailDomains } from "@tempmail";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
import ISO6391 from "iso-639-1";
|
import ISO6391 from "iso-639-1";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { createNewLocalUser, findFirstUser } from "~database/entities/User";
|
import { createNewLocalUser } from "~database/entities/User";
|
||||||
|
import { Users } from "~drizzle/schema";
|
||||||
|
import { User } from "~packages/database-interface/user";
|
||||||
|
|
||||||
export const meta = applyConfig({
|
export const meta = applyConfig({
|
||||||
allowedMethods: ["POST"],
|
allowedMethods: ["POST"],
|
||||||
|
|
@ -133,11 +136,7 @@ export default apiRoute<typeof meta, typeof schema>(
|
||||||
});
|
});
|
||||||
|
|
||||||
// Check if username is taken
|
// Check if username is taken
|
||||||
if (
|
if (await User.fromSql(eq(Users.username, body.username))) {
|
||||||
await findFirstUser({
|
|
||||||
where: (user, { eq }) => eq(user.username, body.username ?? ""),
|
|
||||||
})
|
|
||||||
) {
|
|
||||||
errors.details.username.push({
|
errors.details.username.push({
|
||||||
error: "ERR_TAKEN",
|
error: "ERR_TAKEN",
|
||||||
description: "is already taken",
|
description: "is already taken",
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ describe(meta.route, () => {
|
||||||
const response = await sendTestRequest(
|
const response = await sendTestRequest(
|
||||||
new Request(
|
new Request(
|
||||||
new URL(
|
new URL(
|
||||||
`${meta.route}?acct=${users[0].username}`,
|
`${meta.route}?acct=${users[0].getUser().username}`,
|
||||||
config.http.base_url,
|
config.http.base_url,
|
||||||
),
|
),
|
||||||
{
|
{
|
||||||
|
|
@ -39,8 +39,8 @@ describe(meta.route, () => {
|
||||||
expect(data).toEqual(
|
expect(data).toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: users[0].id,
|
id: users[0].id,
|
||||||
username: users[0].username,
|
username: users[0].getUser().username,
|
||||||
display_name: users[0].displayName,
|
display_name: users[0].getUser().displayName,
|
||||||
avatar: expect.any(String),
|
avatar: expect.any(String),
|
||||||
header: expect.any(String),
|
header: expect.any(String),
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { apiRoute, applyConfig } from "@api";
|
import { apiRoute, applyConfig } from "@api";
|
||||||
import { dualLogger } from "@loggers";
|
import { dualLogger } from "@loggers";
|
||||||
import { errorResponse, jsonResponse } from "@response";
|
import { errorResponse, jsonResponse } from "@response";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
import {
|
import {
|
||||||
anyOf,
|
anyOf,
|
||||||
charIn,
|
charIn,
|
||||||
|
|
@ -13,11 +14,9 @@ import {
|
||||||
oneOrMore,
|
oneOrMore,
|
||||||
} from "magic-regexp";
|
} from "magic-regexp";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import {
|
import { resolveWebFinger } from "~database/entities/User";
|
||||||
findFirstUser,
|
import { Users } from "~drizzle/schema";
|
||||||
resolveWebFinger,
|
import { User } from "~packages/database-interface/user";
|
||||||
userToAPI,
|
|
||||||
} from "~database/entities/User";
|
|
||||||
import { LogLevel } from "~packages/log-manager";
|
import { LogLevel } from "~packages/log-manager";
|
||||||
|
|
||||||
export const meta = applyConfig({
|
export const meta = applyConfig({
|
||||||
|
|
@ -80,7 +79,7 @@ export default apiRoute<typeof meta, typeof schema>(
|
||||||
);
|
);
|
||||||
|
|
||||||
if (foundAccount) {
|
if (foundAccount) {
|
||||||
return jsonResponse(userToAPI(foundAccount));
|
return jsonResponse(foundAccount.toAPI());
|
||||||
}
|
}
|
||||||
|
|
||||||
return errorResponse("Account not found", 404);
|
return errorResponse("Account not found", 404);
|
||||||
|
|
@ -91,12 +90,10 @@ export default apiRoute<typeof meta, typeof schema>(
|
||||||
username = username.slice(1);
|
username = username.slice(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const account = await findFirstUser({
|
const account = await User.fromSql(eq(Users.username, username));
|
||||||
where: (user, { eq }) => eq(user.username, username),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (account) {
|
if (account) {
|
||||||
return jsonResponse(userToAPI(account));
|
return jsonResponse(account.toAPI());
|
||||||
}
|
}
|
||||||
|
|
||||||
return errorResponse(
|
return errorResponse(
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import {
|
||||||
createNewRelationship,
|
createNewRelationship,
|
||||||
relationshipToAPI,
|
relationshipToAPI,
|
||||||
} from "~database/entities/Relationship";
|
} from "~database/entities/Relationship";
|
||||||
import type { User } from "~database/entities/User";
|
import type { UserType } from "~database/entities/User";
|
||||||
import { db } from "~drizzle/db";
|
import { db } from "~drizzle/db";
|
||||||
|
|
||||||
export const meta = applyConfig({
|
export const meta = applyConfig({
|
||||||
|
|
@ -53,7 +53,7 @@ export default apiRoute<typeof meta, typeof schema>(
|
||||||
for (const id of missingIds) {
|
for (const id of missingIds) {
|
||||||
const relationship = await createNewRelationship(self, {
|
const relationship = await createNewRelationship(self, {
|
||||||
id,
|
id,
|
||||||
} as User);
|
} as UserType);
|
||||||
|
|
||||||
relationships.push(relationship);
|
relationships.push(relationship);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ describe(meta.route, () => {
|
||||||
const response = await sendTestRequest(
|
const response = await sendTestRequest(
|
||||||
new Request(
|
new Request(
|
||||||
new URL(
|
new URL(
|
||||||
`${meta.route}?q=${users[0].username}`,
|
`${meta.route}?q=${users[0].getUser().username}`,
|
||||||
config.http.base_url,
|
config.http.base_url,
|
||||||
),
|
),
|
||||||
{
|
{
|
||||||
|
|
@ -40,8 +40,8 @@ describe(meta.route, () => {
|
||||||
expect.arrayContaining([
|
expect.arrayContaining([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: users[0].id,
|
id: users[0].id,
|
||||||
username: users[0].username,
|
username: users[0].getUser().username,
|
||||||
display_name: users[0].displayName,
|
display_name: users[0].getUser().displayName,
|
||||||
avatar: expect.any(String),
|
avatar: expect.any(String),
|
||||||
header: expect.any(String),
|
header: expect.any(String),
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { apiRoute, applyConfig } from "@api";
|
import { apiRoute, applyConfig } from "@api";
|
||||||
import { errorResponse, jsonResponse } from "@response";
|
import { errorResponse, jsonResponse } from "@response";
|
||||||
import { sql } from "drizzle-orm";
|
import { eq, like, not, or, sql } from "drizzle-orm";
|
||||||
import {
|
import {
|
||||||
anyOf,
|
anyOf,
|
||||||
charIn,
|
charIn,
|
||||||
|
|
@ -13,13 +13,9 @@ import {
|
||||||
oneOrMore,
|
oneOrMore,
|
||||||
} from "magic-regexp";
|
} from "magic-regexp";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import {
|
import { resolveWebFinger } from "~database/entities/User";
|
||||||
type UserWithRelations,
|
|
||||||
findManyUsers,
|
|
||||||
resolveWebFinger,
|
|
||||||
userToAPI,
|
|
||||||
} from "~database/entities/User";
|
|
||||||
import { Users } from "~drizzle/schema";
|
import { Users } from "~drizzle/schema";
|
||||||
|
import { User } from "~packages/database-interface/user";
|
||||||
|
|
||||||
export const meta = applyConfig({
|
export const meta = applyConfig({
|
||||||
allowedMethods: ["GET"],
|
allowedMethods: ["GET"],
|
||||||
|
|
@ -78,7 +74,7 @@ export default apiRoute<typeof meta, typeof schema>(
|
||||||
// Remove any leading @
|
// Remove any leading @
|
||||||
const [username, host] = q.replace(/^@/, "").split("@");
|
const [username, host] = q.replace(/^@/, "").split("@");
|
||||||
|
|
||||||
const accounts: UserWithRelations[] = [];
|
const accounts: User[] = [];
|
||||||
|
|
||||||
if (resolve && username && host) {
|
if (resolve && username && host) {
|
||||||
const resolvedUser = await resolveWebFinger(username, host);
|
const resolvedUser = await resolveWebFinger(username, host);
|
||||||
|
|
@ -88,21 +84,22 @@ export default apiRoute<typeof meta, typeof schema>(
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
accounts.push(
|
accounts.push(
|
||||||
...(await findManyUsers({
|
...(await User.manyFromSql(
|
||||||
where: (account, { or, like }) =>
|
|
||||||
or(
|
or(
|
||||||
like(account.displayName, `%${q}%`),
|
like(Users.displayName, `%${q}%`),
|
||||||
like(account.username, `%${q}%`),
|
like(Users.username, `%${q}%`),
|
||||||
following
|
following && self
|
||||||
? sql`EXISTS (SELECT 1 FROM "Relationships" WHERE "Relationships"."subjectId" = ${Users.id} AND "Relationships"."ownerId" = ${account.id} AND "Relationships"."following" = true)`
|
? sql`EXISTS (SELECT 1 FROM "Relationships" WHERE "Relationships"."subjectId" = ${Users.id} AND "Relationships"."ownerId" = ${self.id} AND "Relationships"."following" = true)`
|
||||||
: undefined,
|
: undefined,
|
||||||
|
self ? not(eq(Users.id, self.id)) : undefined,
|
||||||
),
|
),
|
||||||
offset,
|
undefined,
|
||||||
limit,
|
limit,
|
||||||
})),
|
offset,
|
||||||
|
)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return jsonResponse(accounts.map((acct) => userToAPI(acct)));
|
return jsonResponse(accounts.map((acct) => acct.toAPI()));
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -11,9 +11,9 @@ import { z } from "zod";
|
||||||
import { getUrl } from "~database/entities/Attachment";
|
import { getUrl } from "~database/entities/Attachment";
|
||||||
import { parseEmojis } from "~database/entities/Emoji";
|
import { parseEmojis } from "~database/entities/Emoji";
|
||||||
import { contentToHtml } from "~database/entities/Status";
|
import { contentToHtml } from "~database/entities/Status";
|
||||||
import { findFirstUser, userToAPI } from "~database/entities/User";
|
|
||||||
import { db } from "~drizzle/db";
|
import { db } from "~drizzle/db";
|
||||||
import { EmojiToUser, Users } from "~drizzle/schema";
|
import { EmojiToUser, Users } from "~drizzle/schema";
|
||||||
|
import { User } from "~packages/database-interface/user";
|
||||||
|
|
||||||
export const meta = applyConfig({
|
export const meta = applyConfig({
|
||||||
allowedMethods: ["PATCH"],
|
allowedMethods: ["PATCH"],
|
||||||
|
|
@ -51,11 +51,12 @@ export const schema = z.object({
|
||||||
|
|
||||||
export default apiRoute<typeof meta, typeof schema>(
|
export default apiRoute<typeof meta, typeof schema>(
|
||||||
async (req, matchedRoute, extraData) => {
|
async (req, matchedRoute, extraData) => {
|
||||||
const { user: self } = extraData.auth;
|
const { user } = extraData.auth;
|
||||||
|
|
||||||
if (!self) return errorResponse("Unauthorized", 401);
|
if (!user) return errorResponse("Unauthorized", 401);
|
||||||
|
|
||||||
const config = await extraData.configManager.getConfig();
|
const config = await extraData.configManager.getConfig();
|
||||||
|
const self = user.getUser();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
display_name,
|
display_name,
|
||||||
|
|
@ -231,12 +232,9 @@ export default apiRoute<typeof meta, typeof schema>(
|
||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
const output = await findFirstUser({
|
const output = await User.fromId(self.id);
|
||||||
where: (user, { eq }) => eq(user.id, self.id),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!output) return errorResponse("Couldn't edit user", 500);
|
if (!output) return errorResponse("Couldn't edit user", 500);
|
||||||
|
|
||||||
return jsonResponse(userToAPI(output));
|
return jsonResponse(output.toAPI());
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { apiRoute, applyConfig } from "@api";
|
import { apiRoute, applyConfig } from "@api";
|
||||||
import { errorResponse, jsonResponse } from "@response";
|
import { errorResponse, jsonResponse } from "@response";
|
||||||
import { userToAPI } from "~database/entities/User";
|
|
||||||
|
|
||||||
export const meta = applyConfig({
|
export const meta = applyConfig({
|
||||||
allowedMethods: ["GET"],
|
allowedMethods: ["GET"],
|
||||||
|
|
@ -22,7 +21,5 @@ export default apiRoute((req, matchedRoute, extraData) => {
|
||||||
|
|
||||||
if (!user) return errorResponse("Unauthorized", 401);
|
if (!user) return errorResponse("Unauthorized", 401);
|
||||||
|
|
||||||
return jsonResponse({
|
return jsonResponse(user.toAPI(true));
|
||||||
...userToAPI(user, true),
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,9 @@
|
||||||
import { apiRoute, applyConfig, idValidator } from "@api";
|
import { apiRoute, applyConfig, idValidator } from "@api";
|
||||||
import { errorResponse, jsonResponse } from "@response";
|
import { errorResponse, jsonResponse } from "@response";
|
||||||
import { fetchTimeline } from "@timelines";
|
import { and, gt, gte, lt, sql } from "drizzle-orm";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import {
|
import { Users } from "~drizzle/schema";
|
||||||
type UserWithRelations,
|
import { Timeline } from "~packages/database-interface/timeline";
|
||||||
findManyUsers,
|
|
||||||
userToAPI,
|
|
||||||
} from "~database/entities/User";
|
|
||||||
|
|
||||||
export const meta = applyConfig({
|
export const meta = applyConfig({
|
||||||
allowedMethods: ["GET"],
|
allowedMethods: ["GET"],
|
||||||
|
|
@ -36,27 +33,19 @@ export default apiRoute<typeof meta, typeof schema>(
|
||||||
|
|
||||||
const { max_id, since_id, min_id, limit } = extraData.parsedRequest;
|
const { max_id, since_id, min_id, limit } = extraData.parsedRequest;
|
||||||
|
|
||||||
const { objects: blocks, link } =
|
const { objects: blocks, link } = await Timeline.getUserTimeline(
|
||||||
await fetchTimeline<UserWithRelations>(
|
|
||||||
findManyUsers,
|
|
||||||
{
|
|
||||||
// @ts-expect-error Yes I KNOW the types are wrong
|
|
||||||
where: (subject, { lt, gte, gt, and, sql }) =>
|
|
||||||
and(
|
and(
|
||||||
max_id ? lt(subject.id, max_id) : undefined,
|
max_id ? lt(Users.id, max_id) : undefined,
|
||||||
since_id ? gte(subject.id, since_id) : undefined,
|
since_id ? gte(Users.id, since_id) : undefined,
|
||||||
min_id ? gt(subject.id, min_id) : undefined,
|
min_id ? gt(Users.id, min_id) : undefined,
|
||||||
sql`EXISTS (SELECT 1 FROM "Relationships" WHERE "Relationships"."subjectId" = ${subject.id} AND "Relationships"."ownerId" = ${user.id} AND "Relationships"."blocking" = true)`,
|
sql`EXISTS (SELECT 1 FROM "Relationships" WHERE "Relationships"."subjectId" = ${Users.id} AND "Relationships"."ownerId" = ${user.id} AND "Relationships"."blocking" = true)`,
|
||||||
),
|
),
|
||||||
limit,
|
limit,
|
||||||
// @ts-expect-error Yes I KNOW the types are wrong
|
req.url,
|
||||||
orderBy: (subject, { desc }) => desc(subject.id),
|
|
||||||
},
|
|
||||||
req,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return jsonResponse(
|
return jsonResponse(
|
||||||
blocks.map((u) => userToAPI(u)),
|
blocks.map((u) => u.toAPI()),
|
||||||
200,
|
200,
|
||||||
{
|
{
|
||||||
Link: link,
|
Link: link,
|
||||||
|
|
|
||||||
|
|
@ -6,12 +6,12 @@ import {
|
||||||
relationshipToAPI,
|
relationshipToAPI,
|
||||||
} from "~database/entities/Relationship";
|
} from "~database/entities/Relationship";
|
||||||
import {
|
import {
|
||||||
findFirstUser,
|
|
||||||
getRelationshipToOtherUser,
|
getRelationshipToOtherUser,
|
||||||
sendFollowAccept,
|
sendFollowAccept,
|
||||||
} from "~database/entities/User";
|
} from "~database/entities/User";
|
||||||
import { db } from "~drizzle/db";
|
import { db } from "~drizzle/db";
|
||||||
import { Relationships } from "~drizzle/schema";
|
import { Relationships } from "~drizzle/schema";
|
||||||
|
import { User } from "~packages/database-interface/user";
|
||||||
|
|
||||||
export const meta = applyConfig({
|
export const meta = applyConfig({
|
||||||
allowedMethods: ["POST"],
|
allowedMethods: ["POST"],
|
||||||
|
|
@ -32,9 +32,7 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
|
||||||
|
|
||||||
const { account_id } = matchedRoute.params;
|
const { account_id } = matchedRoute.params;
|
||||||
|
|
||||||
const account = await findFirstUser({
|
const account = await User.fromId(account_id);
|
||||||
where: (user, { eq }) => eq(user.id, account_id),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!account) return errorResponse("Account not found", 404);
|
if (!account) return errorResponse("Account not found", 404);
|
||||||
|
|
||||||
|
|
@ -73,7 +71,7 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
|
||||||
if (!foundRelationship) return errorResponse("Relationship not found", 404);
|
if (!foundRelationship) return errorResponse("Relationship not found", 404);
|
||||||
|
|
||||||
// Check if accepting remote follow
|
// Check if accepting remote follow
|
||||||
if (account.instanceId) {
|
if (account.isRemote()) {
|
||||||
// Federate follow accept
|
// Federate follow accept
|
||||||
await sendFollowAccept(account, user);
|
await sendFollowAccept(account, user);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,12 +6,12 @@ import {
|
||||||
relationshipToAPI,
|
relationshipToAPI,
|
||||||
} from "~database/entities/Relationship";
|
} from "~database/entities/Relationship";
|
||||||
import {
|
import {
|
||||||
findFirstUser,
|
|
||||||
getRelationshipToOtherUser,
|
getRelationshipToOtherUser,
|
||||||
sendFollowReject,
|
sendFollowReject,
|
||||||
} from "~database/entities/User";
|
} from "~database/entities/User";
|
||||||
import { db } from "~drizzle/db";
|
import { db } from "~drizzle/db";
|
||||||
import { Relationships } from "~drizzle/schema";
|
import { Relationships } from "~drizzle/schema";
|
||||||
|
import { User } from "~packages/database-interface/user";
|
||||||
|
|
||||||
export const meta = applyConfig({
|
export const meta = applyConfig({
|
||||||
allowedMethods: ["POST"],
|
allowedMethods: ["POST"],
|
||||||
|
|
@ -32,9 +32,7 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
|
||||||
|
|
||||||
const { account_id } = matchedRoute.params;
|
const { account_id } = matchedRoute.params;
|
||||||
|
|
||||||
const account = await findFirstUser({
|
const account = await User.fromId(account_id);
|
||||||
where: (user, { eq }) => eq(user.id, account_id),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!account) return errorResponse("Account not found", 404);
|
if (!account) return errorResponse("Account not found", 404);
|
||||||
|
|
||||||
|
|
@ -73,7 +71,7 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
|
||||||
if (!foundRelationship) return errorResponse("Relationship not found", 404);
|
if (!foundRelationship) return errorResponse("Relationship not found", 404);
|
||||||
|
|
||||||
// Check if rejecting remote follow
|
// Check if rejecting remote follow
|
||||||
if (account.instanceId) {
|
if (account.isRemote()) {
|
||||||
// Federate follow reject
|
// Federate follow reject
|
||||||
await sendFollowReject(account, user);
|
await sendFollowReject(account, user);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,9 @@
|
||||||
import { apiRoute, applyConfig, idValidator } from "@api";
|
import { apiRoute, applyConfig, idValidator } from "@api";
|
||||||
import { errorResponse, jsonResponse } from "@response";
|
import { errorResponse, jsonResponse } from "@response";
|
||||||
import { fetchTimeline } from "@timelines";
|
import { and, gt, gte, lt, sql } from "drizzle-orm";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import {
|
import { Users } from "~drizzle/schema";
|
||||||
type UserWithRelations,
|
import { Timeline } from "~packages/database-interface/timeline";
|
||||||
findManyUsers,
|
|
||||||
userToAPI,
|
|
||||||
} from "~database/entities/User";
|
|
||||||
|
|
||||||
export const meta = applyConfig({
|
export const meta = applyConfig({
|
||||||
allowedMethods: ["GET"],
|
allowedMethods: ["GET"],
|
||||||
|
|
@ -35,26 +32,19 @@ export default apiRoute<typeof meta, typeof schema>(
|
||||||
|
|
||||||
if (!user) return errorResponse("Unauthorized", 401);
|
if (!user) return errorResponse("Unauthorized", 401);
|
||||||
|
|
||||||
const { objects, link } = await fetchTimeline<UserWithRelations>(
|
const { objects, link } = await Timeline.getUserTimeline(
|
||||||
findManyUsers,
|
|
||||||
{
|
|
||||||
// @ts-expect-error Yes I KNOW the types are wrong
|
|
||||||
where: (subject, { lt, gte, gt, and, sql }) =>
|
|
||||||
and(
|
and(
|
||||||
max_id ? lt(subject.id, max_id) : undefined,
|
max_id ? lt(Users.id, max_id) : undefined,
|
||||||
since_id ? gte(subject.id, since_id) : undefined,
|
since_id ? gte(Users.id, since_id) : undefined,
|
||||||
min_id ? gt(subject.id, min_id) : undefined,
|
min_id ? gt(Users.id, min_id) : undefined,
|
||||||
sql`EXISTS (SELECT 1 FROM "Relationships" WHERE "Relationships"."subjectId" = ${user.id} AND "Relationships"."ownerId" = ${subject.id} AND "Relationships"."requested" = true)`,
|
sql`EXISTS (SELECT 1 FROM "Relationships" WHERE "Relationships"."subjectId" = ${user.id} AND "Relationships"."ownerId" = ${Users.id} AND "Relationships"."requested" = true)`,
|
||||||
),
|
),
|
||||||
limit,
|
limit,
|
||||||
// @ts-expect-error Yes I KNOW the types are wrong
|
req.url,
|
||||||
orderBy: (subject, { desc }) => desc(subject.id),
|
|
||||||
},
|
|
||||||
req,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return jsonResponse(
|
return jsonResponse(
|
||||||
objects.map((user) => userToAPI(user)),
|
objects.map((user) => user.toAPI()),
|
||||||
200,
|
200,
|
||||||
{
|
{
|
||||||
Link: link,
|
Link: link,
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import { apiRoute, applyConfig } from "@api";
|
import { apiRoute, applyConfig } from "@api";
|
||||||
import { jsonResponse } from "@response";
|
import { jsonResponse } from "@response";
|
||||||
import { and, count, countDistinct, eq, gte, isNull, sql } from "drizzle-orm";
|
import { and, count, countDistinct, eq, gte, isNull, sql } from "drizzle-orm";
|
||||||
import { findFirstUser, userToAPI } from "~database/entities/User";
|
|
||||||
import { db } from "~drizzle/db";
|
import { db } from "~drizzle/db";
|
||||||
import { Instances, Notes, Users } from "~drizzle/schema";
|
import { Instances, Notes, Users } from "~drizzle/schema";
|
||||||
import manifest from "~package.json";
|
import manifest from "~package.json";
|
||||||
|
import { User } from "~packages/database-interface/user";
|
||||||
import type { Instance as APIInstance } from "~types/mastodon/instance";
|
import type { Instance as APIInstance } from "~types/mastodon/instance";
|
||||||
|
|
||||||
export const meta = applyConfig({
|
export const meta = applyConfig({
|
||||||
|
|
@ -45,11 +45,9 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
|
||||||
.where(isNull(Users.instanceId))
|
.where(isNull(Users.instanceId))
|
||||||
)[0].count;
|
)[0].count;
|
||||||
|
|
||||||
const contactAccount = await findFirstUser({
|
const contactAccount = await User.fromSql(
|
||||||
where: (user, { isNull, eq, and }) =>
|
and(isNull(Users.instanceId), eq(Users.isAdmin, true)),
|
||||||
and(isNull(user.instanceId), eq(user.isAdmin, true)),
|
);
|
||||||
orderBy: (user, { asc }) => asc(user.id),
|
|
||||||
});
|
|
||||||
|
|
||||||
const monthlyActiveUsers = (
|
const monthlyActiveUsers = (
|
||||||
await db
|
await db
|
||||||
|
|
@ -186,7 +184,7 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
|
||||||
},
|
},
|
||||||
vapid_public_key: "",
|
vapid_public_key: "",
|
||||||
},
|
},
|
||||||
contact_account: contactAccount ? userToAPI(contactAccount) : undefined,
|
contact_account: contactAccount?.toAPI() || undefined,
|
||||||
} satisfies APIInstance & {
|
} satisfies APIInstance & {
|
||||||
pleroma: object;
|
pleroma: object;
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { apiRoute, applyConfig, idValidator } from "@api";
|
import { apiRoute, applyConfig, idValidator } from "@api";
|
||||||
import { errorResponse, jsonResponse } from "@response";
|
import { errorResponse, jsonResponse } from "@response";
|
||||||
import { fetchTimeline } from "@timelines";
|
|
||||||
import { and, count, eq } from "drizzle-orm";
|
import { and, count, eq } from "drizzle-orm";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { db } from "~drizzle/db";
|
import { db } from "~drizzle/db";
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,9 @@
|
||||||
import { apiRoute, applyConfig, idValidator } from "@api";
|
import { apiRoute, applyConfig, idValidator } from "@api";
|
||||||
import { errorResponse, jsonResponse } from "@response";
|
import { errorResponse, jsonResponse } from "@response";
|
||||||
import { fetchTimeline } from "@timelines";
|
import { and, gt, gte, lt, sql } from "drizzle-orm";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import {
|
import { Users } from "~drizzle/schema";
|
||||||
type UserWithRelations,
|
import { Timeline } from "~packages/database-interface/timeline";
|
||||||
findManyUsers,
|
|
||||||
userToAPI,
|
|
||||||
} from "~database/entities/User";
|
|
||||||
|
|
||||||
export const meta = applyConfig({
|
export const meta = applyConfig({
|
||||||
allowedMethods: ["GET"],
|
allowedMethods: ["GET"],
|
||||||
|
|
@ -35,25 +32,17 @@ export default apiRoute<typeof meta, typeof schema>(
|
||||||
|
|
||||||
if (!user) return errorResponse("Unauthorized", 401);
|
if (!user) return errorResponse("Unauthorized", 401);
|
||||||
|
|
||||||
const { objects: blocks, link } =
|
const { objects: mutes, link } = await Timeline.getUserTimeline(
|
||||||
await fetchTimeline<UserWithRelations>(
|
|
||||||
findManyUsers,
|
|
||||||
{
|
|
||||||
// @ts-expect-error Yes I KNOW the types are wrong
|
|
||||||
where: (subject, { lt, gte, gt, and, sql }) =>
|
|
||||||
and(
|
and(
|
||||||
max_id ? lt(subject.id, max_id) : undefined,
|
max_id ? lt(Users.id, max_id) : undefined,
|
||||||
since_id ? gte(subject.id, since_id) : undefined,
|
since_id ? gte(Users.id, since_id) : undefined,
|
||||||
min_id ? gt(subject.id, min_id) : undefined,
|
min_id ? gt(Users.id, min_id) : undefined,
|
||||||
sql`EXISTS (SELECT 1 FROM "Relationships" WHERE "Relationships"."subjectId" = ${subject.id} AND "Relationships"."ownerId" = ${user.id} AND "Relationships"."muting" = true)`,
|
sql`EXISTS (SELECT 1 FROM "Relationships" WHERE "Relationships"."subjectId" = ${Users.id} AND "Relationships"."ownerId" = ${user.id} AND "Relationships"."muting" = true)`,
|
||||||
),
|
),
|
||||||
limit,
|
limit,
|
||||||
// @ts-expect-error Yes I KNOW the types are wrong
|
req.url,
|
||||||
orderBy: (subject, { desc }) => desc(subject.id),
|
|
||||||
},
|
|
||||||
req,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return jsonResponse(blocks.map((u) => userToAPI(u)));
|
return jsonResponse(mutes.map((u) => u.toAPI()));
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,7 @@ beforeAll(async () => {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
status: `@${users[0].username} test mention`,
|
status: `@${users[0].getUser().username} test mention`,
|
||||||
visibility: "direct",
|
visibility: "direct",
|
||||||
federate: false,
|
federate: false,
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import { apiRoute, applyConfig } from "@api";
|
import { apiRoute, applyConfig } from "@api";
|
||||||
import { errorResponse, jsonResponse } from "@response";
|
import { errorResponse, jsonResponse } from "@response";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { userToAPI } from "~database/entities/User";
|
|
||||||
import { db } from "~drizzle/db";
|
import { db } from "~drizzle/db";
|
||||||
import { Users } from "~drizzle/schema";
|
import { Users } from "~drizzle/schema";
|
||||||
|
import { User } from "~packages/database-interface/user";
|
||||||
|
|
||||||
export const meta = applyConfig({
|
export const meta = applyConfig({
|
||||||
allowedMethods: ["DELETE"],
|
allowedMethods: ["DELETE"],
|
||||||
|
|
@ -27,10 +27,8 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
|
||||||
|
|
||||||
await db.update(Users).set({ avatar: "" }).where(eq(Users.id, self.id));
|
await db.update(Users).set({ avatar: "" }).where(eq(Users.id, self.id));
|
||||||
|
|
||||||
return jsonResponse(
|
return jsonResponse({
|
||||||
userToAPI({
|
...(await User.fromId(self.id))?.toAPI(),
|
||||||
...self,
|
|
||||||
avatar: "",
|
avatar: "",
|
||||||
}),
|
});
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import { apiRoute, applyConfig } from "@api";
|
import { apiRoute, applyConfig } from "@api";
|
||||||
import { errorResponse, jsonResponse } from "@response";
|
import { errorResponse, jsonResponse } from "@response";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { userToAPI } from "~database/entities/User";
|
|
||||||
import { db } from "~drizzle/db";
|
import { db } from "~drizzle/db";
|
||||||
import { Users } from "~drizzle/schema";
|
import { Users } from "~drizzle/schema";
|
||||||
|
import { User } from "~packages/database-interface/user";
|
||||||
|
|
||||||
export const meta = applyConfig({
|
export const meta = applyConfig({
|
||||||
allowedMethods: ["DELETE"],
|
allowedMethods: ["DELETE"],
|
||||||
|
|
@ -28,10 +28,8 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
|
||||||
// Delete user header
|
// Delete user header
|
||||||
await db.update(Users).set({ header: "" }).where(eq(Users.id, self.id));
|
await db.update(Users).set({ header: "" }).where(eq(Users.id, self.id));
|
||||||
|
|
||||||
return jsonResponse(
|
return jsonResponse({
|
||||||
userToAPI({
|
...(await User.fromId(self.id))?.toAPI(),
|
||||||
...self,
|
|
||||||
header: "",
|
header: "",
|
||||||
}),
|
});
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -30,27 +30,27 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
|
||||||
|
|
||||||
if (!user) return errorResponse("Unauthorized", 401);
|
if (!user) return errorResponse("Unauthorized", 401);
|
||||||
|
|
||||||
const status = await Note.fromId(id);
|
const note = await Note.fromId(id);
|
||||||
|
|
||||||
// Check if user is authorized to view this status (if it's private)
|
// Check if user is authorized to view this status (if it's private)
|
||||||
if (!status?.isViewableByUser(user))
|
if (!note?.isViewableByUser(user))
|
||||||
return errorResponse("Record not found", 404);
|
return errorResponse("Record not found", 404);
|
||||||
|
|
||||||
const existingLike = await db.query.Likes.findFirst({
|
const existingLike = await db.query.Likes.findFirst({
|
||||||
where: (like, { and, eq }) =>
|
where: (like, { and, eq }) =>
|
||||||
and(
|
and(
|
||||||
eq(like.likedId, status.getStatus().id),
|
eq(like.likedId, note.getStatus().id),
|
||||||
eq(like.likerId, user.id),
|
eq(like.likerId, user.id),
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!existingLike) {
|
if (!existingLike) {
|
||||||
await createLike(user, status.getStatus());
|
await createLike(user, note);
|
||||||
}
|
}
|
||||||
|
|
||||||
return jsonResponse({
|
return jsonResponse({
|
||||||
...(await status.toAPI(user)),
|
...(await note.toAPI(user)),
|
||||||
favourited: true,
|
favourited: true,
|
||||||
favourites_count: status.getStatus().likeCount + 1,
|
favourites_count: note.getStatus().likeCount + 1,
|
||||||
} as APIStatus);
|
} as APIStatus);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -73,7 +73,7 @@ describe(meta.route, () => {
|
||||||
expect(objects.length).toBe(1);
|
expect(objects.length).toBe(1);
|
||||||
for (const [index, status] of objects.entries()) {
|
for (const [index, status] of objects.entries()) {
|
||||||
expect(status.id).toBe(users[1].id);
|
expect(status.id).toBe(users[1].id);
|
||||||
expect(status.username).toBe(users[1].username);
|
expect(status.username).toBe(users[1].getUser().username);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,10 @@
|
||||||
import { apiRoute, applyConfig, idValidator } from "@api";
|
import { apiRoute, applyConfig, idValidator } from "@api";
|
||||||
import { errorResponse, jsonResponse } from "@response";
|
import { errorResponse, jsonResponse } from "@response";
|
||||||
import { fetchTimeline } from "@timelines";
|
import { and, gt, gte, lt, sql } from "drizzle-orm";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import {
|
import { Users } from "~drizzle/schema";
|
||||||
type UserWithRelations,
|
|
||||||
findManyUsers,
|
|
||||||
userToAPI,
|
|
||||||
} from "~database/entities/User";
|
|
||||||
import { Note } from "~packages/database-interface/note";
|
import { Note } from "~packages/database-interface/note";
|
||||||
|
import { Timeline } from "~packages/database-interface/timeline";
|
||||||
|
|
||||||
export const meta = applyConfig({
|
export const meta = applyConfig({
|
||||||
allowedMethods: ["GET"],
|
allowedMethods: ["GET"],
|
||||||
|
|
@ -48,28 +45,19 @@ export default apiRoute<typeof meta, typeof schema>(
|
||||||
|
|
||||||
const { max_id, min_id, since_id, limit } = extraData.parsedRequest;
|
const { max_id, min_id, since_id, limit } = extraData.parsedRequest;
|
||||||
|
|
||||||
const { objects, link } = await fetchTimeline<UserWithRelations>(
|
const { objects, link } = await Timeline.getUserTimeline(
|
||||||
findManyUsers,
|
|
||||||
{
|
|
||||||
// @ts-ignore
|
|
||||||
where: (liker, { and, lt, gt, gte, eq, sql }) =>
|
|
||||||
and(
|
and(
|
||||||
max_id ? lt(liker.id, max_id) : undefined,
|
max_id ? lt(Users.id, max_id) : undefined,
|
||||||
since_id ? gte(liker.id, since_id) : undefined,
|
since_id ? gte(Users.id, since_id) : undefined,
|
||||||
min_id ? gt(liker.id, min_id) : undefined,
|
min_id ? gt(Users.id, min_id) : undefined,
|
||||||
sql`EXISTS (SELECT 1 FROM "Likes" WHERE "Likes"."likedId" = ${
|
sql`EXISTS (SELECT 1 FROM "Likes" WHERE "Likes"."likedId" = ${status.id} AND "Likes"."likerId" = ${Users.id})`,
|
||||||
status.getStatus().id
|
|
||||||
} AND "Likes"."likerId" = ${liker.id})`,
|
|
||||||
),
|
),
|
||||||
// @ts-expect-error Yes I KNOW the types are wrong
|
|
||||||
orderBy: (liker, { desc }) => desc(liker.id),
|
|
||||||
limit,
|
limit,
|
||||||
},
|
req.url,
|
||||||
req,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return jsonResponse(
|
return jsonResponse(
|
||||||
objects.map((user) => userToAPI(user)),
|
objects.map((user) => user.toAPI()),
|
||||||
200,
|
200,
|
||||||
{
|
{
|
||||||
Link: link,
|
Link: link,
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
|
||||||
return errorResponse("Already pinned", 422);
|
return errorResponse("Already pinned", 422);
|
||||||
}
|
}
|
||||||
|
|
||||||
await foundStatus.pin(user);
|
await user.pin(foundStatus);
|
||||||
|
|
||||||
return jsonResponse(await foundStatus.toAPI(user));
|
return jsonResponse(await foundStatus.toAPI(user));
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -73,7 +73,7 @@ describe(meta.route, () => {
|
||||||
expect(objects.length).toBe(1);
|
expect(objects.length).toBe(1);
|
||||||
for (const [index, status] of objects.entries()) {
|
for (const [index, status] of objects.entries()) {
|
||||||
expect(status.id).toBe(users[1].id);
|
expect(status.id).toBe(users[1].id);
|
||||||
expect(status.username).toBe(users[1].username);
|
expect(status.username).toBe(users[1].getUser().username);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,10 @@
|
||||||
import { apiRoute, applyConfig, idValidator } from "@api";
|
import { apiRoute, applyConfig, idValidator } from "@api";
|
||||||
import { errorResponse, jsonResponse } from "@response";
|
import { errorResponse, jsonResponse } from "@response";
|
||||||
import { fetchTimeline } from "@timelines";
|
import { and, gt, gte, lt, sql } from "drizzle-orm";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import {
|
import { Users } from "~drizzle/schema";
|
||||||
type UserWithRelations,
|
|
||||||
findManyUsers,
|
|
||||||
userToAPI,
|
|
||||||
} from "~database/entities/User";
|
|
||||||
import { Note } from "~packages/database-interface/note";
|
import { Note } from "~packages/database-interface/note";
|
||||||
|
import { Timeline } from "~packages/database-interface/timeline";
|
||||||
|
|
||||||
export const meta = applyConfig({
|
export const meta = applyConfig({
|
||||||
allowedMethods: ["GET"],
|
allowedMethods: ["GET"],
|
||||||
|
|
@ -48,28 +45,19 @@ export default apiRoute<typeof meta, typeof schema>(
|
||||||
|
|
||||||
const { max_id, min_id, since_id, limit } = extraData.parsedRequest;
|
const { max_id, min_id, since_id, limit } = extraData.parsedRequest;
|
||||||
|
|
||||||
const { objects, link } = await fetchTimeline<UserWithRelations>(
|
const { objects, link } = await Timeline.getUserTimeline(
|
||||||
findManyUsers,
|
|
||||||
{
|
|
||||||
// @ts-ignore
|
|
||||||
where: (reblogger, { and, lt, gt, gte, eq, sql }) =>
|
|
||||||
and(
|
and(
|
||||||
max_id ? lt(reblogger.id, max_id) : undefined,
|
max_id ? lt(Users.id, max_id) : undefined,
|
||||||
since_id ? gte(reblogger.id, since_id) : undefined,
|
since_id ? gte(Users.id, since_id) : undefined,
|
||||||
min_id ? gt(reblogger.id, min_id) : undefined,
|
min_id ? gt(Users.id, min_id) : undefined,
|
||||||
sql`EXISTS (SELECT 1 FROM "Notes" WHERE "Notes"."reblogId" = ${
|
sql`EXISTS (SELECT 1 FROM "Notes" WHERE "Notes"."reblogId" = ${status.id} AND "Notes"."authorId" = ${Users.id})`,
|
||||||
status.getStatus().id
|
|
||||||
} AND "Notes"."authorId" = ${reblogger.id})`,
|
|
||||||
),
|
),
|
||||||
// @ts-expect-error Yes I KNOW the types are wrong
|
|
||||||
orderBy: (liker, { desc }) => desc(liker.id),
|
|
||||||
limit,
|
limit,
|
||||||
},
|
req.url,
|
||||||
req,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return jsonResponse(
|
return jsonResponse(
|
||||||
objects.map((user) => userToAPI(user)),
|
objects.map((user) => user.toAPI()),
|
||||||
200,
|
200,
|
||||||
{
|
{
|
||||||
Link: link,
|
Link: link,
|
||||||
|
|
|
||||||
|
|
@ -29,17 +29,17 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
|
||||||
|
|
||||||
if (!user) return errorResponse("Unauthorized", 401);
|
if (!user) return errorResponse("Unauthorized", 401);
|
||||||
|
|
||||||
const foundStatus = await Note.fromId(id);
|
const note = await Note.fromId(id);
|
||||||
|
|
||||||
// Check if user is authorized to view this status (if it's private)
|
// Check if user is authorized to view this status (if it's private)
|
||||||
if (!foundStatus?.isViewableByUser(user))
|
if (!note?.isViewableByUser(user))
|
||||||
return errorResponse("Record not found", 404);
|
return errorResponse("Record not found", 404);
|
||||||
|
|
||||||
await deleteLike(user, foundStatus.getStatus());
|
await deleteLike(user, note);
|
||||||
|
|
||||||
return jsonResponse({
|
return jsonResponse({
|
||||||
...(await foundStatus.toAPI(user)),
|
...(await note.toAPI(user)),
|
||||||
favourited: false,
|
favourited: false,
|
||||||
favourites_count: foundStatus.getStatus().likeCount - 1,
|
favourites_count: note.getStatus().likeCount - 1,
|
||||||
} as APIStatus);
|
} as APIStatus);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
|
||||||
if (status.getAuthor().id !== user.id)
|
if (status.getAuthor().id !== user.id)
|
||||||
return errorResponse("Unauthorized", 401);
|
return errorResponse("Unauthorized", 401);
|
||||||
|
|
||||||
await status.unpin(user);
|
await user.unpin(status);
|
||||||
|
|
||||||
if (!status) return errorResponse("Record not found", 404);
|
if (!status) return errorResponse("Record not found", 404);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -306,7 +306,7 @@ describe(meta.route, () => {
|
||||||
Authorization: `Bearer ${tokens[0].accessToken}`,
|
Authorization: `Bearer ${tokens[0].accessToken}`,
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
status: `Hello, @${users[1].username}!`,
|
status: `Hello, @${users[1].getUser().username}!`,
|
||||||
federate: false,
|
federate: false,
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
|
@ -322,8 +322,8 @@ describe(meta.route, () => {
|
||||||
expect(object.mentions).toBeArrayOfSize(1);
|
expect(object.mentions).toBeArrayOfSize(1);
|
||||||
expect(object.mentions[0]).toMatchObject({
|
expect(object.mentions[0]).toMatchObject({
|
||||||
id: users[1].id,
|
id: users[1].id,
|
||||||
username: users[1].username,
|
username: users[1].getUser().username,
|
||||||
acct: users[1].username,
|
acct: users[1].getUser().username,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -336,7 +336,7 @@ describe(meta.route, () => {
|
||||||
Authorization: `Bearer ${tokens[0].accessToken}`,
|
Authorization: `Bearer ${tokens[0].accessToken}`,
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
status: `Hello, @${users[1].username}@${
|
status: `Hello, @${users[1].getUser().username}@${
|
||||||
new URL(config.http.base_url).host
|
new URL(config.http.base_url).host
|
||||||
}!`,
|
}!`,
|
||||||
federate: false,
|
federate: false,
|
||||||
|
|
@ -354,8 +354,8 @@ describe(meta.route, () => {
|
||||||
expect(object.mentions).toBeArrayOfSize(1);
|
expect(object.mentions).toBeArrayOfSize(1);
|
||||||
expect(object.mentions[0]).toMatchObject({
|
expect(object.mentions[0]).toMatchObject({
|
||||||
id: users[1].id,
|
id: users[1].id,
|
||||||
username: users[1].username,
|
username: users[1].getUser().username,
|
||||||
acct: users[1].username,
|
acct: users[1].getUser().username,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import { apiRoute, applyConfig } from "@api";
|
import { apiRoute, applyConfig } from "@api";
|
||||||
import { jsonResponse } from "@response";
|
import { jsonResponse } from "@response";
|
||||||
import { and, countDistinct, eq, gte, isNull } from "drizzle-orm";
|
import { and, countDistinct, eq, gte, isNull } from "drizzle-orm";
|
||||||
import { findFirstUser, userToAPI } from "~database/entities/User";
|
|
||||||
import { db } from "~drizzle/db";
|
import { db } from "~drizzle/db";
|
||||||
import { Notes, Users } from "~drizzle/schema";
|
import { Notes, Users } from "~drizzle/schema";
|
||||||
import manifest from "~package.json";
|
import manifest from "~package.json";
|
||||||
|
import { User } from "~packages/database-interface/user";
|
||||||
|
|
||||||
export const meta = applyConfig({
|
export const meta = applyConfig({
|
||||||
allowedMethods: ["GET"],
|
allowedMethods: ["GET"],
|
||||||
|
|
@ -23,11 +23,10 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
|
||||||
|
|
||||||
// Get software version from package.json
|
// Get software version from package.json
|
||||||
const version = manifest.version;
|
const version = manifest.version;
|
||||||
const contactAccount = await findFirstUser({
|
|
||||||
where: (user, { isNull, eq, and }) =>
|
const contactAccount = await User.fromSql(
|
||||||
and(isNull(user.instanceId), eq(user.isAdmin, true)),
|
and(isNull(Users.instanceId), eq(Users.isAdmin, true)),
|
||||||
orderBy: (user, { asc }) => asc(user.id),
|
);
|
||||||
});
|
|
||||||
|
|
||||||
const monthlyActiveUsers = (
|
const monthlyActiveUsers = (
|
||||||
await db
|
await db
|
||||||
|
|
@ -104,8 +103,8 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
|
||||||
url: null,
|
url: null,
|
||||||
},
|
},
|
||||||
contact: {
|
contact: {
|
||||||
email: contactAccount?.email || null,
|
email: contactAccount?.getUser().email || null,
|
||||||
account: contactAccount ? userToAPI(contactAccount) : null,
|
account: contactAccount?.toAPI() || null,
|
||||||
},
|
},
|
||||||
rules: config.signups.rules.map((rule, index) => ({
|
rules: config.signups.rules.map((rule, index) => ({
|
||||||
id: String(index),
|
id: String(index),
|
||||||
|
|
|
||||||
|
|
@ -4,15 +4,11 @@ import { MeiliIndexType, meilisearch } from "@meilisearch";
|
||||||
import { errorResponse, jsonResponse } from "@response";
|
import { errorResponse, jsonResponse } from "@response";
|
||||||
import { and, eq, inArray, sql } from "drizzle-orm";
|
import { and, eq, inArray, sql } from "drizzle-orm";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import {
|
import { resolveWebFinger } from "~database/entities/User";
|
||||||
findFirstUser,
|
|
||||||
findManyUsers,
|
|
||||||
resolveWebFinger,
|
|
||||||
userToAPI,
|
|
||||||
} from "~database/entities/User";
|
|
||||||
import { db } from "~drizzle/db";
|
import { db } from "~drizzle/db";
|
||||||
import { Instances, Notes, Users } from "~drizzle/schema";
|
import { Instances, Notes, Users } from "~drizzle/schema";
|
||||||
import { Note } from "~packages/database-interface/note";
|
import { Note } from "~packages/database-interface/note";
|
||||||
|
import { User } from "~packages/database-interface/user";
|
||||||
import { LogLevel } from "~packages/log-manager";
|
import { LogLevel } from "~packages/log-manager";
|
||||||
|
|
||||||
export const meta = applyConfig({
|
export const meta = applyConfig({
|
||||||
|
|
@ -100,15 +96,11 @@ export default apiRoute<typeof meta, typeof schema>(
|
||||||
)
|
)
|
||||||
)[0]?.id;
|
)[0]?.id;
|
||||||
|
|
||||||
const account = accountId
|
const account = accountId ? await User.fromId(accountId) : null;
|
||||||
? await findFirstUser({
|
|
||||||
where: (user, { eq }) => eq(user.id, accountId),
|
|
||||||
})
|
|
||||||
: null;
|
|
||||||
|
|
||||||
if (account) {
|
if (account) {
|
||||||
return jsonResponse({
|
return jsonResponse({
|
||||||
accounts: [userToAPI(account)],
|
accounts: [account.toAPI()],
|
||||||
statuses: [],
|
statuses: [],
|
||||||
hashtags: [],
|
hashtags: [],
|
||||||
});
|
});
|
||||||
|
|
@ -129,7 +121,7 @@ export default apiRoute<typeof meta, typeof schema>(
|
||||||
|
|
||||||
if (newUser) {
|
if (newUser) {
|
||||||
return jsonResponse({
|
return jsonResponse({
|
||||||
accounts: [userToAPI(newUser)],
|
accounts: [newUser.toAPI()],
|
||||||
statuses: [],
|
statuses: [],
|
||||||
hashtags: [],
|
hashtags: [],
|
||||||
});
|
});
|
||||||
|
|
@ -160,23 +152,21 @@ export default apiRoute<typeof meta, typeof schema>(
|
||||||
).hits;
|
).hits;
|
||||||
}
|
}
|
||||||
|
|
||||||
const accounts = await findManyUsers({
|
const accounts = await User.manyFromSql(
|
||||||
where: (user, { and, eq, inArray }) =>
|
|
||||||
and(
|
and(
|
||||||
inArray(
|
inArray(
|
||||||
user.id,
|
Users.id,
|
||||||
accountResults.map((hit) => hit.id),
|
accountResults.map((hit) => hit.id),
|
||||||
),
|
),
|
||||||
self
|
self
|
||||||
? sql`EXISTS (SELECT 1 FROM Relationships WHERE Relationships.subjectId = ${
|
? sql`EXISTS (SELECT 1 FROM Relationships WHERE Relationships.subjectId = ${
|
||||||
self?.id
|
self?.id
|
||||||
} AND Relationships.following = ${!!following} AND Relationships.ownerId = ${
|
} AND Relationships.following = ${!!following} AND Relationships.ownerId = ${
|
||||||
user.id
|
Users.id
|
||||||
})`
|
})`
|
||||||
: undefined,
|
: undefined,
|
||||||
),
|
),
|
||||||
orderBy: (user, { desc }) => desc(user.createdAt),
|
);
|
||||||
});
|
|
||||||
|
|
||||||
const statuses = await Note.manyFromSql(
|
const statuses = await Note.manyFromSql(
|
||||||
and(
|
and(
|
||||||
|
|
@ -196,7 +186,7 @@ export default apiRoute<typeof meta, typeof schema>(
|
||||||
);
|
);
|
||||||
|
|
||||||
return jsonResponse({
|
return jsonResponse({
|
||||||
accounts: accounts.map((account) => userToAPI(account)),
|
accounts: accounts.map((account) => account.toAPI()),
|
||||||
statuses: await Promise.all(
|
statuses: await Promise.all(
|
||||||
statuses.map((status) => status.toAPI(self)),
|
statuses.map((status) => status.toAPI(self)),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,10 @@ import { response } from "@response";
|
||||||
import { SignJWT, jwtVerify } from "jose";
|
import { SignJWT, jwtVerify } from "jose";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { TokenType } from "~database/entities/Token";
|
import { TokenType } from "~database/entities/Token";
|
||||||
import { findFirstUser } from "~database/entities/User";
|
|
||||||
import { db } from "~drizzle/db";
|
import { db } from "~drizzle/db";
|
||||||
import { Tokens } from "~drizzle/schema";
|
import { Tokens } from "~drizzle/schema";
|
||||||
import { config } from "~packages/config-manager";
|
import { config } from "~packages/config-manager";
|
||||||
|
import { User } from "~packages/database-interface/user";
|
||||||
|
|
||||||
export const meta = applyConfig({
|
export const meta = applyConfig({
|
||||||
allowedMethods: ["POST"],
|
allowedMethods: ["POST"],
|
||||||
|
|
@ -136,9 +136,7 @@ export default apiRoute<typeof meta, typeof schema>(
|
||||||
if (!payload.exp) return returnError("invalid_request", "Invalid exp");
|
if (!payload.exp) return returnError("invalid_request", "Invalid exp");
|
||||||
|
|
||||||
// Check if the user is authenticated
|
// Check if the user is authenticated
|
||||||
const user = await findFirstUser({
|
const user = await User.fromId(payload.sub);
|
||||||
where: (user, { eq }) => eq(user.id, payload.sub ?? ""),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!user) return returnError("invalid_request", "Invalid sub");
|
if (!user) return returnError("invalid_request", "Invalid sub");
|
||||||
|
|
||||||
|
|
@ -226,17 +224,20 @@ export default apiRoute<typeof meta, typeof schema>(
|
||||||
// Include the user's profile information
|
// Include the user's profile information
|
||||||
idTokenPayload = {
|
idTokenPayload = {
|
||||||
...idTokenPayload,
|
...idTokenPayload,
|
||||||
name: user.displayName,
|
name: user.getUser().displayName,
|
||||||
preferred_username: user.username,
|
preferred_username: user.getUser().username,
|
||||||
picture: user.avatar,
|
picture: user.getAvatarUrl(config),
|
||||||
updated_at: new Date(user.updatedAt).toISOString(),
|
updated_at: new Date(
|
||||||
|
user.getUser().updatedAt,
|
||||||
|
).toISOString(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (scopeIncludesEmail) {
|
if (scopeIncludesEmail) {
|
||||||
// Include the user's email address
|
// Include the user's email address
|
||||||
idTokenPayload = {
|
idTokenPayload = {
|
||||||
...idTokenPayload,
|
...idTokenPayload,
|
||||||
email: user.email,
|
email: user.getUser().email,
|
||||||
|
// TODO: Add verification system
|
||||||
email_verified: true,
|
email_verified: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,9 +14,9 @@ import {
|
||||||
validateAuthResponse,
|
validateAuthResponse,
|
||||||
} from "oauth4webapi";
|
} from "oauth4webapi";
|
||||||
import { TokenType } from "~database/entities/Token";
|
import { TokenType } from "~database/entities/Token";
|
||||||
import { findFirstUser } from "~database/entities/User";
|
|
||||||
import { db } from "~drizzle/db";
|
import { db } from "~drizzle/db";
|
||||||
import { Tokens } from "~drizzle/schema";
|
import { Tokens } from "~drizzle/schema";
|
||||||
|
import { User } from "~packages/database-interface/user";
|
||||||
|
|
||||||
export const meta = applyConfig({
|
export const meta = applyConfig({
|
||||||
allowedMethods: ["GET"],
|
allowedMethods: ["GET"],
|
||||||
|
|
@ -154,9 +154,7 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
|
||||||
return redirectToLogin("No user found with that account");
|
return redirectToLogin("No user found with that account");
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await findFirstUser({
|
const user = await User.fromId(userId);
|
||||||
where: (user, { eq }) => eq(user.id, userId),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return redirectToLogin("No user found with that account");
|
return redirectToLogin("No user found with that account");
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,19 @@
|
||||||
import { apiRoute, applyConfig } from "@api";
|
import { apiRoute, applyConfig } from "@api";
|
||||||
import { dualLogger } from "@loggers";
|
import { dualLogger } from "@loggers";
|
||||||
import { errorResponse, response } from "@response";
|
import { errorResponse, jsonResponse, response } from "@response";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import type * as Lysand from "lysand-types";
|
import type * as Lysand from "lysand-types";
|
||||||
|
import { isValidationError } from "zod-validation-error";
|
||||||
import { resolveNote } from "~database/entities/Status";
|
import { resolveNote } from "~database/entities/Status";
|
||||||
import {
|
import {
|
||||||
findFirstUser,
|
|
||||||
getRelationshipToOtherUser,
|
getRelationshipToOtherUser,
|
||||||
resolveUser,
|
|
||||||
sendFollowAccept,
|
sendFollowAccept,
|
||||||
} from "~database/entities/User";
|
} from "~database/entities/User";
|
||||||
import { db } from "~drizzle/db";
|
import { db } from "~drizzle/db";
|
||||||
import { Notifications, Relationships } from "~drizzle/schema";
|
import { Notifications, Relationships } from "~drizzle/schema";
|
||||||
|
import { User } from "~packages/database-interface/user";
|
||||||
import { LogLevel } from "~packages/log-manager";
|
import { LogLevel } from "~packages/log-manager";
|
||||||
|
import { EntityValidator, SignatureValidator } from "~packages/lysand-utils";
|
||||||
|
|
||||||
export const meta = applyConfig({
|
export const meta = applyConfig({
|
||||||
allowedMethods: ["POST"],
|
allowedMethods: ["POST"],
|
||||||
|
|
@ -29,111 +30,80 @@ export const meta = applyConfig({
|
||||||
export default apiRoute(async (req, matchedRoute, extraData) => {
|
export default apiRoute(async (req, matchedRoute, extraData) => {
|
||||||
const uuid = matchedRoute.params.uuid;
|
const uuid = matchedRoute.params.uuid;
|
||||||
|
|
||||||
const user = await findFirstUser({
|
const user = await User.fromId(uuid);
|
||||||
where: (user, { eq }) => eq(user.id, uuid),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return errorResponse("User not found", 404);
|
return errorResponse("User not found", 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process incoming request
|
|
||||||
const body = extraData.parsedRequest as Lysand.Entity;
|
|
||||||
|
|
||||||
// Verify request signature
|
// Verify request signature
|
||||||
// TODO: Check if instance is defederated
|
// TODO: Check if instance is defederated
|
||||||
|
// TODO: Reverse DNS lookup with Origin header
|
||||||
// biome-ignore lint/correctness/noConstantCondition: Temporary
|
// biome-ignore lint/correctness/noConstantCondition: Temporary
|
||||||
if (true) {
|
if (true) {
|
||||||
// request is a Request object containing the previous request
|
const Signature = req.headers.get("Signature");
|
||||||
|
const DateHeader = req.headers.get("Date");
|
||||||
|
|
||||||
const signatureHeader = req.headers.get("Signature");
|
if (!Signature) {
|
||||||
const origin = req.headers.get("Origin");
|
|
||||||
const date = req.headers.get("Date");
|
|
||||||
|
|
||||||
if (!signatureHeader) {
|
|
||||||
return errorResponse("Missing Signature header", 400);
|
return errorResponse("Missing Signature header", 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!origin) {
|
if (!DateHeader) {
|
||||||
return errorResponse("Missing Origin header", 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!date) {
|
|
||||||
return errorResponse("Missing Date header", 400);
|
return errorResponse("Missing Date header", 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
const signature = signatureHeader
|
const keyId = Signature.split("keyId=")[1]
|
||||||
.split("signature=")[1]
|
|
||||||
.replace(/"/g, "");
|
|
||||||
|
|
||||||
const digest = await crypto.subtle.digest(
|
|
||||||
"SHA-256",
|
|
||||||
new TextEncoder().encode(JSON.stringify(body)),
|
|
||||||
);
|
|
||||||
|
|
||||||
const keyId = signatureHeader
|
|
||||||
.split("keyId=")[1]
|
|
||||||
.split(",")[0]
|
.split(",")[0]
|
||||||
.replace(/"/g, "");
|
.replace(/"/g, "");
|
||||||
|
|
||||||
console.log(`Resolving keyId ${keyId}`);
|
const sender = await User.resolve(keyId);
|
||||||
|
|
||||||
const sender = await resolveUser(keyId);
|
|
||||||
|
|
||||||
if (!sender) {
|
if (!sender) {
|
||||||
return errorResponse("Invalid keyId", 400);
|
return errorResponse("Could not resolve keyId", 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
const public_key = await crypto.subtle.importKey(
|
const validator = await SignatureValidator.fromStringKey(
|
||||||
"spki",
|
sender.getUser().publicKey,
|
||||||
Buffer.from(sender.publicKey, "base64"),
|
Signature,
|
||||||
"Ed25519",
|
DateHeader,
|
||||||
false,
|
req.method,
|
||||||
["verify"],
|
new URL(req.url),
|
||||||
|
await req.text(),
|
||||||
);
|
);
|
||||||
|
|
||||||
const expectedSignedString =
|
const isValid = await validator.validate();
|
||||||
`(request-target): ${req.method.toLowerCase()} ${
|
|
||||||
new URL(req.url).pathname
|
|
||||||
}\n` +
|
|
||||||
`host: ${new URL(req.url).host}\n` +
|
|
||||||
`date: ${date}\n` +
|
|
||||||
`digest: SHA-256=${btoa(
|
|
||||||
String.fromCharCode(...new Uint8Array(digest)),
|
|
||||||
)}\n`;
|
|
||||||
|
|
||||||
// Check if signed string is valid
|
|
||||||
const isValid = await crypto.subtle.verify(
|
|
||||||
"Ed25519",
|
|
||||||
public_key,
|
|
||||||
Uint8Array.from(atob(signature), (c) => c.charCodeAt(0)),
|
|
||||||
new TextEncoder().encode(expectedSignedString),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!isValid) {
|
if (!isValid) {
|
||||||
return errorResponse("Invalid signature", 400);
|
return errorResponse("Invalid signature", 400);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add sent data to database
|
const validator = new EntityValidator(
|
||||||
switch (body.type) {
|
extraData.parsedRequest as Lysand.Entity,
|
||||||
case "Note": {
|
);
|
||||||
const note = body as Lysand.Note;
|
|
||||||
|
|
||||||
const account = await resolveUser(note.author);
|
try {
|
||||||
|
// Add sent data to database
|
||||||
|
switch (validator.getType()) {
|
||||||
|
case "Note": {
|
||||||
|
const note = await validator.validate<Lysand.Note>();
|
||||||
|
|
||||||
|
const account = await User.resolve(note.author);
|
||||||
|
|
||||||
if (!account) {
|
if (!account) {
|
||||||
return errorResponse("Author not found", 400);
|
return errorResponse("Author not found", 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
const newStatus = await resolveNote(undefined, note).catch((e) => {
|
const newStatus = await resolveNote(undefined, note).catch(
|
||||||
|
(e) => {
|
||||||
dualLogger.logError(
|
dualLogger.logError(
|
||||||
LogLevel.ERROR,
|
LogLevel.ERROR,
|
||||||
"Inbox.NoteResolve",
|
"Inbox.NoteResolve",
|
||||||
e as Error,
|
e as Error,
|
||||||
);
|
);
|
||||||
return null;
|
return null;
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
if (!newStatus) {
|
if (!newStatus) {
|
||||||
return errorResponse("Failed to add status", 500);
|
return errorResponse("Failed to add status", 500);
|
||||||
|
|
@ -142,9 +112,9 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
|
||||||
return response("Note created", 201);
|
return response("Note created", 201);
|
||||||
}
|
}
|
||||||
case "Follow": {
|
case "Follow": {
|
||||||
const follow = body as Lysand.Follow;
|
const follow = await validator.validate<Lysand.Follow>();
|
||||||
|
|
||||||
const account = await resolveUser(follow.author);
|
const account = await User.resolve(follow.author);
|
||||||
|
|
||||||
if (!account) {
|
if (!account) {
|
||||||
return errorResponse("Author not found", 400);
|
return errorResponse("Author not found", 400);
|
||||||
|
|
@ -163,8 +133,8 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
|
||||||
await db
|
await db
|
||||||
.update(Relationships)
|
.update(Relationships)
|
||||||
.set({
|
.set({
|
||||||
following: !user.isLocked,
|
following: !user.getUser().isLocked,
|
||||||
requested: user.isLocked,
|
requested: user.getUser().isLocked,
|
||||||
showingReblogs: true,
|
showingReblogs: true,
|
||||||
notifying: true,
|
notifying: true,
|
||||||
languages: [],
|
languages: [],
|
||||||
|
|
@ -173,11 +143,11 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
|
||||||
|
|
||||||
await db.insert(Notifications).values({
|
await db.insert(Notifications).values({
|
||||||
accountId: account.id,
|
accountId: account.id,
|
||||||
type: user.isLocked ? "follow_request" : "follow",
|
type: user.getUser().isLocked ? "follow_request" : "follow",
|
||||||
notifiedId: user.id,
|
notifiedId: user.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!user.isLocked) {
|
if (!user.getUser().isLocked) {
|
||||||
// Federate FollowAccept
|
// Federate FollowAccept
|
||||||
await sendFollowAccept(account, user);
|
await sendFollowAccept(account, user);
|
||||||
}
|
}
|
||||||
|
|
@ -185,11 +155,12 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
|
||||||
return response("Follow request sent", 200);
|
return response("Follow request sent", 200);
|
||||||
}
|
}
|
||||||
case "FollowAccept": {
|
case "FollowAccept": {
|
||||||
const followAccept = body as Lysand.FollowAccept;
|
const followAccept =
|
||||||
|
await validator.validate<Lysand.FollowAccept>();
|
||||||
|
|
||||||
console.log(followAccept);
|
console.log(followAccept);
|
||||||
|
|
||||||
const account = await resolveUser(followAccept.author);
|
const account = await User.resolve(followAccept.author);
|
||||||
|
|
||||||
if (!account) {
|
if (!account) {
|
||||||
return errorResponse("Author not found", 400);
|
return errorResponse("Author not found", 400);
|
||||||
|
|
@ -205,7 +176,10 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
|
||||||
console.log(foundRelationship);
|
console.log(foundRelationship);
|
||||||
|
|
||||||
if (!foundRelationship.requested) {
|
if (!foundRelationship.requested) {
|
||||||
return response("There is no follow request to accept", 200);
|
return response(
|
||||||
|
"There is no follow request to accept",
|
||||||
|
200,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await db
|
await db
|
||||||
|
|
@ -219,9 +193,10 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
|
||||||
return response("Follow request accepted", 200);
|
return response("Follow request accepted", 200);
|
||||||
}
|
}
|
||||||
case "FollowReject": {
|
case "FollowReject": {
|
||||||
const followReject = body as Lysand.FollowReject;
|
const followReject =
|
||||||
|
await validator.validate<Lysand.FollowReject>();
|
||||||
|
|
||||||
const account = await resolveUser(followReject.author);
|
const account = await User.resolve(followReject.author);
|
||||||
|
|
||||||
if (!account) {
|
if (!account) {
|
||||||
return errorResponse("Author not found", 400);
|
return errorResponse("Author not found", 400);
|
||||||
|
|
@ -233,7 +208,10 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!foundRelationship.requested) {
|
if (!foundRelationship.requested) {
|
||||||
return response("There is no follow request to reject", 200);
|
return response(
|
||||||
|
"There is no follow request to reject",
|
||||||
|
200,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await db
|
await db
|
||||||
|
|
@ -247,9 +225,20 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
|
||||||
return response("Follow request rejected", 200);
|
return response("Follow request rejected", 200);
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
return errorResponse("Unknown object type", 400);
|
return errorResponse("Object has not been implemented", 400);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
//return jsonResponse(userToLysand(user));
|
if (isValidationError(e)) {
|
||||||
|
return errorResponse(e.message, 400);
|
||||||
|
}
|
||||||
|
dualLogger.logError(LogLevel.ERROR, "Inbox", e as Error);
|
||||||
|
return jsonResponse(
|
||||||
|
{
|
||||||
|
error: "Failed to process request",
|
||||||
|
message: (e as Error).message,
|
||||||
|
},
|
||||||
|
500,
|
||||||
|
);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { apiRoute, applyConfig } from "@api";
|
import { apiRoute, applyConfig } from "@api";
|
||||||
import { errorResponse, jsonResponse } from "@response";
|
import { errorResponse, jsonResponse } from "@response";
|
||||||
import { findFirstUser, userToLysand } from "~database/entities/User";
|
import { User } from "~packages/database-interface/user";
|
||||||
|
|
||||||
export const meta = applyConfig({
|
export const meta = applyConfig({
|
||||||
allowedMethods: ["GET"],
|
allowedMethods: ["GET"],
|
||||||
|
|
@ -17,13 +17,11 @@ export const meta = applyConfig({
|
||||||
export default apiRoute(async (req, matchedRoute) => {
|
export default apiRoute(async (req, matchedRoute) => {
|
||||||
const uuid = matchedRoute.params.uuid;
|
const uuid = matchedRoute.params.uuid;
|
||||||
|
|
||||||
const user = await findFirstUser({
|
const user = await User.fromId(uuid);
|
||||||
where: (user, { eq }) => eq(user.id, uuid),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return errorResponse("User not found", 404);
|
return errorResponse("User not found", 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
return jsonResponse(userToLysand(user));
|
return jsonResponse(user.toLysand());
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
import { apiRoute, applyConfig, idValidator } from "@api";
|
import { apiRoute, applyConfig, idValidator } from "@api";
|
||||||
import { errorResponse, jsonResponse } from "@response";
|
import { errorResponse, jsonResponse } from "@response";
|
||||||
import { z } from "zod";
|
import { eq } from "drizzle-orm";
|
||||||
import { findFirstUser, getAvatarUrl } from "~database/entities/User";
|
|
||||||
import { lookup } from "mime-types";
|
import { lookup } from "mime-types";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { Users } from "~drizzle/schema";
|
||||||
|
import { User } from "~packages/database-interface/user";
|
||||||
|
|
||||||
export const meta = applyConfig({
|
export const meta = applyConfig({
|
||||||
allowedMethods: ["GET"],
|
allowedMethods: ["GET"],
|
||||||
|
|
@ -44,20 +46,18 @@ export default apiRoute<typeof meta, typeof schema>(
|
||||||
|
|
||||||
const isUuid = requestedUser.split("@")[0].match(idValidator);
|
const isUuid = requestedUser.split("@")[0].match(idValidator);
|
||||||
|
|
||||||
const user = await findFirstUser({
|
const user = await User.fromSql(
|
||||||
where: (user, { eq }) =>
|
eq(isUuid ? Users.id : Users.username, requestedUser.split("@")[0]),
|
||||||
eq(
|
);
|
||||||
isUuid ? user.id : user.username,
|
|
||||||
requestedUser.split("@")[0],
|
|
||||||
),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return errorResponse("User not found", 404);
|
return errorResponse("User not found", 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
return jsonResponse({
|
return jsonResponse({
|
||||||
subject: `acct:${isUuid ? user.id : user.username}@${host}`,
|
subject: `acct:${
|
||||||
|
isUuid ? user.id : user.getUser().username
|
||||||
|
}@${host}`,
|
||||||
|
|
||||||
links: [
|
links: [
|
||||||
{
|
{
|
||||||
|
|
@ -70,8 +70,8 @@ export default apiRoute<typeof meta, typeof schema>(
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
rel: "avatar",
|
rel: "avatar",
|
||||||
type: lookup(getAvatarUrl(user, config)),
|
type: lookup(user.getAvatarUrl(config)),
|
||||||
href: getAvatarUrl(user, config),
|
href: user.getAvatarUrl(config),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -72,7 +72,7 @@ describe("API Tests", () => {
|
||||||
|
|
||||||
const account = (await response.json()) as APIAccount;
|
const account = (await response.json()) as APIAccount;
|
||||||
|
|
||||||
expect(account.username).toBe(user.username);
|
expect(account.username).toBe(user.getUser().username);
|
||||||
expect(account.bot).toBe(false);
|
expect(account.bot).toBe(false);
|
||||||
expect(account.locked).toBe(false);
|
expect(account.locked).toBe(false);
|
||||||
expect(account.created_at).toBeDefined();
|
expect(account.created_at).toBeDefined();
|
||||||
|
|
@ -81,7 +81,10 @@ describe("API Tests", () => {
|
||||||
expect(account.statuses_count).toBe(0);
|
expect(account.statuses_count).toBe(0);
|
||||||
expect(account.note).toBe("");
|
expect(account.note).toBe("");
|
||||||
expect(account.url).toBe(
|
expect(account.url).toBe(
|
||||||
new URL(`/@${user.username}`, config.http.base_url).toString(),
|
new URL(
|
||||||
|
`/@${user.getUser().username}`,
|
||||||
|
config.http.base_url,
|
||||||
|
).toString(),
|
||||||
);
|
);
|
||||||
expect(account.avatar).toBeDefined();
|
expect(account.avatar).toBeDefined();
|
||||||
expect(account.avatar_static).toBeDefined();
|
expect(account.avatar_static).toBeDefined();
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,7 @@ describe("POST /api/auth/login/", () => {
|
||||||
test("should get a JWT", async () => {
|
test("should get a JWT", async () => {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
|
|
||||||
formData.append("email", users[0]?.email ?? "");
|
formData.append("email", users[0]?.getUser().email ?? "");
|
||||||
formData.append("password", passwords[0]);
|
formData.append("password", passwords[0]);
|
||||||
|
|
||||||
const response = await sendTestRequest(
|
const response = await sendTestRequest(
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,12 @@
|
||||||
import { randomBytes } from "node:crypto";
|
import { randomBytes } from "node:crypto";
|
||||||
import { asc, inArray, like } from "drizzle-orm";
|
import { asc, inArray, like } from "drizzle-orm";
|
||||||
import type { Status } from "~database/entities/Status";
|
import type { Status } from "~database/entities/Status";
|
||||||
import {
|
import { createNewLocalUser } from "~database/entities/User";
|
||||||
type User,
|
|
||||||
type UserWithRelations,
|
|
||||||
createNewLocalUser,
|
|
||||||
} from "~database/entities/User";
|
|
||||||
import { db } from "~drizzle/db";
|
import { db } from "~drizzle/db";
|
||||||
import { Notes, Tokens, Users } from "~drizzle/schema";
|
import { Notes, Tokens, Users } from "~drizzle/schema";
|
||||||
import { server } from "~index";
|
import { server } from "~index";
|
||||||
import { Note } from "~packages/database-interface/note";
|
import { Note } from "~packages/database-interface/note";
|
||||||
|
import type { User } from "~packages/database-interface/user";
|
||||||
/**
|
/**
|
||||||
* This allows us to send a test request to the server even when it isnt running
|
* This allows us to send a test request to the server even when it isnt running
|
||||||
* CURRENTLY NOT WORKING, NEEDS TO BE FIXED
|
* CURRENTLY NOT WORKING, NEEDS TO BE FIXED
|
||||||
|
|
@ -30,7 +27,7 @@ export const deleteOldTestUsers = async () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getTestUsers = async (count: number) => {
|
export const getTestUsers = async (count: number) => {
|
||||||
const users: UserWithRelations[] = [];
|
const users: User[] = [];
|
||||||
const passwords: string[] = [];
|
const passwords: string[] = [];
|
||||||
|
|
||||||
for (let i = 0; i < count; i++) {
|
for (let i = 0; i < count; i++) {
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,10 @@ import { count } from "drizzle-orm";
|
||||||
import { LogLevel, type LogManager, type MultiLogManager } from "log-manager";
|
import { LogLevel, type LogManager, type MultiLogManager } from "log-manager";
|
||||||
import { Meilisearch } from "meilisearch";
|
import { Meilisearch } from "meilisearch";
|
||||||
import type { Status } from "~database/entities/Status";
|
import type { Status } from "~database/entities/Status";
|
||||||
import type { User } from "~database/entities/User";
|
import type { UserType } from "~database/entities/User";
|
||||||
import { db } from "~drizzle/db";
|
import { db } from "~drizzle/db";
|
||||||
import { Notes, Users } from "~drizzle/schema";
|
import { Notes, Users } from "~drizzle/schema";
|
||||||
|
import type { User } from "~packages/database-interface/user";
|
||||||
|
|
||||||
export const meilisearch = new Meilisearch({
|
export const meilisearch = new Meilisearch({
|
||||||
host: `${config.meilisearch.host}:${config.meilisearch.port}`,
|
host: `${config.meilisearch.host}:${config.meilisearch.port}`,
|
||||||
|
|
@ -71,10 +72,10 @@ export const addUserToMeilisearch = async (user: User) => {
|
||||||
await meilisearch.index(MeiliIndexType.Accounts).addDocuments([
|
await meilisearch.index(MeiliIndexType.Accounts).addDocuments([
|
||||||
{
|
{
|
||||||
id: user.id,
|
id: user.id,
|
||||||
username: user.username,
|
username: user.getUser().username,
|
||||||
displayName: user.displayName,
|
displayName: user.getUser().displayName,
|
||||||
note: user.note,
|
note: user.getUser().note,
|
||||||
createdAt: user.createdAt,
|
createdAt: user.getUser().createdAt,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,10 @@ import type {
|
||||||
findManyNotifications,
|
findManyNotifications,
|
||||||
} from "~database/entities/Notification";
|
} from "~database/entities/Notification";
|
||||||
import type { Status, findManyNotes } from "~database/entities/Status";
|
import type { Status, findManyNotes } from "~database/entities/Status";
|
||||||
import type { User, findManyUsers } from "~database/entities/User";
|
import type { UserType, findManyUsers } from "~database/entities/User";
|
||||||
import type { db } from "~drizzle/db";
|
import type { db } from "~drizzle/db";
|
||||||
|
|
||||||
export async function fetchTimeline<T extends User | Status | Notification>(
|
export async function fetchTimeline<T extends UserType | Status | Notification>(
|
||||||
model:
|
model:
|
||||||
| typeof findManyNotes
|
| typeof findManyNotes
|
||||||
| typeof findManyUsers
|
| typeof findManyUsers
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue