mirror of
https://github.com/versia-pub/server.git
synced 2025-12-06 08:28:19 +01:00
refactor(database): 🎨 Refactor note handling into its own class instead of separate functions
This commit is contained in:
parent
2998cb4deb
commit
9081036c6d
24
biome.json
24
biome.json
|
|
@ -2,37 +2,19 @@
|
|||
"$schema": "https://biomejs.dev/schemas/1.6.4/schema.json",
|
||||
"organizeImports": {
|
||||
"enabled": true,
|
||||
"ignore": [
|
||||
"node_modules/**/*",
|
||||
"dist/**/*",
|
||||
"packages/frontend/.output",
|
||||
"packages/frontend/.nuxt",
|
||||
"glitch"
|
||||
]
|
||||
"ignore": ["node_modules", "dist", "glitch", "glitch-dev"]
|
||||
},
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"rules": {
|
||||
"recommended": true
|
||||
},
|
||||
"ignore": [
|
||||
"node_modules/**/*",
|
||||
"dist/**/*",
|
||||
"packages/frontend/.output",
|
||||
"packages/frontend/.nuxt",
|
||||
"glitch"
|
||||
]
|
||||
"ignore": ["node_modules", "dist", "glitch", "glitch-dev"]
|
||||
},
|
||||
"formatter": {
|
||||
"enabled": true,
|
||||
"indentStyle": "space",
|
||||
"indentWidth": 4,
|
||||
"ignore": [
|
||||
"node_modules/**/*",
|
||||
"dist/**/*",
|
||||
"packages/frontend/.output",
|
||||
"packages/frontend/.nuxt",
|
||||
"glitch"
|
||||
]
|
||||
"ignore": ["node_modules", "dist", "glitch", "glitch-dev"]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
36
cli.ts
36
cli.ts
|
|
@ -8,28 +8,20 @@ import { CliBuilder, CliCommand } from "cli-parser";
|
|||
import { CliParameterType } from "cli-parser/cli-builder.type";
|
||||
import Table from "cli-table";
|
||||
import { config } from "config-manager";
|
||||
import {
|
||||
type SQL,
|
||||
eq,
|
||||
inArray,
|
||||
isNotNull,
|
||||
isNull,
|
||||
like,
|
||||
sql,
|
||||
} from "drizzle-orm";
|
||||
import { type SQL, and, eq, inArray, like, or, sql } from "drizzle-orm";
|
||||
import extract from "extract-zip";
|
||||
import { MediaBackend } from "media-manager";
|
||||
import { lookup } from "mime-types";
|
||||
import { getUrl } from "~database/entities/Attachment";
|
||||
import { findFirstStatuses, findManyStatuses } from "~database/entities/Status";
|
||||
import {
|
||||
type User,
|
||||
createNewLocalUser,
|
||||
findFirstUser,
|
||||
findManyUsers,
|
||||
} from "~database/entities/User";
|
||||
import { db, client } from "~drizzle/db";
|
||||
import { client, db } from "~drizzle/db";
|
||||
import { emoji, openIdAccount, status, user } from "~drizzle/schema";
|
||||
import { Note } from "~packages/database-interface/note";
|
||||
|
||||
await client.connect();
|
||||
const args = process.argv;
|
||||
|
|
@ -803,9 +795,7 @@ const cliBuilder = new CliBuilder([
|
|||
return 1;
|
||||
}
|
||||
|
||||
const note = await findFirstStatuses({
|
||||
where: (status, { eq }) => eq(status.id, id),
|
||||
});
|
||||
const note = await Note.fromId(id);
|
||||
|
||||
if (!note) {
|
||||
console.log(`${chalk.red("✗")} Note not found`);
|
||||
|
|
@ -815,7 +805,7 @@ const cliBuilder = new CliBuilder([
|
|||
if (!args.noconfirm) {
|
||||
process.stdout.write(
|
||||
`Are you sure you want to delete note ${chalk.blue(
|
||||
note.id,
|
||||
note.getStatus().id,
|
||||
)}?\n${chalk.red(
|
||||
chalk.bold(
|
||||
"This is a destructive action and cannot be undone!",
|
||||
|
|
@ -832,10 +822,12 @@ const cliBuilder = new CliBuilder([
|
|||
}
|
||||
}
|
||||
|
||||
await db.delete(status).where(eq(status.id, note.id));
|
||||
await note.delete();
|
||||
|
||||
console.log(
|
||||
`${chalk.green("✓")} Deleted note ${chalk.blue(note.id)}`,
|
||||
`${chalk.green("✓")} Deleted note ${chalk.blue(
|
||||
note.getStatus().id,
|
||||
)}`,
|
||||
);
|
||||
|
||||
return 0;
|
||||
|
|
@ -968,8 +960,8 @@ const cliBuilder = new CliBuilder([
|
|||
instanceQuery = sql`EXISTS (SELECT 1 FROM "User" WHERE "User"."id" = ${status.authorId} AND "User"."instanceId" IS NOT NULL)`;
|
||||
}
|
||||
|
||||
const notes = await findManyStatuses({
|
||||
where: (status, { or, and }) =>
|
||||
const notes = (
|
||||
await Note.manyFromSql(
|
||||
and(
|
||||
or(
|
||||
...fields.map((field) =>
|
||||
|
|
@ -979,8 +971,10 @@ const cliBuilder = new CliBuilder([
|
|||
),
|
||||
instanceQuery,
|
||||
),
|
||||
limit: Number(limit),
|
||||
});
|
||||
undefined,
|
||||
Number(limit),
|
||||
)
|
||||
).map((n) => n.getStatus());
|
||||
|
||||
if (redact) {
|
||||
for (const note of notes) {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ import { config } from "config-manager";
|
|||
import type * as Lysand from "lysand-types";
|
||||
import { type User, getUserUri } from "./User";
|
||||
|
||||
export const localObjectURI = (id: string) => `/objects/${id}`;
|
||||
|
||||
export const objectToInboxRequest = async (
|
||||
object: Lysand.Entity,
|
||||
author: User,
|
||||
|
|
|
|||
|
|
@ -1,12 +1,9 @@
|
|||
import type { InferSelectModel } from "drizzle-orm";
|
||||
import { db } from "~drizzle/db";
|
||||
import type { notification } from "~drizzle/schema";
|
||||
import { Note } from "~packages/database-interface/note";
|
||||
import type { Notification as APINotification } from "~types/mastodon/notification";
|
||||
import {
|
||||
type StatusWithRelations,
|
||||
findFirstStatuses,
|
||||
statusToAPI,
|
||||
} from "./Status";
|
||||
import type { StatusWithRelations } from "./Status";
|
||||
import {
|
||||
type UserWithRelations,
|
||||
transformOutputToUserWithRelations,
|
||||
|
|
@ -45,12 +42,7 @@ export const findManyNotifications = async (
|
|||
output.map(async (notif) => ({
|
||||
...notif,
|
||||
account: transformOutputToUserWithRelations(notif.account),
|
||||
status: notif.statusId
|
||||
? await findFirstStatuses({
|
||||
where: (status, { eq }) =>
|
||||
eq(status.id, notif.statusId ?? ""),
|
||||
})
|
||||
: null,
|
||||
status: (await Note.fromId(notif.statusId))?.getStatus() ?? null,
|
||||
})),
|
||||
);
|
||||
};
|
||||
|
|
@ -64,7 +56,9 @@ export const notificationToAPI = async (
|
|||
id: notification.id,
|
||||
type: notification.type,
|
||||
status: notification.status
|
||||
? await statusToAPI(notification.status, notification.account)
|
||||
? await Note.fromStatus(notification.status).toAPI(
|
||||
notification.account,
|
||||
)
|
||||
: undefined,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { config } from "config-manager";
|
||||
// import { Worker } from "bullmq";
|
||||
import { type StatusWithRelations, statusToLysand } from "./Status";
|
||||
|
||||
/* export const federationWorker = new Worker(
|
||||
"federation",
|
||||
|
|
|
|||
|
|
@ -27,38 +27,31 @@ import {
|
|||
import { parse } from "marked";
|
||||
import { db } from "~drizzle/db";
|
||||
import {
|
||||
type application,
|
||||
attachment,
|
||||
emojiToStatus,
|
||||
instance,
|
||||
type like,
|
||||
notification,
|
||||
status,
|
||||
statusToMentions,
|
||||
user,
|
||||
} from "~drizzle/schema";
|
||||
import { Note } from "~packages/database-interface/note";
|
||||
import { LogLevel } from "~packages/log-manager";
|
||||
import type { Note } from "~types/lysand/Object";
|
||||
import type { Attachment as APIAttachment } from "~types/mastodon/attachment";
|
||||
import type { Status as APIStatus } from "~types/mastodon/status";
|
||||
import { type Application, applicationToAPI } from "./Application";
|
||||
import {
|
||||
attachmentFromLysand,
|
||||
attachmentToAPI,
|
||||
attachmentToLysand,
|
||||
} from "./Attachment";
|
||||
import type { Application } from "./Application";
|
||||
import { attachmentFromLysand, attachmentToLysand } from "./Attachment";
|
||||
import {
|
||||
type EmojiWithInstance,
|
||||
emojiToAPI,
|
||||
emojiToLysand,
|
||||
fetchEmoji,
|
||||
parseEmojis,
|
||||
} from "./Emoji";
|
||||
import { objectToInboxRequest } from "./Federation";
|
||||
import type { Like } from "./Like";
|
||||
import {
|
||||
type User,
|
||||
type UserWithInstance,
|
||||
type UserWithRelations,
|
||||
type UserWithRelationsAndRelationships,
|
||||
findManyUsers,
|
||||
getUserUri,
|
||||
resolveUser,
|
||||
|
|
@ -66,21 +59,20 @@ import {
|
|||
transformOutputToUserWithRelations,
|
||||
userExtrasTemplate,
|
||||
userRelations,
|
||||
userToAPI,
|
||||
} from "./User";
|
||||
|
||||
export type Status = InferSelectModel<typeof status>;
|
||||
|
||||
export type StatusWithRelations = Status & {
|
||||
author: UserWithRelations;
|
||||
mentions: UserWithRelations[];
|
||||
mentions: UserWithInstance[];
|
||||
attachments: InferSelectModel<typeof attachment>[];
|
||||
reblog: StatusWithoutRecursiveRelations | null;
|
||||
emojis: EmojiWithInstance[];
|
||||
likes: InferSelectModel<typeof like>[];
|
||||
inReplyTo: StatusWithoutRecursiveRelations | null;
|
||||
quoting: StatusWithoutRecursiveRelations | null;
|
||||
application: InferSelectModel<typeof application> | null;
|
||||
likes: Like[];
|
||||
inReplyTo: Status | null;
|
||||
quoting: Status | null;
|
||||
application: Application | null;
|
||||
reblogCount: number;
|
||||
likeCount: number;
|
||||
replyCount: number;
|
||||
|
|
@ -88,15 +80,10 @@ export type StatusWithRelations = Status & {
|
|||
|
||||
export type StatusWithoutRecursiveRelations = Omit<
|
||||
StatusWithRelations,
|
||||
| "inReplyTo"
|
||||
| "quoting"
|
||||
| "reblog"
|
||||
| "reblogCount"
|
||||
| "likeCount"
|
||||
| "replyCount"
|
||||
"inReplyTo" | "quoting" | "reblog"
|
||||
>;
|
||||
|
||||
export const statusExtras = {
|
||||
export const noteExtras = {
|
||||
reblogCount:
|
||||
sql`(SELECT COUNT(*) FROM "Status" "status" WHERE "status"."reblogId" = "status".id)`.as(
|
||||
"reblog_count",
|
||||
|
|
@ -111,49 +98,12 @@ export const statusExtras = {
|
|||
),
|
||||
};
|
||||
|
||||
export const statusExtrasTemplate = (name: string) => ({
|
||||
// @ts-ignore
|
||||
reblogCount: sql([
|
||||
`(SELECT COUNT(*) FROM "Status" "status" WHERE "status"."reblogId" = ${name}.id)`,
|
||||
]).as("reblog_count"),
|
||||
// @ts-ignore
|
||||
likeCount: sql([
|
||||
`(SELECT COUNT(*) FROM "Like" "like" WHERE "like"."likedId" = ${name}.id)`,
|
||||
]).as("like_count"),
|
||||
// @ts-ignore
|
||||
replyCount: sql([
|
||||
`(SELECT COUNT(*) FROM "Status" "status" WHERE "status"."inReplyToPostId" = ${name}.id)`,
|
||||
]).as("reply_count"),
|
||||
});
|
||||
|
||||
/**
|
||||
* Returns whether this status is viewable by a user.
|
||||
* @param user The user to check.
|
||||
* @returns Whether this status is viewable by the user.
|
||||
* Wrapper against the Status object to make it easier to work with
|
||||
* @param query
|
||||
* @returns
|
||||
*/
|
||||
export const isViewableByUser = async (
|
||||
status: StatusWithRelations,
|
||||
user: UserWithRelations | null,
|
||||
) => {
|
||||
if (status.authorId === user?.id) return true;
|
||||
if (status.visibility === "public") return true;
|
||||
if (status.visibility === "unlisted") return true;
|
||||
if (status.visibility === "private") {
|
||||
return user
|
||||
? await db.query.relationship.findFirst({
|
||||
where: (relationship, { and, eq }) =>
|
||||
and(
|
||||
eq(relationship.ownerId, user?.id),
|
||||
eq(relationship.subjectId, status.authorId),
|
||||
eq(relationship.following, true),
|
||||
),
|
||||
})
|
||||
: false;
|
||||
}
|
||||
return user && status.mentions.includes(user);
|
||||
};
|
||||
|
||||
export const findManyStatuses = async (
|
||||
export const findManyNotes = async (
|
||||
query: Parameters<typeof db.query.status.findMany>[0],
|
||||
): Promise<StatusWithRelations[]> => {
|
||||
const output = await db.query.status.findMany({
|
||||
|
|
@ -182,8 +132,9 @@ export const findManyStatuses = async (
|
|||
mentions: {
|
||||
with: {
|
||||
user: {
|
||||
with: userRelations,
|
||||
extras: userExtrasTemplate("status_mentions_user"),
|
||||
with: {
|
||||
instance: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -218,74 +169,15 @@ export const findManyStatuses = async (
|
|||
extras: userExtrasTemplate("status_reblog_author"),
|
||||
},
|
||||
},
|
||||
},
|
||||
inReplyTo: {
|
||||
with: {
|
||||
attachments: true,
|
||||
emojis: {
|
||||
with: {
|
||||
emoji: {
|
||||
with: {
|
||||
instance: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
likes: true,
|
||||
application: true,
|
||||
mentions: {
|
||||
with: {
|
||||
user: {
|
||||
with: userRelations,
|
||||
extras: userExtrasTemplate(
|
||||
"status_inReplyTo_mentions_user",
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
author: {
|
||||
with: {
|
||||
...userRelations,
|
||||
},
|
||||
extras: userExtrasTemplate("status_inReplyTo_author"),
|
||||
},
|
||||
},
|
||||
},
|
||||
quoting: {
|
||||
with: {
|
||||
attachments: true,
|
||||
emojis: {
|
||||
with: {
|
||||
emoji: {
|
||||
with: {
|
||||
instance: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
likes: true,
|
||||
application: true,
|
||||
mentions: {
|
||||
with: {
|
||||
user: {
|
||||
with: userRelations,
|
||||
extras: userExtrasTemplate(
|
||||
"status_quoting_mentions_user",
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
author: {
|
||||
with: {
|
||||
...userRelations,
|
||||
},
|
||||
extras: userExtrasTemplate("status_quoting_author"),
|
||||
},
|
||||
extras: {
|
||||
...noteExtras,
|
||||
},
|
||||
},
|
||||
inReplyTo: true,
|
||||
quoting: true,
|
||||
},
|
||||
extras: {
|
||||
...statusExtras,
|
||||
...noteExtras,
|
||||
...query?.extras,
|
||||
},
|
||||
});
|
||||
|
|
@ -293,65 +185,38 @@ export const findManyStatuses = async (
|
|||
return output.map((post) => ({
|
||||
...post,
|
||||
author: transformOutputToUserWithRelations(post.author),
|
||||
mentions: post.mentions.map(
|
||||
(mention) =>
|
||||
mention.user &&
|
||||
transformOutputToUserWithRelations(mention.user),
|
||||
),
|
||||
reblog: post.reblog && {
|
||||
...post.reblog,
|
||||
author: transformOutputToUserWithRelations(post.reblog.author),
|
||||
mentions: post.reblog.mentions.map(
|
||||
(mention) =>
|
||||
mention.user &&
|
||||
transformOutputToUserWithRelations(mention.user),
|
||||
),
|
||||
emojis: post.reblog.emojis.map(
|
||||
(emoji) =>
|
||||
(emoji as unknown as Record<string, object>)
|
||||
.emoji as EmojiWithInstance,
|
||||
),
|
||||
},
|
||||
inReplyTo: post.inReplyTo && {
|
||||
...post.inReplyTo,
|
||||
author: transformOutputToUserWithRelations(post.inReplyTo.author),
|
||||
mentions: post.inReplyTo.mentions.map(
|
||||
(mention) =>
|
||||
mention.user &&
|
||||
transformOutputToUserWithRelations(mention.user),
|
||||
),
|
||||
emojis: post.inReplyTo.emojis.map(
|
||||
(emoji) =>
|
||||
(emoji as unknown as Record<string, object>)
|
||||
.emoji as EmojiWithInstance,
|
||||
),
|
||||
},
|
||||
quoting: post.quoting && {
|
||||
...post.quoting,
|
||||
author: transformOutputToUserWithRelations(post.quoting.author),
|
||||
mentions: post.quoting.mentions.map(
|
||||
(mention) =>
|
||||
mention.user &&
|
||||
transformOutputToUserWithRelations(mention.user),
|
||||
),
|
||||
emojis: post.quoting.emojis.map(
|
||||
(emoji) =>
|
||||
(emoji as unknown as Record<string, object>)
|
||||
.emoji as EmojiWithInstance,
|
||||
),
|
||||
},
|
||||
mentions: post.mentions.map((mention) => ({
|
||||
...mention.user,
|
||||
endpoints: mention.user.endpoints as User["endpoints"],
|
||||
})),
|
||||
emojis: (post.emojis ?? []).map(
|
||||
(emoji) =>
|
||||
(emoji as unknown as Record<string, object>)
|
||||
.emoji as EmojiWithInstance,
|
||||
),
|
||||
reblog: post.reblog && {
|
||||
...post.reblog,
|
||||
author: transformOutputToUserWithRelations(post.reblog.author),
|
||||
mentions: post.reblog.mentions.map((mention) => ({
|
||||
...mention.user,
|
||||
endpoints: mention.user.endpoints as User["endpoints"],
|
||||
})),
|
||||
emojis: (post.reblog.emojis ?? []).map(
|
||||
(emoji) =>
|
||||
(emoji as unknown as Record<string, object>)
|
||||
.emoji as EmojiWithInstance,
|
||||
),
|
||||
reblogCount: Number(post.reblog.reblogCount),
|
||||
likeCount: Number(post.reblog.likeCount),
|
||||
replyCount: Number(post.reblog.replyCount),
|
||||
},
|
||||
reblogCount: Number(post.reblogCount),
|
||||
likeCount: Number(post.likeCount),
|
||||
replyCount: Number(post.replyCount),
|
||||
}));
|
||||
};
|
||||
|
||||
export const findFirstStatuses = async (
|
||||
export const findFirstNote = async (
|
||||
query: Parameters<typeof db.query.status.findFirst>[0],
|
||||
): Promise<StatusWithRelations | null> => {
|
||||
const output = await db.query.status.findFirst({
|
||||
|
|
@ -380,8 +245,9 @@ export const findFirstStatuses = async (
|
|||
mentions: {
|
||||
with: {
|
||||
user: {
|
||||
with: userRelations,
|
||||
extras: userExtrasTemplate("status_mentions_user"),
|
||||
with: {
|
||||
instance: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -416,74 +282,15 @@ export const findFirstStatuses = async (
|
|||
extras: userExtrasTemplate("status_reblog_author"),
|
||||
},
|
||||
},
|
||||
},
|
||||
inReplyTo: {
|
||||
with: {
|
||||
attachments: true,
|
||||
emojis: {
|
||||
with: {
|
||||
emoji: {
|
||||
with: {
|
||||
instance: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
likes: true,
|
||||
application: true,
|
||||
mentions: {
|
||||
with: {
|
||||
user: {
|
||||
with: userRelations,
|
||||
extras: userExtrasTemplate(
|
||||
"status_inReplyTo_mentions_user",
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
author: {
|
||||
with: {
|
||||
...userRelations,
|
||||
},
|
||||
extras: userExtrasTemplate("status_inReplyTo_author"),
|
||||
},
|
||||
},
|
||||
},
|
||||
quoting: {
|
||||
with: {
|
||||
attachments: true,
|
||||
emojis: {
|
||||
with: {
|
||||
emoji: {
|
||||
with: {
|
||||
instance: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
likes: true,
|
||||
application: true,
|
||||
mentions: {
|
||||
with: {
|
||||
user: {
|
||||
with: userRelations,
|
||||
extras: userExtrasTemplate(
|
||||
"status_quoting_mentions_user",
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
author: {
|
||||
with: {
|
||||
...userRelations,
|
||||
},
|
||||
extras: userExtrasTemplate("status_quoting_author"),
|
||||
},
|
||||
extras: {
|
||||
...noteExtras,
|
||||
},
|
||||
},
|
||||
inReplyTo: true,
|
||||
quoting: true,
|
||||
},
|
||||
extras: {
|
||||
...statusExtras,
|
||||
...noteExtras,
|
||||
...query?.extras,
|
||||
},
|
||||
});
|
||||
|
|
@ -493,74 +300,48 @@ export const findFirstStatuses = async (
|
|||
return {
|
||||
...output,
|
||||
author: transformOutputToUserWithRelations(output.author),
|
||||
mentions: output.mentions.map((mention) =>
|
||||
transformOutputToUserWithRelations(mention.user),
|
||||
),
|
||||
reblog: output.reblog && {
|
||||
...output.reblog,
|
||||
author: transformOutputToUserWithRelations(output.reblog.author),
|
||||
mentions: output.reblog.mentions.map(
|
||||
(mention) =>
|
||||
mention.user &&
|
||||
transformOutputToUserWithRelations(mention.user),
|
||||
),
|
||||
emojis: output.reblog.emojis.map(
|
||||
(emoji) =>
|
||||
(emoji as unknown as Record<string, object>)
|
||||
.emoji as EmojiWithInstance,
|
||||
),
|
||||
},
|
||||
inReplyTo: output.inReplyTo && {
|
||||
...output.inReplyTo,
|
||||
author: transformOutputToUserWithRelations(output.inReplyTo.author),
|
||||
mentions: output.inReplyTo.mentions.map(
|
||||
(mention) =>
|
||||
mention.user &&
|
||||
transformOutputToUserWithRelations(mention.user),
|
||||
),
|
||||
emojis: output.inReplyTo.emojis.map(
|
||||
(emoji) =>
|
||||
(emoji as unknown as Record<string, object>)
|
||||
.emoji as EmojiWithInstance,
|
||||
),
|
||||
},
|
||||
quoting: output.quoting && {
|
||||
...output.quoting,
|
||||
author: transformOutputToUserWithRelations(output.quoting.author),
|
||||
mentions: output.quoting.mentions.map(
|
||||
(mention) =>
|
||||
mention.user &&
|
||||
transformOutputToUserWithRelations(mention.user),
|
||||
),
|
||||
emojis: output.quoting.emojis.map(
|
||||
(emoji) =>
|
||||
(emoji as unknown as Record<string, object>)
|
||||
.emoji as EmojiWithInstance,
|
||||
),
|
||||
},
|
||||
mentions: output.mentions.map((mention) => ({
|
||||
...mention.user,
|
||||
endpoints: mention.user.endpoints as User["endpoints"],
|
||||
})),
|
||||
emojis: (output.emojis ?? []).map(
|
||||
(emoji) =>
|
||||
(emoji as unknown as Record<string, object>)
|
||||
.emoji as EmojiWithInstance,
|
||||
),
|
||||
reblog: output.reblog && {
|
||||
...output.reblog,
|
||||
author: transformOutputToUserWithRelations(output.reblog.author),
|
||||
mentions: output.reblog.mentions.map((mention) => ({
|
||||
...mention.user,
|
||||
endpoints: mention.user.endpoints as User["endpoints"],
|
||||
})),
|
||||
emojis: (output.reblog.emojis ?? []).map(
|
||||
(emoji) =>
|
||||
(emoji as unknown as Record<string, object>)
|
||||
.emoji as EmojiWithInstance,
|
||||
),
|
||||
reblogCount: Number(output.reblog.reblogCount),
|
||||
likeCount: Number(output.reblog.likeCount),
|
||||
replyCount: Number(output.reblog.replyCount),
|
||||
},
|
||||
reblogCount: Number(output.reblogCount),
|
||||
likeCount: Number(output.likeCount),
|
||||
replyCount: Number(output.replyCount),
|
||||
};
|
||||
};
|
||||
|
||||
export const resolveStatus = async (
|
||||
export const resolveNote = async (
|
||||
uri?: string,
|
||||
providedNote?: Lysand.Note,
|
||||
): Promise<StatusWithRelations> => {
|
||||
): Promise<Note> => {
|
||||
if (!uri && !providedNote) {
|
||||
throw new Error("No URI or note provided");
|
||||
}
|
||||
|
||||
const foundStatus = await findFirstStatuses({
|
||||
where: (status, { eq }) =>
|
||||
const foundStatus = await Note.fromSql(
|
||||
eq(status.uri, uri ?? providedNote?.uri ?? ""),
|
||||
});
|
||||
);
|
||||
|
||||
if (foundStatus) return foundStatus;
|
||||
|
||||
|
|
@ -632,7 +413,7 @@ export const resolveStatus = async (
|
|||
}
|
||||
}
|
||||
|
||||
const createdStatus = await createNewStatus(
|
||||
const createdNote = await Note.fromData(
|
||||
author,
|
||||
note.content ?? {
|
||||
"text/plain": {
|
||||
|
|
@ -652,86 +433,19 @@ export const resolveStatus = async (
|
|||
) as Promise<UserWithRelations>[],
|
||||
),
|
||||
attachments.map((a) => a.id),
|
||||
note.replies_to ? await resolveStatus(note.replies_to) : undefined,
|
||||
note.quotes ? await resolveStatus(note.quotes) : undefined,
|
||||
note.replies_to
|
||||
? (await resolveNote(note.replies_to)).getStatus().id
|
||||
: undefined,
|
||||
note.quotes
|
||||
? (await resolveNote(note.quotes)).getStatus().id
|
||||
: undefined,
|
||||
);
|
||||
|
||||
if (!createdStatus) {
|
||||
if (!createdNote) {
|
||||
throw new Error("Failed to create status");
|
||||
}
|
||||
|
||||
return createdStatus;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return all the ancestors of this post,
|
||||
*/
|
||||
export const getAncestors = async (
|
||||
status: StatusWithRelations,
|
||||
fetcher: UserWithRelationsAndRelationships | null,
|
||||
) => {
|
||||
const ancestors: StatusWithRelations[] = [];
|
||||
|
||||
let currentStatus = status;
|
||||
|
||||
while (currentStatus.inReplyToPostId) {
|
||||
const parent = await findFirstStatuses({
|
||||
where: (status, { eq }) =>
|
||||
eq(status.id, currentStatus.inReplyToPostId ?? ""),
|
||||
});
|
||||
|
||||
if (!parent) break;
|
||||
|
||||
ancestors.push(parent);
|
||||
|
||||
currentStatus = parent;
|
||||
}
|
||||
|
||||
// Filter for posts that are viewable by the user
|
||||
const viewableAncestors = ancestors.filter((ancestor) =>
|
||||
isViewableByUser(ancestor, fetcher),
|
||||
);
|
||||
return viewableAncestors;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return all the descendants of this post (recursive)
|
||||
* Temporary implementation, will be replaced with a recursive SQL query when Prisma adds support for it
|
||||
*/
|
||||
export const getDescendants = async (
|
||||
status: StatusWithRelations,
|
||||
fetcher: UserWithRelationsAndRelationships | null,
|
||||
depth = 0,
|
||||
) => {
|
||||
const descendants: StatusWithRelations[] = [];
|
||||
|
||||
const currentStatus = status;
|
||||
|
||||
// Fetch all children of children of children recursively calling getDescendants
|
||||
|
||||
const children = await findManyStatuses({
|
||||
where: (status, { eq }) => eq(status.inReplyToPostId, currentStatus.id),
|
||||
});
|
||||
|
||||
for (const child of children) {
|
||||
descendants.push(child);
|
||||
|
||||
if (depth < 20) {
|
||||
const childDescendants = await getDescendants(
|
||||
child,
|
||||
fetcher,
|
||||
depth + 1,
|
||||
);
|
||||
descendants.push(...childDescendants);
|
||||
}
|
||||
}
|
||||
|
||||
// Filter for posts that are viewable by the user
|
||||
|
||||
const viewableDescendants = descendants.filter((descendant) =>
|
||||
isViewableByUser(descendant, fetcher),
|
||||
);
|
||||
return viewableDescendants;
|
||||
return createdNote;
|
||||
};
|
||||
|
||||
export const createMentionRegExp = () =>
|
||||
|
|
@ -907,122 +621,12 @@ export const contentToHtml = async (
|
|||
return htmlContent;
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a new status and saves it to the database.
|
||||
* @returns A promise that resolves with the new status.
|
||||
*/
|
||||
export const createNewStatus = async (
|
||||
author: User,
|
||||
content: Lysand.ContentFormat,
|
||||
visibility: APIStatus["visibility"],
|
||||
is_sensitive: boolean,
|
||||
spoiler_text: string,
|
||||
emojis: EmojiWithInstance[],
|
||||
uri?: string,
|
||||
mentions?: UserWithRelations[],
|
||||
/** List of IDs of database Attachment objects */
|
||||
media_attachments?: string[],
|
||||
inReplyTo?: StatusWithRelations,
|
||||
quoting?: StatusWithRelations,
|
||||
application?: Application,
|
||||
): Promise<StatusWithRelations | null> => {
|
||||
const htmlContent = await contentToHtml(content, mentions);
|
||||
|
||||
// Parse emojis and fuse with existing emojis
|
||||
let foundEmojis = emojis;
|
||||
|
||||
if (author.instanceId === null) {
|
||||
const parsedEmojis = await parseEmojis(htmlContent);
|
||||
// Fuse and deduplicate
|
||||
foundEmojis = [...emojis, ...parsedEmojis].filter(
|
||||
(emoji, index, self) =>
|
||||
index === self.findIndex((t) => t.id === emoji.id),
|
||||
);
|
||||
}
|
||||
|
||||
const newStatus = (
|
||||
await db
|
||||
.insert(status)
|
||||
.values({
|
||||
authorId: author.id,
|
||||
content: htmlContent,
|
||||
contentSource:
|
||||
content["text/plain"]?.content ||
|
||||
content["text/markdown"]?.content ||
|
||||
Object.entries(content)[0][1].content ||
|
||||
"",
|
||||
contentType: "text/html",
|
||||
visibility,
|
||||
sensitive: is_sensitive,
|
||||
spoilerText: spoiler_text,
|
||||
uri: uri || null,
|
||||
inReplyToPostId: inReplyTo?.id,
|
||||
quotingPostId: quoting?.id,
|
||||
applicationId: application?.id ?? null,
|
||||
updatedAt: new Date().toISOString(),
|
||||
})
|
||||
.returning()
|
||||
)[0];
|
||||
|
||||
// Connect emojis
|
||||
for (const emoji of foundEmojis) {
|
||||
await db
|
||||
.insert(emojiToStatus)
|
||||
.values({
|
||||
emojiId: emoji.id,
|
||||
statusId: newStatus.id,
|
||||
})
|
||||
.execute();
|
||||
}
|
||||
|
||||
// Connect mentions
|
||||
for (const mention of mentions ?? []) {
|
||||
await db
|
||||
.insert(statusToMentions)
|
||||
.values({
|
||||
statusId: newStatus.id,
|
||||
userId: mention.id,
|
||||
})
|
||||
.execute();
|
||||
}
|
||||
|
||||
// Set attachment parents
|
||||
if (media_attachments && media_attachments.length > 0) {
|
||||
await db
|
||||
.update(attachment)
|
||||
.set({
|
||||
statusId: newStatus.id,
|
||||
})
|
||||
.where(inArray(attachment.id, media_attachments));
|
||||
}
|
||||
|
||||
// Send notifications for mentioned local users
|
||||
for (const mention of mentions ?? []) {
|
||||
if (mention.instanceId === null) {
|
||||
await db.insert(notification).values({
|
||||
accountId: author.id,
|
||||
notifiedId: mention.id,
|
||||
type: "mention",
|
||||
statusId: newStatus.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
(await findFirstStatuses({
|
||||
where: (status, { eq }) => eq(status.id, newStatus.id),
|
||||
})) || null
|
||||
);
|
||||
};
|
||||
|
||||
export const federateStatus = async (status: StatusWithRelations) => {
|
||||
const toFederateTo = await getUsersToFederateTo(status);
|
||||
|
||||
for (const user of toFederateTo) {
|
||||
export const federateNote = async (note: Note) => {
|
||||
for (const user of await note.getUsersToFederateTo()) {
|
||||
// TODO: Add queue system
|
||||
const request = await objectToInboxRequest(
|
||||
statusToLysand(status),
|
||||
status.author,
|
||||
note.toLysand(),
|
||||
note.getAuthor(),
|
||||
user,
|
||||
);
|
||||
|
||||
|
|
@ -1038,57 +642,14 @@ export const federateStatus = async (status: StatusWithRelations) => {
|
|||
dualLogger.log(
|
||||
LogLevel.ERROR,
|
||||
"Federation.Status",
|
||||
`Failed to federate status ${status.id} to ${user.uri}`,
|
||||
`Failed to federate status ${note.getStatus().id} to ${
|
||||
user.uri
|
||||
}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const getUsersToFederateTo = async (
|
||||
status: StatusWithRelations,
|
||||
): Promise<UserWithRelations[]> => {
|
||||
// Mentioned users
|
||||
const mentionedUsers =
|
||||
status.mentions.length > 0
|
||||
? await findManyUsers({
|
||||
where: (user, { or, and, isNotNull, eq, inArray }) =>
|
||||
and(
|
||||
isNotNull(user.instanceId),
|
||||
inArray(
|
||||
user.id,
|
||||
status.mentions.map((mention) => mention.id),
|
||||
),
|
||||
),
|
||||
with: {
|
||||
...userRelations,
|
||||
},
|
||||
})
|
||||
: [];
|
||||
|
||||
const usersThatCanSeePost = await findManyUsers({
|
||||
where: (user, { isNotNull }) => isNotNull(user.instanceId),
|
||||
with: {
|
||||
...userRelations,
|
||||
relationships: {
|
||||
where: (relationship, { eq, and }) =>
|
||||
and(
|
||||
eq(relationship.subjectId, user.id),
|
||||
eq(relationship.following, true),
|
||||
),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const fusedUsers = [...mentionedUsers, ...usersThatCanSeePost];
|
||||
|
||||
const deduplicatedUsersById = fusedUsers.filter(
|
||||
(user, index, self) =>
|
||||
index === self.findIndex((t) => t.id === user.id),
|
||||
);
|
||||
|
||||
return deduplicatedUsersById;
|
||||
};
|
||||
|
||||
export const editStatus = async (
|
||||
statusToEdit: StatusWithRelations,
|
||||
data: {
|
||||
|
|
@ -1102,7 +663,7 @@ export const editStatus = async (
|
|||
mentions?: User[];
|
||||
media_attachments?: string[];
|
||||
},
|
||||
): Promise<StatusWithRelations | null> => {
|
||||
): Promise<Note | null> => {
|
||||
const mentions = await parseTextMentions(data.content);
|
||||
|
||||
// Parse emojis
|
||||
|
|
@ -1122,20 +683,20 @@ export const editStatus = async (
|
|||
},
|
||||
});
|
||||
|
||||
const updated = (
|
||||
await db
|
||||
.update(status)
|
||||
.set({
|
||||
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,
|
||||
})
|
||||
.where(eq(status.id, statusToEdit.id))
|
||||
.returning()
|
||||
)[0];
|
||||
});
|
||||
|
||||
// Connect emojis
|
||||
for (const emoji of data.emojis) {
|
||||
|
|
@ -1179,11 +740,7 @@ export const editStatus = async (
|
|||
})
|
||||
.where(inArray(attachment.id, data.media_attachments ?? []));
|
||||
|
||||
return (
|
||||
(await findFirstStatuses({
|
||||
where: (status, { eq }) => eq(status.id, updated.id),
|
||||
})) || null
|
||||
);
|
||||
return await Note.fromId(updated.id);
|
||||
};
|
||||
|
||||
export const isFavouritedBy = async (status: Status, user: User) => {
|
||||
|
|
@ -1193,128 +750,6 @@ export const isFavouritedBy = async (status: Status, user: User) => {
|
|||
}));
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts this status to an API status.
|
||||
* @returns A promise that resolves with the API status.
|
||||
*/
|
||||
export const statusToAPI = async (
|
||||
statusToConvert: StatusWithRelations,
|
||||
userFetching?: UserWithRelations,
|
||||
): Promise<APIStatus> => {
|
||||
const wasPinnedByUser = userFetching
|
||||
? !!(await db.query.userPinnedNotes.findFirst({
|
||||
where: (relation, { and, eq }) =>
|
||||
and(
|
||||
eq(relation.statusId, statusToConvert.id),
|
||||
eq(relation.userId, userFetching?.id),
|
||||
),
|
||||
}))
|
||||
: false;
|
||||
|
||||
const wasRebloggedByUser = userFetching
|
||||
? !!(await db.query.status.findFirst({
|
||||
where: (status, { eq, and }) =>
|
||||
and(
|
||||
eq(status.authorId, userFetching?.id),
|
||||
eq(status.reblogId, statusToConvert.id),
|
||||
),
|
||||
}))
|
||||
: false;
|
||||
|
||||
const wasMutedByUser = userFetching
|
||||
? !!(await db.query.relationship.findFirst({
|
||||
where: (relationship, { and, eq }) =>
|
||||
and(
|
||||
eq(relationship.ownerId, userFetching.id),
|
||||
eq(relationship.subjectId, statusToConvert.authorId),
|
||||
eq(relationship.muting, true),
|
||||
),
|
||||
}))
|
||||
: false;
|
||||
|
||||
// Convert mentions of local users from @username@host to @username
|
||||
const mentionedLocalUsers = statusToConvert.mentions.filter(
|
||||
(mention) => mention.instanceId === null,
|
||||
);
|
||||
|
||||
let replacedContent = statusToConvert.content;
|
||||
|
||||
for (const mention of mentionedLocalUsers) {
|
||||
replacedContent = replacedContent.replace(
|
||||
createRegExp(
|
||||
exactly(
|
||||
`@${mention.username}@${
|
||||
new URL(config.http.base_url).host
|
||||
}`,
|
||||
),
|
||||
[global],
|
||||
),
|
||||
`@${mention.username}`,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
id: statusToConvert.id,
|
||||
in_reply_to_id: statusToConvert.inReplyToPostId || null,
|
||||
in_reply_to_account_id: statusToConvert.inReplyTo?.authorId || null,
|
||||
account: userToAPI(statusToConvert.author),
|
||||
created_at: new Date(statusToConvert.createdAt).toISOString(),
|
||||
application: statusToConvert.application
|
||||
? applicationToAPI(statusToConvert.application)
|
||||
: null,
|
||||
card: null,
|
||||
content: replacedContent,
|
||||
emojis: statusToConvert.emojis.map((emoji) => emojiToAPI(emoji)),
|
||||
favourited: !!(statusToConvert.likes ?? []).find(
|
||||
(like) => like.likerId === userFetching?.id,
|
||||
),
|
||||
favourites_count: (statusToConvert.likes ?? []).length,
|
||||
media_attachments: (statusToConvert.attachments ?? []).map(
|
||||
(a) => attachmentToAPI(a) as APIAttachment,
|
||||
),
|
||||
mentions: statusToConvert.mentions.map((mention) => userToAPI(mention)),
|
||||
language: null,
|
||||
muted: wasMutedByUser,
|
||||
pinned: wasPinnedByUser,
|
||||
// TODO: Add polls
|
||||
poll: null,
|
||||
reblog: statusToConvert.reblog
|
||||
? await statusToAPI(
|
||||
statusToConvert.reblog as unknown as StatusWithRelations,
|
||||
userFetching,
|
||||
)
|
||||
: null,
|
||||
reblogged: wasRebloggedByUser,
|
||||
reblogs_count: statusToConvert.reblogCount,
|
||||
replies_count: statusToConvert.replyCount,
|
||||
sensitive: statusToConvert.sensitive,
|
||||
spoiler_text: statusToConvert.spoilerText,
|
||||
tags: [],
|
||||
uri:
|
||||
statusToConvert.uri ||
|
||||
new URL(
|
||||
`/@${statusToConvert.author.username}/${statusToConvert.id}`,
|
||||
config.http.base_url,
|
||||
).toString(),
|
||||
visibility: statusToConvert.visibility as APIStatus["visibility"],
|
||||
url:
|
||||
statusToConvert.uri ||
|
||||
new URL(
|
||||
`/@${statusToConvert.author.username}/${statusToConvert.id}`,
|
||||
config.http.base_url,
|
||||
).toString(),
|
||||
bookmarked: false,
|
||||
quote: !!statusToConvert.quotingPostId /* statusToConvert.quoting
|
||||
? await statusToAPI(
|
||||
statusToConvert.quoting as unknown as StatusWithRelations,
|
||||
userFetching,
|
||||
)
|
||||
: null, */,
|
||||
// @ts-expect-error Pleroma extension
|
||||
quote_id: statusToConvert.quotingPostId || undefined,
|
||||
};
|
||||
};
|
||||
|
||||
export const getStatusUri = (status?: Status | null) => {
|
||||
if (!status) return undefined;
|
||||
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import {
|
|||
} from "~drizzle/schema";
|
||||
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 {
|
||||
|
|
@ -30,16 +31,10 @@ import { addInstanceIfNotExists } from "./Instance";
|
|||
import { createNewRelationship } from "./Relationship";
|
||||
import type { Token } from "./Token";
|
||||
|
||||
export type User = InferSelectModel<typeof user> & {
|
||||
endpoints?: Partial<{
|
||||
dislikes: string;
|
||||
featured: string;
|
||||
likes: string;
|
||||
followers: string;
|
||||
following: string;
|
||||
inbox: string;
|
||||
outbox: string;
|
||||
}>;
|
||||
export type User = InferSelectModel<typeof user>;
|
||||
|
||||
export type UserWithInstance = User & {
|
||||
instance: InferSelectModel<typeof instance> | null;
|
||||
};
|
||||
|
||||
export type UserWithRelations = User & {
|
||||
|
|
@ -109,21 +104,6 @@ export const userExtrasTemplate = (name: string) => ({
|
|||
]).as("status_count"),
|
||||
});
|
||||
|
||||
/* const a = await db.query.user.findFirst({
|
||||
with: {
|
||||
instance: true,
|
||||
emojis: {
|
||||
with: {
|
||||
emoji: {
|
||||
with: {
|
||||
instance: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}); */
|
||||
|
||||
export interface AuthData {
|
||||
user: UserWithRelations | null;
|
||||
token: string;
|
||||
|
|
@ -774,6 +754,16 @@ 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,
|
||||
|
|
|
|||
|
|
@ -289,7 +289,15 @@ export const user = pgTable(
|
|||
email: text("email"),
|
||||
note: text("note").default("").notNull(),
|
||||
isAdmin: boolean("is_admin").default(false).notNull(),
|
||||
endpoints: jsonb("endpoints"),
|
||||
endpoints: jsonb("endpoints").$type<Partial<{
|
||||
dislikes: string;
|
||||
featured: string;
|
||||
likes: string;
|
||||
followers: string;
|
||||
following: string;
|
||||
inbox: string;
|
||||
outbox: string;
|
||||
}> | null>(),
|
||||
source: jsonb("source").notNull(),
|
||||
avatar: text("avatar").notNull(),
|
||||
header: text("header").notNull(),
|
||||
|
|
|
|||
39
drizzle/types.ts
Normal file
39
drizzle/types.ts
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import type {
|
||||
BuildQueryResult,
|
||||
DBQueryConfig,
|
||||
ExtractTablesWithRelations,
|
||||
} from "drizzle-orm";
|
||||
|
||||
import type * as schema from "./schema";
|
||||
|
||||
type Schema = typeof schema;
|
||||
type TablesWithRelations = ExtractTablesWithRelations<Schema>;
|
||||
|
||||
export type IncludeRelation<TableName extends keyof TablesWithRelations> =
|
||||
DBQueryConfig<
|
||||
"one" | "many",
|
||||
boolean,
|
||||
TablesWithRelations,
|
||||
TablesWithRelations[TableName]
|
||||
>["with"];
|
||||
|
||||
export type IncludeColumns<TableName extends keyof TablesWithRelations> =
|
||||
DBQueryConfig<
|
||||
"one" | "many",
|
||||
boolean,
|
||||
TablesWithRelations,
|
||||
TablesWithRelations[TableName]
|
||||
>["columns"];
|
||||
|
||||
export type InferQueryModel<
|
||||
TableName extends keyof TablesWithRelations,
|
||||
Columns extends IncludeColumns<TableName> | undefined = undefined,
|
||||
With extends IncludeRelation<TableName> | undefined = undefined,
|
||||
> = BuildQueryResult<
|
||||
TablesWithRelations,
|
||||
TablesWithRelations[TableName],
|
||||
{
|
||||
columns: Columns;
|
||||
with: With;
|
||||
}
|
||||
>;
|
||||
4
packages/database-interface/main.ts
Normal file
4
packages/database-interface/main.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
import { Note } from "./note";
|
||||
import { Timeline } from "./timeline";
|
||||
|
||||
export { Note, Timeline };
|
||||
618
packages/database-interface/note.ts
Normal file
618
packages/database-interface/note.ts
Normal file
|
|
@ -0,0 +1,618 @@
|
|||
import {
|
||||
type InferInsertModel,
|
||||
type SQL,
|
||||
and,
|
||||
desc,
|
||||
eq,
|
||||
inArray,
|
||||
} from "drizzle-orm";
|
||||
import { htmlToText } from "html-to-text";
|
||||
import type * as Lysand from "lysand-types";
|
||||
import { createRegExp, exactly, global } from "magic-regexp";
|
||||
import {
|
||||
type Application,
|
||||
applicationToAPI,
|
||||
} from "~database/entities/Application";
|
||||
import {
|
||||
attachmentToAPI,
|
||||
attachmentToLysand,
|
||||
} from "~database/entities/Attachment";
|
||||
import {
|
||||
type EmojiWithInstance,
|
||||
emojiToAPI,
|
||||
emojiToLysand,
|
||||
parseEmojis,
|
||||
} from "~database/entities/Emoji";
|
||||
import { localObjectURI } from "~database/entities/Federation";
|
||||
import {
|
||||
type Status,
|
||||
type StatusWithRelations,
|
||||
contentToHtml,
|
||||
findFirstNote,
|
||||
findManyNotes,
|
||||
getStatusUri,
|
||||
} from "~database/entities/Status";
|
||||
import {
|
||||
type User,
|
||||
type UserWithRelations,
|
||||
type UserWithRelationsAndRelationships,
|
||||
findManyUsers,
|
||||
getUserUri,
|
||||
userToAPI,
|
||||
userToMention,
|
||||
} from "~database/entities/User";
|
||||
import { db } from "~drizzle/db";
|
||||
import {
|
||||
attachment,
|
||||
emojiToStatus,
|
||||
notification,
|
||||
status,
|
||||
statusToMentions,
|
||||
user,
|
||||
userRelations,
|
||||
} from "~drizzle/schema";
|
||||
import { config } from "~packages/config-manager";
|
||||
import type { Attachment as APIAttachment } from "~types/mastodon/attachment";
|
||||
import type { Status as APIStatus } from "~types/mastodon/status";
|
||||
|
||||
/**
|
||||
* Gives helpers to fetch notes from database in a nice format
|
||||
*/
|
||||
export class Note {
|
||||
private constructor(private status: StatusWithRelations) {}
|
||||
|
||||
static async fromId(id: string | null): Promise<Note | null> {
|
||||
if (!id) return null;
|
||||
|
||||
return await Note.fromSql(eq(status.id, id));
|
||||
}
|
||||
|
||||
static async fromIds(ids: string[]): Promise<Note[]> {
|
||||
return await Note.manyFromSql(inArray(status.id, ids));
|
||||
}
|
||||
|
||||
static async fromSql(
|
||||
sql: SQL<unknown> | undefined,
|
||||
orderBy: SQL<unknown> | undefined = desc(status.id),
|
||||
) {
|
||||
const found = await findFirstNote({
|
||||
where: sql,
|
||||
orderBy,
|
||||
});
|
||||
|
||||
if (!found) return null;
|
||||
return new Note(found);
|
||||
}
|
||||
|
||||
static async manyFromSql(
|
||||
sql: SQL<unknown> | undefined,
|
||||
orderBy: SQL<unknown> | undefined = desc(status.id),
|
||||
limit?: number,
|
||||
offset?: number,
|
||||
) {
|
||||
const found = await findManyNotes({
|
||||
where: sql,
|
||||
orderBy,
|
||||
limit,
|
||||
offset,
|
||||
});
|
||||
|
||||
return found.map((s) => new Note(s));
|
||||
}
|
||||
|
||||
async getUsersToFederateTo() {
|
||||
// Mentioned users
|
||||
const mentionedUsers =
|
||||
this.getStatus().mentions.length > 0
|
||||
? await findManyUsers({
|
||||
where: (user, { and, isNotNull, inArray }) =>
|
||||
and(
|
||||
isNotNull(user.instanceId),
|
||||
inArray(
|
||||
user.id,
|
||||
this.getStatus().mentions.map(
|
||||
(mention) => mention.id,
|
||||
),
|
||||
),
|
||||
),
|
||||
})
|
||||
: [];
|
||||
|
||||
const usersThatCanSeePost = await findManyUsers({
|
||||
where: (user, { isNotNull }) => isNotNull(user.instanceId),
|
||||
with: {
|
||||
...userRelations,
|
||||
relationships: {
|
||||
where: (relationship, { eq, and }) =>
|
||||
and(
|
||||
eq(relationship.subjectId, user.id),
|
||||
eq(relationship.following, true),
|
||||
),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const fusedUsers = [...mentionedUsers, ...usersThatCanSeePost];
|
||||
|
||||
const deduplicatedUsersById = fusedUsers.filter(
|
||||
(user, index, self) =>
|
||||
index === self.findIndex((t) => t.id === user.id),
|
||||
);
|
||||
|
||||
return deduplicatedUsersById;
|
||||
}
|
||||
|
||||
static fromStatus(status: StatusWithRelations) {
|
||||
return new Note(status);
|
||||
}
|
||||
|
||||
static fromStatuses(statuses: StatusWithRelations[]) {
|
||||
return statuses.map((s) => new Note(s));
|
||||
}
|
||||
|
||||
isNull() {
|
||||
return this.status === null;
|
||||
}
|
||||
|
||||
getStatus() {
|
||||
return this.status;
|
||||
}
|
||||
|
||||
getAuthor() {
|
||||
return this.status.author;
|
||||
}
|
||||
|
||||
async getReplyChildren() {
|
||||
return await Note.manyFromSql(
|
||||
eq(status.inReplyToPostId, this.status.id),
|
||||
);
|
||||
}
|
||||
|
||||
async unpin(unpinner: User) {
|
||||
return await db
|
||||
.delete(statusToMentions)
|
||||
.where(
|
||||
and(
|
||||
eq(statusToMentions.statusId, this.status.id),
|
||||
eq(statusToMentions.userId, unpinner.id),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static async insert(values: InferInsertModel<typeof status>) {
|
||||
return (await db.insert(status).values(values).returning())[0];
|
||||
}
|
||||
|
||||
static async fromData(
|
||||
author: User,
|
||||
content: Lysand.ContentFormat,
|
||||
visibility: APIStatus["visibility"],
|
||||
is_sensitive: boolean,
|
||||
spoiler_text: string,
|
||||
emojis: EmojiWithInstance[],
|
||||
uri?: string,
|
||||
mentions?: UserWithRelations[],
|
||||
/** List of IDs of database Attachment objects */
|
||||
media_attachments?: string[],
|
||||
replyId?: string,
|
||||
quoteId?: string,
|
||||
application?: Application,
|
||||
): Promise<Note | null> {
|
||||
const htmlContent = await contentToHtml(content, mentions);
|
||||
|
||||
// Parse emojis and fuse with existing emojis
|
||||
let foundEmojis = emojis;
|
||||
|
||||
if (author.instanceId === null) {
|
||||
const parsedEmojis = await parseEmojis(htmlContent);
|
||||
// Fuse and deduplicate
|
||||
foundEmojis = [...emojis, ...parsedEmojis].filter(
|
||||
(emoji, index, self) =>
|
||||
index === self.findIndex((t) => t.id === emoji.id),
|
||||
);
|
||||
}
|
||||
|
||||
const newNote = await Note.insert({
|
||||
authorId: author.id,
|
||||
content: htmlContent,
|
||||
contentSource:
|
||||
content["text/plain"]?.content ||
|
||||
content["text/markdown"]?.content ||
|
||||
Object.entries(content)[0][1].content ||
|
||||
"",
|
||||
contentType: "text/html",
|
||||
visibility,
|
||||
sensitive: is_sensitive,
|
||||
spoilerText: spoiler_text,
|
||||
uri: uri || null,
|
||||
inReplyToPostId: replyId ?? null,
|
||||
quotingPostId: quoteId ?? null,
|
||||
applicationId: application?.id ?? null,
|
||||
});
|
||||
|
||||
// Connect emojis
|
||||
for (const emoji of foundEmojis) {
|
||||
await db
|
||||
.insert(emojiToStatus)
|
||||
.values({
|
||||
emojiId: emoji.id,
|
||||
statusId: newNote.id,
|
||||
})
|
||||
.execute();
|
||||
}
|
||||
|
||||
// Connect mentions
|
||||
for (const mention of mentions ?? []) {
|
||||
await db
|
||||
.insert(statusToMentions)
|
||||
.values({
|
||||
statusId: newNote.id,
|
||||
userId: mention.id,
|
||||
})
|
||||
.execute();
|
||||
}
|
||||
|
||||
// Set attachment parents
|
||||
if (media_attachments && media_attachments.length > 0) {
|
||||
await db
|
||||
.update(attachment)
|
||||
.set({
|
||||
statusId: newNote.id,
|
||||
})
|
||||
.where(inArray(attachment.id, media_attachments));
|
||||
}
|
||||
|
||||
// Send notifications for mentioned local users
|
||||
for (const mention of mentions ?? []) {
|
||||
if (mention.instanceId === null) {
|
||||
await db.insert(notification).values({
|
||||
accountId: author.id,
|
||||
notifiedId: mention.id,
|
||||
type: "mention",
|
||||
statusId: newNote.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return await Note.fromId(newNote.id);
|
||||
}
|
||||
|
||||
async updateFromData(
|
||||
content?: Lysand.ContentFormat,
|
||||
visibility?: APIStatus["visibility"],
|
||||
is_sensitive?: boolean,
|
||||
spoiler_text?: string,
|
||||
emojis: EmojiWithInstance[] = [],
|
||||
mentions: UserWithRelations[] = [],
|
||||
/** List of IDs of database Attachment objects */
|
||||
media_attachments: string[] = [],
|
||||
) {
|
||||
const htmlContent = content
|
||||
? await contentToHtml(content, mentions)
|
||||
: undefined;
|
||||
|
||||
// Parse emojis and fuse with existing emojis
|
||||
let foundEmojis = emojis;
|
||||
|
||||
if (this.getAuthor().instanceId === null && htmlContent) {
|
||||
const parsedEmojis = await parseEmojis(htmlContent);
|
||||
// Fuse and deduplicate
|
||||
foundEmojis = [...emojis, ...parsedEmojis].filter(
|
||||
(emoji, index, self) =>
|
||||
index === self.findIndex((t) => t.id === emoji.id),
|
||||
);
|
||||
}
|
||||
|
||||
const newNote = await this.update({
|
||||
content: htmlContent,
|
||||
contentSource: content
|
||||
? content["text/plain"]?.content ||
|
||||
content["text/markdown"]?.content ||
|
||||
Object.entries(content)[0][1].content ||
|
||||
""
|
||||
: undefined,
|
||||
contentType: "text/html",
|
||||
visibility,
|
||||
sensitive: is_sensitive,
|
||||
spoilerText: spoiler_text,
|
||||
});
|
||||
|
||||
// Connect emojis
|
||||
await db
|
||||
.delete(emojiToStatus)
|
||||
.where(eq(emojiToStatus.statusId, this.status.id));
|
||||
|
||||
for (const emoji of foundEmojis) {
|
||||
await db
|
||||
.insert(emojiToStatus)
|
||||
.values({
|
||||
emojiId: emoji.id,
|
||||
statusId: this.status.id,
|
||||
})
|
||||
.execute();
|
||||
}
|
||||
|
||||
// Connect mentions
|
||||
await db
|
||||
.delete(statusToMentions)
|
||||
.where(eq(statusToMentions.statusId, this.status.id));
|
||||
|
||||
for (const mention of mentions ?? []) {
|
||||
await db
|
||||
.insert(statusToMentions)
|
||||
.values({
|
||||
statusId: this.status.id,
|
||||
userId: mention.id,
|
||||
})
|
||||
.execute();
|
||||
}
|
||||
|
||||
// Set attachment parents
|
||||
if (media_attachments && media_attachments.length > 0) {
|
||||
await db
|
||||
.update(attachment)
|
||||
.set({
|
||||
statusId: this.status.id,
|
||||
})
|
||||
.where(inArray(attachment.id, media_attachments));
|
||||
}
|
||||
|
||||
return await Note.fromId(newNote.id);
|
||||
}
|
||||
|
||||
async delete() {
|
||||
return (
|
||||
await db
|
||||
.delete(status)
|
||||
.where(eq(status.id, this.status.id))
|
||||
.returning()
|
||||
)[0];
|
||||
}
|
||||
|
||||
async update(newStatus: Partial<Status>) {
|
||||
return (
|
||||
await db
|
||||
.update(status)
|
||||
.set(newStatus)
|
||||
.where(eq(status.id, this.status.id))
|
||||
.returning()
|
||||
)[0];
|
||||
}
|
||||
|
||||
static async deleteMany(ids: string[]) {
|
||||
return await db
|
||||
.delete(status)
|
||||
.where(inArray(status.id, ids))
|
||||
.returning();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether this status is viewable by a user.
|
||||
* @param user The user to check.
|
||||
* @returns Whether this status is viewable by the user.
|
||||
*/
|
||||
async isViewableByUser(user: UserWithRelations | null) {
|
||||
if (this.getAuthor().id === user?.id) return true;
|
||||
if (this.getStatus().visibility === "public") return true;
|
||||
if (this.getStatus().visibility === "unlisted") return true;
|
||||
if (this.getStatus().visibility === "private") {
|
||||
return user
|
||||
? await db.query.relationship.findFirst({
|
||||
where: (relationship, { and, eq }) =>
|
||||
and(
|
||||
eq(relationship.ownerId, user?.id),
|
||||
eq(relationship.subjectId, status.authorId),
|
||||
eq(relationship.following, true),
|
||||
),
|
||||
})
|
||||
: false;
|
||||
}
|
||||
return (
|
||||
user &&
|
||||
this.getStatus().mentions.find((mention) => mention.id === user.id)
|
||||
);
|
||||
}
|
||||
|
||||
async toAPI(userFetching?: UserWithRelations | null): Promise<APIStatus> {
|
||||
const data = this.getStatus();
|
||||
const wasPinnedByUser = userFetching
|
||||
? !!(await db.query.userPinnedNotes.findFirst({
|
||||
where: (relation, { and, eq }) =>
|
||||
and(
|
||||
eq(relation.statusId, data.id),
|
||||
eq(relation.userId, userFetching?.id),
|
||||
),
|
||||
}))
|
||||
: false;
|
||||
|
||||
const wasRebloggedByUser = userFetching
|
||||
? !!(await Note.fromSql(
|
||||
and(
|
||||
eq(status.authorId, userFetching?.id),
|
||||
eq(status.reblogId, data.id),
|
||||
),
|
||||
))
|
||||
: false;
|
||||
|
||||
const wasMutedByUser = userFetching
|
||||
? !!(await db.query.relationship.findFirst({
|
||||
where: (relationship, { and, eq }) =>
|
||||
and(
|
||||
eq(relationship.ownerId, userFetching.id),
|
||||
eq(relationship.subjectId, data.authorId),
|
||||
eq(relationship.muting, true),
|
||||
),
|
||||
}))
|
||||
: false;
|
||||
|
||||
// Convert mentions of local users from @username@host to @username
|
||||
const mentionedLocalUsers = data.mentions.filter(
|
||||
(mention) => mention.instanceId === null,
|
||||
);
|
||||
|
||||
let replacedContent = data.content;
|
||||
|
||||
for (const mention of mentionedLocalUsers) {
|
||||
replacedContent = replacedContent.replace(
|
||||
createRegExp(
|
||||
exactly(
|
||||
`@${mention.username}@${
|
||||
new URL(config.http.base_url).host
|
||||
}`,
|
||||
),
|
||||
[global],
|
||||
),
|
||||
`@${mention.username}`,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
id: data.id,
|
||||
in_reply_to_id: data.inReplyToPostId || null,
|
||||
in_reply_to_account_id: data.inReplyTo?.authorId || null,
|
||||
account: userToAPI(data.author),
|
||||
created_at: new Date(data.createdAt).toISOString(),
|
||||
application: data.application
|
||||
? applicationToAPI(data.application)
|
||||
: null,
|
||||
card: null,
|
||||
content: replacedContent,
|
||||
emojis: data.emojis.map((emoji) => emojiToAPI(emoji)),
|
||||
favourited: !!(data.likes ?? []).find(
|
||||
(like) => like.likerId === userFetching?.id,
|
||||
),
|
||||
favourites_count: (data.likes ?? []).length,
|
||||
media_attachments: (data.attachments ?? []).map(
|
||||
(a) => attachmentToAPI(a) as APIAttachment,
|
||||
),
|
||||
mentions: data.mentions.map((mention) => userToMention(mention)),
|
||||
language: null,
|
||||
muted: wasMutedByUser,
|
||||
pinned: wasPinnedByUser,
|
||||
// TODO: Add polls
|
||||
poll: null,
|
||||
reblog: data.reblog
|
||||
? await Note.fromStatus(
|
||||
data.reblog as StatusWithRelations,
|
||||
).toAPI(userFetching)
|
||||
: null,
|
||||
reblogged: wasRebloggedByUser,
|
||||
reblogs_count: data.reblogCount,
|
||||
replies_count: data.replyCount,
|
||||
sensitive: data.sensitive,
|
||||
spoiler_text: data.spoilerText,
|
||||
tags: [],
|
||||
uri:
|
||||
data.uri ||
|
||||
new URL(
|
||||
`/@${data.author.username}/${data.id}`,
|
||||
config.http.base_url,
|
||||
).toString(),
|
||||
visibility: data.visibility as APIStatus["visibility"],
|
||||
url: data.uri || this.getMastoURI(),
|
||||
bookmarked: false,
|
||||
quote: !!data.quotingPostId,
|
||||
// @ts-expect-error Pleroma extension
|
||||
quote_id: data.quotingPostId || undefined,
|
||||
};
|
||||
}
|
||||
|
||||
getURI() {
|
||||
return localObjectURI(this.getStatus().id);
|
||||
}
|
||||
|
||||
getMastoURI() {
|
||||
return `/@${this.getAuthor().username}/${this.getStatus().id}`;
|
||||
}
|
||||
|
||||
toLysand(): Lysand.Note {
|
||||
const status = this.getStatus();
|
||||
return {
|
||||
type: "Note",
|
||||
created_at: new Date(status.createdAt).toISOString(),
|
||||
id: status.id,
|
||||
author: getUserUri(status.author),
|
||||
uri: this.getURI(),
|
||||
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.quoting) ?? undefined,
|
||||
replies_to: getStatusUri(status.inReplyTo) ?? 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
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all the ancestors of this post,
|
||||
*/
|
||||
async getAncestors(fetcher: UserWithRelationsAndRelationships | null) {
|
||||
const ancestors: Note[] = [];
|
||||
|
||||
let currentStatus: Note = this;
|
||||
|
||||
while (currentStatus.getStatus().inReplyToPostId) {
|
||||
const parent = await Note.fromId(
|
||||
currentStatus.getStatus().inReplyToPostId,
|
||||
);
|
||||
|
||||
if (!parent) {
|
||||
break;
|
||||
}
|
||||
|
||||
ancestors.push(parent);
|
||||
currentStatus = parent;
|
||||
}
|
||||
|
||||
// Filter for posts that are viewable by the user
|
||||
const viewableAncestors = ancestors.filter((ancestor) =>
|
||||
ancestor.isViewableByUser(fetcher),
|
||||
);
|
||||
return viewableAncestors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all the descendants of this post (recursive)
|
||||
* Temporary implementation, will be replaced with a recursive SQL query when I get to it
|
||||
*/
|
||||
async getDescendants(
|
||||
fetcher: UserWithRelationsAndRelationships | null,
|
||||
depth = 0,
|
||||
) {
|
||||
const descendants: Note[] = [];
|
||||
for (const child of await this.getReplyChildren()) {
|
||||
descendants.push(child);
|
||||
|
||||
if (depth < 20) {
|
||||
const childDescendants = await child.getDescendants(
|
||||
fetcher,
|
||||
depth + 1,
|
||||
);
|
||||
descendants.push(...childDescendants);
|
||||
}
|
||||
}
|
||||
|
||||
// Filter for posts that are viewable by the user
|
||||
|
||||
const viewableDescendants = descendants.filter((descendant) =>
|
||||
descendant.isViewableByUser(fetcher),
|
||||
);
|
||||
return viewableDescendants;
|
||||
}
|
||||
}
|
||||
6
packages/database-interface/package.json
Normal file
6
packages/database-interface/package.json
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"name": "database-interface",
|
||||
"version": "0.0.0",
|
||||
"main": "index.ts",
|
||||
"dependencies": {}
|
||||
}
|
||||
85
packages/database-interface/timeline.ts
Normal file
85
packages/database-interface/timeline.ts
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
import { type SQL, gt } from "drizzle-orm";
|
||||
import { status } from "~drizzle/schema";
|
||||
import { config } from "~packages/config-manager";
|
||||
import { Note } from "./note";
|
||||
|
||||
enum TimelineType {
|
||||
NOTE = "Note",
|
||||
}
|
||||
|
||||
export class Timeline {
|
||||
constructor(private type: TimelineType) {}
|
||||
|
||||
static async getNoteTimeline(
|
||||
sql: SQL<unknown> | undefined,
|
||||
limit: number,
|
||||
url: string,
|
||||
) {
|
||||
return new Timeline(TimelineType.NOTE).fetchTimeline(sql, limit, url);
|
||||
}
|
||||
|
||||
private async fetchTimeline<T>(
|
||||
sql: SQL<unknown> | undefined,
|
||||
limit: number,
|
||||
url: string,
|
||||
) {
|
||||
const objects: Note[] = [];
|
||||
|
||||
switch (this.type) {
|
||||
case TimelineType.NOTE:
|
||||
objects.push(
|
||||
...(await Note.manyFromSql(sql, undefined, limit)),
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
const linkHeader = [];
|
||||
const urlWithoutQuery = new URL(
|
||||
new URL(url).pathname,
|
||||
config.http.base_url,
|
||||
).toString();
|
||||
|
||||
if (objects.length > 0) {
|
||||
switch (this.type) {
|
||||
case TimelineType.NOTE: {
|
||||
const objectBefore = await Note.fromSql(
|
||||
gt(status.id, objects[0].getStatus().id),
|
||||
);
|
||||
|
||||
if (objectBefore) {
|
||||
linkHeader.push(
|
||||
`<${urlWithoutQuery}?limit=${limit ?? 20}&min_id=${
|
||||
objects[0].getStatus().id
|
||||
}>; rel="prev"`,
|
||||
);
|
||||
}
|
||||
|
||||
if (objects.length >= (limit ?? 20)) {
|
||||
const objectAfter = await Note.fromSql(
|
||||
gt(
|
||||
status.id,
|
||||
objects[objects.length - 1].getStatus().id,
|
||||
),
|
||||
);
|
||||
|
||||
if (objectAfter) {
|
||||
linkHeader.push(
|
||||
`<${urlWithoutQuery}?limit=${
|
||||
limit ?? 20
|
||||
}&max_id=${
|
||||
objects[objects.length - 1].getStatus().id
|
||||
}>; rel="next"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
link: linkHeader.join(", "),
|
||||
objects,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +1,10 @@
|
|||
import { apiRoute, applyConfig, idValidator } from "@api";
|
||||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { fetchTimeline } from "@timelines";
|
||||
import { and, eq, gt, gte, isNull, lt, sql } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
import {
|
||||
type StatusWithRelations,
|
||||
findManyStatuses,
|
||||
statusToAPI,
|
||||
} from "~database/entities/Status";
|
||||
import { findFirstUser } from "~database/entities/User";
|
||||
import { status } from "~drizzle/schema";
|
||||
import { Timeline } from "~packages/database-interface/timeline";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["GET"],
|
||||
|
|
@ -62,11 +59,7 @@ export default apiRoute<typeof meta, typeof schema>(
|
|||
if (!user) return errorResponse("User not found", 404);
|
||||
|
||||
if (pinned) {
|
||||
const { objects, link } = await fetchTimeline<StatusWithRelations>(
|
||||
findManyStatuses,
|
||||
{
|
||||
// @ts-ignore
|
||||
where: (status, { and, lt, gt, gte, eq, sql }) =>
|
||||
const { objects, link } = await Timeline.getNoteTimeline(
|
||||
and(
|
||||
max_id ? lt(status.id, max_id) : undefined,
|
||||
since_id ? gte(status.id, since_id) : undefined,
|
||||
|
|
@ -77,17 +70,12 @@ export default apiRoute<typeof meta, typeof schema>(
|
|||
? sql`EXISTS (SELECT 1 FROM "Attachment" WHERE "Attachment"."statusId" = ${status.id})`
|
||||
: undefined,
|
||||
),
|
||||
// @ts-expect-error Yes I KNOW the types are wrong
|
||||
orderBy: (status, { desc }) => desc(status.id),
|
||||
limit,
|
||||
},
|
||||
req,
|
||||
req.url,
|
||||
);
|
||||
|
||||
return jsonResponse(
|
||||
await Promise.all(
|
||||
objects.map((status) => statusToAPI(status, user)),
|
||||
),
|
||||
await Promise.all(objects.map((note) => note.toAPI(user))),
|
||||
200,
|
||||
{
|
||||
Link: link,
|
||||
|
|
@ -95,11 +83,7 @@ export default apiRoute<typeof meta, typeof schema>(
|
|||
);
|
||||
}
|
||||
|
||||
const { objects, link } = await fetchTimeline<StatusWithRelations>(
|
||||
findManyStatuses,
|
||||
{
|
||||
// @ts-ignore
|
||||
where: (status, { and, lt, gt, gte, eq, sql }) =>
|
||||
const { objects, link } = await Timeline.getNoteTimeline(
|
||||
and(
|
||||
max_id ? lt(status.id, max_id) : undefined,
|
||||
since_id ? gte(status.id, since_id) : undefined,
|
||||
|
|
@ -108,19 +92,14 @@ export default apiRoute<typeof meta, typeof schema>(
|
|||
only_media
|
||||
? sql`EXISTS (SELECT 1 FROM "Attachment" WHERE "Attachment"."statusId" = ${status.id})`
|
||||
: undefined,
|
||||
exclude_reblogs ? eq(status.reblogId, null) : undefined,
|
||||
exclude_reblogs ? isNull(status.reblogId) : undefined,
|
||||
),
|
||||
// @ts-expect-error Yes I KNOW the types are wrong
|
||||
orderBy: (status, { desc }) => desc(status.id),
|
||||
limit,
|
||||
},
|
||||
req,
|
||||
req.url,
|
||||
);
|
||||
|
||||
return jsonResponse(
|
||||
await Promise.all(
|
||||
objects.map((status) => statusToAPI(status, user)),
|
||||
),
|
||||
await Promise.all(objects.map((note) => note.toAPI(user))),
|
||||
200,
|
||||
{
|
||||
Link: link,
|
||||
|
|
|
|||
|
|
@ -1,12 +1,9 @@
|
|||
import { apiRoute, applyConfig, idValidator } from "@api";
|
||||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { fetchTimeline } from "@timelines";
|
||||
import { and, gt, gte, lt, sql } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
import {
|
||||
type StatusWithRelations,
|
||||
findManyStatuses,
|
||||
statusToAPI,
|
||||
} from "~database/entities/Status";
|
||||
import { status } from "~drizzle/schema";
|
||||
import { Timeline } from "~packages/database-interface/timeline";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["GET"],
|
||||
|
|
@ -35,28 +32,19 @@ export default apiRoute<typeof meta, typeof schema>(
|
|||
|
||||
if (!user) return errorResponse("Unauthorized", 401);
|
||||
|
||||
const { objects, link } = await fetchTimeline<StatusWithRelations>(
|
||||
findManyStatuses,
|
||||
{
|
||||
// @ts-ignore
|
||||
where: (status, { and, lt, gt, gte, eq, sql }) =>
|
||||
const { objects, link } = await Timeline.getNoteTimeline(
|
||||
and(
|
||||
max_id ? lt(status.id, max_id) : undefined,
|
||||
since_id ? gte(status.id, since_id) : undefined,
|
||||
min_id ? gt(status.id, min_id) : undefined,
|
||||
sql`EXISTS (SELECT 1 FROM "Like" WHERE "Like"."likedId" = ${status.id} AND "Like"."likerId" = ${user.id})`,
|
||||
),
|
||||
// @ts-expect-error Yes I KNOW the types are wrong
|
||||
orderBy: (status, { desc }) => desc(status.id),
|
||||
limit,
|
||||
},
|
||||
req,
|
||||
req.url,
|
||||
);
|
||||
|
||||
return jsonResponse(
|
||||
await Promise.all(
|
||||
objects.map(async (status) => statusToAPI(status, user)),
|
||||
),
|
||||
await Promise.all(objects.map(async (note) => note.toAPI(user))),
|
||||
200,
|
||||
{
|
||||
Link: link,
|
||||
|
|
|
|||
|
|
@ -1,13 +1,8 @@
|
|||
import { apiRoute, applyConfig, idValidator } from "@api";
|
||||
import { errorResponse, jsonResponse } from "@response";
|
||||
import type { Relationship } from "~database/entities/Relationship";
|
||||
import {
|
||||
findFirstStatuses,
|
||||
getAncestors,
|
||||
getDescendants,
|
||||
statusToAPI,
|
||||
} from "~database/entities/Status";
|
||||
import { db } from "~drizzle/db";
|
||||
import { Note } from "~packages/database-interface/note";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["GET"],
|
||||
|
|
@ -34,9 +29,7 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
|
|||
|
||||
const { user } = extraData.auth;
|
||||
|
||||
const foundStatus = await findFirstStatuses({
|
||||
where: (status, { eq }) => eq(status.id, id),
|
||||
});
|
||||
const foundStatus = await Note.fromId(id);
|
||||
|
||||
if (!foundStatus) return errorResponse("Record not found", 404);
|
||||
|
||||
|
|
@ -55,8 +48,7 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
|
|||
: null;
|
||||
|
||||
// Get all ancestors
|
||||
const ancestors = await getAncestors(
|
||||
foundStatus,
|
||||
const ancestors = await foundStatus.getAncestors(
|
||||
user
|
||||
? {
|
||||
...user,
|
||||
|
|
@ -65,8 +57,8 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
|
|||
}
|
||||
: null,
|
||||
);
|
||||
const descendants = await getDescendants(
|
||||
foundStatus,
|
||||
|
||||
const descendants = await foundStatus.getDescendants(
|
||||
user
|
||||
? {
|
||||
...user,
|
||||
|
|
@ -78,10 +70,10 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
|
|||
|
||||
return jsonResponse({
|
||||
ancestors: await Promise.all(
|
||||
ancestors.map((status) => statusToAPI(status, user || undefined)),
|
||||
ancestors.map((status) => status.toAPI(user)),
|
||||
),
|
||||
descendants: await Promise.all(
|
||||
descendants.map((status) => statusToAPI(status, user || undefined)),
|
||||
descendants.map((status) => status.toAPI(user)),
|
||||
),
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,12 +1,8 @@
|
|||
import { apiRoute, applyConfig, idValidator } from "@api";
|
||||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { createLike } from "~database/entities/Like";
|
||||
import {
|
||||
findFirstStatuses,
|
||||
isViewableByUser,
|
||||
statusToAPI,
|
||||
} from "~database/entities/Status";
|
||||
import { db } from "~drizzle/db";
|
||||
import { Note } from "~packages/database-interface/note";
|
||||
import type { Status as APIStatus } from "~types/mastodon/status";
|
||||
|
||||
export const meta = applyConfig({
|
||||
|
|
@ -34,26 +30,27 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
|
|||
|
||||
if (!user) return errorResponse("Unauthorized", 401);
|
||||
|
||||
const status = await findFirstStatuses({
|
||||
where: (status, { eq }) => eq(status.id, id),
|
||||
});
|
||||
const status = await Note.fromId(id);
|
||||
|
||||
// Check if user is authorized to view this status (if it's private)
|
||||
if (!status || !isViewableByUser(status, user))
|
||||
if (!status?.isViewableByUser(user))
|
||||
return errorResponse("Record not found", 404);
|
||||
|
||||
const existingLike = await db.query.like.findFirst({
|
||||
where: (like, { and, eq }) =>
|
||||
and(eq(like.likedId, status.id), eq(like.likerId, user.id)),
|
||||
and(
|
||||
eq(like.likedId, status.getStatus().id),
|
||||
eq(like.likerId, user.id),
|
||||
),
|
||||
});
|
||||
|
||||
if (!existingLike) {
|
||||
await createLike(user, status);
|
||||
await createLike(user, status.getStatus());
|
||||
}
|
||||
|
||||
return jsonResponse({
|
||||
...(await statusToAPI(status, user)),
|
||||
...(await status.toAPI(user)),
|
||||
favourited: true,
|
||||
favourites_count: status.likeCount + 1,
|
||||
favourites_count: status.getStatus().likeCount + 1,
|
||||
} as APIStatus);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,12 +2,12 @@ import { apiRoute, applyConfig, idValidator } from "@api";
|
|||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { fetchTimeline } from "@timelines";
|
||||
import { z } from "zod";
|
||||
import { findFirstStatuses, isViewableByUser } from "~database/entities/Status";
|
||||
import {
|
||||
type UserWithRelations,
|
||||
findManyUsers,
|
||||
userToAPI,
|
||||
} from "~database/entities/User";
|
||||
import { Note } from "~packages/database-interface/note";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["GET"],
|
||||
|
|
@ -40,12 +40,10 @@ export default apiRoute<typeof meta, typeof schema>(
|
|||
|
||||
const { user } = extraData.auth;
|
||||
|
||||
const status = await findFirstStatuses({
|
||||
where: (status, { eq }) => eq(status.id, id),
|
||||
});
|
||||
const status = await Note.fromId(id);
|
||||
|
||||
// Check if user is authorized to view this status (if it's private)
|
||||
if (!status || !isViewableByUser(status, user))
|
||||
if (!status?.isViewableByUser(user))
|
||||
return errorResponse("Record not found", 404);
|
||||
|
||||
const { max_id, min_id, since_id, limit } = extraData.parsedRequest;
|
||||
|
|
@ -59,7 +57,9 @@ export default apiRoute<typeof meta, typeof schema>(
|
|||
max_id ? lt(liker.id, max_id) : undefined,
|
||||
since_id ? gte(liker.id, since_id) : undefined,
|
||||
min_id ? gt(liker.id, min_id) : undefined,
|
||||
sql`EXISTS (SELECT 1 FROM "Like" WHERE "Like"."likedId" = ${status.id} AND "Like"."likerId" = ${liker.id})`,
|
||||
sql`EXISTS (SELECT 1 FROM "Like" WHERE "Like"."likedId" = ${
|
||||
status.getStatus().id
|
||||
} AND "Like"."likerId" = ${liker.id})`,
|
||||
),
|
||||
// @ts-expect-error Yes I KNOW the types are wrong
|
||||
orderBy: (liker, { desc }) => desc(liker.id),
|
||||
|
|
|
|||
|
|
@ -1,19 +1,10 @@
|
|||
import { apiRoute, applyConfig, idValidator } from "@api";
|
||||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { sanitizeHtml } from "@sanitization";
|
||||
import { config } from "config-manager";
|
||||
import { eq } from "drizzle-orm";
|
||||
import ISO6391 from "iso-639-1";
|
||||
import { parse } from "marked";
|
||||
import { z } from "zod";
|
||||
import {
|
||||
editStatus,
|
||||
findFirstStatuses,
|
||||
isViewableByUser,
|
||||
statusToAPI,
|
||||
} from "~database/entities/Status";
|
||||
import { db } from "~drizzle/db";
|
||||
import { status } from "~drizzle/schema";
|
||||
import { Note } from "~packages/database-interface/note";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["GET", "DELETE", "PUT"],
|
||||
|
|
@ -31,7 +22,7 @@ export const meta = applyConfig({
|
|||
export const schema = z.object({
|
||||
status: z.string().max(config.validation.max_note_size).optional(),
|
||||
// TODO: Add regex to validate
|
||||
content_type: z.string().optional(),
|
||||
content_type: z.string().optional().default("text/plain"),
|
||||
media_ids: z
|
||||
.array(z.string().regex(idValidator))
|
||||
.max(config.validation.max_media_attachments)
|
||||
|
|
@ -65,49 +56,37 @@ export default apiRoute<typeof meta, typeof schema>(
|
|||
|
||||
const { user } = extraData.auth;
|
||||
|
||||
const foundStatus = await findFirstStatuses({
|
||||
where: (status, { eq }) => eq(status.id, id),
|
||||
});
|
||||
const foundStatus = await Note.fromId(id);
|
||||
|
||||
const config = await extraData.configManager.getConfig();
|
||||
|
||||
// Check if user is authorized to view this status (if it's private)
|
||||
if (!foundStatus || !isViewableByUser(foundStatus, user))
|
||||
if (!foundStatus?.isViewableByUser(user))
|
||||
return errorResponse("Record not found", 404);
|
||||
|
||||
if (req.method === "GET") {
|
||||
return jsonResponse(await statusToAPI(foundStatus));
|
||||
return jsonResponse(await foundStatus.toAPI(user));
|
||||
}
|
||||
if (req.method === "DELETE") {
|
||||
if (foundStatus.authorId !== user?.id) {
|
||||
if (foundStatus.getAuthor().id !== user?.id) {
|
||||
return errorResponse("Unauthorized", 401);
|
||||
}
|
||||
|
||||
// TODO: Implement delete and redraft functionality
|
||||
|
||||
// Delete status and all associated objects
|
||||
await db.delete(status).where(eq(status.id, id));
|
||||
await foundStatus.delete();
|
||||
|
||||
return jsonResponse(
|
||||
{
|
||||
...(await statusToAPI(foundStatus, user)),
|
||||
// TODO: Add
|
||||
// text: Add source text
|
||||
// poll: Add source poll
|
||||
// media_attachments
|
||||
},
|
||||
200,
|
||||
);
|
||||
return jsonResponse(await foundStatus.toAPI(user), 200);
|
||||
}
|
||||
if (req.method === "PUT") {
|
||||
if (foundStatus.authorId !== user?.id) {
|
||||
if (foundStatus.getAuthor().id !== user?.id) {
|
||||
return errorResponse("Unauthorized", 401);
|
||||
}
|
||||
|
||||
const {
|
||||
status: statusText,
|
||||
content_type,
|
||||
"poll[expires_in]": expires_in,
|
||||
"poll[options]": options,
|
||||
media_ids,
|
||||
spoiler_text,
|
||||
|
|
@ -131,22 +110,6 @@ export default apiRoute<typeof meta, typeof schema>(
|
|||
);
|
||||
}
|
||||
|
||||
let sanitizedStatus: string;
|
||||
|
||||
if (content_type === "text/markdown") {
|
||||
sanitizedStatus = await sanitizeHtml(
|
||||
await parse(statusText ?? ""),
|
||||
);
|
||||
} else if (content_type === "text/x.misskeymarkdown") {
|
||||
// Parse as MFM
|
||||
// TODO: Parse as MFM
|
||||
sanitizedStatus = await sanitizeHtml(
|
||||
await parse(statusText ?? ""),
|
||||
);
|
||||
} else {
|
||||
sanitizedStatus = await sanitizeHtml(statusText ?? "");
|
||||
}
|
||||
|
||||
// Check if status body doesnt match filters
|
||||
if (
|
||||
config.filters.note_content.some((filter) =>
|
||||
|
|
@ -168,20 +131,27 @@ export default apiRoute<typeof meta, typeof schema>(
|
|||
}
|
||||
}
|
||||
|
||||
// Update status
|
||||
const newStatus = await editStatus(foundStatus, {
|
||||
content: sanitizedStatus,
|
||||
content_type,
|
||||
media_attachments: media_ids,
|
||||
spoiler_text: spoiler_text ?? "",
|
||||
sensitive: sensitive ?? false,
|
||||
});
|
||||
const newNote = await foundStatus.updateFromData(
|
||||
statusText
|
||||
? {
|
||||
[content_type]: {
|
||||
content: statusText,
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
undefined,
|
||||
sensitive,
|
||||
spoiler_text,
|
||||
undefined,
|
||||
undefined,
|
||||
media_ids,
|
||||
);
|
||||
|
||||
if (!newStatus) {
|
||||
if (!newNote) {
|
||||
return errorResponse("Failed to update status", 500);
|
||||
}
|
||||
|
||||
return jsonResponse(await statusToAPI(newStatus, user));
|
||||
return jsonResponse(await newNote.toAPI(user));
|
||||
}
|
||||
|
||||
return jsonResponse({});
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { apiRoute, applyConfig, idValidator } from "@api";
|
||||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { findFirstStatuses, statusToAPI } from "~database/entities/Status";
|
||||
import { db } from "~drizzle/db";
|
||||
import { statusToMentions } from "~drizzle/schema";
|
||||
import { Note } from "~packages/database-interface/note";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["POST"],
|
||||
|
|
@ -29,15 +29,13 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
|
|||
|
||||
if (!user) return errorResponse("Unauthorized", 401);
|
||||
|
||||
const foundStatus = await findFirstStatuses({
|
||||
where: (status, { eq }) => eq(status.id, id),
|
||||
});
|
||||
const foundStatus = await Note.fromId(id);
|
||||
|
||||
// Check if status exists
|
||||
if (!foundStatus) return errorResponse("Record not found", 404);
|
||||
|
||||
// Check if status is user's
|
||||
if (foundStatus.authorId !== user.id)
|
||||
if (foundStatus.getAuthor().id !== user.id)
|
||||
return errorResponse("Unauthorized", 401);
|
||||
|
||||
// Check if post is already pinned
|
||||
|
|
@ -45,7 +43,7 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
|
|||
await db.query.userPinnedNotes.findFirst({
|
||||
where: (userPinnedNote, { and, eq }) =>
|
||||
and(
|
||||
eq(userPinnedNote.statusId, foundStatus.id),
|
||||
eq(userPinnedNote.statusId, foundStatus.getStatus().id),
|
||||
eq(userPinnedNote.userId, user.id),
|
||||
),
|
||||
})
|
||||
|
|
@ -54,9 +52,9 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
|
|||
}
|
||||
|
||||
await db.insert(statusToMentions).values({
|
||||
statusId: foundStatus.id,
|
||||
statusId: foundStatus.getStatus().id,
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
return jsonResponse(statusToAPI(foundStatus, user));
|
||||
return jsonResponse(await foundStatus.toAPI(user));
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,13 +1,10 @@
|
|||
import { apiRoute, applyConfig, idValidator } from "@api";
|
||||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
import {
|
||||
findFirstStatuses,
|
||||
isViewableByUser,
|
||||
statusToAPI,
|
||||
} from "~database/entities/Status";
|
||||
import { db } from "~drizzle/db";
|
||||
import { notification, status } from "~drizzle/schema";
|
||||
import { Note } from "~packages/database-interface/note";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["POST"],
|
||||
|
|
@ -41,12 +38,10 @@ export default apiRoute<typeof meta, typeof schema>(
|
|||
|
||||
const { visibility } = extraData.parsedRequest;
|
||||
|
||||
const foundStatus = await findFirstStatuses({
|
||||
where: (status, { eq }) => eq(status.id, id),
|
||||
});
|
||||
const foundStatus = await Note.fromId(id);
|
||||
|
||||
// Check if user is authorized to view this status (if it's private)
|
||||
if (!foundStatus || !isViewableByUser(foundStatus, user))
|
||||
if (!foundStatus?.isViewableByUser(user))
|
||||
return errorResponse("Record not found", 404);
|
||||
|
||||
const existingReblog = await db.query.status.findFirst({
|
||||
|
|
@ -61,42 +56,35 @@ export default apiRoute<typeof meta, typeof schema>(
|
|||
return errorResponse("Already reblogged", 422);
|
||||
}
|
||||
|
||||
const newReblog = (
|
||||
await db
|
||||
.insert(status)
|
||||
.values({
|
||||
const newReblog = await Note.insert({
|
||||
authorId: user.id,
|
||||
reblogId: foundStatus.id,
|
||||
reblogId: foundStatus.getStatus().id,
|
||||
visibility,
|
||||
sensitive: false,
|
||||
updatedAt: new Date().toISOString(),
|
||||
applicationId: application?.id ?? null,
|
||||
})
|
||||
.returning()
|
||||
)[0];
|
||||
});
|
||||
|
||||
if (!newReblog) {
|
||||
return errorResponse("Failed to reblog", 500);
|
||||
}
|
||||
|
||||
const finalNewReblog = await findFirstStatuses({
|
||||
where: (status, { eq }) => eq(status.id, newReblog.id),
|
||||
});
|
||||
const finalNewReblog = await Note.fromId(newReblog.id);
|
||||
|
||||
if (!finalNewReblog) {
|
||||
return errorResponse("Failed to reblog", 500);
|
||||
}
|
||||
|
||||
// Create notification for reblog if reblogged user is on the same instance
|
||||
if (foundStatus.author.instanceId === user.instanceId) {
|
||||
if (foundStatus.getAuthor().instanceId === user.instanceId) {
|
||||
await db.insert(notification).values({
|
||||
accountId: user.id,
|
||||
notifiedId: foundStatus.authorId,
|
||||
notifiedId: foundStatus.getAuthor().id,
|
||||
type: "reblog",
|
||||
statusId: foundStatus.reblogId,
|
||||
statusId: foundStatus.getStatus().reblogId,
|
||||
});
|
||||
}
|
||||
|
||||
return jsonResponse(await statusToAPI(finalNewReblog, user));
|
||||
return jsonResponse(await finalNewReblog.toAPI(user));
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -2,12 +2,12 @@ import { apiRoute, applyConfig, idValidator } from "@api";
|
|||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { fetchTimeline } from "@timelines";
|
||||
import { z } from "zod";
|
||||
import { findFirstStatuses, isViewableByUser } from "~database/entities/Status";
|
||||
import {
|
||||
type UserWithRelations,
|
||||
findManyUsers,
|
||||
userToAPI,
|
||||
} from "~database/entities/User";
|
||||
import { Note } from "~packages/database-interface/note";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["GET"],
|
||||
|
|
@ -40,12 +40,10 @@ export default apiRoute<typeof meta, typeof schema>(
|
|||
|
||||
const { user } = extraData.auth;
|
||||
|
||||
const status = await findFirstStatuses({
|
||||
where: (status, { eq }) => eq(status.id, id),
|
||||
});
|
||||
const status = await Note.fromId(id);
|
||||
|
||||
// Check if user is authorized to view this status (if it's private)
|
||||
if (!status || !isViewableByUser(status, user))
|
||||
if (!status?.isViewableByUser(user))
|
||||
return errorResponse("Record not found", 404);
|
||||
|
||||
const { max_id, min_id, since_id, limit } = extraData.parsedRequest;
|
||||
|
|
@ -59,7 +57,9 @@ export default apiRoute<typeof meta, typeof schema>(
|
|||
max_id ? lt(reblogger.id, max_id) : undefined,
|
||||
since_id ? gte(reblogger.id, since_id) : undefined,
|
||||
min_id ? gt(reblogger.id, min_id) : undefined,
|
||||
sql`EXISTS (SELECT 1 FROM "Status" WHERE "Status"."reblogId" = ${status.id} AND "Status"."authorId" = ${reblogger.id})`,
|
||||
sql`EXISTS (SELECT 1 FROM "Status" WHERE "Status"."reblogId" = ${
|
||||
status.getStatus().id
|
||||
} AND "Status"."authorId" = ${reblogger.id})`,
|
||||
),
|
||||
// @ts-expect-error Yes I KNOW the types are wrong
|
||||
orderBy: (liker, { desc }) => desc(liker.id),
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { apiRoute, applyConfig, idValidator } from "@api";
|
||||
import { errorResponse } from "@response";
|
||||
import { findFirstStatuses, isViewableByUser } from "~database/entities/Status";
|
||||
import { Note } from "~packages/database-interface/note";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["GET"],
|
||||
|
|
@ -27,12 +27,10 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
|
|||
|
||||
if (!user) return errorResponse("Unauthorized", 401);
|
||||
|
||||
const status = await findFirstStatuses({
|
||||
where: (status, { eq }) => eq(status.id, id),
|
||||
});
|
||||
const status = await Note.fromId(id);
|
||||
|
||||
// Check if user is authorized to view this status (if it's private)
|
||||
if (!status || !isViewableByUser(status, user))
|
||||
if (!status?.isViewableByUser(user))
|
||||
return errorResponse("Record not found", 404);
|
||||
|
||||
return errorResponse("Not implemented yet");
|
||||
|
|
|
|||
|
|
@ -1,11 +1,7 @@
|
|||
import { apiRoute, applyConfig, idValidator } from "@api";
|
||||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { deleteLike } from "~database/entities/Like";
|
||||
import {
|
||||
findFirstStatuses,
|
||||
isViewableByUser,
|
||||
statusToAPI,
|
||||
} from "~database/entities/Status";
|
||||
import { Note } from "~packages/database-interface/note";
|
||||
import type { Status as APIStatus } from "~types/mastodon/status";
|
||||
|
||||
export const meta = applyConfig({
|
||||
|
|
@ -33,19 +29,17 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
|
|||
|
||||
if (!user) return errorResponse("Unauthorized", 401);
|
||||
|
||||
const foundStatus = await findFirstStatuses({
|
||||
where: (status, { eq }) => eq(status.id, id),
|
||||
});
|
||||
const foundStatus = await Note.fromId(id);
|
||||
|
||||
// Check if user is authorized to view this status (if it's private)
|
||||
if (!foundStatus || !isViewableByUser(foundStatus, user))
|
||||
if (!foundStatus?.isViewableByUser(user))
|
||||
return errorResponse("Record not found", 404);
|
||||
|
||||
await deleteLike(user, foundStatus);
|
||||
await deleteLike(user, foundStatus.getStatus());
|
||||
|
||||
return jsonResponse({
|
||||
...(await statusToAPI(foundStatus, user)),
|
||||
...(await foundStatus.toAPI(user)),
|
||||
favourited: false,
|
||||
favourites_count: foundStatus.likeCount - 1,
|
||||
favourites_count: foundStatus.getStatus().likeCount - 1,
|
||||
} as APIStatus);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,9 +1,6 @@
|
|||
import { apiRoute, applyConfig, idValidator } from "@api";
|
||||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { findFirstStatuses, statusToAPI } from "~database/entities/Status";
|
||||
import { db } from "~drizzle/db";
|
||||
import { statusToMentions } from "~drizzle/schema";
|
||||
import { Note } from "~packages/database-interface/note";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["POST"],
|
||||
|
|
@ -30,26 +27,18 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
|
|||
|
||||
if (!user) return errorResponse("Unauthorized", 401);
|
||||
|
||||
const status = await findFirstStatuses({
|
||||
where: (status, { eq }) => eq(status.id, id),
|
||||
});
|
||||
const status = await Note.fromId(id);
|
||||
|
||||
// Check if status exists
|
||||
if (!status) return errorResponse("Record not found", 404);
|
||||
|
||||
// Check if status is user's
|
||||
if (status.authorId !== user.id) return errorResponse("Unauthorized", 401);
|
||||
if (status.getAuthor().id !== user.id)
|
||||
return errorResponse("Unauthorized", 401);
|
||||
|
||||
await db
|
||||
.delete(statusToMentions)
|
||||
.where(
|
||||
and(
|
||||
eq(statusToMentions.statusId, status.id),
|
||||
eq(statusToMentions.userId, user.id),
|
||||
),
|
||||
);
|
||||
await status.unpin(user);
|
||||
|
||||
if (!status) return errorResponse("Record not found", 404);
|
||||
|
||||
return jsonResponse(statusToAPI(status, user));
|
||||
return jsonResponse(await status.toAPI(user));
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,13 +1,8 @@
|
|||
import { apiRoute, applyConfig, idValidator } from "@api";
|
||||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { eq } from "drizzle-orm";
|
||||
import {
|
||||
findFirstStatuses,
|
||||
isViewableByUser,
|
||||
statusToAPI,
|
||||
} from "~database/entities/Status";
|
||||
import { db } from "~drizzle/db";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { status } from "~drizzle/schema";
|
||||
import { Note } from "~packages/database-interface/note";
|
||||
import type { Status as APIStatus } from "~types/mastodon/status";
|
||||
|
||||
export const meta = applyConfig({
|
||||
|
|
@ -35,28 +30,28 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
|
|||
|
||||
if (!user) return errorResponse("Unauthorized", 401);
|
||||
|
||||
const foundStatus = await findFirstStatuses({
|
||||
where: (status, { eq }) => eq(status.id, id),
|
||||
});
|
||||
const foundStatus = await Note.fromId(id);
|
||||
|
||||
// Check if user is authorized to view this status (if it's private)
|
||||
if (!foundStatus || !isViewableByUser(foundStatus, user))
|
||||
if (!foundStatus?.isViewableByUser(user))
|
||||
return errorResponse("Record not found", 404);
|
||||
|
||||
const existingReblog = await findFirstStatuses({
|
||||
where: (status, { eq }) =>
|
||||
eq(status.authorId, user.id) && eq(status.reblogId, foundStatus.id),
|
||||
});
|
||||
const existingReblog = await Note.fromSql(
|
||||
and(
|
||||
eq(status.authorId, user.id),
|
||||
eq(status.reblogId, foundStatus.getStatus().id),
|
||||
),
|
||||
);
|
||||
|
||||
if (!existingReblog) {
|
||||
return errorResponse("Not already reblogged", 422);
|
||||
}
|
||||
|
||||
await db.delete(status).where(eq(status.id, existingReblog.id));
|
||||
await existingReblog.delete();
|
||||
|
||||
return jsonResponse({
|
||||
...(await statusToAPI(foundStatus, user)),
|
||||
...(await foundStatus.toAPI(user)),
|
||||
reblogged: false,
|
||||
reblogs_count: foundStatus.reblogCount - 1,
|
||||
reblogs_count: foundStatus.getStatus().reblogCount - 1,
|
||||
} as APIStatus);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,14 +6,9 @@ import ISO6391 from "iso-639-1";
|
|||
import { parse } from "marked";
|
||||
import { z } from "zod";
|
||||
import type { StatusWithRelations } from "~database/entities/Status";
|
||||
import {
|
||||
createNewStatus,
|
||||
federateStatus,
|
||||
findFirstStatuses,
|
||||
parseTextMentions,
|
||||
statusToAPI,
|
||||
} from "~database/entities/Status";
|
||||
import { federateNote, parseTextMentions } from "~database/entities/Status";
|
||||
import { db } from "~drizzle/db";
|
||||
import { Note } from "~packages/database-interface/note";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["POST"],
|
||||
|
|
@ -125,28 +120,10 @@ export default apiRoute<typeof meta, typeof schema>(
|
|||
}
|
||||
|
||||
// Get reply account and status if exists
|
||||
let replyStatus: StatusWithRelations | null = null;
|
||||
let quote: StatusWithRelations | null = null;
|
||||
|
||||
if (in_reply_to_id) {
|
||||
replyStatus = await findFirstStatuses({
|
||||
where: (status, { eq }) => eq(status.id, in_reply_to_id),
|
||||
}).catch(() => null);
|
||||
|
||||
if (!replyStatus) {
|
||||
return errorResponse("Reply status not found", 404);
|
||||
}
|
||||
}
|
||||
|
||||
if (quote_id) {
|
||||
quote = await findFirstStatuses({
|
||||
where: (status, { eq }) => eq(status.id, quote_id),
|
||||
}).catch(() => null);
|
||||
|
||||
if (!quote) {
|
||||
return errorResponse("Quote status not found", 404);
|
||||
}
|
||||
}
|
||||
const replyStatus: StatusWithRelations | null =
|
||||
(await Note.fromId(in_reply_to_id ?? null))?.getStatus() ?? null;
|
||||
const quote: StatusWithRelations | null =
|
||||
(await Note.fromId(quote_id ?? null))?.getStatus() ?? null;
|
||||
|
||||
// Check if status body doesnt match filters
|
||||
if (
|
||||
|
|
@ -171,7 +148,7 @@ export default apiRoute<typeof meta, typeof schema>(
|
|||
|
||||
const mentions = await parseTextMentions(sanitizedStatus);
|
||||
|
||||
const newStatus = await createNewStatus(
|
||||
const newNote = await Note.fromData(
|
||||
user,
|
||||
{
|
||||
[content_type]: {
|
||||
|
|
@ -185,19 +162,19 @@ export default apiRoute<typeof meta, typeof schema>(
|
|||
undefined,
|
||||
mentions,
|
||||
media_ids,
|
||||
replyStatus ?? undefined,
|
||||
quote ?? undefined,
|
||||
in_reply_to_id ?? undefined,
|
||||
quote_id ?? undefined,
|
||||
application ?? undefined,
|
||||
);
|
||||
|
||||
if (!newStatus) {
|
||||
if (!newNote) {
|
||||
return errorResponse("Failed to create status", 500);
|
||||
}
|
||||
|
||||
if (federate) {
|
||||
await federateStatus(newStatus);
|
||||
await federateNote(newNote);
|
||||
}
|
||||
|
||||
return jsonResponse(await statusToAPI(newStatus, user));
|
||||
return jsonResponse(await newNote.toAPI(user));
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,12 +1,9 @@
|
|||
import { apiRoute, applyConfig, idValidator } from "@api";
|
||||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { fetchTimeline } from "@timelines";
|
||||
import { and, eq, gt, gte, lt, or, sql } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
import {
|
||||
type StatusWithRelations,
|
||||
findManyStatuses,
|
||||
statusToAPI,
|
||||
} from "~database/entities/Status";
|
||||
import { status } from "~drizzle/schema";
|
||||
import { Timeline } from "~packages/database-interface/timeline";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["GET"],
|
||||
|
|
@ -38,11 +35,7 @@ export default apiRoute<typeof meta, typeof schema>(
|
|||
|
||||
if (!user) return errorResponse("Unauthorized", 401);
|
||||
|
||||
const { objects, link } = await fetchTimeline<StatusWithRelations>(
|
||||
findManyStatuses,
|
||||
{
|
||||
// @ts-expect-error Yes I KNOW the types are wrong
|
||||
where: (status, { lt, gte, gt, and, or, eq, inArray, sql }) =>
|
||||
const { objects, link } = await Timeline.getNoteTimeline(
|
||||
and(
|
||||
and(
|
||||
max_id ? lt(status.id, max_id) : undefined,
|
||||
|
|
@ -60,16 +53,11 @@ export default apiRoute<typeof meta, typeof schema>(
|
|||
),
|
||||
),
|
||||
limit,
|
||||
// @ts-expect-error Yes I KNOW the types are wrong
|
||||
orderBy: (status, { desc }) => desc(status.id),
|
||||
},
|
||||
req,
|
||||
req.url,
|
||||
);
|
||||
|
||||
return jsonResponse(
|
||||
await Promise.all(
|
||||
objects.map(async (status) => statusToAPI(status, user)),
|
||||
),
|
||||
await Promise.all(objects.map(async (note) => note.toAPI(user))),
|
||||
200,
|
||||
{
|
||||
Link: link,
|
||||
|
|
|
|||
|
|
@ -1,13 +1,9 @@
|
|||
import { apiRoute, applyConfig, idValidator } from "@api";
|
||||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { fetchTimeline } from "@timelines";
|
||||
import { sql } from "drizzle-orm";
|
||||
import { and, gt, gte, lt, sql } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
import {
|
||||
type StatusWithRelations,
|
||||
findManyStatuses,
|
||||
statusToAPI,
|
||||
} from "~database/entities/Status";
|
||||
import { status } from "~drizzle/schema";
|
||||
import { Timeline } from "~packages/database-interface/timeline";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["GET"],
|
||||
|
|
@ -41,11 +37,7 @@ export default apiRoute<typeof meta, typeof schema>(
|
|||
return errorResponse("Cannot use both local and remote", 400);
|
||||
}
|
||||
|
||||
const { objects, link } = await fetchTimeline<StatusWithRelations>(
|
||||
findManyStatuses,
|
||||
{
|
||||
// @ts-expect-error Yes I KNOW the types are wrong
|
||||
where: (status, { lt, gte, gt, and, isNull, isNotNull }) =>
|
||||
const { objects, link } = await Timeline.getNoteTimeline(
|
||||
and(
|
||||
max_id ? lt(status.id, max_id) : undefined,
|
||||
since_id ? gte(status.id, since_id) : undefined,
|
||||
|
|
@ -62,18 +54,11 @@ export default apiRoute<typeof meta, typeof schema>(
|
|||
: undefined,
|
||||
),
|
||||
limit,
|
||||
// @ts-expect-error Yes I KNOW the types are wrong
|
||||
orderBy: (status, { desc }) => desc(status.id),
|
||||
},
|
||||
req,
|
||||
req.url,
|
||||
);
|
||||
|
||||
return jsonResponse(
|
||||
await Promise.all(
|
||||
objects.map(async (status) =>
|
||||
statusToAPI(status, user || undefined),
|
||||
),
|
||||
),
|
||||
await Promise.all(objects.map(async (note) => note.toAPI(user))),
|
||||
200,
|
||||
{
|
||||
Link: link,
|
||||
|
|
|
|||
|
|
@ -2,9 +2,8 @@ import { apiRoute, applyConfig } from "@api";
|
|||
import { dualLogger } from "@loggers";
|
||||
import { MeiliIndexType, meilisearch } from "@meilisearch";
|
||||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { and, eq, sql } from "drizzle-orm";
|
||||
import { and, eq, inArray, sql } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
import { findManyStatuses, statusToAPI } from "~database/entities/Status";
|
||||
import {
|
||||
findFirstUser,
|
||||
findManyUsers,
|
||||
|
|
@ -12,7 +11,8 @@ import {
|
|||
userToAPI,
|
||||
} from "~database/entities/User";
|
||||
import { db } from "~drizzle/db";
|
||||
import { instance, user } from "~drizzle/schema";
|
||||
import { instance, status, user } from "~drizzle/schema";
|
||||
import { Note } from "~packages/database-interface/note";
|
||||
import { LogLevel } from "~packages/log-manager";
|
||||
|
||||
export const meta = applyConfig({
|
||||
|
|
@ -178,8 +178,7 @@ export default apiRoute<typeof meta, typeof schema>(
|
|||
orderBy: (user, { desc }) => desc(user.createdAt),
|
||||
});
|
||||
|
||||
const statuses = await findManyStatuses({
|
||||
where: (status, { and, eq, inArray }) =>
|
||||
const statuses = await Note.manyFromSql(
|
||||
and(
|
||||
inArray(
|
||||
status.id,
|
||||
|
|
@ -194,13 +193,12 @@ export default apiRoute<typeof meta, typeof schema>(
|
|||
} AND Relationships.ownerId = ${status.authorId})`
|
||||
: undefined,
|
||||
),
|
||||
orderBy: (status, { desc }) => desc(status.createdAt),
|
||||
});
|
||||
);
|
||||
|
||||
return jsonResponse({
|
||||
accounts: accounts.map((account) => userToAPI(account)),
|
||||
statuses: await Promise.all(
|
||||
statuses.map((status) => statusToAPI(status)),
|
||||
statuses.map((status) => status.toAPI(self)),
|
||||
),
|
||||
hashtags: [],
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,14 +1,11 @@
|
|||
import { apiRoute, applyConfig } from "@api";
|
||||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { sql } from "drizzle-orm";
|
||||
import { likeToLysand, type Like } from "~database/entities/Like";
|
||||
import {
|
||||
findFirstStatuses,
|
||||
statusToLysand,
|
||||
type StatusWithRelations,
|
||||
} from "~database/entities/Status";
|
||||
import { db } from "~drizzle/db";
|
||||
import { and, eq, inArray, sql } from "drizzle-orm";
|
||||
import type * as Lysand from "lysand-types";
|
||||
import { type Like, likeToLysand } from "~database/entities/Like";
|
||||
import { db } from "~drizzle/db";
|
||||
import { status } from "~drizzle/schema";
|
||||
import { Note } from "~packages/database-interface/note";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["GET"],
|
||||
|
|
@ -25,18 +22,16 @@ export const meta = applyConfig({
|
|||
export default apiRoute(async (req, matchedRoute) => {
|
||||
const uuid = matchedRoute.params.uuid;
|
||||
|
||||
let foundObject: StatusWithRelations | Like | null = null;
|
||||
let foundObject: Note | Like | null = null;
|
||||
let apiObject: Lysand.Entity | null = null;
|
||||
|
||||
foundObject =
|
||||
(await findFirstStatuses({
|
||||
where: (status, { eq, and, inArray }) =>
|
||||
foundObject = await Note.fromSql(
|
||||
and(
|
||||
eq(status.id, uuid),
|
||||
inArray(status.visibility, ["public", "unlisted"]),
|
||||
),
|
||||
})) ?? null;
|
||||
apiObject = foundObject ? statusToLysand(foundObject) : null;
|
||||
);
|
||||
apiObject = foundObject ? foundObject.toLysand() : null;
|
||||
|
||||
if (!foundObject) {
|
||||
foundObject =
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { dualLogger } from "@loggers";
|
|||
import { errorResponse, response } from "@response";
|
||||
import { eq } from "drizzle-orm";
|
||||
import type * as Lysand from "lysand-types";
|
||||
import { resolveStatus } from "~database/entities/Status";
|
||||
import { resolveNote } from "~database/entities/Status";
|
||||
import {
|
||||
findFirstUser,
|
||||
getRelationshipToOtherUser,
|
||||
|
|
@ -126,16 +126,14 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
|
|||
return errorResponse("Author not found", 400);
|
||||
}
|
||||
|
||||
const newStatus = await resolveStatus(undefined, note).catch(
|
||||
(e) => {
|
||||
const newStatus = await resolveNote(undefined, note).catch((e) => {
|
||||
dualLogger.logError(
|
||||
LogLevel.ERROR,
|
||||
"Inbox.NoteResolve",
|
||||
e as Error,
|
||||
);
|
||||
return null;
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
if (!newStatus) {
|
||||
return errorResponse("Failed to add status", 500);
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import { apiRoute, applyConfig } from "@api";
|
||||
import { jsonResponse } from "@response";
|
||||
import { and, count, eq, inArray } from "drizzle-orm";
|
||||
import { findManyStatuses, statusToLysand } from "~database/entities/Status";
|
||||
import { db } from "~drizzle/db";
|
||||
import { status } from "~drizzle/schema";
|
||||
import { Note } from "~packages/database-interface/note";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["GET"],
|
||||
|
|
@ -23,18 +23,17 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
|
|||
const config = await extraData.configManager.getConfig();
|
||||
const host = new URL(config.http.base_url).hostname;
|
||||
|
||||
const statuses = await findManyStatuses({
|
||||
where: (status, { eq, and, inArray }) =>
|
||||
const notes = await Note.manyFromSql(
|
||||
and(
|
||||
eq(status.authorId, uuid),
|
||||
inArray(status.visibility, ["public", "unlisted"]),
|
||||
),
|
||||
offset: 20 * (pageNumber - 1),
|
||||
limit: 20,
|
||||
orderBy: (status, { desc }) => desc(status.createdAt),
|
||||
});
|
||||
undefined,
|
||||
20,
|
||||
20 * (pageNumber - 1),
|
||||
);
|
||||
|
||||
const totalStatuses = await db
|
||||
const totalNotes = await db
|
||||
.select({
|
||||
count: count(),
|
||||
})
|
||||
|
|
@ -49,11 +48,11 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
|
|||
return jsonResponse({
|
||||
first: `${host}/users/${uuid}/outbox?page=1`,
|
||||
last: `${host}/users/${uuid}/outbox?page=1`,
|
||||
total_items: totalStatuses,
|
||||
total_items: totalNotes,
|
||||
// Server actor
|
||||
author: new URL("/users/actor", config.http.base_url).toString(),
|
||||
next:
|
||||
statuses.length === 20
|
||||
notes.length === 20
|
||||
? new URL(
|
||||
`/users/${uuid}/outbox?page=${pageNumber + 1}`,
|
||||
config.http.base_url,
|
||||
|
|
@ -66,6 +65,6 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
|
|||
config.http.base_url,
|
||||
).toString()
|
||||
: undefined,
|
||||
items: statuses.map((s) => statusToLysand(s)),
|
||||
items: notes.map((note) => note.toLysand()),
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { randomBytes } from "node:crypto";
|
||||
import { inArray, like } from "drizzle-orm";
|
||||
import { type Status, findManyStatuses } from "~database/entities/Status";
|
||||
import { asc, inArray, like } from "drizzle-orm";
|
||||
import type { Status } from "~database/entities/Status";
|
||||
import {
|
||||
type User,
|
||||
type UserWithRelations,
|
||||
|
|
@ -9,6 +9,7 @@ import {
|
|||
import { db } from "~drizzle/db";
|
||||
import { status, token, user } from "~drizzle/schema";
|
||||
import { server } from "~index";
|
||||
import { Note } from "~packages/database-interface/note";
|
||||
/**
|
||||
* This allows us to send a test request to the server even when it isnt running
|
||||
* CURRENTLY NOT WORKING, NEEDS TO BE FIXED
|
||||
|
|
@ -86,10 +87,7 @@ export const getTestStatuses = async (
|
|||
const statuses: Status[] = [];
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const newStatus = (
|
||||
await db
|
||||
.insert(status)
|
||||
.values({
|
||||
const newStatus = await Note.insert({
|
||||
content: `${i} ${randomBytes(32).toString("hex")}`,
|
||||
authorId: user.id,
|
||||
sensitive: false,
|
||||
|
|
@ -97,9 +95,7 @@ export const getTestStatuses = async (
|
|||
visibility: "public",
|
||||
applicationId: null,
|
||||
...partial,
|
||||
})
|
||||
.returning()
|
||||
)[0];
|
||||
});
|
||||
|
||||
if (!newStatus) {
|
||||
throw new Error("Failed to create test status");
|
||||
|
|
@ -108,14 +104,13 @@ export const getTestStatuses = async (
|
|||
statuses.push(newStatus);
|
||||
}
|
||||
|
||||
const statusesWithRelations = await findManyStatuses({
|
||||
where: (status, { inArray }) =>
|
||||
return (
|
||||
await Note.manyFromSql(
|
||||
inArray(
|
||||
status.id,
|
||||
statuses.map((s) => s.id),
|
||||
),
|
||||
orderBy: (status, { asc }) => asc(status.id),
|
||||
});
|
||||
|
||||
return statusesWithRelations;
|
||||
asc(status.id),
|
||||
)
|
||||
).map((n) => n.getStatus());
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,17 +3,17 @@ import type {
|
|||
Notification,
|
||||
findManyNotifications,
|
||||
} from "~database/entities/Notification";
|
||||
import type { Status, findManyStatuses } from "~database/entities/Status";
|
||||
import type { Status, findManyNotes } from "~database/entities/Status";
|
||||
import type { User, findManyUsers } from "~database/entities/User";
|
||||
import type { db } from "~drizzle/db";
|
||||
|
||||
export async function fetchTimeline<T extends User | Status | Notification>(
|
||||
model:
|
||||
| typeof findManyStatuses
|
||||
| typeof findManyNotes
|
||||
| typeof findManyUsers
|
||||
| typeof findManyNotifications,
|
||||
args:
|
||||
| Parameters<typeof findManyStatuses>[0]
|
||||
| Parameters<typeof findManyNotes>[0]
|
||||
| Parameters<typeof findManyUsers>[0]
|
||||
| Parameters<typeof db.query.notification.findMany>[0],
|
||||
req: Request,
|
||||
|
|
|
|||
Loading…
Reference in a new issue