mirror of
https://github.com/versia-pub/server.git
synced 2026-03-13 05:49:16 +01:00
refactor: 🚚 Rename functions, move getUrl to Attachment
This commit is contained in:
parent
11c3931007
commit
d09f74e58a
60 changed files with 93 additions and 96 deletions
36
classes/functions/application.ts
Normal file
36
classes/functions/application.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import type { InferSelectModel } from "drizzle-orm";
|
||||
import { db } from "~/drizzle/db";
|
||||
import type { Applications } from "~/drizzle/schema";
|
||||
import type { Application as apiApplication } from "~/types/mastodon/application";
|
||||
|
||||
export type Application = InferSelectModel<typeof Applications>;
|
||||
|
||||
/**
|
||||
* Retrieves the application associated with the given access token.
|
||||
* @param token The access token to retrieve the application for.
|
||||
* @returns The application associated with the given access token, or null if no such application exists.
|
||||
*/
|
||||
export const getFromToken = async (
|
||||
token: string,
|
||||
): Promise<Application | null> => {
|
||||
const result = await db.query.Tokens.findFirst({
|
||||
where: (tokens, { eq }) => eq(tokens.accessToken, token),
|
||||
with: {
|
||||
application: true,
|
||||
},
|
||||
});
|
||||
|
||||
return result?.application || null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts this application to an API application.
|
||||
* @returns The API application representation of this application.
|
||||
*/
|
||||
export const applicationToApi = (app: Application): apiApplication => {
|
||||
return {
|
||||
name: app.name,
|
||||
website: app.website,
|
||||
vapid_key: app.vapidKey,
|
||||
};
|
||||
};
|
||||
27
classes/functions/emoji.ts
Normal file
27
classes/functions/emoji.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import { emojiValidatorWithColons } from "@/api";
|
||||
import { type InferSelectModel, inArray } from "drizzle-orm";
|
||||
import { Emojis, type Instances } from "~/drizzle/schema";
|
||||
import { Emoji } from "~/packages/database-interface/emoji";
|
||||
|
||||
export type EmojiWithInstance = InferSelectModel<typeof Emojis> & {
|
||||
instance: InferSelectModel<typeof Instances> | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Used for parsing emojis from local text
|
||||
* @param text The text to parse
|
||||
* @returns An array of emojis
|
||||
*/
|
||||
export const parseEmojis = async (text: string): Promise<Emoji[]> => {
|
||||
const matches = text.match(emojiValidatorWithColons);
|
||||
if (!matches || matches.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return Emoji.manyFromSql(
|
||||
inArray(
|
||||
Emojis.shortcode,
|
||||
matches.map((match) => match.replace(/:/g, "")),
|
||||
),
|
||||
);
|
||||
};
|
||||
73
classes/functions/federation.ts
Normal file
73
classes/functions/federation.ts
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
import { debugRequest } from "@/api";
|
||||
import { getLogger } from "@logtape/logtape";
|
||||
import { SignatureConstructor } from "@lysand-org/federation";
|
||||
import type { Entity, Undo } from "@lysand-org/federation/types";
|
||||
import { config } from "config-manager";
|
||||
import type { User } from "~/packages/database-interface/user";
|
||||
|
||||
export const localObjectUri = (id: string) =>
|
||||
new URL(`/objects/${id}`, config.http.base_url).toString();
|
||||
|
||||
export const objectToInboxRequest = async (
|
||||
object: Entity,
|
||||
author: User,
|
||||
userToSendTo: User,
|
||||
): Promise<Request> => {
|
||||
if (userToSendTo.isLocal() || !userToSendTo.data.endpoints?.inbox) {
|
||||
throw new Error("UserToSendTo has no inbox or is a local user");
|
||||
}
|
||||
|
||||
if (author.isRemote()) {
|
||||
throw new Error("Author is a remote user");
|
||||
}
|
||||
|
||||
const privateKey = await crypto.subtle.importKey(
|
||||
"pkcs8",
|
||||
Buffer.from(author.data.privateKey ?? "", "base64"),
|
||||
"Ed25519",
|
||||
false,
|
||||
["sign"],
|
||||
);
|
||||
|
||||
const ctor = new SignatureConstructor(privateKey, author.getUri());
|
||||
|
||||
const userInbox = new URL(userToSendTo.data.endpoints?.inbox ?? "");
|
||||
|
||||
const request = new Request(userInbox, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Origin: new URL(config.http.base_url).host,
|
||||
},
|
||||
body: JSON.stringify(object),
|
||||
});
|
||||
|
||||
const { request: signed, signedString } = await ctor.sign(request);
|
||||
|
||||
if (config.debug.federation) {
|
||||
// Debug request
|
||||
await debugRequest(signed);
|
||||
|
||||
const logger = getLogger("federation");
|
||||
|
||||
// Log public key
|
||||
logger.debug`Sender public key: ${author.data.publicKey}`;
|
||||
|
||||
// Log signed string
|
||||
logger.debug`Signed string:\n${signedString}`;
|
||||
}
|
||||
|
||||
return signed;
|
||||
};
|
||||
|
||||
export const undoFederationRequest = (undoer: User, uri: string): Undo => {
|
||||
const id = crypto.randomUUID();
|
||||
return {
|
||||
type: "Undo",
|
||||
id,
|
||||
author: undoer.getUri(),
|
||||
created_at: new Date().toISOString(),
|
||||
object: uri,
|
||||
uri: new URL(`/undos/${id}`, config.http.base_url).toString(),
|
||||
};
|
||||
};
|
||||
51
classes/functions/instance.ts
Normal file
51
classes/functions/instance.ts
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import type { ServerMetadata } from "@lysand-org/federation/types";
|
||||
import { db } from "~/drizzle/db";
|
||||
import { Instances } from "~/drizzle/schema";
|
||||
import { config } from "~/packages/config-manager";
|
||||
|
||||
/**
|
||||
* Represents an instance in the database.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Adds an instance to the database if it doesn't already exist.
|
||||
* @param url
|
||||
* @returns Either the database instance if it already exists, or a newly created instance.
|
||||
*/
|
||||
export const addInstanceIfNotExists = async (url: string) => {
|
||||
const origin = new URL(url).origin;
|
||||
const host = new URL(url).host;
|
||||
|
||||
const found = await db.query.Instances.findFirst({
|
||||
where: (instance, { eq }) => eq(instance.baseUrl, host),
|
||||
});
|
||||
|
||||
if (found) {
|
||||
return found;
|
||||
}
|
||||
|
||||
// Fetch the instance configuration
|
||||
const metadata = (await fetch(new URL("/.well-known/lysand", origin), {
|
||||
proxy: config.http.proxy.address,
|
||||
}).then((res) => res.json())) as ServerMetadata;
|
||||
|
||||
if (metadata.type !== "ServerMetadata") {
|
||||
throw new Error("Invalid instance metadata (wrong type)");
|
||||
}
|
||||
|
||||
if (!(metadata.name && metadata.version)) {
|
||||
throw new Error("Invalid instance metadata (missing name or version)");
|
||||
}
|
||||
|
||||
return (
|
||||
await db
|
||||
.insert(Instances)
|
||||
.values({
|
||||
baseUrl: host,
|
||||
name: metadata.name,
|
||||
version: metadata.version,
|
||||
logo: metadata.logo,
|
||||
})
|
||||
.returning()
|
||||
)[0];
|
||||
};
|
||||
77
classes/functions/like.ts
Normal file
77
classes/functions/like.ts
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
import type { Like } from "@lysand-org/federation/types";
|
||||
import { config } from "config-manager";
|
||||
import { type InferSelectModel, and, eq } from "drizzle-orm";
|
||||
import { db } from "~/drizzle/db";
|
||||
import { Likes, Notifications } from "~/drizzle/schema";
|
||||
import type { Note } from "~/packages/database-interface/note";
|
||||
import type { User } from "~/packages/database-interface/user";
|
||||
|
||||
export type LikeType = InferSelectModel<typeof Likes>;
|
||||
|
||||
/**
|
||||
* Represents a Like entity in the database.
|
||||
*/
|
||||
export const likeToLysand = (like: LikeType): Like => {
|
||||
return {
|
||||
id: like.id,
|
||||
// biome-ignore lint/suspicious/noExplicitAny: to be rewritten
|
||||
author: (like as any).liker?.uri,
|
||||
type: "Like",
|
||||
created_at: new Date(like.createdAt).toISOString(),
|
||||
// biome-ignore lint/suspicious/noExplicitAny: to be rewritten
|
||||
object: (like as any).liked?.uri,
|
||||
uri: new URL(`/objects/${like.id}`, config.http.base_url).toString(),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a like
|
||||
* @param user User liking the status
|
||||
* @param note Status being liked
|
||||
*/
|
||||
export const createLike = async (user: User, note: Note) => {
|
||||
await db.insert(Likes).values({
|
||||
likedId: note.id,
|
||||
likerId: user.id,
|
||||
});
|
||||
|
||||
if (note.author.data.instanceId === user.data.instanceId) {
|
||||
// Notify the user that their post has been favourited
|
||||
await db.insert(Notifications).values({
|
||||
accountId: user.id,
|
||||
type: "favourite",
|
||||
notifiedId: note.author.id,
|
||||
noteId: note.id,
|
||||
});
|
||||
} else {
|
||||
// TODO: Add database jobs for federating this
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete a like
|
||||
* @param user User deleting their like
|
||||
* @param note Status being unliked
|
||||
*/
|
||||
export const deleteLike = async (user: User, note: Note) => {
|
||||
await db
|
||||
.delete(Likes)
|
||||
.where(and(eq(Likes.likedId, note.id), eq(Likes.likerId, user.id)));
|
||||
|
||||
// Notify the user that their post has been favourited
|
||||
await db
|
||||
.delete(Notifications)
|
||||
.where(
|
||||
and(
|
||||
eq(Notifications.accountId, user.id),
|
||||
eq(Notifications.type, "favourite"),
|
||||
eq(Notifications.notifiedId, note.author.id),
|
||||
eq(Notifications.noteId, note.id),
|
||||
),
|
||||
);
|
||||
|
||||
if (user.isLocal() && note.author.isRemote()) {
|
||||
// User is local, federate the delete
|
||||
// TODO: Federate this
|
||||
}
|
||||
};
|
||||
64
classes/functions/notification.ts
Normal file
64
classes/functions/notification.ts
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
import type { InferSelectModel } from "drizzle-orm";
|
||||
import { db } from "~/drizzle/db";
|
||||
import type { Notifications } from "~/drizzle/schema";
|
||||
import { Note } from "~/packages/database-interface/note";
|
||||
import { User } from "~/packages/database-interface/user";
|
||||
import type { Notification as apiNotification } from "~/types/mastodon/notification";
|
||||
import type { StatusWithRelations } from "./status";
|
||||
import {
|
||||
type UserWithRelations,
|
||||
transformOutputToUserWithRelations,
|
||||
userExtrasTemplate,
|
||||
userRelations,
|
||||
} from "./user";
|
||||
|
||||
export type Notification = InferSelectModel<typeof Notifications>;
|
||||
|
||||
export type NotificationWithRelations = Notification & {
|
||||
status: StatusWithRelations | null;
|
||||
account: UserWithRelations;
|
||||
};
|
||||
|
||||
export const findManyNotifications = async (
|
||||
query: Parameters<typeof db.query.Notifications.findMany>[0],
|
||||
userId?: string,
|
||||
): Promise<NotificationWithRelations[]> => {
|
||||
const output = await db.query.Notifications.findMany({
|
||||
...query,
|
||||
with: {
|
||||
...query?.with,
|
||||
account: {
|
||||
with: {
|
||||
...userRelations,
|
||||
},
|
||||
extras: userExtrasTemplate("Notifications_account"),
|
||||
},
|
||||
},
|
||||
extras: {
|
||||
...query?.extras,
|
||||
},
|
||||
});
|
||||
|
||||
return await Promise.all(
|
||||
output.map(async (notif) => ({
|
||||
...notif,
|
||||
account: transformOutputToUserWithRelations(notif.account),
|
||||
status: (await Note.fromId(notif.noteId, userId))?.data ?? null,
|
||||
})),
|
||||
);
|
||||
};
|
||||
|
||||
export const notificationToApi = async (
|
||||
notification: NotificationWithRelations,
|
||||
): Promise<apiNotification> => {
|
||||
const account = new User(notification.account);
|
||||
return {
|
||||
account: account.toApi(),
|
||||
created_at: new Date(notification.createdAt).toISOString(),
|
||||
id: notification.id,
|
||||
type: notification.type,
|
||||
status: notification.status
|
||||
? await new Note(notification.status).toApi(account)
|
||||
: undefined,
|
||||
};
|
||||
};
|
||||
96
classes/functions/relationship.ts
Normal file
96
classes/functions/relationship.ts
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
import type { InferSelectModel } from "drizzle-orm";
|
||||
import { db } from "~/drizzle/db";
|
||||
import { Relationships } from "~/drizzle/schema";
|
||||
import type { User } from "~/packages/database-interface/user";
|
||||
import type { Relationship as apiRelationship } from "~/types/mastodon/relationship";
|
||||
|
||||
export type Relationship = InferSelectModel<typeof Relationships> & {
|
||||
requestedBy: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a new relationship between two users.
|
||||
* @param owner The user who owns the relationship.
|
||||
* @param other The user who is the subject of the relationship.
|
||||
* @returns The newly created relationship.
|
||||
*/
|
||||
export const createNewRelationship = async (
|
||||
owner: User,
|
||||
other: User,
|
||||
): Promise<Relationship> => {
|
||||
return {
|
||||
...(
|
||||
await db
|
||||
.insert(Relationships)
|
||||
.values({
|
||||
ownerId: owner.id,
|
||||
subjectId: other.id,
|
||||
languages: [],
|
||||
following: false,
|
||||
showingReblogs: false,
|
||||
notifying: false,
|
||||
followedBy: false,
|
||||
blocking: false,
|
||||
blockedBy: false,
|
||||
muting: false,
|
||||
mutingNotifications: false,
|
||||
requested: false,
|
||||
domainBlocking: false,
|
||||
endorsed: false,
|
||||
note: "",
|
||||
updatedAt: new Date().toISOString(),
|
||||
})
|
||||
.returning()
|
||||
)[0],
|
||||
requestedBy: false,
|
||||
};
|
||||
};
|
||||
|
||||
export const checkForBidirectionalRelationships = async (
|
||||
user1: User,
|
||||
user2: User,
|
||||
createIfNotExists = true,
|
||||
): Promise<boolean> => {
|
||||
const relationship1 = await db.query.Relationships.findFirst({
|
||||
where: (rel, { and, eq }) =>
|
||||
and(eq(rel.ownerId, user1.id), eq(rel.subjectId, user2.id)),
|
||||
});
|
||||
|
||||
const relationship2 = await db.query.Relationships.findFirst({
|
||||
where: (rel, { and, eq }) =>
|
||||
and(eq(rel.ownerId, user2.id), eq(rel.subjectId, user1.id)),
|
||||
});
|
||||
|
||||
if (!relationship1 && createIfNotExists) {
|
||||
await createNewRelationship(user1, user2);
|
||||
}
|
||||
|
||||
if (!relationship2 && createIfNotExists) {
|
||||
await createNewRelationship(user2, user1);
|
||||
}
|
||||
|
||||
return !!relationship1 && !!relationship2;
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts the relationship to an API-friendly format.
|
||||
* @returns The API-friendly relationship.
|
||||
*/
|
||||
export const relationshipToApi = (rel: Relationship): apiRelationship => {
|
||||
return {
|
||||
blocked_by: rel.blockedBy,
|
||||
blocking: rel.blocking,
|
||||
domain_blocking: rel.domainBlocking,
|
||||
endorsed: rel.endorsed,
|
||||
followed_by: rel.followedBy,
|
||||
following: rel.following,
|
||||
id: rel.subjectId,
|
||||
muting: rel.muting,
|
||||
muting_notifications: rel.mutingNotifications,
|
||||
notifying: rel.notifying,
|
||||
requested: rel.requested,
|
||||
requested_by: rel.requestedBy,
|
||||
showing_reblogs: rel.showingReblogs,
|
||||
note: rel.note,
|
||||
};
|
||||
};
|
||||
461
classes/functions/status.ts
Normal file
461
classes/functions/status.ts
Normal file
|
|
@ -0,0 +1,461 @@
|
|||
import { mentionValidator } from "@/api";
|
||||
import { sanitizeHtml, sanitizeHtmlInline } from "@/sanitization";
|
||||
import markdownItTaskLists from "@hackmd/markdown-it-task-lists";
|
||||
import { getLogger } from "@logtape/logtape";
|
||||
import type { ContentFormat } from "@lysand-org/federation/types";
|
||||
import { config } from "config-manager";
|
||||
import {
|
||||
type InferSelectModel,
|
||||
and,
|
||||
eq,
|
||||
inArray,
|
||||
isNull,
|
||||
or,
|
||||
sql,
|
||||
} from "drizzle-orm";
|
||||
import linkifyHtml from "linkify-html";
|
||||
import {
|
||||
anyOf,
|
||||
charIn,
|
||||
createRegExp,
|
||||
digit,
|
||||
exactly,
|
||||
global,
|
||||
letter,
|
||||
} from "magic-regexp";
|
||||
import MarkdownIt from "markdown-it";
|
||||
import markdownItContainer from "markdown-it-container";
|
||||
import markdownItTocDoneRight from "markdown-it-toc-done-right";
|
||||
import { db } from "~/drizzle/db";
|
||||
import {
|
||||
type Attachments,
|
||||
Instances,
|
||||
type Notes,
|
||||
Users,
|
||||
} from "~/drizzle/schema";
|
||||
import type { Note } from "~/packages/database-interface/note";
|
||||
import { User } from "~/packages/database-interface/user";
|
||||
import type { Application } from "./application";
|
||||
import type { EmojiWithInstance } from "./emoji";
|
||||
import { objectToInboxRequest } from "./federation";
|
||||
import {
|
||||
type UserWithInstance,
|
||||
type UserWithRelations,
|
||||
resolveWebFinger,
|
||||
transformOutputToUserWithRelations,
|
||||
userExtrasTemplate,
|
||||
userRelations,
|
||||
} from "./user";
|
||||
|
||||
export type Status = InferSelectModel<typeof Notes>;
|
||||
|
||||
export type StatusWithRelations = Status & {
|
||||
author: UserWithRelations;
|
||||
mentions: UserWithInstance[];
|
||||
attachments: InferSelectModel<typeof Attachments>[];
|
||||
reblog: StatusWithoutRecursiveRelations | null;
|
||||
emojis: EmojiWithInstance[];
|
||||
reply: Status | null;
|
||||
quote: Status | null;
|
||||
application: Application | null;
|
||||
reblogCount: number;
|
||||
likeCount: number;
|
||||
replyCount: number;
|
||||
pinned: boolean;
|
||||
reblogged: boolean;
|
||||
muted: boolean;
|
||||
liked: boolean;
|
||||
};
|
||||
|
||||
export type StatusWithoutRecursiveRelations = Omit<
|
||||
StatusWithRelations,
|
||||
"reply" | "quote" | "reblog"
|
||||
>;
|
||||
|
||||
/**
|
||||
* Wrapper against the Status object to make it easier to work with
|
||||
* @param query
|
||||
* @returns
|
||||
*/
|
||||
export const findManyNotes = async (
|
||||
query: Parameters<typeof db.query.Notes.findMany>[0],
|
||||
userId?: string,
|
||||
): Promise<StatusWithRelations[]> => {
|
||||
const output = await db.query.Notes.findMany({
|
||||
...query,
|
||||
with: {
|
||||
...query?.with,
|
||||
attachments: true,
|
||||
emojis: {
|
||||
with: {
|
||||
emoji: {
|
||||
with: {
|
||||
instance: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
author: {
|
||||
with: {
|
||||
...userRelations,
|
||||
},
|
||||
extras: userExtrasTemplate("Notes_author"),
|
||||
},
|
||||
mentions: {
|
||||
with: {
|
||||
user: {
|
||||
with: {
|
||||
instance: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
reblog: {
|
||||
with: {
|
||||
attachments: true,
|
||||
emojis: {
|
||||
with: {
|
||||
emoji: {
|
||||
with: {
|
||||
instance: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
likes: true,
|
||||
application: true,
|
||||
mentions: {
|
||||
with: {
|
||||
user: {
|
||||
with: userRelations,
|
||||
extras: userExtrasTemplate(
|
||||
"Notes_reblog_mentions_user",
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
author: {
|
||||
with: {
|
||||
...userRelations,
|
||||
},
|
||||
extras: userExtrasTemplate("Notes_reblog_author"),
|
||||
},
|
||||
},
|
||||
extras: {
|
||||
reblogCount:
|
||||
sql`(SELECT COUNT(*) FROM "Notes" WHERE "Notes"."reblogId" = "Notes_reblog".id)`.as(
|
||||
"reblog_count",
|
||||
),
|
||||
likeCount:
|
||||
sql`(SELECT COUNT(*) FROM "Likes" WHERE "Likes"."likedId" = "Notes_reblog".id)`.as(
|
||||
"like_count",
|
||||
),
|
||||
replyCount:
|
||||
sql`(SELECT COUNT(*) FROM "Notes" WHERE "Notes"."replyId" = "Notes_reblog".id)`.as(
|
||||
"reply_count",
|
||||
),
|
||||
pinned: userId
|
||||
? sql`EXISTS (SELECT 1 FROM "UserToPinnedNotes" WHERE "UserToPinnedNotes"."noteId" = "Notes_reblog".id AND "UserToPinnedNotes"."userId" = ${userId})`.as(
|
||||
"pinned",
|
||||
)
|
||||
: sql`false`.as("pinned"),
|
||||
reblogged: userId
|
||||
? sql`EXISTS (SELECT 1 FROM "Notes" WHERE "Notes"."authorId" = ${userId} AND "Notes"."reblogId" = "Notes_reblog".id)`.as(
|
||||
"reblogged",
|
||||
)
|
||||
: sql`false`.as("reblogged"),
|
||||
muted: userId
|
||||
? sql`EXISTS (SELECT 1 FROM "Relationships" WHERE "Relationships"."ownerId" = ${userId} AND "Relationships"."subjectId" = "Notes_reblog"."authorId" AND "Relationships"."muting" = true)`.as(
|
||||
"muted",
|
||||
)
|
||||
: sql`false`.as("muted"),
|
||||
liked: userId
|
||||
? sql`EXISTS (SELECT 1 FROM "Likes" WHERE "Likes"."likedId" = "Notes_reblog".id AND "Likes"."likerId" = ${userId})`.as(
|
||||
"liked",
|
||||
)
|
||||
: sql`false`.as("liked"),
|
||||
},
|
||||
},
|
||||
reply: true,
|
||||
quote: true,
|
||||
},
|
||||
extras: {
|
||||
reblogCount:
|
||||
sql`(SELECT COUNT(*) FROM "Notes" WHERE "Notes"."reblogId" = "Notes".id)`.as(
|
||||
"reblog_count",
|
||||
),
|
||||
likeCount:
|
||||
sql`(SELECT COUNT(*) FROM "Likes" WHERE "Likes"."likedId" = "Notes".id)`.as(
|
||||
"like_count",
|
||||
),
|
||||
replyCount:
|
||||
sql`(SELECT COUNT(*) FROM "Notes" WHERE "Notes"."replyId" = "Notes".id)`.as(
|
||||
"reply_count",
|
||||
),
|
||||
pinned: userId
|
||||
? sql`EXISTS (SELECT 1 FROM "UserToPinnedNotes" WHERE "UserToPinnedNotes"."noteId" = "Notes".id AND "UserToPinnedNotes"."userId" = ${userId})`.as(
|
||||
"pinned",
|
||||
)
|
||||
: sql`false`.as("pinned"),
|
||||
reblogged: userId
|
||||
? sql`EXISTS (SELECT 1 FROM "Notes" WHERE "Notes"."authorId" = ${userId} AND "Notes"."reblogId" = "Notes".id)`.as(
|
||||
"reblogged",
|
||||
)
|
||||
: sql`false`.as("reblogged"),
|
||||
muted: userId
|
||||
? sql`EXISTS (SELECT 1 FROM "Relationships" WHERE "Relationships"."ownerId" = ${userId} AND "Relationships"."subjectId" = "Notes"."authorId" AND "Relationships"."muting" = true)`.as(
|
||||
"muted",
|
||||
)
|
||||
: sql`false`.as("muted"),
|
||||
liked: userId
|
||||
? sql`EXISTS (SELECT 1 FROM "Likes" WHERE "Likes"."likedId" = "Notes".id AND "Likes"."likerId" = ${userId})`.as(
|
||||
"liked",
|
||||
)
|
||||
: sql`false`.as("liked"),
|
||||
...query?.extras,
|
||||
},
|
||||
});
|
||||
|
||||
return output.map((post) => ({
|
||||
...post,
|
||||
author: transformOutputToUserWithRelations(post.author),
|
||||
mentions: post.mentions.map((mention) => ({
|
||||
...mention.user,
|
||||
endpoints: mention.user.endpoints,
|
||||
})),
|
||||
emojis: (post.emojis ?? []).map((emoji) => emoji.emoji),
|
||||
reblog: post.reblog && {
|
||||
...post.reblog,
|
||||
author: transformOutputToUserWithRelations(post.reblog.author),
|
||||
mentions: post.reblog.mentions.map((mention) => ({
|
||||
...mention.user,
|
||||
endpoints: mention.user.endpoints,
|
||||
})),
|
||||
emojis: (post.reblog.emojis ?? []).map((emoji) => emoji.emoji),
|
||||
reblogCount: Number(post.reblog.reblogCount),
|
||||
likeCount: Number(post.reblog.likeCount),
|
||||
replyCount: Number(post.reblog.replyCount),
|
||||
pinned: Boolean(post.reblog.pinned),
|
||||
reblogged: Boolean(post.reblog.reblogged),
|
||||
muted: Boolean(post.reblog.muted),
|
||||
liked: Boolean(post.reblog.liked),
|
||||
},
|
||||
reblogCount: Number(post.reblogCount),
|
||||
likeCount: Number(post.likeCount),
|
||||
replyCount: Number(post.replyCount),
|
||||
pinned: Boolean(post.pinned),
|
||||
reblogged: Boolean(post.reblogged),
|
||||
muted: Boolean(post.muted),
|
||||
liked: Boolean(post.liked),
|
||||
}));
|
||||
};
|
||||
|
||||
/**
|
||||
* Get people mentioned in the content (match @username or @username@domain.com mentions)
|
||||
* @param text The text to parse mentions from.
|
||||
* @returns An array of users mentioned in the text.
|
||||
*/
|
||||
export const parseTextMentions = async (text: string): Promise<User[]> => {
|
||||
const mentionedPeople = [...text.matchAll(mentionValidator)] ?? [];
|
||||
if (mentionedPeople.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const baseUrlHost = new URL(config.http.base_url).host;
|
||||
|
||||
const isLocal = (host?: string) => host === baseUrlHost || !host;
|
||||
|
||||
const foundUsers = await db
|
||||
.select({
|
||||
id: Users.id,
|
||||
username: Users.username,
|
||||
baseUrl: Instances.baseUrl,
|
||||
})
|
||||
.from(Users)
|
||||
.leftJoin(Instances, eq(Users.instanceId, Instances.id))
|
||||
.where(
|
||||
or(
|
||||
...mentionedPeople.map((person) =>
|
||||
and(
|
||||
eq(Users.username, person?.[1] ?? ""),
|
||||
isLocal(person?.[2])
|
||||
? isNull(Users.instanceId)
|
||||
: eq(Instances.baseUrl, person?.[2] ?? ""),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
const notFoundRemoteUsers = mentionedPeople.filter(
|
||||
(person) =>
|
||||
!(
|
||||
isLocal(person?.[2]) ||
|
||||
foundUsers.find(
|
||||
(user) =>
|
||||
user.username === person?.[1] &&
|
||||
user.baseUrl === person?.[2],
|
||||
)
|
||||
),
|
||||
);
|
||||
|
||||
const finalList =
|
||||
foundUsers.length > 0
|
||||
? await User.manyFromSql(
|
||||
inArray(
|
||||
Users.id,
|
||||
foundUsers.map((u) => u.id),
|
||||
),
|
||||
)
|
||||
: [];
|
||||
|
||||
// Attempt to resolve mentions that were not found
|
||||
for (const person of notFoundRemoteUsers) {
|
||||
const user = await resolveWebFinger(
|
||||
person?.[1] ?? "",
|
||||
person?.[2] ?? "",
|
||||
);
|
||||
|
||||
if (user) {
|
||||
finalList.push(user);
|
||||
}
|
||||
}
|
||||
|
||||
return finalList;
|
||||
};
|
||||
|
||||
export const replaceTextMentions = (text: string, mentions: User[]) => {
|
||||
let finalText = text;
|
||||
for (const mention of mentions) {
|
||||
const user = mention.data;
|
||||
// Replace @username and @username@domain
|
||||
if (user.instance) {
|
||||
finalText = finalText.replace(
|
||||
createRegExp(
|
||||
exactly(`@${user.username}@${user.instance.baseUrl}`),
|
||||
[global],
|
||||
),
|
||||
`<a class="u-url mention" rel="nofollow noopener noreferrer" target="_blank" href="${mention.getUri()}">@${
|
||||
user.username
|
||||
}@${user.instance.baseUrl}</a>`,
|
||||
);
|
||||
} else {
|
||||
finalText = finalText.replace(
|
||||
// Only replace @username if it doesn't have another @ right after
|
||||
createRegExp(
|
||||
exactly(`@${user.username}`)
|
||||
.notBefore(anyOf(letter, digit, charIn("@")))
|
||||
.notAfter(anyOf(letter, digit, charIn("@"))),
|
||||
[global],
|
||||
),
|
||||
`<a class="u-url mention" rel="nofollow noopener noreferrer" target="_blank" href="${mention.getUri()}">@${
|
||||
user.username
|
||||
}</a>`,
|
||||
);
|
||||
|
||||
finalText = finalText.replace(
|
||||
createRegExp(
|
||||
exactly(
|
||||
`@${user.username}@${
|
||||
new URL(config.http.base_url).host
|
||||
}`,
|
||||
),
|
||||
[global],
|
||||
),
|
||||
`<a class="u-url mention" rel="nofollow noopener noreferrer" target="_blank" href="${mention.getUri()}">@${
|
||||
user.username
|
||||
}</a>`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return finalText;
|
||||
};
|
||||
|
||||
export const contentToHtml = async (
|
||||
content: ContentFormat,
|
||||
mentions: User[] = [],
|
||||
inline = false,
|
||||
): Promise<string> => {
|
||||
let htmlContent: string;
|
||||
const sanitizer = inline ? sanitizeHtmlInline : sanitizeHtml;
|
||||
|
||||
if (content["text/html"]) {
|
||||
htmlContent = await sanitizer(content["text/html"].content);
|
||||
} else if (content["text/markdown"]) {
|
||||
htmlContent = await sanitizer(
|
||||
await markdownParse(content["text/markdown"].content),
|
||||
);
|
||||
} else if (content["text/plain"]?.content) {
|
||||
// Split by newline and add <p> tags
|
||||
htmlContent = (await sanitizer(content["text/plain"].content))
|
||||
.split("\n")
|
||||
.map((line) => `<p>${line}</p>`)
|
||||
.join("\n");
|
||||
} else {
|
||||
htmlContent = "";
|
||||
}
|
||||
|
||||
// Replace mentions text
|
||||
htmlContent = await replaceTextMentions(htmlContent, mentions ?? []);
|
||||
|
||||
// Linkify
|
||||
htmlContent = linkifyHtml(htmlContent, {
|
||||
defaultProtocol: "https",
|
||||
validate: {
|
||||
email: () => false,
|
||||
},
|
||||
target: "_blank",
|
||||
rel: "nofollow noopener noreferrer",
|
||||
});
|
||||
|
||||
return htmlContent;
|
||||
};
|
||||
|
||||
export const markdownParse = async (content: string) => {
|
||||
return (await getMarkdownRenderer()).render(content);
|
||||
};
|
||||
|
||||
export const getMarkdownRenderer = () => {
|
||||
const renderer = MarkdownIt({
|
||||
html: true,
|
||||
linkify: true,
|
||||
});
|
||||
|
||||
renderer.use(markdownItTocDoneRight, {
|
||||
containerClass: "toc",
|
||||
level: [1, 2, 3, 4],
|
||||
listType: "ul",
|
||||
listClass: "toc-list",
|
||||
itemClass: "toc-item",
|
||||
linkClass: "toc-link",
|
||||
});
|
||||
|
||||
renderer.use(markdownItTaskLists);
|
||||
|
||||
renderer.use(markdownItContainer);
|
||||
|
||||
return renderer;
|
||||
};
|
||||
|
||||
export const federateNote = async (note: Note) => {
|
||||
for (const user of await note.getUsersToFederateTo()) {
|
||||
// TODO: Add queue system
|
||||
const request = await objectToInboxRequest(
|
||||
note.toLysand(),
|
||||
note.author,
|
||||
user,
|
||||
);
|
||||
|
||||
// Send request
|
||||
const response = await fetch(request, {
|
||||
proxy: config.http.proxy.address,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const logger = getLogger("federation");
|
||||
|
||||
logger.debug`${await response.text()}`;
|
||||
logger.error`Failed to federate status ${note.data.id} to ${user.getUri()}`;
|
||||
}
|
||||
}
|
||||
};
|
||||
11
classes/functions/token.ts
Normal file
11
classes/functions/token.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import type { InferSelectModel } from "drizzle-orm";
|
||||
import type { Tokens } from "~/drizzle/schema";
|
||||
|
||||
/**
|
||||
* The type of token.
|
||||
*/
|
||||
export enum TokenType {
|
||||
Bearer = "Bearer",
|
||||
}
|
||||
|
||||
export type Token = InferSelectModel<typeof Tokens>;
|
||||
552
classes/functions/user.ts
Normal file
552
classes/functions/user.ts
Normal file
|
|
@ -0,0 +1,552 @@
|
|||
import { getLogger } from "@logtape/logtape";
|
||||
import type {
|
||||
Follow,
|
||||
FollowAccept,
|
||||
FollowReject,
|
||||
} from "@lysand-org/federation/types";
|
||||
import { config } from "config-manager";
|
||||
import { type InferSelectModel, and, eq, sql } from "drizzle-orm";
|
||||
import { db } from "~/drizzle/db";
|
||||
import {
|
||||
Applications,
|
||||
Instances,
|
||||
Notifications,
|
||||
Relationships,
|
||||
type Roles,
|
||||
Tokens,
|
||||
Users,
|
||||
} from "~/drizzle/schema";
|
||||
import { User } from "~/packages/database-interface/user";
|
||||
import type { Application } from "./application";
|
||||
import type { EmojiWithInstance } from "./emoji";
|
||||
import { objectToInboxRequest } from "./federation";
|
||||
import {
|
||||
type Relationship,
|
||||
checkForBidirectionalRelationships,
|
||||
createNewRelationship,
|
||||
} from "./relationship";
|
||||
import type { Token } from "./token";
|
||||
|
||||
export type UserType = InferSelectModel<typeof Users>;
|
||||
|
||||
export type UserWithInstance = UserType & {
|
||||
instance: InferSelectModel<typeof Instances> | null;
|
||||
};
|
||||
|
||||
export type UserWithRelations = UserType & {
|
||||
instance: InferSelectModel<typeof Instances> | null;
|
||||
emojis: EmojiWithInstance[];
|
||||
followerCount: number;
|
||||
followingCount: number;
|
||||
statusCount: number;
|
||||
roles: InferSelectModel<typeof Roles>[];
|
||||
};
|
||||
|
||||
export const userRelations: {
|
||||
instance: true;
|
||||
emojis: {
|
||||
with: {
|
||||
emoji: {
|
||||
with: {
|
||||
instance: true;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
roles: {
|
||||
with: {
|
||||
role: true;
|
||||
};
|
||||
};
|
||||
} = {
|
||||
instance: true,
|
||||
emojis: {
|
||||
with: {
|
||||
emoji: {
|
||||
with: {
|
||||
instance: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
roles: {
|
||||
with: {
|
||||
role: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const userExtras = {
|
||||
followerCount:
|
||||
sql`(SELECT COUNT(*) FROM "Relationships" "relationships" WHERE ("relationships"."ownerId" = "Users".id AND "relationships"."following" = true))`.as(
|
||||
"follower_count",
|
||||
),
|
||||
followingCount:
|
||||
sql`(SELECT COUNT(*) FROM "Relationships" "relationshipSubjects" WHERE ("relationshipSubjects"."subjectId" = "Users".id AND "relationshipSubjects"."following" = true))`.as(
|
||||
"following_count",
|
||||
),
|
||||
statusCount:
|
||||
sql`(SELECT COUNT(*) FROM "Notes" WHERE "Notes"."authorId" = "Users".id)`.as(
|
||||
"status_count",
|
||||
),
|
||||
};
|
||||
|
||||
export const userExtrasTemplate = (name: string) => ({
|
||||
// @ts-ignore
|
||||
followerCount: sql([
|
||||
`(SELECT COUNT(*) FROM "Relationships" "relationships" WHERE ("relationships"."ownerId" = "${name}".id AND "relationships"."following" = true))`,
|
||||
]).as("follower_count"),
|
||||
// @ts-ignore
|
||||
followingCount: sql([
|
||||
`(SELECT COUNT(*) FROM "Relationships" "relationshipSubjects" WHERE ("relationshipSubjects"."subjectId" = "${name}".id AND "relationshipSubjects"."following" = true))`,
|
||||
]).as("following_count"),
|
||||
// @ts-ignore
|
||||
statusCount: sql([
|
||||
`(SELECT COUNT(*) FROM "Notes" WHERE "Notes"."authorId" = "${name}".id)`,
|
||||
]).as("status_count"),
|
||||
});
|
||||
|
||||
export interface AuthData {
|
||||
user: User | null;
|
||||
token: string;
|
||||
application: Application | null;
|
||||
}
|
||||
|
||||
export const getFromHeader = async (value: string): Promise<AuthData> => {
|
||||
const token = value.split(" ")[1];
|
||||
|
||||
const { user, application } =
|
||||
await retrieveUserAndApplicationFromToken(token);
|
||||
|
||||
return { user, token, application };
|
||||
};
|
||||
|
||||
export const followRequestUser = async (
|
||||
follower: User,
|
||||
followee: User,
|
||||
relationshipId: string,
|
||||
reblogs = false,
|
||||
notify = false,
|
||||
languages: string[] = [],
|
||||
): Promise<Relationship> => {
|
||||
const isRemote = followee.isRemote();
|
||||
|
||||
await db
|
||||
.update(Relationships)
|
||||
.set({
|
||||
following: isRemote ? false : !followee.data.isLocked,
|
||||
requested: isRemote ? true : followee.data.isLocked,
|
||||
showingReblogs: reblogs,
|
||||
notifying: notify,
|
||||
languages: languages,
|
||||
})
|
||||
.where(eq(Relationships.id, relationshipId));
|
||||
|
||||
// Set requested_by on other side
|
||||
await db
|
||||
.update(Relationships)
|
||||
.set({
|
||||
requestedBy: isRemote ? true : followee.data.isLocked,
|
||||
followedBy: isRemote ? false : followee.data.isLocked,
|
||||
})
|
||||
.where(
|
||||
and(
|
||||
eq(Relationships.ownerId, followee.id),
|
||||
eq(Relationships.subjectId, follower.id),
|
||||
),
|
||||
);
|
||||
|
||||
const updatedRelationship = await db.query.Relationships.findFirst({
|
||||
where: (rel, { eq }) => eq(rel.id, relationshipId),
|
||||
});
|
||||
|
||||
if (!updatedRelationship) {
|
||||
throw new Error("Failed to update relationship");
|
||||
}
|
||||
|
||||
if (isRemote) {
|
||||
// Federate
|
||||
// TODO: Make database job
|
||||
const request = await objectToInboxRequest(
|
||||
followRequestToLysand(follower, followee),
|
||||
follower,
|
||||
followee,
|
||||
);
|
||||
|
||||
// Send request
|
||||
const response = await fetch(request, {
|
||||
proxy: config.http.proxy.address,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const logger = getLogger("federation");
|
||||
|
||||
logger.debug`${await response.text()}`;
|
||||
logger.error`Failed to federate follow request from ${follower.id} to ${followee.getUri()}`;
|
||||
|
||||
await db
|
||||
.update(Relationships)
|
||||
.set({
|
||||
following: false,
|
||||
requested: false,
|
||||
})
|
||||
.where(eq(Relationships.id, relationshipId));
|
||||
|
||||
const result = await db.query.Relationships.findFirst({
|
||||
where: (rel, { eq }) => eq(rel.id, relationshipId),
|
||||
});
|
||||
|
||||
if (!result) {
|
||||
throw new Error("Failed to update relationship");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
} else {
|
||||
await db.insert(Notifications).values({
|
||||
accountId: follower.id,
|
||||
type: followee.data.isLocked ? "follow_request" : "follow",
|
||||
notifiedId: followee.id,
|
||||
});
|
||||
}
|
||||
|
||||
return updatedRelationship;
|
||||
};
|
||||
|
||||
export const sendFollowAccept = async (follower: User, followee: User) => {
|
||||
// TODO: Make database job
|
||||
const request = await objectToInboxRequest(
|
||||
followAcceptToLysand(follower, followee),
|
||||
followee,
|
||||
follower,
|
||||
);
|
||||
|
||||
// Send request
|
||||
const response = await fetch(request, {
|
||||
proxy: config.http.proxy.address,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const logger = getLogger("federation");
|
||||
|
||||
logger.debug`${await response.text()}`;
|
||||
logger.error`Failed to federate follow accept from ${followee.id} to ${follower.getUri()}`;
|
||||
}
|
||||
};
|
||||
|
||||
export const sendFollowReject = async (follower: User, followee: User) => {
|
||||
// TODO: Make database job
|
||||
const request = await objectToInboxRequest(
|
||||
followRejectToLysand(follower, followee),
|
||||
followee,
|
||||
follower,
|
||||
);
|
||||
|
||||
// Send request
|
||||
const response = await fetch(request, {
|
||||
proxy: config.http.proxy.address,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const logger = getLogger("federation");
|
||||
|
||||
logger.debug`${await response.text()}`;
|
||||
logger.error`Failed to federate follow reject from ${followee.id} to ${follower.getUri()}`;
|
||||
}
|
||||
};
|
||||
|
||||
export const transformOutputToUserWithRelations = (
|
||||
user: Omit<UserType, "endpoints"> & {
|
||||
followerCount: unknown;
|
||||
followingCount: unknown;
|
||||
statusCount: unknown;
|
||||
emojis: {
|
||||
userId: string;
|
||||
emojiId: string;
|
||||
emoji?: EmojiWithInstance;
|
||||
}[];
|
||||
instance: InferSelectModel<typeof Instances> | null;
|
||||
roles: {
|
||||
userId: string;
|
||||
roleId: string;
|
||||
role?: InferSelectModel<typeof Roles>;
|
||||
}[];
|
||||
endpoints: unknown;
|
||||
},
|
||||
): UserWithRelations => {
|
||||
return {
|
||||
...user,
|
||||
followerCount: Number(user.followerCount),
|
||||
followingCount: Number(user.followingCount),
|
||||
statusCount: Number(user.statusCount),
|
||||
endpoints:
|
||||
user.endpoints ??
|
||||
({} as Partial<{
|
||||
dislikes: string;
|
||||
featured: string;
|
||||
likes: string;
|
||||
followers: string;
|
||||
following: string;
|
||||
inbox: string;
|
||||
outbox: string;
|
||||
}>),
|
||||
emojis: user.emojis.map(
|
||||
(emoji) =>
|
||||
(emoji as unknown as Record<string, object>)
|
||||
.emoji as EmojiWithInstance,
|
||||
),
|
||||
roles: user.roles
|
||||
.map((role) => role.role)
|
||||
.filter(Boolean) as InferSelectModel<typeof Roles>[],
|
||||
};
|
||||
};
|
||||
|
||||
export const findManyUsers = async (
|
||||
query: Parameters<typeof db.query.Users.findMany>[0],
|
||||
): Promise<UserWithRelations[]> => {
|
||||
const output = await db.query.Users.findMany({
|
||||
...query,
|
||||
with: {
|
||||
...userRelations,
|
||||
...query?.with,
|
||||
},
|
||||
extras: {
|
||||
...userExtras,
|
||||
...query?.extras,
|
||||
},
|
||||
});
|
||||
|
||||
return output.map((user) => transformOutputToUserWithRelations(user));
|
||||
};
|
||||
|
||||
/**
|
||||
* Resolves a WebFinger identifier to a user.
|
||||
* @param identifier Either a UUID or a username
|
||||
*/
|
||||
export const resolveWebFinger = async (
|
||||
identifier: string,
|
||||
host: string,
|
||||
): Promise<User | null> => {
|
||||
// Check if user not already in database
|
||||
const foundUser = await db
|
||||
.select()
|
||||
.from(Users)
|
||||
.innerJoin(Instances, eq(Users.instanceId, Instances.id))
|
||||
.where(and(eq(Users.username, identifier), eq(Instances.baseUrl, host)))
|
||||
.limit(1);
|
||||
|
||||
if (foundUser[0]) {
|
||||
return await User.fromId(foundUser[0].Users.id);
|
||||
}
|
||||
|
||||
const hostWithProtocol = host.startsWith("http") ? host : `https://${host}`;
|
||||
|
||||
const response = await fetch(
|
||||
new URL(
|
||||
`/.well-known/webfinger?${new URLSearchParams({
|
||||
resource: `acct:${identifier}@${host}`,
|
||||
})}`,
|
||||
hostWithProtocol,
|
||||
),
|
||||
{
|
||||
method: "GET",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
},
|
||||
proxy: config.http.proxy.address,
|
||||
},
|
||||
);
|
||||
|
||||
if (response.status === 404) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = (await response.json()) as {
|
||||
subject: string;
|
||||
links: {
|
||||
rel: string;
|
||||
type: string;
|
||||
href: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
if (!(data.subject && data.links)) {
|
||||
throw new Error(
|
||||
"Invalid WebFinger data (missing subject or links from response)",
|
||||
);
|
||||
}
|
||||
|
||||
const relevantLink = data.links.find((link) => link.rel === "self");
|
||||
|
||||
if (!relevantLink) {
|
||||
throw new Error(
|
||||
"Invalid WebFinger data (missing link with rel: 'self')",
|
||||
);
|
||||
}
|
||||
|
||||
return User.resolve(relevantLink.href);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves a user from a token.
|
||||
* @param access_token The access token to retrieve the user from.
|
||||
* @returns The user associated with the given access token.
|
||||
*/
|
||||
export const retrieveUserFromToken = async (
|
||||
accessToken: string,
|
||||
): Promise<User | null> => {
|
||||
if (!accessToken) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const token = await retrieveToken(accessToken);
|
||||
|
||||
if (!token?.userId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const user = await User.fromId(token.userId);
|
||||
|
||||
return user;
|
||||
};
|
||||
|
||||
export const retrieveUserAndApplicationFromToken = async (
|
||||
accessToken: string,
|
||||
): Promise<{
|
||||
user: User | null;
|
||||
application: Application | null;
|
||||
}> => {
|
||||
if (!accessToken) {
|
||||
return { user: null, application: null };
|
||||
}
|
||||
|
||||
const output = (
|
||||
await db
|
||||
.select({
|
||||
token: Tokens,
|
||||
application: Applications,
|
||||
})
|
||||
.from(Tokens)
|
||||
.leftJoin(Applications, eq(Tokens.applicationId, Applications.id))
|
||||
.where(eq(Tokens.accessToken, accessToken))
|
||||
.limit(1)
|
||||
)[0];
|
||||
|
||||
if (!output?.token.userId) {
|
||||
return { user: null, application: null };
|
||||
}
|
||||
|
||||
const user = await User.fromId(output.token.userId);
|
||||
|
||||
return { user, application: output.application ?? null };
|
||||
};
|
||||
|
||||
export const retrieveToken = async (
|
||||
accessToken: string,
|
||||
): Promise<Token | null> => {
|
||||
if (!accessToken) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
(await db.query.Tokens.findFirst({
|
||||
where: (tokens, { eq }) => eq(tokens.accessToken, accessToken),
|
||||
})) ?? null
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the relationship to another user.
|
||||
* @param other The other user to get the relationship to.
|
||||
* @returns The relationship to the other user.
|
||||
*/
|
||||
export const getRelationshipToOtherUser = async (
|
||||
user: User,
|
||||
other: User,
|
||||
): Promise<Relationship> => {
|
||||
await checkForBidirectionalRelationships(user, other);
|
||||
|
||||
const foundRelationship = await db.query.Relationships.findFirst({
|
||||
where: (relationship, { and, eq }) =>
|
||||
and(
|
||||
eq(relationship.ownerId, user.id),
|
||||
eq(relationship.subjectId, other.id),
|
||||
),
|
||||
});
|
||||
|
||||
if (!foundRelationship) {
|
||||
// Create new relationship
|
||||
|
||||
const newRelationship = await createNewRelationship(user, other);
|
||||
|
||||
return newRelationship;
|
||||
}
|
||||
|
||||
return foundRelationship;
|
||||
};
|
||||
|
||||
export const followRequestToLysand = (
|
||||
follower: User,
|
||||
followee: User,
|
||||
): Follow => {
|
||||
if (follower.isRemote()) {
|
||||
throw new Error("Follower must be a local user");
|
||||
}
|
||||
|
||||
if (!followee.isRemote()) {
|
||||
throw new Error("Followee must be a remote user");
|
||||
}
|
||||
|
||||
if (!followee.data.uri) {
|
||||
throw new Error("Followee must have a URI in database");
|
||||
}
|
||||
|
||||
const id = crypto.randomUUID();
|
||||
|
||||
return {
|
||||
type: "Follow",
|
||||
id: id,
|
||||
author: follower.getUri(),
|
||||
followee: followee.getUri(),
|
||||
created_at: new Date().toISOString(),
|
||||
uri: new URL(`/follows/${id}`, config.http.base_url).toString(),
|
||||
};
|
||||
};
|
||||
|
||||
export const followAcceptToLysand = (
|
||||
follower: User,
|
||||
followee: User,
|
||||
): FollowAccept => {
|
||||
if (!follower.isRemote()) {
|
||||
throw new Error("Follower must be a remote user");
|
||||
}
|
||||
|
||||
if (followee.isRemote()) {
|
||||
throw new Error("Followee must be a local user");
|
||||
}
|
||||
|
||||
if (!follower.data.uri) {
|
||||
throw new Error("Follower must have a URI in database");
|
||||
}
|
||||
|
||||
const id = crypto.randomUUID();
|
||||
|
||||
return {
|
||||
type: "FollowAccept",
|
||||
id: id,
|
||||
author: followee.getUri(),
|
||||
created_at: new Date().toISOString(),
|
||||
follower: follower.getUri(),
|
||||
uri: new URL(`/follows/${id}`, config.http.base_url).toString(),
|
||||
};
|
||||
};
|
||||
|
||||
export const followRejectToLysand = (
|
||||
follower: User,
|
||||
followee: User,
|
||||
): FollowReject => {
|
||||
return {
|
||||
...followAcceptToLysand(follower, followee),
|
||||
type: "FollowReject",
|
||||
};
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue