refactor: 🚨 Turn every linter rule on and fix issues (there were a LOT :3)

This commit is contained in:
Jesse Wierzbinski 2024-06-12 16:26:43 -10:00
parent 2e98859153
commit a1e02d0d78
No known key found for this signature in database
177 changed files with 1826 additions and 1248 deletions

View file

@ -1,5 +1,5 @@
{ {
"$schema": "https://biomejs.dev/schemas/1.6.4/schema.json", "$schema": "https://biomejs.dev/schemas/1.8.1/schema.json",
"organizeImports": { "organizeImports": {
"enabled": true, "enabled": true,
"ignore": ["node_modules", "dist", "glitch", "glitch-dev"] "ignore": ["node_modules", "dist", "glitch", "glitch-dev"]
@ -7,7 +7,48 @@
"linter": { "linter": {
"enabled": true, "enabled": true,
"rules": { "rules": {
"recommended": true "all": true,
"correctness": {
"noNodejsModules": "off"
},
"style": {
"noDefaultExport": "off",
"noParameterProperties": "off",
"noNamespaceImport": "off",
"useFilenamingConvention": "off",
"useNamingConvention": {
"level": "warn",
"options": {
"requireAscii": false,
"strictCase": false,
"conventions": [
{
"selector": {
"kind": "typeProperty"
},
"formats": [
"camelCase",
"CONSTANT_CASE",
"PascalCase",
"snake_case"
]
},
{
"selector": {
"kind": "objectLiteralProperty",
"scope": "any"
},
"formats": [
"camelCase",
"CONSTANT_CASE",
"PascalCase",
"snake_case"
]
}
]
}
}
}
}, },
"ignore": ["node_modules", "dist", "glitch", "glitch-dev"] "ignore": ["node_modules", "dist", "glitch", "glitch-dev"]
}, },
@ -16,5 +57,8 @@
"indentStyle": "space", "indentStyle": "space",
"indentWidth": 4, "indentWidth": 4,
"ignore": ["node_modules", "dist", "glitch", "glitch-dev"] "ignore": ["node_modules", "dist", "glitch", "glitch-dev"]
},
"javascript": {
"globals": ["Bun", "HTMLRewriter"]
} }
} }

View file

@ -1,5 +1,4 @@
import { $ } from "bun"; import { $ } from "bun";
import chalk from "chalk";
import ora from "ora"; import ora from "ora";
import { routes } from "~/routes"; import { routes } from "~/routes";
@ -21,7 +20,6 @@ await Bun.build({
external: ["unzipit"], external: ["unzipit"],
}).then((output) => { }).then((output) => {
if (!output.success) { if (!output.success) {
console.log(output.logs);
process.exit(1); process.exit(1);
} }
}); });
@ -56,9 +54,3 @@ await $`cp package.json dist/package.json`;
await $`cp cli/theme.json dist/cli/theme.json`; await $`cp cli/theme.json dist/cli/theme.json`;
buildSpinner.stop(); buildSpinner.stop();
console.log(
`${chalk.green("✓")} Built project. You can now run it with ${chalk.green(
"bun run dist/index.js",
)}`,
);

View file

@ -2,7 +2,7 @@ import { consoleLogger } from "@/loggers";
import { Command } from "@oclif/core"; import { Command } from "@oclif/core";
import { setupDatabase } from "~/drizzle/db"; import { setupDatabase } from "~/drizzle/db";
export abstract class BaseCommand<T extends typeof Command> extends Command { export abstract class BaseCommand<_T extends typeof Command> extends Command {
protected async init(): Promise<void> { protected async init(): Promise<void> {
await super.init(); await super.init();

View file

@ -2,7 +2,7 @@ import { Args } from "@oclif/core";
import chalk from "chalk"; import chalk from "chalk";
import ora from "ora"; import ora from "ora";
import { BaseCommand } from "~/cli/base"; import { BaseCommand } from "~/cli/base";
import { getUrl } from "~/database/entities/Attachment"; import { getUrl } from "~/database/entities/attachment";
import { db } from "~/drizzle/db"; import { db } from "~/drizzle/db";
import { Emojis } from "~/drizzle/schema"; import { Emojis } from "~/drizzle/schema";
import { config } from "~/packages/config-manager"; import { config } from "~/packages/config-manager";

View file

@ -5,7 +5,7 @@ import { lookup } from "mime-types";
import ora from "ora"; import ora from "ora";
import { unzip } from "unzipit"; import { unzip } from "unzipit";
import { BaseCommand } from "~/cli/base"; import { BaseCommand } from "~/cli/base";
import { getUrl } from "~/database/entities/Attachment"; import { getUrl } from "~/database/entities/attachment";
import { db } from "~/drizzle/db"; import { db } from "~/drizzle/db";
import { Emojis } from "~/drizzle/schema"; import { Emojis } from "~/drizzle/schema";
import { config } from "~/packages/config-manager"; import { config } from "~/packages/config-manager";

View file

@ -33,14 +33,14 @@ export default class Start extends BaseCommand<typeof Start> {
public async run(): Promise<void> { public async run(): Promise<void> {
const { flags } = await this.parse(Start); const { flags } = await this.parse(Start);
const numCPUs = flags["all-threads"] ? os.cpus().length : flags.threads; const numCpUs = flags["all-threads"] ? os.cpus().length : flags.threads;
// Check if index is a JS or TS file (depending on the environment) // Check if index is a JS or TS file (depending on the environment)
const index = (await Bun.file("index.ts").exists()) const index = (await Bun.file("index.ts").exists())
? "index.ts" ? "index.ts"
: "index.js"; : "index.js";
for (let i = 0; i < numCPUs; i++) { for (let i = 0; i < numCpUs; i++) {
const args = ["bun", index]; const args = ["bun", index];
if (i !== 0 || flags.silent) { if (i !== 0 || flags.silent) {
args.push("--silent"); args.push("--silent");

View file

@ -137,7 +137,7 @@ export default class UserCreate extends BaseCommand<typeof UserCreate> {
), ),
); );
if (!flags.format && !flags["set-password"]) { if (!(flags.format || flags["set-password"])) {
const link = ""; const link = "";
this.log( this.log(

View file

@ -1,7 +1,7 @@
import type { InferSelectModel } from "drizzle-orm"; import type { InferSelectModel } from "drizzle-orm";
import { db } from "~/drizzle/db"; import { db } from "~/drizzle/db";
import type { Applications } from "~/drizzle/schema"; import type { Applications } from "~/drizzle/schema";
import type { Application as APIApplication } from "~/types/mastodon/application"; import type { Application as apiApplication } from "~/types/mastodon/application";
export type Application = InferSelectModel<typeof Applications>; export type Application = InferSelectModel<typeof Applications>;
@ -27,7 +27,7 @@ export const getFromToken = async (
* Converts this application to an API application. * Converts this application to an API application.
* @returns The API application representation of this application. * @returns The API application representation of this application.
*/ */
export const applicationToAPI = (app: Application): APIApplication => { export const applicationToApi = (app: Application): apiApplication => {
return { return {
name: app.name, name: app.name,
website: app.website, website: app.website,

View file

@ -2,7 +2,7 @@ import { MediaBackendType } from "media-manager";
import type { Config } from "~/packages/config-manager"; import type { Config } from "~/packages/config-manager";
export const getUrl = (name: string, config: Config) => { export const getUrl = (name: string, config: Config) => {
if (config.media.backend === MediaBackendType.LOCAL) { if (config.media.backend === MediaBackendType.Local) {
return new URL(`/media/${name}`, config.http.base_url).toString(); return new URL(`/media/${name}`, config.http.base_url).toString();
} }
if (config.media.backend === MediaBackendType.S3) { if (config.media.backend === MediaBackendType.S3) {

View file

@ -4,8 +4,8 @@ import type { EntityValidator } from "@lysand-org/federation";
import { type InferSelectModel, and, eq } from "drizzle-orm"; import { type InferSelectModel, and, eq } from "drizzle-orm";
import { db } from "~/drizzle/db"; import { db } from "~/drizzle/db";
import { Emojis, Instances } from "~/drizzle/schema"; import { Emojis, Instances } from "~/drizzle/schema";
import type { Emoji as APIEmoji } from "~/types/mastodon/emoji"; import type { Emoji as apiEmoji } from "~/types/mastodon/emoji";
import { addInstanceIfNotExists } from "./Instance"; import { addInstanceIfNotExists } from "./instance";
export type EmojiWithInstance = InferSelectModel<typeof Emojis> & { export type EmojiWithInstance = InferSelectModel<typeof Emojis> & {
instance: InferSelectModel<typeof Instances> | null; instance: InferSelectModel<typeof Instances> | null;
@ -18,7 +18,9 @@ export type EmojiWithInstance = InferSelectModel<typeof Emojis> & {
*/ */
export const parseEmojis = async (text: string) => { export const parseEmojis = async (text: string) => {
const matches = text.match(emojiValidatorWithColons); const matches = text.match(emojiValidatorWithColons);
if (!matches) return []; if (!matches) {
return [];
}
const emojis = await db.query.Emojis.findMany({ const emojis = await db.query.Emojis.findMany({
where: (emoji, { eq, or }) => where: (emoji, { eq, or }) =>
or( or(
@ -56,11 +58,12 @@ export const fetchEmoji = async (
) )
.limit(1); .limit(1);
if (existingEmoji[0]) if (existingEmoji[0]) {
return { return {
...existingEmoji[0].Emojis, ...existingEmoji[0].Emojis,
instance: existingEmoji[0].Instances, instance: existingEmoji[0].Instances,
}; };
}
const foundInstance = host ? await addInstanceIfNotExists(host) : null; const foundInstance = host ? await addInstanceIfNotExists(host) : null;
@ -90,7 +93,7 @@ export const fetchEmoji = async (
* Converts the emoji to an APIEmoji object. * Converts the emoji to an APIEmoji object.
* @returns The APIEmoji object. * @returns The APIEmoji object.
*/ */
export const emojiToAPI = (emoji: EmojiWithInstance): APIEmoji => { export const emojiToApi = (emoji: EmojiWithInstance): apiEmoji => {
return { return {
// @ts-expect-error ID is not in regular Mastodon API // @ts-expect-error ID is not in regular Mastodon API
id: emoji.id, id: emoji.id,

View file

@ -7,7 +7,7 @@ import { config } from "config-manager";
import type { User } from "~/packages/database-interface/user"; import type { User } from "~/packages/database-interface/user";
import { LogLevel, LogManager } from "~/packages/log-manager"; import { LogLevel, LogManager } from "~/packages/log-manager";
export const localObjectURI = (id: string) => export const localObjectUri = (id: string) =>
new URL(`/objects/${id}`, config.http.base_url).toString(); new URL(`/objects/${id}`, config.http.base_url).toString();
export const objectToInboxRequest = async ( export const objectToInboxRequest = async (
@ -52,14 +52,14 @@ export const objectToInboxRequest = async (
// Log public key // Log public key
new LogManager(Bun.stdout).log( new LogManager(Bun.stdout).log(
LogLevel.DEBUG, LogLevel.Debug,
"Inbox.Signature", "Inbox.Signature",
`Sender public key: ${author.data.publicKey}`, `Sender public key: ${author.data.publicKey}`,
); );
// Log signed string // Log signed string
new LogManager(Bun.stdout).log( new LogManager(Bun.stdout).log(
LogLevel.DEBUG, LogLevel.Debug,
"Inbox.Signature", "Inbox.Signature",
`Signed string:\n${signedString}`, `Signed string:\n${signedString}`,
); );

View file

@ -19,9 +19,9 @@ export const addInstanceIfNotExists = async (url: string) => {
where: (instance, { eq }) => eq(instance.baseUrl, host), where: (instance, { eq }) => eq(instance.baseUrl, host),
}); });
if (found) return found; if (found) {
return found;
console.log(`Fetching instance metadata for ${origin}`); }
// Fetch the instance configuration // Fetch the instance configuration
const metadata = (await fetch(new URL("/.well-known/lysand", origin)).then( const metadata = (await fetch(new URL("/.well-known/lysand", origin)).then(

View file

@ -3,14 +3,14 @@ import { db } from "~/drizzle/db";
import type { Notifications } from "~/drizzle/schema"; import type { Notifications } from "~/drizzle/schema";
import { Note } from "~/packages/database-interface/note"; import { Note } from "~/packages/database-interface/note";
import { User } from "~/packages/database-interface/user"; import { User } from "~/packages/database-interface/user";
import type { Notification as APINotification } from "~/types/mastodon/notification"; import type { Notification as apiNotification } from "~/types/mastodon/notification";
import type { StatusWithRelations } from "./Status"; import type { StatusWithRelations } from "./status";
import { import {
type UserWithRelations, type UserWithRelations,
transformOutputToUserWithRelations, transformOutputToUserWithRelations,
userExtrasTemplate, userExtrasTemplate,
userRelations, userRelations,
} from "./User"; } from "./user";
export type Notification = InferSelectModel<typeof Notifications>; export type Notification = InferSelectModel<typeof Notifications>;
@ -48,17 +48,17 @@ export const findManyNotifications = async (
); );
}; };
export const notificationToAPI = async ( export const notificationToApi = async (
notification: NotificationWithRelations, notification: NotificationWithRelations,
): Promise<APINotification> => { ): Promise<apiNotification> => {
const account = new User(notification.account); const account = new User(notification.account);
return { return {
account: account.toAPI(), account: account.toApi(),
created_at: new Date(notification.createdAt).toISOString(), created_at: new Date(notification.createdAt).toISOString(),
id: notification.id, id: notification.id,
type: notification.type, type: notification.type,
status: notification.status status: notification.status
? await new Note(notification.status).toAPI(account) ? await new Note(notification.status).toApi(account)
: undefined, : undefined,
}; };
}; };

View file

@ -2,7 +2,7 @@ import type { InferSelectModel } from "drizzle-orm";
import { db } from "~/drizzle/db"; import { db } from "~/drizzle/db";
import { Relationships } from "~/drizzle/schema"; import { Relationships } from "~/drizzle/schema";
import type { User } from "~/packages/database-interface/user"; import type { User } from "~/packages/database-interface/user";
import type { Relationship as APIRelationship } from "~/types/mastodon/relationship"; import type { Relationship as apiRelationship } from "~/types/mastodon/relationship";
export type Relationship = InferSelectModel<typeof Relationships> & { export type Relationship = InferSelectModel<typeof Relationships> & {
requestedBy: boolean; requestedBy: boolean;
@ -76,7 +76,7 @@ export const checkForBidirectionalRelationships = async (
* Converts the relationship to an API-friendly format. * Converts the relationship to an API-friendly format.
* @returns The API-friendly relationship. * @returns The API-friendly relationship.
*/ */
export const relationshipToAPI = (rel: Relationship): APIRelationship => { export const relationshipToApi = (rel: Relationship): apiRelationship => {
return { return {
blocked_by: rel.blockedBy, blocked_by: rel.blockedBy,
blocking: rel.blocking, blocking: rel.blocking,

View file

@ -36,9 +36,9 @@ import {
import type { Note } from "~/packages/database-interface/note"; import type { Note } from "~/packages/database-interface/note";
import { User } from "~/packages/database-interface/user"; import { User } from "~/packages/database-interface/user";
import { LogLevel } from "~/packages/log-manager"; import { LogLevel } from "~/packages/log-manager";
import type { Application } from "./Application"; import type { Application } from "./application";
import type { EmojiWithInstance } from "./Emoji"; import type { EmojiWithInstance } from "./emoji";
import { objectToInboxRequest } from "./Federation"; import { objectToInboxRequest } from "./federation";
import { import {
type UserWithInstance, type UserWithInstance,
type UserWithRelations, type UserWithRelations,
@ -46,7 +46,7 @@ import {
transformOutputToUserWithRelations, transformOutputToUserWithRelations,
userExtrasTemplate, userExtrasTemplate,
userRelations, userRelations,
} from "./User"; } from "./user";
export type Status = InferSelectModel<typeof Notes>; export type Status = InferSelectModel<typeof Notes>;
@ -258,7 +258,9 @@ export const findManyNotes = async (
*/ */
export const parseTextMentions = async (text: string): Promise<User[]> => { export const parseTextMentions = async (text: string): Promise<User[]> => {
const mentionedPeople = [...text.matchAll(mentionValidator)] ?? []; const mentionedPeople = [...text.matchAll(mentionValidator)] ?? [];
if (mentionedPeople.length === 0) return []; if (mentionedPeople.length === 0) {
return [];
}
const baseUrlHost = new URL(config.http.base_url).host; const baseUrlHost = new URL(config.http.base_url).host;
@ -287,11 +289,13 @@ export const parseTextMentions = async (text: string): Promise<User[]> => {
const notFoundRemoteUsers = mentionedPeople.filter( const notFoundRemoteUsers = mentionedPeople.filter(
(person) => (person) =>
!isLocal(person?.[2]) && !(
!foundUsers.find( isLocal(person?.[2]) ||
foundUsers.find(
(user) => (user) =>
user.username === person?.[1] && user.username === person?.[1] &&
user.baseUrl === person?.[2], user.baseUrl === person?.[2],
)
), ),
); );
@ -320,7 +324,7 @@ export const parseTextMentions = async (text: string): Promise<User[]> => {
return finalList; return finalList;
}; };
export const replaceTextMentions = async (text: string, mentions: User[]) => { export const replaceTextMentions = (text: string, mentions: User[]) => {
let finalText = text; let finalText = text;
for (const mention of mentions) { for (const mention of mentions) {
const user = mention.data; const user = mention.data;
@ -412,7 +416,7 @@ export const markdownParse = async (content: string) => {
return (await getMarkdownRenderer()).render(content); return (await getMarkdownRenderer()).render(content);
}; };
export const getMarkdownRenderer = async () => { export const getMarkdownRenderer = () => {
const renderer = MarkdownIt({ const renderer = MarkdownIt({
html: true, html: true,
linkify: true, linkify: true,
@ -448,12 +452,12 @@ export const federateNote = async (note: Note) => {
if (!response.ok) { if (!response.ok) {
dualLogger.log( dualLogger.log(
LogLevel.DEBUG, LogLevel.Debug,
"Federation.Status", "Federation.Status",
await response.text(), await response.text(),
); );
dualLogger.log( dualLogger.log(
LogLevel.ERROR, LogLevel.Error,
"Federation.Status", "Federation.Status",
`Failed to federate status ${note.data.id} to ${user.getUri()}`, `Failed to federate status ${note.data.id} to ${user.getUri()}`,
); );

View file

@ -5,7 +5,7 @@ import type { Tokens } from "~/drizzle/schema";
* The type of token. * The type of token.
*/ */
export enum TokenType { export enum TokenType {
BEARER = "Bearer", Bearer = "Bearer",
} }
export type Token = InferSelectModel<typeof Tokens>; export type Token = InferSelectModel<typeof Tokens>;

View file

@ -14,15 +14,15 @@ import {
} from "~/drizzle/schema"; } from "~/drizzle/schema";
import { User } from "~/packages/database-interface/user"; import { User } from "~/packages/database-interface/user";
import { LogLevel } from "~/packages/log-manager"; import { LogLevel } from "~/packages/log-manager";
import type { Application } from "./Application"; import type { Application } from "./application";
import type { EmojiWithInstance } from "./Emoji"; import type { EmojiWithInstance } from "./emoji";
import { objectToInboxRequest } from "./Federation"; import { objectToInboxRequest } from "./federation";
import { import {
type Relationship, type Relationship,
checkForBidirectionalRelationships, checkForBidirectionalRelationships,
createNewRelationship, createNewRelationship,
} from "./Relationship"; } from "./relationship";
import type { Token } from "./Token"; import type { Token } from "./token";
export type UserType = InferSelectModel<typeof Users>; export type UserType = InferSelectModel<typeof Users>;
@ -175,13 +175,13 @@ export const followRequestUser = async (
if (!response.ok) { if (!response.ok) {
dualLogger.log( dualLogger.log(
LogLevel.DEBUG, LogLevel.Debug,
"Federation.FollowRequest", "Federation.FollowRequest",
await response.text(), await response.text(),
); );
dualLogger.log( dualLogger.log(
LogLevel.ERROR, LogLevel.Error,
"Federation.FollowRequest", "Federation.FollowRequest",
`Failed to federate follow request from ${ `Failed to federate follow request from ${
follower.id follower.id
@ -230,13 +230,13 @@ export const sendFollowAccept = async (follower: User, followee: User) => {
if (!response.ok) { if (!response.ok) {
dualLogger.log( dualLogger.log(
LogLevel.DEBUG, LogLevel.Debug,
"Federation.FollowAccept", "Federation.FollowAccept",
await response.text(), await response.text(),
); );
dualLogger.log( dualLogger.log(
LogLevel.ERROR, LogLevel.Error,
"Federation.FollowAccept", "Federation.FollowAccept",
`Failed to federate follow accept from ${ `Failed to federate follow accept from ${
followee.id followee.id
@ -258,13 +258,13 @@ export const sendFollowReject = async (follower: User, followee: User) => {
if (!response.ok) { if (!response.ok) {
dualLogger.log( dualLogger.log(
LogLevel.DEBUG, LogLevel.Debug,
"Federation.FollowReject", "Federation.FollowReject",
await response.text(), await response.text(),
); );
dualLogger.log( dualLogger.log(
LogLevel.ERROR, LogLevel.Error,
"Federation.FollowReject", "Federation.FollowReject",
`Failed to federate follow reject from ${ `Failed to federate follow reject from ${
followee.id followee.id
@ -352,7 +352,9 @@ export const findFirstUser = async (
}, },
}); });
if (!output) return null; if (!output) {
return null;
}
return transformOutputToUserWithRelations(output); return transformOutputToUserWithRelations(output);
}; };
@ -373,7 +375,9 @@ export const resolveWebFinger = async (
.where(and(eq(Users.username, identifier), eq(Instances.baseUrl, host))) .where(and(eq(Users.username, identifier), eq(Instances.baseUrl, host)))
.limit(1); .limit(1);
if (foundUser[0]) return await User.fromId(foundUser[0].Users.id); if (foundUser[0]) {
return await User.fromId(foundUser[0].Users.id);
}
const hostWithProtocol = host.startsWith("http") ? host : `https://${host}`; const hostWithProtocol = host.startsWith("http") ? host : `https://${host}`;
@ -405,7 +409,7 @@ export const resolveWebFinger = async (
}[]; }[];
}; };
if (!data.subject || !data.links) { if (!(data.subject && data.links)) {
throw new Error( throw new Error(
"Invalid WebFinger data (missing subject or links from response)", "Invalid WebFinger data (missing subject or links from response)",
); );
@ -428,13 +432,17 @@ export const resolveWebFinger = async (
* @returns The user associated with the given access token. * @returns The user associated with the given access token.
*/ */
export const retrieveUserFromToken = async ( export const retrieveUserFromToken = async (
access_token: string, accessToken: string,
): Promise<User | null> => { ): Promise<User | null> => {
if (!access_token) return null; if (!accessToken) {
return null;
}
const token = await retrieveToken(access_token); const token = await retrieveToken(accessToken);
if (!token || !token.userId) return null; if (!token?.userId) {
return null;
}
const user = await User.fromId(token.userId); const user = await User.fromId(token.userId);
@ -442,12 +450,14 @@ export const retrieveUserFromToken = async (
}; };
export const retrieveUserAndApplicationFromToken = async ( export const retrieveUserAndApplicationFromToken = async (
access_token: string, accessToken: string,
): Promise<{ ): Promise<{
user: User | null; user: User | null;
application: Application | null; application: Application | null;
}> => { }> => {
if (!access_token) return { user: null, application: null }; if (!accessToken) {
return { user: null, application: null };
}
const output = ( const output = (
await db await db
@ -457,11 +467,13 @@ export const retrieveUserAndApplicationFromToken = async (
}) })
.from(Tokens) .from(Tokens)
.leftJoin(Applications, eq(Tokens.applicationId, Applications.id)) .leftJoin(Applications, eq(Tokens.applicationId, Applications.id))
.where(eq(Tokens.accessToken, access_token)) .where(eq(Tokens.accessToken, accessToken))
.limit(1) .limit(1)
)[0]; )[0];
if (!output?.token.userId) return { user: null, application: null }; if (!output?.token.userId) {
return { user: null, application: null };
}
const user = await User.fromId(output.token.userId); const user = await User.fromId(output.token.userId);
@ -469,13 +481,15 @@ export const retrieveUserAndApplicationFromToken = async (
}; };
export const retrieveToken = async ( export const retrieveToken = async (
access_token: string, accessToken: string,
): Promise<Token | null> => { ): Promise<Token | null> => {
if (!access_token) return null; if (!accessToken) {
return null;
}
return ( return (
(await db.query.Tokens.findFirst({ (await db.query.Tokens.findFirst({
where: (tokens, { eq }) => eq(tokens.accessToken, access_token), where: (tokens, { eq }) => eq(tokens.accessToken, accessToken),
})) ?? null })) ?? null
); );
}; };

View file

@ -23,13 +23,14 @@ export const setupDatabase = async (
if ( if (
(e as Error).message === (e as Error).message ===
"Client has already been connected. You cannot reuse a client." "Client has already been connected. You cannot reuse a client."
) ) {
return; return;
}
await logger.logError(LogLevel.CRITICAL, "Database", e as Error); await logger.logError(LogLevel.Critical, "Database", e as Error);
await logger.log( await logger.log(
LogLevel.CRITICAL, LogLevel.Critical,
"Database", "Database",
"Failed to connect to database. Please check your configuration.", "Failed to connect to database. Please check your configuration.",
); );
@ -38,23 +39,23 @@ export const setupDatabase = async (
// Migrate the database // Migrate the database
info && info &&
(await logger.log(LogLevel.INFO, "Database", "Migrating database...")); (await logger.log(LogLevel.Info, "Database", "Migrating database..."));
try { try {
await migrate(db, { await migrate(db, {
migrationsFolder: "./drizzle/migrations", migrationsFolder: "./drizzle/migrations",
}); });
} catch (e) { } catch (e) {
await logger.logError(LogLevel.CRITICAL, "Database", e as Error); await logger.logError(LogLevel.Critical, "Database", e as Error);
await logger.log( await logger.log(
LogLevel.CRITICAL, LogLevel.Critical,
"Database", "Database",
"Failed to migrate database. Please check your configuration.", "Failed to migrate database. Please check your configuration.",
); );
process.exit(1); process.exit(1);
} }
info && (await logger.log(LogLevel.INFO, "Database", "Database migrated")); info && (await logger.log(LogLevel.Info, "Database", "Database migrated"));
}; };
export const db = drizzle(client, { schema }); export const db = drizzle(client, { schema });

View file

@ -14,7 +14,7 @@ import {
uniqueIndex, uniqueIndex,
uuid, uuid,
} from "drizzle-orm/pg-core"; } from "drizzle-orm/pg-core";
import type { Source as APISource } from "~/types/mastodon/source"; import type { Source as apiSource } from "~/types/mastodon/source";
export const CaptchaChallenges = pgTable("CaptchaChallenges", { export const CaptchaChallenges = pgTable("CaptchaChallenges", {
id: uuid("id").default(sql`uuid_generate_v7()`).primaryKey().notNull(), id: uuid("id").default(sql`uuid_generate_v7()`).primaryKey().notNull(),
@ -386,7 +386,7 @@ export const Users = pgTable(
inbox: string; inbox: string;
outbox: string; outbox: string;
}> | null>(), }> | null>(),
source: jsonb("source").notNull().$type<APISource>(), source: jsonb("source").notNull().$type<apiSource>(),
avatar: text("avatar").notNull(), avatar: text("avatar").notNull(),
header: text("header").notNull(), header: text("header").notNull(),
createdAt: timestamp("created_at", { precision: 3, mode: "string" }) createdAt: timestamp("created_at", { precision: 3, mode: "string" })
@ -508,100 +508,100 @@ export const ModTags = pgTable("ModTags", {
* - Owner: Only manage their own resources * - Owner: Only manage their own resources
*/ */
export enum RolePermissions { export enum RolePermissions {
MANAGE_NOTES = "notes", ManageNotes = "notes",
MANAGE_OWN_NOTES = "owner:note", ManageOwnNotes = "owner:note",
VIEW_NOTES = "read:note", ViewNotes = "read:note",
VIEW_NOTE_LIKES = "read:note_likes", ViewNoteLikes = "read:note_likes",
VIEW_NOTE_BOOSTS = "read:note_boosts", ViewNoteBoosts = "read:note_boosts",
MANAGE_ACCOUNTS = "accounts", ManageAccounts = "accounts",
MANAGE_OWN_ACCOUNT = "owner:account", ManageOwnAccount = "owner:account",
VIEW_ACCOUNT_FOLLOWS = "read:account_follows", ViewAccountFollows = "read:account_follows",
MANAGE_LIKES = "likes", ManageLikes = "likes",
MANAGE_OWN_LIKES = "owner:like", ManageOwnLikes = "owner:like",
MANAGE_BOOSTS = "boosts", ManageBoosts = "boosts",
MANAGE_OWN_BOOSTS = "owner:boost", ManageOwnBoosts = "owner:boost",
VIEW_ACCOUNTS = "read:account", ViewAccounts = "read:account",
MANAGE_EMOJIS = "emojis", ManageEmojis = "emojis",
VIEW_EMOJIS = "read:emoji", ViewEmojis = "read:emoji",
MANAGE_OWN_EMOJIS = "owner:emoji", ManageOwnEmojis = "owner:emoji",
MANAGE_MEDIA = "media", ManageMedia = "media",
MANAGE_OWN_MEDIA = "owner:media", ManageOwnMedia = "owner:media",
MANAGE_BLOCKS = "blocks", ManageBlocks = "blocks",
MANAGE_OWN_BLOCKS = "owner:block", ManageOwnBlocks = "owner:block",
MANAGE_FILTERS = "filters", ManageFilters = "filters",
MANAGE_OWN_FILTERS = "owner:filter", ManageOwnFilters = "owner:filter",
MANAGE_MUTES = "mutes", ManageMutes = "mutes",
MANAGE_OWN_MUTES = "owner:mute", ManageOwnMutes = "owner:mute",
MANAGE_REPORTS = "reports", ManageReports = "reports",
MANAGE_OWN_REPORTS = "owner:report", ManageOwnReports = "owner:report",
MANAGE_SETTINGS = "settings", ManageSettings = "settings",
MANAGE_OWN_SETTINGS = "owner:settings", ManageOwnSettings = "owner:settings",
MANAGE_ROLES = "roles", ManageRoles = "roles",
MANAGE_NOTIFICATIONS = "notifications", ManageNotifications = "notifications",
MANAGE_OWN_NOTIFICATIONS = "owner:notification", ManageOwnNotifications = "owner:notification",
MANAGE_FOLLOWS = "follows", ManageFollows = "follows",
MANAGE_OWN_FOLLOWS = "owner:follow", ManageOwnFollows = "owner:follow",
MANAGE_OWN_APPS = "owner:app", ManageOwnApps = "owner:app",
SEARCH = "search", Search = "search",
VIEW_PUBLIC_TIMELINES = "public_timelines", ViewPublicTimelines = "public_timelines",
VIEW_PRIVATE_TIMELINES = "private_timelines", ViewPrimateTimelines = "private_timelines",
IGNORE_RATE_LIMITS = "ignore_rate_limits", IgnoreRateLimits = "ignore_rate_limits",
IMPERSONATE = "impersonate", Impersonate = "impersonate",
MANAGE_INSTANCE = "instance", ManageInstance = "instance",
MANAGE_INSTANCE_FEDERATION = "instance:federation", ManageInstanceFederation = "instance:federation",
MANAGE_INSTANCE_SETTINGS = "instance:settings", ManageInstanceSettings = "instance:settings",
/** Users who do not have this permission will not be able to login! */ /** Users who do not have this permission will not be able to login! */
OAUTH = "oauth", OAuth = "oauth",
} }
export const DEFAULT_ROLES = [ export const DEFAULT_ROLES = [
RolePermissions.MANAGE_OWN_NOTES, RolePermissions.ManageOwnNotes,
RolePermissions.VIEW_NOTES, RolePermissions.ViewNotes,
RolePermissions.VIEW_NOTE_LIKES, RolePermissions.ViewNoteLikes,
RolePermissions.VIEW_NOTE_BOOSTS, RolePermissions.ViewNoteBoosts,
RolePermissions.MANAGE_OWN_ACCOUNT, RolePermissions.ManageOwnAccount,
RolePermissions.VIEW_ACCOUNT_FOLLOWS, RolePermissions.ViewAccountFollows,
RolePermissions.MANAGE_OWN_LIKES, RolePermissions.ManageOwnLikes,
RolePermissions.MANAGE_OWN_BOOSTS, RolePermissions.ManageOwnBoosts,
RolePermissions.VIEW_ACCOUNTS, RolePermissions.ViewAccounts,
RolePermissions.MANAGE_OWN_EMOJIS, RolePermissions.ManageOwnEmojis,
RolePermissions.VIEW_EMOJIS, RolePermissions.ViewEmojis,
RolePermissions.MANAGE_OWN_MEDIA, RolePermissions.ManageOwnMedia,
RolePermissions.MANAGE_OWN_BLOCKS, RolePermissions.ManageOwnBlocks,
RolePermissions.MANAGE_OWN_FILTERS, RolePermissions.ManageOwnFilters,
RolePermissions.MANAGE_OWN_MUTES, RolePermissions.ManageOwnMutes,
RolePermissions.MANAGE_OWN_REPORTS, RolePermissions.ManageOwnReports,
RolePermissions.MANAGE_OWN_SETTINGS, RolePermissions.ManageOwnSettings,
RolePermissions.MANAGE_OWN_NOTIFICATIONS, RolePermissions.ManageOwnNotifications,
RolePermissions.MANAGE_OWN_FOLLOWS, RolePermissions.ManageOwnFollows,
RolePermissions.MANAGE_OWN_APPS, RolePermissions.ManageOwnApps,
RolePermissions.SEARCH, RolePermissions.Search,
RolePermissions.VIEW_PUBLIC_TIMELINES, RolePermissions.ViewPublicTimelines,
RolePermissions.VIEW_PRIVATE_TIMELINES, RolePermissions.ViewPrimateTimelines,
RolePermissions.OAUTH, RolePermissions.OAuth,
]; ];
export const ADMIN_ROLES = [ export const ADMIN_ROLES = [
...DEFAULT_ROLES, ...DEFAULT_ROLES,
RolePermissions.MANAGE_NOTES, RolePermissions.ManageNotes,
RolePermissions.MANAGE_ACCOUNTS, RolePermissions.ManageAccounts,
RolePermissions.MANAGE_LIKES, RolePermissions.ManageLikes,
RolePermissions.MANAGE_BOOSTS, RolePermissions.ManageBoosts,
RolePermissions.MANAGE_EMOJIS, RolePermissions.ManageEmojis,
RolePermissions.MANAGE_MEDIA, RolePermissions.ManageMedia,
RolePermissions.MANAGE_BLOCKS, RolePermissions.ManageBlocks,
RolePermissions.MANAGE_FILTERS, RolePermissions.ManageFilters,
RolePermissions.MANAGE_MUTES, RolePermissions.ManageMutes,
RolePermissions.MANAGE_REPORTS, RolePermissions.ManageReports,
RolePermissions.MANAGE_SETTINGS, RolePermissions.ManageSettings,
RolePermissions.MANAGE_ROLES, RolePermissions.ManageRoles,
RolePermissions.MANAGE_NOTIFICATIONS, RolePermissions.ManageNotifications,
RolePermissions.MANAGE_FOLLOWS, RolePermissions.ManageFollows,
RolePermissions.IMPERSONATE, RolePermissions.Impersonate,
RolePermissions.IGNORE_RATE_LIMITS, RolePermissions.IgnoreRateLimits,
RolePermissions.MANAGE_INSTANCE, RolePermissions.ManageInstance,
RolePermissions.MANAGE_INSTANCE_FEDERATION, RolePermissions.ManageInstanceFederation,
RolePermissions.MANAGE_INSTANCE_SETTINGS, RolePermissions.ManageInstanceSettings,
]; ];
export const Roles = pgTable("Roles", { export const Roles = pgTable("Roles", {

View file

@ -15,7 +15,7 @@ import { Note } from "~/packages/database-interface/note";
import { handleGlitchRequest } from "~/packages/glitch-server/main"; import { handleGlitchRequest } from "~/packages/glitch-server/main";
import { routes } from "~/routes"; import { routes } from "~/routes";
import { createServer } from "~/server"; import { createServer } from "~/server";
import type { APIRouteExports } from "~/types/api"; import type { ApiRouteExports } from "~/types/api";
const timeAtStart = performance.now(); const timeAtStart = performance.now();
@ -30,7 +30,7 @@ if (isEntry) {
dualServerLogger = dualLogger; dualServerLogger = dualLogger;
} }
await dualServerLogger.log(LogLevel.INFO, "Lysand", "Starting Lysand..."); await dualServerLogger.log(LogLevel.Info, "Lysand", "Starting Lysand...");
await setupDatabase(dualServerLogger); await setupDatabase(dualServerLogger);
@ -45,12 +45,12 @@ if (isEntry) {
// Check if JWT private key is set in config // Check if JWT private key is set in config
if (!config.oidc.jwt_key) { if (!config.oidc.jwt_key) {
await dualServerLogger.log( await dualServerLogger.log(
LogLevel.CRITICAL, LogLevel.Critical,
"Server", "Server",
"The JWT private key is not set in the config", "The JWT private key is not set in the config",
); );
await dualServerLogger.log( await dualServerLogger.log(
LogLevel.CRITICAL, LogLevel.Critical,
"Server", "Server",
"Below is a generated key for you to copy in the config at oidc.jwt_key", "Below is a generated key for you to copy in the config at oidc.jwt_key",
); );
@ -69,7 +69,7 @@ if (isEntry) {
).toString("base64"); ).toString("base64");
await dualServerLogger.log( await dualServerLogger.log(
LogLevel.CRITICAL, LogLevel.Critical,
"Server", "Server",
chalk.gray(`${privateKey};${publicKey}`), chalk.gray(`${privateKey};${publicKey}`),
); );
@ -100,7 +100,7 @@ if (isEntry) {
if (privateKey instanceof Error || publicKey instanceof Error) { if (privateKey instanceof Error || publicKey instanceof Error) {
await dualServerLogger.log( await dualServerLogger.log(
LogLevel.CRITICAL, LogLevel.Critical,
"Server", "Server",
"The JWT key could not be imported! You may generate a new one by removing the old one from the config and restarting the server (this will invalidate all current JWTs).", "The JWT key could not be imported! You may generate a new one by removing the old one from the config and restarting the server (this will invalidate all current JWTs).",
); );
@ -123,16 +123,16 @@ app.use(boundaryCheck);
// Inject own filesystem router // Inject own filesystem router
for (const [, path] of Object.entries(routes)) { for (const [, path] of Object.entries(routes)) {
// use app.get(path, handler) to add routes // use app.get(path, handler) to add routes
const route: APIRouteExports = await import(path); const route: ApiRouteExports = await import(path);
if (!route.meta || !route.default) { if (!(route.meta && route.default)) {
throw new Error(`Route ${path} does not have the correct exports.`); throw new Error(`Route ${path} does not have the correct exports.`);
} }
route.default(app); route.default(app);
} }
app.options("*", async () => { app.options("*", () => {
return response(null); return response(null);
}); });
@ -145,17 +145,14 @@ app.all("*", async (context) => {
} }
} }
const base_url_with_http = config.http.base_url.replace( const baseUrlWithHttp = config.http.base_url.replace("https://", "http://");
"https://",
"http://",
);
const replacedUrl = context.req.url const replacedUrl = context.req.url
.replace(config.http.base_url, config.frontend.url) .replace(config.http.base_url, config.frontend.url)
.replace(base_url_with_http, config.frontend.url); .replace(baseUrlWithHttp, config.frontend.url);
await dualLogger.log( await dualLogger.log(
LogLevel.DEBUG, LogLevel.Debug,
"Server.Proxy", "Server.Proxy",
`Proxying ${replacedUrl}`, `Proxying ${replacedUrl}`,
); );
@ -168,9 +165,9 @@ app.all("*", async (context) => {
}, },
redirect: "manual", redirect: "manual",
}).catch(async (e) => { }).catch(async (e) => {
await dualLogger.logError(LogLevel.ERROR, "Server.Proxy", e as Error); await dualLogger.logError(LogLevel.Error, "Server.Proxy", e as Error);
await dualLogger.log( await dualLogger.log(
LogLevel.ERROR, LogLevel.Error,
"Server.Proxy", "Server.Proxy",
`The Frontend is not running or the route is not found: ${replacedUrl}`, `The Frontend is not running or the route is not found: ${replacedUrl}`,
); );
@ -192,13 +189,13 @@ app.all("*", async (context) => {
createServer(config, app); createServer(config, app);
await dualServerLogger.log( await dualServerLogger.log(
LogLevel.INFO, LogLevel.Info,
"Server", "Server",
`Lysand started at ${config.http.bind}:${config.http.bind_port} in ${(performance.now() - timeAtStart).toFixed(0)}ms`, `Lysand started at ${config.http.bind}:${config.http.bind_port} in ${(performance.now() - timeAtStart).toFixed(0)}ms`,
); );
await dualServerLogger.log( await dualServerLogger.log(
LogLevel.INFO, LogLevel.Info,
"Database", "Database",
`Database is online, now serving ${postCount} posts`, `Database is online, now serving ${postCount} posts`,
); );
@ -206,7 +203,7 @@ await dualServerLogger.log(
if (config.frontend.enabled) { if (config.frontend.enabled) {
if (!URL.canParse(config.frontend.url)) { if (!URL.canParse(config.frontend.url)) {
await dualServerLogger.log( await dualServerLogger.log(
LogLevel.ERROR, LogLevel.Error,
"Server", "Server",
`Frontend URL is not a valid URL: ${config.frontend.url}`, `Frontend URL is not a valid URL: ${config.frontend.url}`,
); );
@ -220,19 +217,19 @@ if (config.frontend.enabled) {
if (!response) { if (!response) {
await dualServerLogger.log( await dualServerLogger.log(
LogLevel.ERROR, LogLevel.Error,
"Server", "Server",
`Frontend is unreachable at ${config.frontend.url}`, `Frontend is unreachable at ${config.frontend.url}`,
); );
await dualServerLogger.log( await dualServerLogger.log(
LogLevel.ERROR, LogLevel.Error,
"Server", "Server",
"Please ensure the frontend is online and reachable", "Please ensure the frontend is online and reachable",
); );
} }
} else { } else {
await dualServerLogger.log( await dualServerLogger.log(
LogLevel.WARNING, LogLevel.Warning,
"Server", "Server",
"Frontend is disabled, skipping check", "Frontend is disabled, skipping check",
); );

View file

@ -7,14 +7,14 @@ import { config } from "~/packages/config-manager";
import { LogLevel } from "~/packages/log-manager"; import { LogLevel } from "~/packages/log-manager";
export const bait = createMiddleware(async (context, next) => { export const bait = createMiddleware(async (context, next) => {
const request_ip = context.env?.ip as SocketAddress | undefined | null; const requestIp = context.env?.ip as SocketAddress | undefined | null;
if (config.http.bait.enabled) { if (config.http.bait.enabled) {
// Check for bait IPs // Check for bait IPs
if (request_ip?.address) { if (requestIp?.address) {
for (const ip of config.http.bait.bait_ips) { for (const ip of config.http.bait.bait_ips) {
try { try {
if (matches(ip, request_ip.address)) { if (matches(ip, requestIp.address)) {
const file = Bun.file( const file = Bun.file(
config.http.bait.send_file || "./beemovie.txt", config.http.bait.send_file || "./beemovie.txt",
); );
@ -23,19 +23,19 @@ export const bait = createMiddleware(async (context, next) => {
return response(file); return response(file);
} }
await logger.log( await logger.log(
LogLevel.ERROR, LogLevel.Error,
"Server.Bait", "Server.Bait",
`Bait file not found: ${config.http.bait.send_file}`, `Bait file not found: ${config.http.bait.send_file}`,
); );
} }
} catch (e) { } catch (e) {
logger.log( logger.log(
LogLevel.ERROR, LogLevel.Error,
"Server.IPCheck", "Server.IPCheck",
`Error while parsing bait IP "${ip}" `, `Error while parsing bait IP "${ip}" `,
); );
logger.logError( logger.logError(
LogLevel.ERROR, LogLevel.Error,
"Server.IPCheck", "Server.IPCheck",
e as Error, e as Error,
); );
@ -61,7 +61,7 @@ export const bait = createMiddleware(async (context, next) => {
return response(file); return response(file);
} }
await logger.log( await logger.log(
LogLevel.ERROR, LogLevel.Error,
"Server.Bait", "Server.Bait",
`Bait file not found: ${config.http.bait.send_file}`, `Bait file not found: ${config.http.bait.send_file}`,
); );

View file

@ -9,25 +9,25 @@ import { LogLevel } from "~/packages/log-manager";
export const ipBans = createMiddleware(async (context, next) => { export const ipBans = createMiddleware(async (context, next) => {
// Check for banned IPs // Check for banned IPs
const request_ip = context.env?.ip as SocketAddress | undefined | null; const requestIp = context.env?.ip as SocketAddress | undefined | null;
if (!request_ip?.address) { if (!requestIp?.address) {
await next(); await next();
return; return;
} }
for (const ip of config.http.banned_ips) { for (const ip of config.http.banned_ips) {
try { try {
if (matches(ip, request_ip?.address)) { if (matches(ip, requestIp?.address)) {
return errorResponse("Forbidden", 403); return errorResponse("Forbidden", 403);
} }
} catch (e) { } catch (e) {
logger.log( logger.log(
LogLevel.ERROR, LogLevel.Error,
"Server.IPCheck", "Server.IPCheck",
`Error while parsing banned IP "${ip}" `, `Error while parsing banned IP "${ip}" `,
); );
logger.logError(LogLevel.ERROR, "Server.IPCheck", e as Error); logger.logError(LogLevel.Error, "Server.IPCheck", e as Error);
return errorResponse( return errorResponse(
`A server error occured: ${(e as Error).message}`, `A server error occured: ${(e as Error).message}`,

View file

@ -4,12 +4,12 @@ import { createMiddleware } from "hono/factory";
import { config } from "~/packages/config-manager"; import { config } from "~/packages/config-manager";
export const logger = createMiddleware(async (context, next) => { export const logger = createMiddleware(async (context, next) => {
const request_ip = context.env?.ip as SocketAddress | undefined | null; const requestIp = context.env?.ip as SocketAddress | undefined | null;
if (config.logging.log_requests) { if (config.logging.log_requests) {
await dualLogger.logRequest( await dualLogger.logRequest(
context.req.raw, context.req.raw,
config.logging.log_ip ? request_ip?.address : undefined, config.logging.log_ip ? requestIp?.address : undefined,
config.logging.log_requests_verbose, config.logging.log_requests_verbose,
); );
} }

View file

@ -3,7 +3,7 @@ import { z } from "zod";
import { ADMIN_ROLES, DEFAULT_ROLES, RolePermissions } from "~/drizzle/schema"; import { ADMIN_ROLES, DEFAULT_ROLES, RolePermissions } from "~/drizzle/schema";
export enum MediaBackendType { export enum MediaBackendType {
LOCAL = "local", Local = "local",
S3 = "s3", S3 = "s3",
} }
@ -220,7 +220,7 @@ export const configValidator = z.object({
.object({ .object({
backend: z backend: z
.nativeEnum(MediaBackendType) .nativeEnum(MediaBackendType)
.default(MediaBackendType.LOCAL), .default(MediaBackendType.Local),
deduplicate_media: z.boolean().default(true), deduplicate_media: z.boolean().default(true),
local_uploads_folder: z.string().min(1).default("uploads"), local_uploads_folder: z.string().min(1).default("uploads"),
conversion: z conversion: z
@ -234,7 +234,7 @@ export const configValidator = z.object({
}), }),
}) })
.default({ .default({
backend: MediaBackendType.LOCAL, backend: MediaBackendType.Local,
deduplicate_media: true, deduplicate_media: true,
local_uploads_folder: "uploads", local_uploads_folder: "uploads",
conversion: { conversion: {

View file

@ -6,8 +6,6 @@
*/ */
import { loadConfig, watchConfig } from "c12"; import { loadConfig, watchConfig } from "c12";
import chalk from "chalk";
import { fromError } from "zod-validation-error";
import { type Config, configValidator } from "./config.type"; import { type Config, configValidator } from "./config.type";
const { config } = await watchConfig({ const { config } = await watchConfig({
@ -23,21 +21,6 @@ const { config } = await watchConfig({
const parsed = await configValidator.safeParseAsync(config); const parsed = await configValidator.safeParseAsync(config);
if (!parsed.success) { if (!parsed.success) {
console.log(
`${chalk.bgRed.white(
" CRITICAL ",
)} There was an error parsing the config file at ${chalk.bold(
"./config/config.toml",
)}. Please fix the file and try again.`,
);
console.log(
`${chalk.bgRed.white(
" CRITICAL ",
)} Follow the installation intructions and get a sample config file from the repository if needed.`,
);
console.log(
`${chalk.bgRed.white(" CRITICAL ")} ${fromError(parsed.error).message}`,
);
process.exit(1); process.exit(1);
} }

View file

@ -10,7 +10,7 @@ import {
} from "drizzle-orm"; } from "drizzle-orm";
import { db } from "~/drizzle/db"; import { db } from "~/drizzle/db";
import { Attachments } from "~/drizzle/schema"; import { Attachments } from "~/drizzle/schema";
import type { AsyncAttachment as APIAsyncAttachment } from "~/types/mastodon/async_attachment"; import type { AsyncAttachment as apiAsyncAttachment } from "~/types/mastodon/async_attachment";
import type { Attachment as APIAttachment } from "~/types/mastodon/attachment"; import type { Attachment as APIAttachment } from "~/types/mastodon/attachment";
import { BaseInterface } from "./base"; import { BaseInterface } from "./base";
@ -28,7 +28,9 @@ export class Attachment extends BaseInterface<typeof Attachments> {
} }
public static async fromId(id: string | null): Promise<Attachment | null> { public static async fromId(id: string | null): Promise<Attachment | null> {
if (!id) return null; if (!id) {
return null;
}
return await Attachment.fromSql(eq(Attachments.id, id)); return await Attachment.fromSql(eq(Attachments.id, id));
} }
@ -46,7 +48,9 @@ export class Attachment extends BaseInterface<typeof Attachments> {
orderBy, orderBy,
}); });
if (!found) return null; if (!found) {
return null;
}
return new Attachment(found); return new Attachment(found);
} }
@ -86,7 +90,7 @@ export class Attachment extends BaseInterface<typeof Attachments> {
return updated.data; return updated.data;
} }
async save(): Promise<AttachmentType> { save(): Promise<AttachmentType> {
return this.update(this.data); return this.update(this.data);
} }
@ -120,25 +124,22 @@ export class Attachment extends BaseInterface<typeof Attachments> {
return this.data.id; return this.data.id;
} }
public toAPI(): APIAttachment | APIAsyncAttachment { public getMastodonType(): APIAttachment["type"] {
let type = "unknown";
if (this.data.mimeType.startsWith("image/")) { if (this.data.mimeType.startsWith("image/")) {
type = "image"; return "image";
} else if (this.data.mimeType.startsWith("video/")) { }
type = "video"; if (this.data.mimeType.startsWith("video/")) {
} else if (this.data.mimeType.startsWith("audio/")) { return "video";
type = "audio"; }
if (this.data.mimeType.startsWith("audio/")) {
return "audio";
} }
return "unknown";
}
public toApiMeta(): APIAttachment["meta"] {
return { return {
id: this.data.id,
type: type as "image" | "video" | "audio" | "unknown",
url: proxyUrl(this.data.url) ?? "",
remote_url: proxyUrl(this.data.remoteUrl),
preview_url: proxyUrl(this.data.thumbnailUrl || this.data.url),
text_url: null,
meta: {
width: this.data.width || undefined, width: this.data.width || undefined,
height: this.data.height || undefined, height: this.data.height || undefined,
fps: this.data.fps || undefined, fps: this.data.fps || undefined,
@ -165,7 +166,18 @@ export class Attachment extends BaseInterface<typeof Attachments> {
: undefined, : undefined,
}, },
// Idk whether size or length is the right value // Idk whether size or length is the right value
}, };
}
public toApi(): APIAttachment | apiAsyncAttachment {
return {
id: this.data.id,
type: this.getMastodonType(),
url: proxyUrl(this.data.url) ?? "",
remote_url: proxyUrl(this.data.remoteUrl),
preview_url: proxyUrl(this.data.thumbnailUrl || this.data.url),
text_url: null,
meta: this.toApiMeta(),
description: this.data.description, description: this.data.description,
blurhash: this.data.blurhash, blurhash: this.data.blurhash,
}; };

View file

@ -19,21 +19,21 @@ import { LogLevel } from "log-manager";
import { createRegExp, exactly, global } from "magic-regexp"; import { createRegExp, exactly, global } from "magic-regexp";
import { import {
type Application, type Application,
applicationToAPI, applicationToApi,
} from "~/database/entities/Application"; } from "~/database/entities/application";
import { import {
type EmojiWithInstance, type EmojiWithInstance,
emojiToAPI, emojiToApi,
emojiToLysand, emojiToLysand,
fetchEmoji, fetchEmoji,
parseEmojis, parseEmojis,
} from "~/database/entities/Emoji"; } from "~/database/entities/emoji";
import { localObjectURI } from "~/database/entities/Federation"; import { localObjectUri } from "~/database/entities/federation";
import { import {
type StatusWithRelations, type StatusWithRelations,
contentToHtml, contentToHtml,
findManyNotes, findManyNotes,
} from "~/database/entities/Status"; } from "~/database/entities/status";
import { db } from "~/drizzle/db"; import { db } from "~/drizzle/db";
import { import {
Attachments, Attachments,
@ -44,8 +44,8 @@ import {
Users, Users,
} from "~/drizzle/schema"; } from "~/drizzle/schema";
import { config } from "~/packages/config-manager"; import { config } from "~/packages/config-manager";
import type { Attachment as APIAttachment } from "~/types/mastodon/attachment"; import type { Attachment as apiAttachment } from "~/types/mastodon/attachment";
import type { Status as APIStatus } from "~/types/mastodon/status"; import type { Status as apiStatus } from "~/types/mastodon/status";
import { Attachment } from "./attachment"; import { Attachment } from "./attachment";
import { BaseInterface } from "./base"; import { BaseInterface } from "./base";
import { User } from "./user"; import { User } from "./user";
@ -54,7 +54,7 @@ import { User } from "./user";
* Gives helpers to fetch notes from database in a nice format * Gives helpers to fetch notes from database in a nice format
*/ */
export class Note extends BaseInterface<typeof Notes, StatusWithRelations> { export class Note extends BaseInterface<typeof Notes, StatusWithRelations> {
async save(): Promise<StatusWithRelations> { save(): Promise<StatusWithRelations> {
return this.update(this.data); return this.update(this.data);
} }
@ -87,7 +87,9 @@ export class Note extends BaseInterface<typeof Notes, StatusWithRelations> {
id: string | null, id: string | null,
userRequestingNoteId?: string, userRequestingNoteId?: string,
): Promise<Note | null> { ): Promise<Note | null> {
if (!id) return null; if (!id) {
return null;
}
return await Note.fromSql( return await Note.fromSql(
eq(Notes.id, id), eq(Notes.id, id),
@ -123,7 +125,9 @@ export class Note extends BaseInterface<typeof Notes, StatusWithRelations> {
userId, userId,
); );
if (!found[0]) return null; if (!found[0]) {
return null;
}
return new Note(found[0]); return new Note(found[0]);
} }
@ -225,7 +229,7 @@ export class Note extends BaseInterface<typeof Notes, StatusWithRelations> {
); );
} }
async isRemote() { isRemote() {
return this.author.isRemote(); return this.author.isRemote();
} }
@ -248,14 +252,14 @@ export class Note extends BaseInterface<typeof Notes, StatusWithRelations> {
static async fromData( static async fromData(
author: User, author: User,
content: typeof EntityValidator.$ContentFormat, content: typeof EntityValidator.$ContentFormat,
visibility: APIStatus["visibility"], visibility: apiStatus["visibility"],
is_sensitive: boolean, isSensitive: boolean,
spoiler_text: string, spoilerText: string,
emojis: EmojiWithInstance[], emojis: EmojiWithInstance[],
uri?: string, uri?: string,
mentions?: User[], mentions?: User[],
/** List of IDs of database Attachment objects */ /** List of IDs of database Attachment objects */
media_attachments?: string[], mediaAttachments?: string[],
replyId?: string, replyId?: string,
quoteId?: string, quoteId?: string,
application?: Application, application?: Application,
@ -284,8 +288,8 @@ export class Note extends BaseInterface<typeof Notes, StatusWithRelations> {
"", "",
contentType: "text/html", contentType: "text/html",
visibility, visibility,
sensitive: is_sensitive, sensitive: isSensitive,
spoilerText: await sanitizedHtmlStrip(spoiler_text), spoilerText: await sanitizedHtmlStrip(spoilerText),
uri: uri || null, uri: uri || null,
replyId: replyId ?? null, replyId: replyId ?? null,
quotingId: quoteId ?? null, quotingId: quoteId ?? null,
@ -315,13 +319,13 @@ export class Note extends BaseInterface<typeof Notes, StatusWithRelations> {
} }
// Set attachment parents // Set attachment parents
if (media_attachments && media_attachments.length > 0) { if (mediaAttachments && mediaAttachments.length > 0) {
await db await db
.update(Attachments) .update(Attachments)
.set({ .set({
noteId: newNote.id, noteId: newNote.id,
}) })
.where(inArray(Attachments.id, media_attachments)); .where(inArray(Attachments.id, mediaAttachments));
} }
// Send notifications for mentioned local users // Send notifications for mentioned local users
@ -341,13 +345,13 @@ export class Note extends BaseInterface<typeof Notes, StatusWithRelations> {
async updateFromData( async updateFromData(
content?: typeof EntityValidator.$ContentFormat, content?: typeof EntityValidator.$ContentFormat,
visibility?: APIStatus["visibility"], visibility?: apiStatus["visibility"],
is_sensitive?: boolean, isSensitive?: boolean,
spoiler_text?: string, spoilerText?: string,
emojis: EmojiWithInstance[] = [], emojis: EmojiWithInstance[] = [],
mentions: User[] = [], mentions: User[] = [],
/** List of IDs of database Attachment objects */ /** List of IDs of database Attachment objects */
media_attachments: string[] = [], mediaAttachments: string[] = [],
replyId?: string, replyId?: string,
quoteId?: string, quoteId?: string,
application?: Application, application?: Application,
@ -378,8 +382,8 @@ export class Note extends BaseInterface<typeof Notes, StatusWithRelations> {
: undefined, : undefined,
contentType: "text/html", contentType: "text/html",
visibility, visibility,
sensitive: is_sensitive, sensitive: isSensitive,
spoilerText: spoiler_text, spoilerText: spoilerText,
replyId, replyId,
quotingId: quoteId, quotingId: quoteId,
applicationId: application?.id, applicationId: application?.id,
@ -416,7 +420,7 @@ export class Note extends BaseInterface<typeof Notes, StatusWithRelations> {
} }
// Set attachment parents // Set attachment parents
if (media_attachments) { if (mediaAttachments) {
await db await db
.update(Attachments) .update(Attachments)
.set({ .set({
@ -424,13 +428,14 @@ export class Note extends BaseInterface<typeof Notes, StatusWithRelations> {
}) })
.where(eq(Attachments.noteId, this.data.id)); .where(eq(Attachments.noteId, this.data.id));
if (media_attachments.length > 0) if (mediaAttachments.length > 0) {
await db await db
.update(Attachments) .update(Attachments)
.set({ .set({
noteId: this.data.id, noteId: this.data.id,
}) })
.where(inArray(Attachments.id, media_attachments)); .where(inArray(Attachments.id, mediaAttachments));
}
} }
return await Note.fromId(newNote.id, newNote.authorId); return await Note.fromId(newNote.id, newNote.authorId);
@ -443,13 +448,15 @@ export class Note extends BaseInterface<typeof Notes, StatusWithRelations> {
// Check if note not already in database // Check if note not already in database
const foundNote = uri && (await Note.fromSql(eq(Notes.uri, uri))); const foundNote = uri && (await Note.fromSql(eq(Notes.uri, uri)));
if (foundNote) return foundNote; if (foundNote) {
return foundNote;
}
// Check if URI is of a local note // Check if URI is of a local note
if (uri?.startsWith(config.http.base_url)) { if (uri?.startsWith(config.http.base_url)) {
const uuid = uri.match(idValidator); const uuid = uri.match(idValidator);
if (!uuid || !uuid[0]) { if (!uuid?.[0]) {
throw new Error( throw new Error(
`URI ${uri} is of a local note, but it could not be parsed`, `URI ${uri} is of a local note, but it could not be parsed`,
); );
@ -465,7 +472,7 @@ export class Note extends BaseInterface<typeof Notes, StatusWithRelations> {
uri?: string, uri?: string,
providedNote?: typeof EntityValidator.$Note, providedNote?: typeof EntityValidator.$Note,
): Promise<Note | null> { ): Promise<Note | null> {
if (!uri && !providedNote) { if (!(uri || providedNote)) {
throw new Error("No URI or note provided"); throw new Error("No URI or note provided");
} }
@ -515,7 +522,7 @@ export class Note extends BaseInterface<typeof Notes, StatusWithRelations> {
attachment, attachment,
).catch((e) => { ).catch((e) => {
dualLogger.logError( dualLogger.logError(
LogLevel.ERROR, LogLevel.Error,
"Federation.StatusResolver", "Federation.StatusResolver",
e, e,
); );
@ -533,7 +540,7 @@ export class Note extends BaseInterface<typeof Notes, StatusWithRelations> {
?.emojis ?? []) { ?.emojis ?? []) {
const resolvedEmoji = await fetchEmoji(emoji).catch((e) => { const resolvedEmoji = await fetchEmoji(emoji).catch((e) => {
dualLogger.logError( dualLogger.logError(
LogLevel.ERROR, LogLevel.Error,
"Federation.StatusResolver", "Federation.StatusResolver",
e, e,
); );
@ -552,7 +559,7 @@ export class Note extends BaseInterface<typeof Notes, StatusWithRelations> {
content: "", content: "",
}, },
}, },
note.visibility as APIStatus["visibility"], note.visibility as apiStatus["visibility"],
note.is_sensitive ?? false, note.is_sensitive ?? false,
note.subject ?? "", note.subject ?? "",
emojis, emojis,
@ -582,7 +589,7 @@ export class Note extends BaseInterface<typeof Notes, StatusWithRelations> {
content: "", content: "",
}, },
}, },
note.visibility as APIStatus["visibility"], note.visibility as apiStatus["visibility"],
note.is_sensitive ?? false, note.is_sensitive ?? false,
note.subject ?? "", note.subject ?? "",
emojis, emojis,
@ -638,9 +645,15 @@ export class Note extends BaseInterface<typeof Notes, StatusWithRelations> {
* @returns Whether this status is viewable by the user. * @returns Whether this status is viewable by the user.
*/ */
async isViewableByUser(user: User | null) { async isViewableByUser(user: User | null) {
if (this.author.id === user?.id) return true; if (this.author.id === user?.id) {
if (this.data.visibility === "public") return true; return true;
if (this.data.visibility === "unlisted") return true; }
if (this.data.visibility === "public") {
return true;
}
if (this.data.visibility === "unlisted") {
return true;
}
if (this.data.visibility === "private") { if (this.data.visibility === "private") {
return user return user
? await db.query.Relationships.findFirst({ ? await db.query.Relationships.findFirst({
@ -658,7 +671,7 @@ export class Note extends BaseInterface<typeof Notes, StatusWithRelations> {
); );
} }
async toAPI(userFetching?: User | null): Promise<APIStatus> { async toApi(userFetching?: User | null): Promise<apiStatus> {
const data = this.data; const data = this.data;
// Convert mentions of local users from @username@host to @username // Convert mentions of local users from @username@host to @username
@ -696,18 +709,18 @@ export class Note extends BaseInterface<typeof Notes, StatusWithRelations> {
id: data.id, id: data.id,
in_reply_to_id: data.replyId || null, in_reply_to_id: data.replyId || null,
in_reply_to_account_id: data.reply?.authorId || null, in_reply_to_account_id: data.reply?.authorId || null,
account: this.author.toAPI(userFetching?.id === data.authorId), account: this.author.toApi(userFetching?.id === data.authorId),
created_at: new Date(data.createdAt).toISOString(), created_at: new Date(data.createdAt).toISOString(),
application: data.application application: data.application
? applicationToAPI(data.application) ? applicationToApi(data.application)
: null, : null,
card: null, card: null,
content: replacedContent, content: replacedContent,
emojis: data.emojis.map((emoji) => emojiToAPI(emoji)), emojis: data.emojis.map((emoji) => emojiToApi(emoji)),
favourited: data.liked, favourited: data.liked,
favourites_count: data.likeCount, favourites_count: data.likeCount,
media_attachments: (data.attachments ?? []).map( media_attachments: (data.attachments ?? []).map(
(a) => new Attachment(a).toAPI() as APIAttachment, (a) => new Attachment(a).toApi() as apiAttachment,
), ),
mentions: data.mentions.map((mention) => ({ mentions: data.mentions.map((mention) => ({
id: mention.id, id: mention.id,
@ -725,7 +738,7 @@ export class Note extends BaseInterface<typeof Notes, StatusWithRelations> {
// TODO: Add polls // TODO: Add polls
poll: null, poll: null,
reblog: data.reblog reblog: data.reblog
? await new Note(data.reblog as StatusWithRelations).toAPI( ? await new Note(data.reblog as StatusWithRelations).toApi(
userFetching, userFetching,
) )
: null, : null,
@ -736,13 +749,13 @@ export class Note extends BaseInterface<typeof Notes, StatusWithRelations> {
spoiler_text: data.spoilerText, spoiler_text: data.spoilerText,
tags: [], tags: [],
uri: data.uri || this.getUri(), uri: data.uri || this.getUri(),
visibility: data.visibility as APIStatus["visibility"], visibility: data.visibility as apiStatus["visibility"],
url: data.uri || this.getMastoUri(), url: data.uri || this.getMastoUri(),
bookmarked: false, bookmarked: false,
// @ts-expect-error Glitch-SOC extension // @ts-expect-error Glitch-SOC extension
quote: data.quotingId quote: data.quotingId
? (await Note.fromId(data.quotingId, userFetching?.id).then( ? (await Note.fromId(data.quotingId, userFetching?.id).then(
(n) => n?.toAPI(userFetching), (n) => n?.toApi(userFetching),
)) ?? null )) ?? null
: null, : null,
quote_id: data.quotingId || undefined, quote_id: data.quotingId || undefined,
@ -750,12 +763,14 @@ export class Note extends BaseInterface<typeof Notes, StatusWithRelations> {
} }
getUri() { getUri() {
return localObjectURI(this.data.id); return localObjectUri(this.data.id);
} }
static getUri(id?: string | null) { static getUri(id?: string | null) {
if (!id) return null; if (!id) {
return localObjectURI(id); return null;
}
return localObjectUri(id);
} }
getMastoUri() { getMastoUri() {

View file

@ -13,7 +13,7 @@ import {
userInfoRequest, userInfoRequest,
validateAuthResponse, validateAuthResponse,
} from "oauth4webapi"; } from "oauth4webapi";
import type { Application } from "~/database/entities/Application"; import type { Application } from "~/database/entities/application";
import { db } from "~/drizzle/db"; import { db } from "~/drizzle/db";
import { type Applications, OpenIdAccounts } from "~/drizzle/schema"; import { type Applications, OpenIdAccounts } from "~/drizzle/schema";
import { config } from "~/packages/config-manager"; import { config } from "~/packages/config-manager";
@ -21,13 +21,13 @@ import { config } from "~/packages/config-manager";
export class OAuthManager { export class OAuthManager {
public issuer: (typeof config.oidc.providers)[0]; public issuer: (typeof config.oidc.providers)[0];
constructor(public issuer_id: string) { constructor(public issuerId: string) {
const found = config.oidc.providers.find( const found = config.oidc.providers.find(
(provider) => provider.id === this.issuer_id, (provider) => provider.id === this.issuerId,
); );
if (!found) { if (!found) {
throw new Error(`Issuer ${this.issuer_id} not found`); throw new Error(`Issuer ${this.issuerId} not found`);
} }
this.issuer = found; this.issuer = found;
@ -48,7 +48,7 @@ export class OAuthManager {
}).then((res) => processDiscoveryResponse(issuerUrl, res)); }).then((res) => processDiscoveryResponse(issuerUrl, res));
} }
async getParameters( getParameters(
authServer: AuthorizationServer, authServer: AuthorizationServer,
issuer: (typeof config.oidc.providers)[0], issuer: (typeof config.oidc.providers)[0],
currentUrl: URL, currentUrl: URL,
@ -101,7 +101,7 @@ export class OAuthManager {
async getUserInfo( async getUserInfo(
authServer: AuthorizationServer, authServer: AuthorizationServer,
issuer: (typeof config.oidc.providers)[0], issuer: (typeof config.oidc.providers)[0],
access_token: string, accessToken: string,
sub: string, sub: string,
) { ) {
return await userInfoRequest( return await userInfoRequest(
@ -110,7 +110,7 @@ export class OAuthManager {
client_id: issuer.client_id, client_id: issuer.client_id,
client_secret: issuer.client_secret, client_secret: issuer.client_secret,
}, },
access_token, accessToken,
).then( ).then(
async (res) => async (res) =>
await processUserInfoResponse( await processUserInfoResponse(
@ -125,7 +125,7 @@ export class OAuthManager {
); );
} }
async processOAuth2Error( processOAuth2Error(
application: InferInsertModel<typeof Applications> | null, application: InferInsertModel<typeof Applications> | null,
) { ) {
return { return {

View file

@ -27,7 +27,9 @@ export class Role extends BaseInterface<typeof Roles> {
} }
public static async fromId(id: string | null): Promise<Role | null> { public static async fromId(id: string | null): Promise<Role | null> {
if (!id) return null; if (!id) {
return null;
}
return await Role.fromSql(eq(Roles.id, id)); return await Role.fromSql(eq(Roles.id, id));
} }
@ -45,7 +47,9 @@ export class Role extends BaseInterface<typeof Roles> {
orderBy, orderBy,
}); });
if (!found) return null; if (!found) {
return null;
}
return new Role(found); return new Role(found);
} }
@ -123,7 +127,7 @@ export class Role extends BaseInterface<typeof Roles> {
return updated.data; return updated.data;
} }
async save(): Promise<RoleType> { save(): Promise<RoleType> {
return this.update(this.data); return this.update(this.data);
} }
@ -173,7 +177,7 @@ export class Role extends BaseInterface<typeof Roles> {
return this.data.id; return this.data.id;
} }
public toAPI() { public toApi() {
return { return {
id: this.id, id: this.id,
name: this.data.name, name: this.data.name,

View file

@ -5,20 +5,20 @@ import { Note } from "./note";
import { User } from "./user"; import { User } from "./user";
enum TimelineType { enum TimelineType {
NOTE = "Note", Note = "Note",
USER = "User", User = "User",
} }
export class Timeline { export class Timeline<Type extends Note | User> {
constructor(private type: TimelineType) {} constructor(private type: TimelineType) {}
static async getNoteTimeline( static getNoteTimeline(
sql: SQL<unknown> | undefined, sql: SQL<unknown> | undefined,
limit: number, limit: number,
url: string, url: string,
userId?: string, userId?: string,
) { ) {
return new Timeline(TimelineType.NOTE).fetchTimeline<Note>( return new Timeline<Note>(TimelineType.Note).fetchTimeline(
sql, sql,
limit, limit,
url, url,
@ -26,19 +26,158 @@ export class Timeline {
); );
} }
static async getUserTimeline( static getUserTimeline(
sql: SQL<unknown> | undefined, sql: SQL<unknown> | undefined,
limit: number, limit: number,
url: string, url: string,
) { ) {
return new Timeline(TimelineType.USER).fetchTimeline<User>( return new Timeline<User>(TimelineType.User).fetchTimeline(
sql, sql,
limit, limit,
url, url,
); );
} }
private async fetchTimeline<T>( private async fetchObjects(
sql: SQL<unknown> | undefined,
limit: number,
userId?: string,
): Promise<Type[]> {
switch (this.type) {
case TimelineType.Note:
return (await Note.manyFromSql(
sql,
undefined,
limit,
undefined,
userId,
)) as Type[];
case TimelineType.User:
return (await User.manyFromSql(
sql,
undefined,
limit,
)) as Type[];
}
}
private async fetchLinkHeader(
objects: Type[],
url: string,
limit: number,
): Promise<string> {
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:
linkHeader.push(
...(await this.fetchNoteLinkHeader(
objects as Note[],
urlWithoutQuery,
limit,
)),
);
break;
case TimelineType.User:
linkHeader.push(
...(await this.fetchUserLinkHeader(
objects as User[],
urlWithoutQuery,
limit,
)),
);
break;
}
}
return linkHeader.join(", ");
}
private async fetchNoteLinkHeader(
notes: Note[],
urlWithoutQuery: string,
limit: number,
): Promise<string[]> {
const linkHeader = [];
const objectBefore = await Note.fromSql(gt(Notes.id, notes[0].data.id));
if (objectBefore) {
linkHeader.push(
`<${urlWithoutQuery}?limit=${limit ?? 20}&min_id=${notes[0].data.id}>; rel="prev"`,
);
}
if (notes.length >= (limit ?? 20)) {
const objectAfter = await Note.fromSql(
gt(Notes.id, notes[notes.length - 1].data.id),
);
if (objectAfter) {
linkHeader.push(
`<${urlWithoutQuery}?limit=${limit ?? 20}&max_id=${notes[notes.length - 1].data.id}>; rel="next"`,
);
}
}
return linkHeader;
}
private async fetchUserLinkHeader(
users: User[],
urlWithoutQuery: string,
limit: number,
): Promise<string[]> {
const linkHeader = [];
const objectBefore = await User.fromSql(gt(Users.id, users[0].id));
if (objectBefore) {
linkHeader.push(
`<${urlWithoutQuery}?limit=${limit ?? 20}&min_id=${users[0].id}>; rel="prev"`,
);
}
if (users.length >= (limit ?? 20)) {
const objectAfter = await User.fromSql(
gt(Users.id, users[users.length - 1].id),
);
if (objectAfter) {
linkHeader.push(
`<${urlWithoutQuery}?limit=${limit ?? 20}&max_id=${users[users.length - 1].id}>; rel="next"`,
);
}
}
return linkHeader;
}
private async fetchTimeline(
sql: SQL<unknown> | undefined,
limit: number,
url: string,
userId?: string,
): Promise<{ link: string; objects: Type[] }> {
const objects = await this.fetchObjects(sql, limit, userId);
const link = await this.fetchLinkHeader(objects, url, limit);
switch (this.type) {
case TimelineType.Note:
return {
link,
objects: objects,
};
case TimelineType.User:
return {
link,
objects: objects,
};
}
}
/* private async fetchTimeline<T>(
sql: SQL<unknown> | undefined, sql: SQL<unknown> | undefined,
limit: number, limit: number,
url: string, url: string,
@ -48,7 +187,7 @@ export class Timeline {
const users: User[] = []; const users: User[] = [];
switch (this.type) { switch (this.type) {
case TimelineType.NOTE: case TimelineType.Note:
notes.push( notes.push(
...(await Note.manyFromSql( ...(await Note.manyFromSql(
sql, sql,
@ -59,7 +198,7 @@ export class Timeline {
)), )),
); );
break; break;
case TimelineType.USER: case TimelineType.User:
users.push(...(await User.manyFromSql(sql, undefined, limit))); users.push(...(await User.manyFromSql(sql, undefined, limit)));
break; break;
} }
@ -72,7 +211,7 @@ export class Timeline {
if (notes.length > 0) { if (notes.length > 0) {
switch (this.type) { switch (this.type) {
case TimelineType.NOTE: { case TimelineType.Note: {
const objectBefore = await Note.fromSql( const objectBefore = await Note.fromSql(
gt(Notes.id, notes[0].data.id), gt(Notes.id, notes[0].data.id),
); );
@ -102,7 +241,7 @@ export class Timeline {
} }
break; break;
} }
case TimelineType.USER: { case TimelineType.User: {
const objectBefore = await User.fromSql( const objectBefore = await User.fromSql(
gt(Users.id, users[0].id), gt(Users.id, users[0].id),
); );
@ -136,16 +275,16 @@ export class Timeline {
} }
switch (this.type) { switch (this.type) {
case TimelineType.NOTE: case TimelineType.Note:
return { return {
link: linkHeader.join(", "), link: linkHeader.join(", "),
objects: notes as T[], objects: notes as T[],
}; };
case TimelineType.USER: case TimelineType.User:
return { return {
link: linkHeader.join(", "), link: linkHeader.join(", "),
objects: users as T[], objects: users as T[],
}; };
} }
} } */
} }

View file

@ -5,6 +5,8 @@ import { addUserToMeilisearch } from "@/meilisearch";
import { proxyUrl } from "@/response"; import { proxyUrl } from "@/response";
import { EntityValidator } from "@lysand-org/federation"; import { EntityValidator } from "@lysand-org/federation";
import { import {
type InferInsertModel,
type InferSelectModel,
type SQL, type SQL,
and, and,
count, count,
@ -19,20 +21,21 @@ import {
} from "drizzle-orm"; } from "drizzle-orm";
import { htmlToText } from "html-to-text"; import { htmlToText } from "html-to-text";
import { import {
emojiToAPI, emojiToApi,
emojiToLysand, emojiToLysand,
fetchEmoji, fetchEmoji,
} from "~/database/entities/Emoji"; } from "~/database/entities/emoji";
import { objectToInboxRequest } from "~/database/entities/Federation"; import { objectToInboxRequest } from "~/database/entities/federation";
import { addInstanceIfNotExists } from "~/database/entities/Instance"; import { addInstanceIfNotExists } from "~/database/entities/instance";
import { import {
type UserWithRelations, type UserWithRelations,
findFirstUser, findFirstUser,
findManyUsers, findManyUsers,
} from "~/database/entities/User"; } from "~/database/entities/user";
import { db } from "~/drizzle/db"; import { db } from "~/drizzle/db";
import { import {
EmojiToUser, EmojiToUser,
type Instances,
NoteToMentions, NoteToMentions,
Notes, Notes,
type RolePermissions, type RolePermissions,
@ -40,8 +43,8 @@ import {
Users, Users,
} from "~/drizzle/schema"; } from "~/drizzle/schema";
import { type Config, config } from "~/packages/config-manager"; import { type Config, config } from "~/packages/config-manager";
import type { Account as APIAccount } from "~/types/mastodon/account"; import type { Account as apiAccount } from "~/types/mastodon/account";
import type { Mention as APIMention } from "~/types/mastodon/mention"; import type { Mention as apiMention } from "~/types/mastodon/mention";
import { BaseInterface } from "./base"; import { BaseInterface } from "./base";
import type { Note } from "./note"; import type { Note } from "./note";
import { Role } from "./role"; import { Role } from "./role";
@ -61,7 +64,9 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
} }
static async fromId(id: string | null): Promise<User | null> { static async fromId(id: string | null): Promise<User | null> {
if (!id) return null; if (!id) {
return null;
}
return await User.fromSql(eq(Users.id, id)); return await User.fromSql(eq(Users.id, id));
} }
@ -79,7 +84,9 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
orderBy, orderBy,
}); });
if (!found) return null; if (!found) {
return null;
}
return new User(found); return new User(found);
} }
@ -137,7 +144,9 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
// If admin, add admin permissions // If admin, add admin permissions
.concat(this.data.isAdmin ? config.permissions.admin : []) .concat(this.data.isAdmin ? config.permissions.admin : [])
.reduce((acc, permission) => { .reduce((acc, permission) => {
if (!acc.includes(permission)) acc.push(permission); if (!acc.includes(permission)) {
acc.push(permission);
}
return acc; return acc;
}, [] as RolePermissions[]) }, [] as RolePermissions[])
); );
@ -220,11 +229,11 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
)[0]; )[0];
} }
async save(): Promise<UserWithRelations> { save(): Promise<UserWithRelations> {
return this.update(this.data); return this.update(this.data);
} }
async updateFromRemote() { async updateFromRemote(): Promise<User> {
if (!this.isRemote()) { if (!this.isRemote()) {
throw new Error( throw new Error(
"Cannot refetch a local user (they are not remote)", "Cannot refetch a local user (they are not remote)",
@ -233,16 +242,12 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
const updated = await User.saveFromRemote(this.getUri()); const updated = await User.saveFromRemote(this.getUri());
if (!updated) {
throw new Error("User not found after update");
}
this.data = updated.data; this.data = updated.data;
return this; return this;
} }
static async saveFromRemote(uri: string): Promise<User | null> { static async saveFromRemote(uri: string): Promise<User> {
if (!URL.canParse(uri)) { if (!URL.canParse(uri)) {
throw new Error(`Invalid URI to parse ${uri}`); throw new Error(`Invalid URI to parse ${uri}`);
} }
@ -274,102 +279,25 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
emojis.push(await fetchEmoji(emoji)); emojis.push(await fetchEmoji(emoji));
} }
// Check if new user already exists const user = await User.fromLysand(data, instance);
const foundUser = await User.fromSql(eq(Users.uri, data.uri));
// If it exists, simply update it
if (foundUser) {
await foundUser.update({
updatedAt: new Date().toISOString(),
endpoints: {
dislikes: data.dislikes,
featured: data.featured,
likes: data.likes,
followers: data.followers,
following: data.following,
inbox: data.inbox,
outbox: data.outbox,
},
avatar: data.avatar
? Object.entries(data.avatar)[0][1].content
: "",
header: data.header
? Object.entries(data.header)[0][1].content
: "",
displayName: data.display_name ?? "",
note: getBestContentType(data.bio).content,
publicKey: data.public_key.public_key,
});
// Add emojis
if (emojis.length > 0) {
await db
.delete(EmojiToUser)
.where(eq(EmojiToUser.userId, foundUser.id));
await db.insert(EmojiToUser).values(
emojis.map((emoji) => ({
emojiId: emoji.id,
userId: foundUser.id,
})),
);
}
return foundUser;
}
const newUser = (
await db
.insert(Users)
.values({
username: data.username,
uri: data.uri,
createdAt: new Date(data.created_at).toISOString(),
endpoints: {
dislikes: data.dislikes,
featured: data.featured,
likes: data.likes,
followers: data.followers,
following: data.following,
inbox: data.inbox,
outbox: data.outbox,
},
fields: data.fields ?? [],
updatedAt: new Date(data.created_at).toISOString(),
instanceId: instance.id,
avatar: data.avatar
? Object.entries(data.avatar)[0][1].content
: "",
header: data.header
? Object.entries(data.header)[0][1].content
: "",
displayName: data.display_name ?? "",
note: getBestContentType(data.bio).content,
publicKey: data.public_key.public_key,
source: {
language: null,
note: "",
privacy: "public",
sensitive: false,
fields: [],
},
})
.returning()
)[0];
// Add emojis to user // Add emojis to user
if (emojis.length > 0) { if (emojis.length > 0) {
await db.delete(EmojiToUser).where(eq(EmojiToUser.userId, user.id));
await db.insert(EmojiToUser).values( await db.insert(EmojiToUser).values(
emojis.map((emoji) => ({ emojis.map((emoji) => ({
emojiId: emoji.id, emojiId: emoji.id,
userId: newUser.id, userId: user.id,
})), })),
); );
} }
const finalUser = await User.fromId(newUser.id); const finalUser = await User.fromId(user.id);
if (!finalUser) return null; if (!finalUser) {
throw new Error("Failed to save user from remote");
}
// Add to Meilisearch // Add to Meilisearch
await addUserToMeilisearch(finalUser); await addUserToMeilisearch(finalUser);
@ -377,17 +305,86 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
return finalUser; return finalUser;
} }
static async fromLysand(
user: typeof EntityValidator.$User,
instance: InferSelectModel<typeof Instances>,
): Promise<User> {
const data = {
username: user.username,
uri: user.uri,
createdAt: new Date(user.created_at).toISOString(),
endpoints: {
dislikes: user.dislikes,
featured: user.featured,
likes: user.likes,
followers: user.followers,
following: user.following,
inbox: user.inbox,
outbox: user.outbox,
},
fields: user.fields ?? [],
updatedAt: new Date(user.created_at).toISOString(),
instanceId: instance.id,
avatar: user.avatar
? Object.entries(user.avatar)[0][1].content
: "",
header: user.header
? Object.entries(user.header)[0][1].content
: "",
displayName: user.display_name ?? "",
note: getBestContentType(user.bio).content,
publicKey: user.public_key.public_key,
source: {
language: null,
note: "",
privacy: "public",
sensitive: false,
fields: [],
},
};
// Check if new user already exists
const foundUser = await User.fromSql(eq(Users.uri, user.uri));
// If it exists, simply update it
if (foundUser) {
await foundUser.update(data);
return foundUser;
}
// Else, create a new user
return await User.insert(data);
}
public static async insert(
data: InferInsertModel<typeof Users>,
): Promise<User> {
const inserted = (await db.insert(Users).values(data).returning())[0];
const user = await User.fromId(inserted.id);
if (!user) {
throw new Error("Failed to insert user");
}
return user;
}
static async resolve(uri: string): Promise<User | null> { static async resolve(uri: string): Promise<User | null> {
// Check if user not already in database // Check if user not already in database
const foundUser = await User.fromSql(eq(Users.uri, uri)); const foundUser = await User.fromSql(eq(Users.uri, uri));
if (foundUser) return foundUser; if (foundUser) {
return foundUser;
}
// Check if URI is of a local user // Check if URI is of a local user
if (uri.startsWith(config.http.base_url)) { if (uri.startsWith(config.http.base_url)) {
const uuid = uri.match(idValidator); const uuid = uri.match(idValidator);
if (!uuid || !uuid[0]) { if (!uuid?.[0]) {
throw new Error( throw new Error(
`URI ${uri} is of a local user, but it could not be parsed`, `URI ${uri} is of a local user, but it could not be parsed`,
); );
@ -405,11 +402,12 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
* @returns The raw URL for the user's avatar * @returns The raw URL for the user's avatar
*/ */
getAvatarUrl(config: Config) { getAvatarUrl(config: Config) {
if (!this.data.avatar) if (!this.data.avatar) {
return ( return (
config.defaults.avatar || config.defaults.avatar ||
`https://api.dicebear.com/8.x/${config.defaults.placeholder_style}/svg?seed=${this.data.username}` `https://api.dicebear.com/8.x/${config.defaults.placeholder_style}/svg?seed=${this.data.username}`
); );
}
return this.data.avatar; return this.data.avatar;
} }
@ -480,7 +478,9 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
const finalUser = await User.fromId(newUser.id); const finalUser = await User.fromId(newUser.id);
if (!finalUser) return null; if (!finalUser) {
return null;
}
// Add to Meilisearch // Add to Meilisearch
await addUserToMeilisearch(finalUser); await addUserToMeilisearch(finalUser);
@ -494,7 +494,9 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
* @returns The raw URL for the user's header * @returns The raw URL for the user's header
*/ */
getHeaderUrl(config: Config) { getHeaderUrl(config: Config) {
if (!this.data.header) return config.defaults.header || ""; if (!this.data.header) {
return config.defaults.header || "";
}
return this.data.header; return this.data.header;
} }
@ -561,7 +563,7 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
} }
} }
toAPI(isOwnAccount = false): APIAccount { toApi(isOwnAccount = false): apiAccount {
const user = this.data; const user = this.data;
return { return {
id: user.id, id: user.id,
@ -578,7 +580,7 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
followers_count: user.followerCount, followers_count: user.followerCount,
following_count: user.followingCount, following_count: user.followingCount,
statuses_count: user.statusCount, statuses_count: user.statusCount,
emojis: user.emojis.map((emoji) => emojiToAPI(emoji)), emojis: user.emojis.map((emoji) => emojiToApi(emoji)),
fields: user.fields.map((field) => ({ fields: user.fields.map((field) => ({
name: htmlToText(getBestContentType(field.key).content), name: htmlToText(getBestContentType(field.key).content),
value: getBestContentType(field.value).content, value: getBestContentType(field.value).content,
@ -625,7 +627,7 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
] ]
: [], : [],
) )
.map((r) => r.toAPI()), .map((r) => r.toApi()),
group: false, group: false,
}; };
} }
@ -699,7 +701,7 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
}; };
} }
toMention(): APIMention { toMention(): apiMention {
return { return {
url: this.getUri(), url: this.getUri(),
username: this.data.username, username: this.data.username,

View file

@ -2,12 +2,12 @@ import { join } from "node:path";
import { redirect } from "@/response"; import { redirect } from "@/response";
import type { BunFile } from "bun"; import type { BunFile } from "bun";
import { config } from "config-manager"; import { config } from "config-manager";
import { retrieveUserFromToken } from "~/database/entities/User"; import { retrieveUserFromToken } from "~/database/entities/user";
import type { User } from "~/packages/database-interface/user"; import type { User } from "~/packages/database-interface/user";
import type { LogManager, MultiLogManager } from "~/packages/log-manager"; import type { LogManager, MultiLogManager } from "~/packages/log-manager";
import { languages } from "./glitch-languages"; import { languages } from "./glitch-languages";
const handleManifestRequest = async () => { const handleManifestRequest = () => {
const manifest = { const manifest = {
id: "/home", id: "/home",
name: config.instance.name, name: config.instance.name,
@ -99,7 +99,7 @@ const handleManifestRequest = async () => {
const handleSignInRequest = async ( const handleSignInRequest = async (
req: Request, req: Request,
path: string, _path: string,
url: URL, url: URL,
user: User | null, user: User | null,
accessToken: string, accessToken: string,
@ -156,7 +156,7 @@ const handleSignInRequest = async (
); );
}; };
const handleSignOutRequest = async (req: Request) => { const handleSignOutRequest = (req: Request) => {
if (req.method === "POST") { if (req.method === "POST") {
return redirect("/api/auth/mastodon-logout", 307); return redirect("/api/auth/mastodon-logout", 307);
} }
@ -176,7 +176,7 @@ const returnFile = async (file: BunFile, content?: string) => {
}; };
const handleDefaultRequest = async ( const handleDefaultRequest = async (
req: Request, _req: Request,
path: string, path: string,
user: User | null, user: User | null,
accessToken: string, accessToken: string,
@ -198,10 +198,10 @@ const handleDefaultRequest = async (
return null; return null;
}; };
const brandingTransforms = async ( const brandingTransforms = (
fileContents: string, fileContents: string,
accessToken: string, _accessToken: string,
user: User | null, _user: User | null,
) => { ) => {
let newFileContents = fileContents; let newFileContents = fileContents;
for (const server of config.frontend.glitch.server) { for (const server of config.frontend.glitch.server) {
@ -287,7 +287,7 @@ const htmlTransforms = async (
}, },
accounts: user accounts: user
? { ? {
[user.id]: user.toAPI(true), [user.id]: user.toApi(true),
} }
: {}, : {},
media_attachments: { media_attachments: {
@ -327,7 +327,7 @@ const htmlTransforms = async (
export const handleGlitchRequest = async ( export const handleGlitchRequest = async (
req: Request, req: Request,
logger: LogManager | MultiLogManager, _logger: LogManager | MultiLogManager,
): Promise<Response | null> => { ): Promise<Response | null> => {
const url = new URL(req.url); const url = new URL(req.url);
let path = url.pathname; let path = url.pathname;
@ -336,7 +336,9 @@ export const handleGlitchRequest = async (
const user = await retrieveUserFromToken(accessToken ?? ""); const user = await retrieveUserFromToken(accessToken ?? "");
// Strip leading /web from path // Strip leading /web from path
if (path.startsWith("/web")) path = path.slice(4); if (path.startsWith("/web")) {
path = path.slice(4);
}
if (path === "/manifest") { if (path === "/manifest") {
return handleManifestRequest(); return handleManifestRequest();

View file

@ -5,19 +5,19 @@ import chalk from "chalk";
import { config } from "config-manager"; import { config } from "config-manager";
export enum LogLevel { export enum LogLevel {
DEBUG = "debug", Debug = "debug",
INFO = "info", Info = "info",
WARNING = "warning", Warning = "warning",
ERROR = "error", Error = "error",
CRITICAL = "critical", Critical = "critical",
} }
const logOrder = [ const logOrder = [
LogLevel.DEBUG, LogLevel.Debug,
LogLevel.INFO, LogLevel.Info,
LogLevel.WARNING, LogLevel.Warning,
LogLevel.ERROR, LogLevel.Error,
LogLevel.CRITICAL, LogLevel.Critical,
]; ];
/** /**
@ -37,15 +37,15 @@ export class LogManager {
getLevelColor(level: LogLevel) { getLevelColor(level: LogLevel) {
switch (level) { switch (level) {
case LogLevel.DEBUG: case LogLevel.Debug:
return chalk.blue; return chalk.blue;
case LogLevel.INFO: case LogLevel.Info:
return chalk.green; return chalk.green;
case LogLevel.WARNING: case LogLevel.Warning:
return chalk.yellow; return chalk.yellow;
case LogLevel.ERROR: case LogLevel.Error:
return chalk.red; return chalk.red;
case LogLevel.CRITICAL: case LogLevel.Critical:
return chalk.bgRed; return chalk.bgRed;
} }
} }
@ -79,8 +79,9 @@ export class LogManager {
if ( if (
logOrder.indexOf(level) < logOrder.indexOf(level) <
logOrder.indexOf(config.logging.log_level as LogLevel) logOrder.indexOf(config.logging.log_level as LogLevel)
) ) {
return; return;
}
if (this.enableColors) { if (this.enableColors) {
await this.write( await this.write(
@ -102,9 +103,8 @@ export class LogManager {
} }
private async write(text: string) { private async write(text: string) {
Bun.stdout.name;
if (this.output === Bun.stdout) { if (this.output === Bun.stdout) {
await console.log(`${text}`); console.info(text);
} else { } else {
if (!(await exists(this.output.name ?? ""))) { if (!(await exists(this.output.name ?? ""))) {
// Create file if it doesn't exist // Create file if it doesn't exist
@ -128,17 +128,112 @@ export class LogManager {
* @param error Error to log * @param error Error to log
*/ */
async logError(level: LogLevel, entity: string, error: Error) { async logError(level: LogLevel, entity: string, error: Error) {
error.stack && (await this.log(LogLevel.DEBUG, entity, error.stack)); error.stack && (await this.log(LogLevel.Debug, entity, error.stack));
await this.log(level, entity, error.message); await this.log(level, entity, error.message);
} }
/**
* Logs the headers of a request
* @param req Request to log
*/
public logHeaders(req: Request): string {
let string = " [Headers]\n";
for (const [key, value] of req.headers.entries()) {
string += ` ${key}: ${value}\n`;
}
return string;
}
/**
* Logs the body of a request
* @param req Request to log
*/
async logBody(req: Request): Promise<string> {
let string = " [Body]\n";
const contentType = req.headers.get("Content-Type");
if (contentType?.includes("application/json")) {
string += await this.logJsonBody(req);
} else if (
contentType &&
(contentType.includes("application/x-www-form-urlencoded") ||
contentType.includes("multipart/form-data"))
) {
string += await this.logFormData(req);
} else {
const text = await req.text();
string += ` ${text}\n`;
}
return string;
}
/**
* Logs the JSON body of a request
* @param req Request to log
*/
async logJsonBody(req: Request): Promise<string> {
let string = "";
try {
const json = await req.clone().json();
const stringified = JSON.stringify(json, null, 4)
.split("\n")
.map((line) => ` ${line}`)
.join("\n");
string += `${stringified}\n`;
} catch {
string += ` [Invalid JSON] (raw: ${await req.clone().text()})\n`;
}
return string;
}
/**
* Logs the form data of a request
* @param req Request to log
*/
async logFormData(req: Request): Promise<string> {
let string = "";
const formData = await req.clone().formData();
for (const [key, value] of formData.entries()) {
if (value.toString().length < 300) {
string += ` ${key}: ${value.toString()}\n`;
} else {
string += ` ${key}: <${value.toString().length} bytes>\n`;
}
}
return string;
}
/** /**
* Logs a request to the output * Logs a request to the output
* @param req Request to log * @param req Request to log
* @param ip IP of the request * @param ip IP of the request
* @param logAllDetails Whether to log all details of the request * @param logAllDetails Whether to log all details of the request
*/ */
async logRequest(req: Request, ip?: string, logAllDetails = false) { async logRequest(
req: Request,
ip?: string,
logAllDetails = false,
): Promise<void> {
let string = ip ? `${ip}: ` : "";
string += `${req.method} ${req.url}`;
if (logAllDetails) {
string += "\n";
string += await this.logHeaders(req);
string += await this.logBody(req);
}
await this.log(LogLevel.Info, "Request", string);
}
/*
* Logs a request to the output
* @param req Request to log
* @param ip IP of the request
* @param logAllDetails Whether to log all details of the request
*/
/**async logRequest(req: Request, ip?: string, logAllDetails = false) {
let string = ip ? `${ip}: ` : ""; let string = ip ? `${ip}: ` : "";
string += `${req.method} ${req.url}`; string += `${req.method} ${req.url}`;
@ -153,9 +248,9 @@ export class LogManager {
// Pretty print body // Pretty print body
string += " [Body]\n"; string += " [Body]\n";
const content_type = req.headers.get("Content-Type"); const contentType = req.headers.get("Content-Type");
if (content_type?.includes("application/json")) { if (contentType?.includes("application/json")) {
try { try {
const json = await req.clone().json(); const json = await req.clone().json();
const stringified = JSON.stringify(json, null, 4) const stringified = JSON.stringify(json, null, 4)
@ -170,9 +265,9 @@ export class LogManager {
.text()})\n`; .text()})\n`;
} }
} else if ( } else if (
content_type && contentType &&
(content_type.includes("application/x-www-form-urlencoded") || (contentType.includes("application/x-www-form-urlencoded") ||
content_type.includes("multipart/form-data")) contentType.includes("multipart/form-data"))
) { ) {
const formData = await req.clone().formData(); const formData = await req.clone().formData();
for (const [key, value] of formData.entries()) { for (const [key, value] of formData.entries()) {
@ -189,8 +284,8 @@ export class LogManager {
string += ` ${text}\n`; string += ` ${text}\n`;
} }
} }
await this.log(LogLevel.INFO, "Request", string); await this.log(LogLevel.Info, "Request", string);
} } */
} }
/** /**

View file

@ -36,7 +36,7 @@ describe("LogManager", () => {
}); });
*/ */
it("should log message with timestamp", async () => { it("should log message with timestamp", async () => {
await logManager.log(LogLevel.INFO, "TestEntity", "Test message"); await logManager.log(LogLevel.Info, "TestEntity", "Test message");
expect(mockAppend).toHaveBeenCalledWith( expect(mockAppend).toHaveBeenCalledWith(
mockOutput.name, mockOutput.name,
expect.stringContaining("[INFO] TestEntity: Test message"), expect.stringContaining("[INFO] TestEntity: Test message"),
@ -45,7 +45,7 @@ describe("LogManager", () => {
it("should log message without timestamp", async () => { it("should log message without timestamp", async () => {
await logManager.log( await logManager.log(
LogLevel.INFO, LogLevel.Info,
"TestEntity", "TestEntity",
"Test message", "Test message",
false, false,
@ -56,9 +56,10 @@ describe("LogManager", () => {
); );
}); });
// biome-ignore lint/suspicious/noSkippedTests: I need to fix this :sob:
test.skip("should write to stdout", async () => { test.skip("should write to stdout", async () => {
logManager = new LogManager(Bun.stdout); logManager = new LogManager(Bun.stdout);
await logManager.log(LogLevel.INFO, "TestEntity", "Test message"); await logManager.log(LogLevel.Info, "TestEntity", "Test message");
const writeMock = jest.fn(); const writeMock = jest.fn();
@ -75,7 +76,7 @@ describe("LogManager", () => {
it("should log error message", async () => { it("should log error message", async () => {
const error = new Error("Test error"); const error = new Error("Test error");
await logManager.logError(LogLevel.ERROR, "TestEntity", error); await logManager.logError(LogLevel.Error, "TestEntity", error);
expect(mockAppend).toHaveBeenCalledWith( expect(mockAppend).toHaveBeenCalledWith(
mockOutput.name, mockOutput.name,
expect.stringContaining("[ERROR] TestEntity: Test error"), expect.stringContaining("[ERROR] TestEntity: Test error"),
@ -191,10 +192,10 @@ describe("MultiLogManager", () => {
}); });
it("should log message to all logManagers", async () => { it("should log message to all logManagers", async () => {
await multiLogManager.log(LogLevel.INFO, "TestEntity", "Test message"); await multiLogManager.log(LogLevel.Info, "TestEntity", "Test message");
expect(mockLog).toHaveBeenCalledTimes(2); expect(mockLog).toHaveBeenCalledTimes(2);
expect(mockLog).toHaveBeenCalledWith( expect(mockLog).toHaveBeenCalledWith(
LogLevel.INFO, LogLevel.Info,
"TestEntity", "TestEntity",
"Test message", "Test message",
true, true,
@ -203,10 +204,10 @@ describe("MultiLogManager", () => {
it("should log error to all logManagers", async () => { it("should log error to all logManagers", async () => {
const error = new Error("Test error"); const error = new Error("Test error");
await multiLogManager.logError(LogLevel.ERROR, "TestEntity", error); await multiLogManager.logError(LogLevel.Error, "TestEntity", error);
expect(mockLogError).toHaveBeenCalledTimes(2); expect(mockLogError).toHaveBeenCalledTimes(2);
expect(mockLogError).toHaveBeenCalledWith( expect(mockLogError).toHaveBeenCalledWith(
LogLevel.ERROR, LogLevel.Error,
"TestEntity", "TestEntity",
error, error,
); );

View file

@ -4,7 +4,7 @@ import type { Config } from "config-manager";
import { MediaConverter } from "./media-converter"; import { MediaConverter } from "./media-converter";
export enum MediaBackendType { export enum MediaBackendType {
LOCAL = "local", Local = "local",
S3 = "s3", S3 = "s3",
} }
@ -35,12 +35,12 @@ export class MediaBackend {
public backend: MediaBackendType, public backend: MediaBackendType,
) {} ) {}
static async fromBackendType( public static fromBackendType(
backend: MediaBackendType, backend: MediaBackendType,
config: Config, config: Config,
): Promise<MediaBackend> { ): MediaBackend {
switch (backend) { switch (backend) {
case MediaBackendType.LOCAL: case MediaBackendType.Local:
return new LocalMediaBackend(config); return new LocalMediaBackend(config);
case MediaBackendType.S3: case MediaBackendType.S3:
return new S3MediaBackend(config); return new S3MediaBackend(config);
@ -64,15 +64,15 @@ export class MediaBackend {
* @returns The file as a File object * @returns The file as a File object
*/ */
public getFileByHash( public getFileByHash(
file: string, _file: string,
databaseHashFetcher: (sha256: string) => Promise<string>, _databaseHashFetcher: (sha256: string) => Promise<string>,
): Promise<File | null> { ): Promise<File | null> {
return Promise.reject( return Promise.reject(
new Error("Do not call MediaBackend directly: use a subclass"), new Error("Do not call MediaBackend directly: use a subclass"),
); );
} }
public deleteFileByUrl(url: string): Promise<void> { public deleteFileByUrl(_url: string): Promise<void> {
return Promise.reject( return Promise.reject(
new Error("Do not call MediaBackend directly: use a subclass"), new Error("Do not call MediaBackend directly: use a subclass"),
); );
@ -83,7 +83,7 @@ export class MediaBackend {
* @param filename File name * @param filename File name
* @returns The file as a File object * @returns The file as a File object
*/ */
public getFile(filename: string): Promise<File | null> { public getFile(_filename: string): Promise<File | null> {
return Promise.reject( return Promise.reject(
new Error("Do not call MediaBackend directly: use a subclass"), new Error("Do not call MediaBackend directly: use a subclass"),
); );
@ -94,7 +94,7 @@ export class MediaBackend {
* @param file File to add * @param file File to add
* @returns Metadata about the uploaded file * @returns Metadata about the uploaded file
*/ */
public addFile(file: File): Promise<UploadedFileMetadata> { public addFile(_file: File): Promise<UploadedFileMetadata> {
return Promise.reject( return Promise.reject(
new Error("Do not call MediaBackend directly: use a subclass"), new Error("Do not call MediaBackend directly: use a subclass"),
); );
@ -103,7 +103,7 @@ export class MediaBackend {
export class LocalMediaBackend extends MediaBackend { export class LocalMediaBackend extends MediaBackend {
constructor(config: Config) { constructor(config: Config) {
super(config, MediaBackendType.LOCAL); super(config, MediaBackendType.Local);
} }
public async addFile(file: File) { public async addFile(file: File) {
@ -164,7 +164,9 @@ export class LocalMediaBackend extends MediaBackend {
): Promise<File | null> { ): Promise<File | null> {
const filename = await databaseHashFetcher(hash); const filename = await databaseHashFetcher(hash);
if (!filename) return null; if (!filename) {
return null;
}
return this.getFile(filename); return this.getFile(filename);
} }
@ -174,7 +176,9 @@ export class LocalMediaBackend extends MediaBackend {
`${this.config.media.local_uploads_folder}/${filename}`, `${this.config.media.local_uploads_folder}/${filename}`,
); );
if (!(await file.exists())) return null; if (!(await file.exists())) {
return null;
}
return new File([await file.arrayBuffer()], filename, { return new File([await file.arrayBuffer()], filename, {
type: file.type, type: file.type,
@ -243,7 +247,9 @@ export class S3MediaBackend extends MediaBackend {
): Promise<File | null> { ): Promise<File | null> {
const filename = await databaseHashFetcher(hash); const filename = await databaseHashFetcher(hash);
if (!filename) return null; if (!filename) {
return null;
}
return this.getFile(filename); return this.getFile(filename);
} }

View file

@ -36,7 +36,7 @@ describe("MediaBackend", () => {
describe("fromBackendType", () => { describe("fromBackendType", () => {
it("should return a LocalMediaBackend instance for LOCAL backend type", async () => { it("should return a LocalMediaBackend instance for LOCAL backend type", async () => {
const backend = await MediaBackend.fromBackendType( const backend = await MediaBackend.fromBackendType(
MediaBackendType.LOCAL, MediaBackendType.Local,
mockConfig, mockConfig,
); );
expect(backend).toBeInstanceOf(LocalMediaBackend); expect(backend).toBeInstanceOf(LocalMediaBackend);
@ -62,8 +62,8 @@ describe("MediaBackend", () => {
it("should throw an error for unknown backend type", () => { it("should throw an error for unknown backend type", () => {
expect( expect(
// @ts-expect-error This is a test // @ts-expect-error This is a test
MediaBackend.fromBackendType("unknown", mockConfig), () => MediaBackend.fromBackendType("unknown", mockConfig),
).rejects.toThrow("Unknown backend type: unknown"); ).toThrow("Unknown backend type: unknown");
}); });
}); });
@ -228,7 +228,7 @@ describe("LocalMediaBackend", () => {
it("should initialize with correct type", () => { it("should initialize with correct type", () => {
expect(localMediaBackend.getBackendType()).toEqual( expect(localMediaBackend.getBackendType()).toEqual(
MediaBackendType.LOCAL, MediaBackendType.Local,
); );
}); });

View file

@ -16,7 +16,7 @@ export const meta = applyConfig({
}); });
export default (app: Hono) => export default (app: Hono) =>
app.on(meta.allowedMethods, meta.route, async (context) => { app.on(meta.allowedMethods, meta.route, (_context) => {
return jsonResponse({ return jsonResponse({
http: { http: {
bind: config.http.bind, bind: config.http.bind,

View file

@ -108,7 +108,7 @@ describe(meta.route, () => {
expect(response.headers.get("Set-Cookie")).toMatch(/jwt=[^;]+;/); expect(response.headers.get("Set-Cookie")).toMatch(/jwt=[^;]+;/);
}); });
describe("should reject invalid credentials", async () => { describe("should reject invalid credentials", () => {
// Redirects to /oauth/authorize on invalid // Redirects to /oauth/authorize on invalid
test("invalid email", async () => { test("invalid email", async () => {
const formData = new FormData(); const formData = new FormData();

View file

@ -65,9 +65,10 @@ const returnError = (query: object, error: string, description: string) => {
// Add all data that is not undefined except email and password // Add all data that is not undefined except email and password
for (const [key, value] of Object.entries(query)) { for (const [key, value] of Object.entries(query)) {
if (key !== "email" && key !== "password" && value !== undefined) if (key !== "email" && key !== "password" && value !== undefined) {
searchParams.append(key, value); searchParams.append(key, value);
} }
}
searchParams.append("error", error); searchParams.append("error", error);
searchParams.append("error_description", description); searchParams.append("error_description", description);
@ -107,14 +108,20 @@ export default (app: Hono) =>
); );
if ( if (
!user || !(
!(await Bun.password.verify(password, user.data.password || "")) user &&
(await Bun.password.verify(
password,
user.data.password || "",
))
) )
) {
return returnError( return returnError(
context.req.query(), context.req.query(),
"invalid_grant", "invalid_grant",
"Invalid identifier or password", "Invalid identifier or password",
); );
}
if (user.data.passwordResetToken) { if (user.data.passwordResetToken) {
return response(null, 302, { return response(null, 302, {
@ -163,8 +170,9 @@ export default (app: Hono) =>
application: application.name, application: application.name,
}); });
if (application.website) if (application.website) {
searchParams.append("website", application.website); searchParams.append("website", application.website);
}
// Add all data that is not undefined except email and password // Add all data that is not undefined except email and password
for (const [key, value] of Object.entries(context.req.query())) { for (const [key, value] of Object.entries(context.req.query())) {
@ -172,9 +180,10 @@ export default (app: Hono) =>
key !== "email" && key !== "email" &&
key !== "password" && key !== "password" &&
value !== undefined value !== undefined
) ) {
searchParams.append(key, String(value)); searchParams.append(key, String(value));
} }
}
// Redirect to OAuth authorize with JWT // Redirect to OAuth authorize with JWT
return response(null, 302, { return response(null, 302, {

View file

@ -5,7 +5,7 @@ import { zValidator } from "@hono/zod-validator";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import type { Hono } from "hono"; import type { Hono } from "hono";
import { z } from "zod"; import { z } from "zod";
import { TokenType } from "~/database/entities/Token"; import { TokenType } from "~/database/entities/token";
import { db } from "~/drizzle/db"; import { db } from "~/drizzle/db";
import { Tokens, Users } from "~/drizzle/schema"; import { Tokens, Users } from "~/drizzle/schema";
import { config } from "~/packages/config-manager"; import { config } from "~/packages/config-manager";
@ -56,10 +56,16 @@ export default (app: Hono) =>
const user = await User.fromSql(eq(Users.email, email)); const user = await User.fromSql(eq(Users.email, email));
if ( if (
!user || !(
!(await Bun.password.verify(password, user.data.password || "")) user &&
(await Bun.password.verify(
password,
user.data.password || "",
))
) )
) {
return redirectToLogin("Invalid email or password"); return redirectToLogin("Invalid email or password");
}
if (user.data.passwordResetToken) { if (user.data.passwordResetToken) {
return response(null, 302, { return response(null, 302, {
@ -82,7 +88,7 @@ export default (app: Hono) =>
accessToken, accessToken,
code: code, code: code,
scope: "read write follow push", scope: "read write follow push",
tokenType: TokenType.BEARER, tokenType: TokenType.Bearer,
applicationId: null, applicationId: null,
userId: user.id, userId: user.id,
}); });

View file

@ -18,7 +18,7 @@ export const meta = applyConfig({
* Mastodon-FE logout route * Mastodon-FE logout route
*/ */
export default (app: Hono) => export default (app: Hono) =>
app.on(meta.allowedMethods, meta.route, async () => { app.on(meta.allowedMethods, meta.route, () => {
return new Response(null, { return new Response(null, {
headers: { headers: {
Location: "/", Location: "/",

View file

@ -63,8 +63,9 @@ export default (app: Hono) =>
) )
.limit(1); .limit(1);
if (!foundToken || foundToken.length <= 0) if (!foundToken || foundToken.length <= 0) {
return redirectToLogin("Invalid code"); return redirectToLogin("Invalid code");
}
// Redirect back to application // Redirect back to application
return Response.redirect(`${redirect_uri}?code=${code}`, 302); return Response.redirect(`${redirect_uri}?code=${code}`, 302);

View file

@ -1,7 +1,7 @@
import { afterAll, describe, expect, test } from "bun:test"; import { afterAll, describe, expect, test } from "bun:test";
import { config } from "config-manager"; import { config } from "config-manager";
import { getTestUsers, sendTestRequest } from "~/tests/utils"; import { getTestUsers, sendTestRequest } from "~/tests/utils";
import type { Relationship as APIRelationship } from "~/types/mastodon/relationship"; import type { Relationship as apiRelationship } from "~/types/mastodon/relationship";
import { meta } from "./block"; import { meta } from "./block";
const { users, tokens, deleteUsers } = await getTestUsers(2); const { users, tokens, deleteUsers } = await getTestUsers(2);
@ -65,7 +65,7 @@ describe(meta.route, () => {
); );
expect(response.status).toBe(200); expect(response.status).toBe(200);
const relationship = (await response.json()) as APIRelationship; const relationship = (await response.json()) as apiRelationship;
expect(relationship.blocking).toBe(true); expect(relationship.blocking).toBe(true);
}); });
@ -86,7 +86,7 @@ describe(meta.route, () => {
); );
expect(response.status).toBe(200); expect(response.status).toBe(200);
const relationship = (await response.json()) as APIRelationship; const relationship = (await response.json()) as apiRelationship;
expect(relationship.blocking).toBe(true); expect(relationship.blocking).toBe(true);
}); });
}); });

View file

@ -4,8 +4,8 @@ import { zValidator } from "@hono/zod-validator";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import type { Hono } from "hono"; import type { Hono } from "hono";
import { z } from "zod"; import { z } from "zod";
import { relationshipToAPI } from "~/database/entities/Relationship"; import { relationshipToApi } from "~/database/entities/relationship";
import { getRelationshipToOtherUser } from "~/database/entities/User"; import { getRelationshipToOtherUser } from "~/database/entities/user";
import { db } from "~/drizzle/db"; import { db } from "~/drizzle/db";
import { Relationships, RolePermissions } from "~/drizzle/schema"; import { Relationships, RolePermissions } from "~/drizzle/schema";
import { User } from "~/packages/database-interface/user"; import { User } from "~/packages/database-interface/user";
@ -23,8 +23,8 @@ export const meta = applyConfig({
}, },
permissions: { permissions: {
required: [ required: [
RolePermissions.MANAGE_OWN_BLOCKS, RolePermissions.ManageOwnBlocks,
RolePermissions.VIEW_ACCOUNTS, RolePermissions.ViewAccounts,
], ],
}, },
}); });
@ -45,11 +45,15 @@ export default (app: Hono) =>
const { id } = context.req.valid("param"); const { id } = context.req.valid("param");
const { user } = context.req.valid("header"); const { user } = context.req.valid("header");
if (!user) return errorResponse("Unauthorized", 401); if (!user) {
return errorResponse("Unauthorized", 401);
}
const otherUser = await User.fromId(id); const otherUser = await User.fromId(id);
if (!otherUser) return errorResponse("User not found", 404); if (!otherUser) {
return errorResponse("User not found", 404);
}
const foundRelationship = await getRelationshipToOtherUser( const foundRelationship = await getRelationshipToOtherUser(
user, user,
@ -67,6 +71,6 @@ export default (app: Hono) =>
}) })
.where(eq(Relationships.id, foundRelationship.id)); .where(eq(Relationships.id, foundRelationship.id));
return jsonResponse(relationshipToAPI(foundRelationship)); return jsonResponse(relationshipToApi(foundRelationship));
}, },
); );

View file

@ -1,7 +1,7 @@
import { afterAll, describe, expect, test } from "bun:test"; import { afterAll, describe, expect, test } from "bun:test";
import { config } from "config-manager"; import { config } from "config-manager";
import { getTestUsers, sendTestRequest } from "~/tests/utils"; import { getTestUsers, sendTestRequest } from "~/tests/utils";
import type { Relationship as APIRelationship } from "~/types/mastodon/relationship"; import type { Relationship as apiRelationship } from "~/types/mastodon/relationship";
import { meta } from "./follow"; import { meta } from "./follow";
const { users, tokens, deleteUsers } = await getTestUsers(2); const { users, tokens, deleteUsers } = await getTestUsers(2);
@ -73,7 +73,7 @@ describe(meta.route, () => {
); );
expect(response.status).toBe(200); expect(response.status).toBe(200);
const relationship = (await response.json()) as APIRelationship; const relationship = (await response.json()) as apiRelationship;
expect(relationship.following).toBe(true); expect(relationship.following).toBe(true);
}); });
@ -96,7 +96,7 @@ describe(meta.route, () => {
); );
expect(response.status).toBe(200); expect(response.status).toBe(200);
const relationship = (await response.json()) as APIRelationship; const relationship = (await response.json()) as apiRelationship;
expect(relationship.following).toBe(true); expect(relationship.following).toBe(true);
}); });
}); });

View file

@ -4,11 +4,11 @@ import { zValidator } from "@hono/zod-validator";
import type { Hono } from "hono"; import type { Hono } from "hono";
import ISO6391 from "iso-639-1"; import ISO6391 from "iso-639-1";
import { z } from "zod"; import { z } from "zod";
import { relationshipToAPI } from "~/database/entities/Relationship"; import { relationshipToApi } from "~/database/entities/relationship";
import { import {
followRequestUser, followRequestUser,
getRelationshipToOtherUser, getRelationshipToOtherUser,
} from "~/database/entities/User"; } from "~/database/entities/user";
import { RolePermissions } from "~/drizzle/schema"; import { RolePermissions } from "~/drizzle/schema";
import { User } from "~/packages/database-interface/user"; import { User } from "~/packages/database-interface/user";
@ -25,8 +25,8 @@ export const meta = applyConfig({
}, },
permissions: { permissions: {
required: [ required: [
RolePermissions.MANAGE_OWN_FOLLOWS, RolePermissions.ManageOwnFollows,
RolePermissions.VIEW_ACCOUNTS, RolePermissions.ViewAccounts,
], ],
}, },
}); });
@ -59,11 +59,15 @@ export default (app: Hono) =>
const { user } = context.req.valid("header"); const { user } = context.req.valid("header");
const { reblogs, notify, languages } = context.req.valid("json"); const { reblogs, notify, languages } = context.req.valid("json");
if (!user) return errorResponse("Unauthorized", 401); if (!user) {
return errorResponse("Unauthorized", 401);
}
const otherUser = await User.fromId(id); const otherUser = await User.fromId(id);
if (!otherUser) return errorResponse("User not found", 404); if (!otherUser) {
return errorResponse("User not found", 404);
}
let relationship = await getRelationshipToOtherUser( let relationship = await getRelationshipToOtherUser(
user, user,
@ -81,6 +85,6 @@ export default (app: Hono) =>
); );
} }
return jsonResponse(relationshipToAPI(relationship)); return jsonResponse(relationshipToApi(relationship));
}, },
); );

View file

@ -1,7 +1,7 @@
import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { config } from "config-manager"; import { config } from "config-manager";
import { getTestUsers, sendTestRequest } from "~/tests/utils"; import { getTestUsers, sendTestRequest } from "~/tests/utils";
import type { Account as APIAccount } from "~/types/mastodon/account"; import type { Account as apiAccount } from "~/types/mastodon/account";
import { meta } from "./followers"; import { meta } from "./followers";
const { users, tokens, deleteUsers } = await getTestUsers(5); const { users, tokens, deleteUsers } = await getTestUsers(5);
@ -51,7 +51,7 @@ describe(meta.route, () => {
expect(response.status).toBe(200); expect(response.status).toBe(200);
const data = (await response.json()) as APIAccount[]; const data = (await response.json()) as apiAccount[];
expect(data).toBeInstanceOf(Array); expect(data).toBeInstanceOf(Array);
expect(data.length).toBe(1); expect(data.length).toBe(1);
@ -93,7 +93,7 @@ describe(meta.route, () => {
expect(response2.status).toBe(200); expect(response2.status).toBe(200);
const data = (await response2.json()) as APIAccount[]; const data = (await response2.json()) as apiAccount[];
expect(data).toBeInstanceOf(Array); expect(data).toBeInstanceOf(Array);
expect(data.length).toBe(0); expect(data.length).toBe(0);

View file

@ -21,8 +21,8 @@ export const meta = applyConfig({
}, },
permissions: { permissions: {
required: [ required: [
RolePermissions.VIEW_ACCOUNT_FOLLOWS, RolePermissions.ViewAccountFollows,
RolePermissions.VIEW_ACCOUNTS, RolePermissions.ViewAccounts,
], ],
}, },
}); });
@ -55,7 +55,9 @@ export default (app: Hono) =>
// TODO: Add follower/following privacy settings // TODO: Add follower/following privacy settings
if (!otherUser) return errorResponse("User not found", 404); if (!otherUser) {
return errorResponse("User not found", 404);
}
const { objects, link } = await Timeline.getUserTimeline( const { objects, link } = await Timeline.getUserTimeline(
and( and(
@ -69,7 +71,7 @@ export default (app: Hono) =>
); );
return jsonResponse( return jsonResponse(
await Promise.all(objects.map((object) => object.toAPI())), await Promise.all(objects.map((object) => object.toApi())),
200, 200,
{ {
Link: link, Link: link,

View file

@ -1,7 +1,7 @@
import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { config } from "config-manager"; import { config } from "config-manager";
import { getTestUsers, sendTestRequest } from "~/tests/utils"; import { getTestUsers, sendTestRequest } from "~/tests/utils";
import type { Account as APIAccount } from "~/types/mastodon/account"; import type { Account as apiAccount } from "~/types/mastodon/account";
import { meta } from "./following"; import { meta } from "./following";
const { users, tokens, deleteUsers } = await getTestUsers(5); const { users, tokens, deleteUsers } = await getTestUsers(5);
@ -51,7 +51,7 @@ describe(meta.route, () => {
expect(response.status).toBe(200); expect(response.status).toBe(200);
const data = (await response.json()) as APIAccount[]; const data = (await response.json()) as apiAccount[];
expect(data).toBeInstanceOf(Array); expect(data).toBeInstanceOf(Array);
expect(data.length).toBe(1); expect(data.length).toBe(1);
@ -93,7 +93,7 @@ describe(meta.route, () => {
expect(response2.status).toBe(200); expect(response2.status).toBe(200);
const data = (await response2.json()) as APIAccount[]; const data = (await response2.json()) as apiAccount[];
expect(data).toBeInstanceOf(Array); expect(data).toBeInstanceOf(Array);
expect(data.length).toBe(0); expect(data.length).toBe(0);

View file

@ -21,8 +21,8 @@ export const meta = applyConfig({
}, },
permissions: { permissions: {
required: [ required: [
RolePermissions.VIEW_ACCOUNT_FOLLOWS, RolePermissions.ViewAccountFollows,
RolePermissions.VIEW_ACCOUNTS, RolePermissions.ViewAccounts,
], ],
}, },
}); });
@ -52,7 +52,9 @@ export default (app: Hono) =>
const otherUser = await User.fromId(id); const otherUser = await User.fromId(id);
if (!otherUser) return errorResponse("User not found", 404); if (!otherUser) {
return errorResponse("User not found", 404);
}
// TODO: Add follower/following privacy settings // TODO: Add follower/following privacy settings
@ -68,7 +70,7 @@ export default (app: Hono) =>
); );
return jsonResponse( return jsonResponse(
await Promise.all(objects.map((object) => object.toAPI())), await Promise.all(objects.map((object) => object.toApi())),
200, 200,
{ {
Link: link, Link: link,

View file

@ -1,7 +1,7 @@
import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { config } from "config-manager"; import { config } from "config-manager";
import { getTestStatuses, getTestUsers, sendTestRequest } from "~/tests/utils"; import { getTestStatuses, getTestUsers, sendTestRequest } from "~/tests/utils";
import type { Account as APIAccount } from "~/types/mastodon/account"; import type { Account as apiAccount } from "~/types/mastodon/account";
import { meta } from "./index"; import { meta } from "./index";
const { users, tokens, deleteUsers } = await getTestUsers(5); const { users, tokens, deleteUsers } = await getTestUsers(5);
@ -58,7 +58,7 @@ describe(meta.route, () => {
expect(response.status).toBe(200); expect(response.status).toBe(200);
const data = (await response.json()) as APIAccount; const data = (await response.json()) as apiAccount;
expect(data).toMatchObject({ expect(data).toMatchObject({
id: users[0].id, id: users[0].id,
username: users[0].data.username, username: users[0].data.username,
@ -93,6 +93,6 @@ describe(meta.route, () => {
icon: null, icon: null,
}), }),
]), ]),
} satisfies APIAccount); } satisfies apiAccount);
}); });
}); });

View file

@ -18,7 +18,7 @@ export const meta = applyConfig({
oauthPermissions: [], oauthPermissions: [],
}, },
permissions: { permissions: {
required: [RolePermissions.VIEW_ACCOUNTS], required: [RolePermissions.ViewAccounts],
}, },
}); });
@ -40,8 +40,10 @@ export default (app: Hono) =>
const foundUser = await User.fromId(id); const foundUser = await User.fromId(id);
if (!foundUser) return errorResponse("User not found", 404); if (!foundUser) {
return errorResponse("User not found", 404);
}
return jsonResponse(foundUser.toAPI(user?.id === foundUser.id)); return jsonResponse(foundUser.toApi(user?.id === foundUser.id));
}, },
); );

View file

@ -1,7 +1,7 @@
import { afterAll, describe, expect, test } from "bun:test"; import { afterAll, describe, expect, test } from "bun:test";
import { config } from "config-manager"; import { config } from "config-manager";
import { getTestUsers, sendTestRequest } from "~/tests/utils"; import { getTestUsers, sendTestRequest } from "~/tests/utils";
import type { Relationship as APIRelationship } from "~/types/mastodon/relationship"; import type { Relationship as apiRelationship } from "~/types/mastodon/relationship";
import { meta } from "./mute"; import { meta } from "./mute";
const { users, tokens, deleteUsers } = await getTestUsers(2); const { users, tokens, deleteUsers } = await getTestUsers(2);
@ -73,7 +73,7 @@ describe(meta.route, () => {
); );
expect(response.status).toBe(200); expect(response.status).toBe(200);
const relationship = (await response.json()) as APIRelationship; const relationship = (await response.json()) as apiRelationship;
expect(relationship.muting).toBe(true); expect(relationship.muting).toBe(true);
}); });
@ -96,7 +96,7 @@ describe(meta.route, () => {
); );
expect(response.status).toBe(200); expect(response.status).toBe(200);
const relationship = (await response.json()) as APIRelationship; const relationship = (await response.json()) as apiRelationship;
expect(relationship.muting).toBe(true); expect(relationship.muting).toBe(true);
}); });
}); });

View file

@ -4,8 +4,8 @@ import { zValidator } from "@hono/zod-validator";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import type { Hono } from "hono"; import type { Hono } from "hono";
import { z } from "zod"; import { z } from "zod";
import { relationshipToAPI } from "~/database/entities/Relationship"; import { relationshipToApi } from "~/database/entities/relationship";
import { getRelationshipToOtherUser } from "~/database/entities/User"; import { getRelationshipToOtherUser } from "~/database/entities/user";
import { db } from "~/drizzle/db"; import { db } from "~/drizzle/db";
import { Relationships, RolePermissions } from "~/drizzle/schema"; import { Relationships, RolePermissions } from "~/drizzle/schema";
import { User } from "~/packages/database-interface/user"; import { User } from "~/packages/database-interface/user";
@ -23,8 +23,8 @@ export const meta = applyConfig({
}, },
permissions: { permissions: {
required: [ required: [
RolePermissions.MANAGE_OWN_MUTES, RolePermissions.ManageOwnMutes,
RolePermissions.VIEW_ACCOUNTS, RolePermissions.ViewAccounts,
], ],
}, },
}); });
@ -57,11 +57,15 @@ export default (app: Hono) =>
// TODO: Add duration support // TODO: Add duration support
const { notifications } = context.req.valid("json"); const { notifications } = context.req.valid("json");
if (!user) return errorResponse("Unauthorized", 401); if (!user) {
return errorResponse("Unauthorized", 401);
}
const otherUser = await User.fromId(id); const otherUser = await User.fromId(id);
if (!otherUser) return errorResponse("User not found", 404); if (!otherUser) {
return errorResponse("User not found", 404);
}
const foundRelationship = await getRelationshipToOtherUser( const foundRelationship = await getRelationshipToOtherUser(
user, user,
@ -85,6 +89,6 @@ export default (app: Hono) =>
// TODO: Implement duration // TODO: Implement duration
return jsonResponse(relationshipToAPI(foundRelationship)); return jsonResponse(relationshipToApi(foundRelationship));
}, },
); );

View file

@ -4,8 +4,8 @@ import { zValidator } from "@hono/zod-validator";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import type { Hono } from "hono"; import type { Hono } from "hono";
import { z } from "zod"; import { z } from "zod";
import { relationshipToAPI } from "~/database/entities/Relationship"; import { relationshipToApi } from "~/database/entities/relationship";
import { getRelationshipToOtherUser } from "~/database/entities/User"; import { getRelationshipToOtherUser } from "~/database/entities/user";
import { db } from "~/drizzle/db"; import { db } from "~/drizzle/db";
import { Relationships, RolePermissions } from "~/drizzle/schema"; import { Relationships, RolePermissions } from "~/drizzle/schema";
import { User } from "~/packages/database-interface/user"; import { User } from "~/packages/database-interface/user";
@ -23,8 +23,8 @@ export const meta = applyConfig({
}, },
permissions: { permissions: {
required: [ required: [
RolePermissions.MANAGE_OWN_ACCOUNT, RolePermissions.ManageOwnAccount,
RolePermissions.VIEW_ACCOUNTS, RolePermissions.ViewAccounts,
], ],
}, },
}); });
@ -50,11 +50,15 @@ export default (app: Hono) =>
const { user } = context.req.valid("header"); const { user } = context.req.valid("header");
const { comment } = context.req.valid("json"); const { comment } = context.req.valid("json");
if (!user) return errorResponse("Unauthorized", 401); if (!user) {
return errorResponse("Unauthorized", 401);
}
const otherUser = await User.fromId(id); const otherUser = await User.fromId(id);
if (!otherUser) return errorResponse("User not found", 404); if (!otherUser) {
return errorResponse("User not found", 404);
}
const foundRelationship = await getRelationshipToOtherUser( const foundRelationship = await getRelationshipToOtherUser(
user, user,
@ -70,6 +74,6 @@ export default (app: Hono) =>
}) })
.where(eq(Relationships.id, foundRelationship.id)); .where(eq(Relationships.id, foundRelationship.id));
return jsonResponse(relationshipToAPI(foundRelationship)); return jsonResponse(relationshipToApi(foundRelationship));
}, },
); );

View file

@ -4,8 +4,8 @@ import { zValidator } from "@hono/zod-validator";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import type { Hono } from "hono"; import type { Hono } from "hono";
import { z } from "zod"; import { z } from "zod";
import { relationshipToAPI } from "~/database/entities/Relationship"; import { relationshipToApi } from "~/database/entities/relationship";
import { getRelationshipToOtherUser } from "~/database/entities/User"; import { getRelationshipToOtherUser } from "~/database/entities/user";
import { db } from "~/drizzle/db"; import { db } from "~/drizzle/db";
import { Relationships, RolePermissions } from "~/drizzle/schema"; import { Relationships, RolePermissions } from "~/drizzle/schema";
import { User } from "~/packages/database-interface/user"; import { User } from "~/packages/database-interface/user";
@ -23,8 +23,8 @@ export const meta = applyConfig({
}, },
permissions: { permissions: {
required: [ required: [
RolePermissions.MANAGE_OWN_ACCOUNT, RolePermissions.ManageOwnAccount,
RolePermissions.VIEW_ACCOUNTS, RolePermissions.ViewAccounts,
], ],
}, },
}); });
@ -45,11 +45,15 @@ export default (app: Hono) =>
const { id } = context.req.valid("param"); const { id } = context.req.valid("param");
const { user } = context.req.valid("header"); const { user } = context.req.valid("header");
if (!user) return errorResponse("Unauthorized", 401); if (!user) {
return errorResponse("Unauthorized", 401);
}
const otherUser = await User.fromId(id); const otherUser = await User.fromId(id);
if (!otherUser) return errorResponse("User not found", 404); if (!otherUser) {
return errorResponse("User not found", 404);
}
const foundRelationship = await getRelationshipToOtherUser( const foundRelationship = await getRelationshipToOtherUser(
user, user,
@ -67,6 +71,6 @@ export default (app: Hono) =>
}) })
.where(eq(Relationships.id, foundRelationship.id)); .where(eq(Relationships.id, foundRelationship.id));
return jsonResponse(relationshipToAPI(foundRelationship)); return jsonResponse(relationshipToApi(foundRelationship));
}, },
); );

View file

@ -4,8 +4,8 @@ import { zValidator } from "@hono/zod-validator";
import { and, eq } from "drizzle-orm"; import { and, eq } from "drizzle-orm";
import type { Hono } from "hono"; import type { Hono } from "hono";
import { z } from "zod"; import { z } from "zod";
import { relationshipToAPI } from "~/database/entities/Relationship"; import { relationshipToApi } from "~/database/entities/relationship";
import { getRelationshipToOtherUser } from "~/database/entities/User"; import { getRelationshipToOtherUser } from "~/database/entities/user";
import { db } from "~/drizzle/db"; import { db } from "~/drizzle/db";
import { Relationships, RolePermissions } from "~/drizzle/schema"; import { Relationships, RolePermissions } from "~/drizzle/schema";
import { User } from "~/packages/database-interface/user"; import { User } from "~/packages/database-interface/user";
@ -23,8 +23,8 @@ export const meta = applyConfig({
}, },
permissions: { permissions: {
required: [ required: [
RolePermissions.MANAGE_OWN_FOLLOWS, RolePermissions.ManageOwnFollows,
RolePermissions.VIEW_ACCOUNTS, RolePermissions.ViewAccounts,
], ],
}, },
}); });
@ -45,11 +45,15 @@ export default (app: Hono) =>
const { id } = context.req.valid("param"); const { id } = context.req.valid("param");
const { user: self } = context.req.valid("header"); const { user: self } = context.req.valid("header");
if (!self) return errorResponse("Unauthorized", 401); if (!self) {
return errorResponse("Unauthorized", 401);
}
const otherUser = await User.fromId(id); const otherUser = await User.fromId(id);
if (!otherUser) return errorResponse("User not found", 404); if (!otherUser) {
return errorResponse("User not found", 404);
}
const foundRelationship = await getRelationshipToOtherUser( const foundRelationship = await getRelationshipToOtherUser(
self, self,
@ -81,6 +85,6 @@ export default (app: Hono) =>
} }
} }
return jsonResponse(relationshipToAPI(foundRelationship)); return jsonResponse(relationshipToApi(foundRelationship));
}, },
); );

View file

@ -1,7 +1,7 @@
import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { config } from "config-manager"; import { config } from "config-manager";
import { getTestStatuses, getTestUsers, sendTestRequest } from "~/tests/utils"; import { getTestStatuses, getTestUsers, sendTestRequest } from "~/tests/utils";
import type { Status as APIStatus } from "~/types/mastodon/status"; import type { Status as apiStatus } from "~/types/mastodon/status";
import { meta } from "./statuses"; import { meta } from "./statuses";
const { users, tokens, deleteUsers } = await getTestUsers(5); const { users, tokens, deleteUsers } = await getTestUsers(5);
@ -50,7 +50,7 @@ describe(meta.route, () => {
expect(response.status).toBe(200); expect(response.status).toBe(200);
const data = (await response.json()) as APIStatus[]; const data = (await response.json()) as apiStatus[];
expect(data.length).toBe(20); expect(data.length).toBe(20);
// Should have reblogs // Should have reblogs
@ -77,7 +77,7 @@ describe(meta.route, () => {
expect(response.status).toBe(200); expect(response.status).toBe(200);
const data = (await response.json()) as APIStatus[]; const data = (await response.json()) as apiStatus[];
expect(data.length).toBe(20); expect(data.length).toBe(20);
// Should not have reblogs // Should not have reblogs
@ -121,7 +121,7 @@ describe(meta.route, () => {
expect(response.status).toBe(200); expect(response.status).toBe(200);
const data = (await response.json()) as APIStatus[]; const data = (await response.json()) as apiStatus[];
expect(data.length).toBe(20); expect(data.length).toBe(20);
// Should not have replies // Should not have replies
@ -145,7 +145,7 @@ describe(meta.route, () => {
expect(response.status).toBe(200); expect(response.status).toBe(200);
const data = (await response.json()) as APIStatus[]; const data = (await response.json()) as apiStatus[];
expect(data.length).toBe(0); expect(data.length).toBe(0);
@ -183,7 +183,7 @@ describe(meta.route, () => {
expect(response2.status).toBe(200); expect(response2.status).toBe(200);
const data2 = (await response2.json()) as APIStatus[]; const data2 = (await response2.json()) as apiStatus[];
expect(data2.length).toBe(1); expect(data2.length).toBe(1);
}); });

View file

@ -20,7 +20,7 @@ export const meta = applyConfig({
oauthPermissions: ["read:statuses"], oauthPermissions: ["read:statuses"],
}, },
permissions: { permissions: {
required: [RolePermissions.VIEW_NOTES, RolePermissions.VIEW_ACCOUNTS], required: [RolePermissions.ViewNotes, RolePermissions.ViewAccounts],
}, },
}); });
@ -69,7 +69,9 @@ export default (app: Hono) =>
const otherUser = await User.fromId(id); const otherUser = await User.fromId(id);
if (!otherUser) return errorResponse("User not found", 404); if (!otherUser) {
return errorResponse("User not found", 404);
}
const { const {
max_id, max_id,
@ -103,7 +105,7 @@ export default (app: Hono) =>
); );
return jsonResponse( return jsonResponse(
await Promise.all(objects.map((note) => note.toAPI(otherUser))), await Promise.all(objects.map((note) => note.toApi(otherUser))),
200, 200,
{ {
Link: link, Link: link,

View file

@ -4,8 +4,8 @@ import { zValidator } from "@hono/zod-validator";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import type { Hono } from "hono"; import type { Hono } from "hono";
import { z } from "zod"; import { z } from "zod";
import { relationshipToAPI } from "~/database/entities/Relationship"; import { relationshipToApi } from "~/database/entities/relationship";
import { getRelationshipToOtherUser } from "~/database/entities/User"; import { getRelationshipToOtherUser } from "~/database/entities/user";
import { db } from "~/drizzle/db"; import { db } from "~/drizzle/db";
import { Relationships, RolePermissions } from "~/drizzle/schema"; import { Relationships, RolePermissions } from "~/drizzle/schema";
import { User } from "~/packages/database-interface/user"; import { User } from "~/packages/database-interface/user";
@ -23,8 +23,8 @@ export const meta = applyConfig({
}, },
permissions: { permissions: {
required: [ required: [
RolePermissions.MANAGE_OWN_BLOCKS, RolePermissions.ManageOwnBlocks,
RolePermissions.VIEW_ACCOUNTS, RolePermissions.ViewAccounts,
], ],
}, },
}); });
@ -45,11 +45,15 @@ export default (app: Hono) =>
const { id } = context.req.valid("param"); const { id } = context.req.valid("param");
const { user } = context.req.valid("header"); const { user } = context.req.valid("header");
if (!user) return errorResponse("Unauthorized", 401); if (!user) {
return errorResponse("Unauthorized", 401);
}
const otherUser = await User.fromId(id); const otherUser = await User.fromId(id);
if (!otherUser) return errorResponse("User not found", 404); if (!otherUser) {
return errorResponse("User not found", 404);
}
const foundRelationship = await getRelationshipToOtherUser( const foundRelationship = await getRelationshipToOtherUser(
user, user,
@ -67,6 +71,6 @@ export default (app: Hono) =>
.where(eq(Relationships.id, foundRelationship.id)); .where(eq(Relationships.id, foundRelationship.id));
} }
return jsonResponse(relationshipToAPI(foundRelationship)); return jsonResponse(relationshipToApi(foundRelationship));
}, },
); );

View file

@ -4,8 +4,8 @@ import { zValidator } from "@hono/zod-validator";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import type { Hono } from "hono"; import type { Hono } from "hono";
import { z } from "zod"; import { z } from "zod";
import { relationshipToAPI } from "~/database/entities/Relationship"; import { relationshipToApi } from "~/database/entities/relationship";
import { getRelationshipToOtherUser } from "~/database/entities/User"; import { getRelationshipToOtherUser } from "~/database/entities/user";
import { db } from "~/drizzle/db"; import { db } from "~/drizzle/db";
import { Relationships, RolePermissions } from "~/drizzle/schema"; import { Relationships, RolePermissions } from "~/drizzle/schema";
import { User } from "~/packages/database-interface/user"; import { User } from "~/packages/database-interface/user";
@ -23,8 +23,8 @@ export const meta = applyConfig({
}, },
permissions: { permissions: {
required: [ required: [
RolePermissions.MANAGE_OWN_FOLLOWS, RolePermissions.ManageOwnFollows,
RolePermissions.VIEW_ACCOUNTS, RolePermissions.ViewAccounts,
], ],
}, },
}); });
@ -45,11 +45,15 @@ export default (app: Hono) =>
const { id } = context.req.valid("param"); const { id } = context.req.valid("param");
const { user: self } = context.req.valid("header"); const { user: self } = context.req.valid("header");
if (!self) return errorResponse("Unauthorized", 401); if (!self) {
return errorResponse("Unauthorized", 401);
}
const otherUser = await User.fromId(id); const otherUser = await User.fromId(id);
if (!otherUser) return errorResponse("User not found", 404); if (!otherUser) {
return errorResponse("User not found", 404);
}
const foundRelationship = await getRelationshipToOtherUser( const foundRelationship = await getRelationshipToOtherUser(
self, self,
@ -68,6 +72,6 @@ export default (app: Hono) =>
.where(eq(Relationships.id, foundRelationship.id)); .where(eq(Relationships.id, foundRelationship.id));
} }
return jsonResponse(relationshipToAPI(foundRelationship)); return jsonResponse(relationshipToApi(foundRelationship));
}, },
); );

View file

@ -1,7 +1,7 @@
import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { config } from "config-manager"; import { config } from "config-manager";
import { getTestUsers, sendTestRequest } from "~/tests/utils"; import { getTestUsers, sendTestRequest } from "~/tests/utils";
import type { Relationship as APIRelationship } from "~/types/mastodon/relationship"; import type { Relationship as apiRelationship } from "~/types/mastodon/relationship";
import { meta } from "./unmute"; import { meta } from "./unmute";
const { users, tokens, deleteUsers } = await getTestUsers(2); const { users, tokens, deleteUsers } = await getTestUsers(2);
@ -82,7 +82,7 @@ describe(meta.route, () => {
); );
expect(response.status).toBe(200); expect(response.status).toBe(200);
const relationship = (await response.json()) as APIRelationship; const relationship = (await response.json()) as apiRelationship;
expect(relationship.muting).toBe(false); expect(relationship.muting).toBe(false);
}); });
@ -103,7 +103,7 @@ describe(meta.route, () => {
); );
expect(response.status).toBe(200); expect(response.status).toBe(200);
const relationship = (await response.json()) as APIRelationship; const relationship = (await response.json()) as apiRelationship;
expect(relationship.muting).toBe(false); expect(relationship.muting).toBe(false);
}); });
}); });

View file

@ -4,8 +4,8 @@ import { zValidator } from "@hono/zod-validator";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import type { Hono } from "hono"; import type { Hono } from "hono";
import { z } from "zod"; import { z } from "zod";
import { relationshipToAPI } from "~/database/entities/Relationship"; import { relationshipToApi } from "~/database/entities/relationship";
import { getRelationshipToOtherUser } from "~/database/entities/User"; import { getRelationshipToOtherUser } from "~/database/entities/user";
import { db } from "~/drizzle/db"; import { db } from "~/drizzle/db";
import { Relationships, RolePermissions } from "~/drizzle/schema"; import { Relationships, RolePermissions } from "~/drizzle/schema";
import { User } from "~/packages/database-interface/user"; import { User } from "~/packages/database-interface/user";
@ -23,8 +23,8 @@ export const meta = applyConfig({
}, },
permissions: { permissions: {
required: [ required: [
RolePermissions.MANAGE_OWN_MUTES, RolePermissions.ManageOwnMutes,
RolePermissions.VIEW_ACCOUNTS, RolePermissions.ViewAccounts,
], ],
}, },
}); });
@ -45,11 +45,15 @@ export default (app: Hono) =>
const { id } = context.req.valid("param"); const { id } = context.req.valid("param");
const { user: self } = context.req.valid("header"); const { user: self } = context.req.valid("header");
if (!self) return errorResponse("Unauthorized", 401); if (!self) {
return errorResponse("Unauthorized", 401);
}
const user = await User.fromId(id); const user = await User.fromId(id);
if (!user) return errorResponse("User not found", 404); if (!user) {
return errorResponse("User not found", 404);
}
const foundRelationship = await getRelationshipToOtherUser( const foundRelationship = await getRelationshipToOtherUser(
self, self,
@ -69,6 +73,6 @@ export default (app: Hono) =>
.where(eq(Relationships.id, foundRelationship.id)); .where(eq(Relationships.id, foundRelationship.id));
} }
return jsonResponse(relationshipToAPI(foundRelationship)); return jsonResponse(relationshipToApi(foundRelationship));
}, },
); );

View file

@ -4,8 +4,8 @@ import { zValidator } from "@hono/zod-validator";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import type { Hono } from "hono"; import type { Hono } from "hono";
import { z } from "zod"; import { z } from "zod";
import { relationshipToAPI } from "~/database/entities/Relationship"; import { relationshipToApi } from "~/database/entities/relationship";
import { getRelationshipToOtherUser } from "~/database/entities/User"; import { getRelationshipToOtherUser } from "~/database/entities/user";
import { db } from "~/drizzle/db"; import { db } from "~/drizzle/db";
import { Relationships, RolePermissions } from "~/drizzle/schema"; import { Relationships, RolePermissions } from "~/drizzle/schema";
import { User } from "~/packages/database-interface/user"; import { User } from "~/packages/database-interface/user";
@ -23,8 +23,8 @@ export const meta = applyConfig({
}, },
permissions: { permissions: {
required: [ required: [
RolePermissions.MANAGE_OWN_ACCOUNT, RolePermissions.ManageOwnAccount,
RolePermissions.VIEW_ACCOUNTS, RolePermissions.ViewAccounts,
], ],
}, },
}); });
@ -45,11 +45,15 @@ export default (app: Hono) =>
const { id } = context.req.valid("param"); const { id } = context.req.valid("param");
const { user: self } = context.req.valid("header"); const { user: self } = context.req.valid("header");
if (!self) return errorResponse("Unauthorized", 401); if (!self) {
return errorResponse("Unauthorized", 401);
}
const otherUser = await User.fromId(id); const otherUser = await User.fromId(id);
if (!otherUser) return errorResponse("User not found", 404); if (!otherUser) {
return errorResponse("User not found", 404);
}
const foundRelationship = await getRelationshipToOtherUser( const foundRelationship = await getRelationshipToOtherUser(
self, self,
@ -67,6 +71,6 @@ export default (app: Hono) =>
.where(eq(Relationships.id, foundRelationship.id)); .where(eq(Relationships.id, foundRelationship.id));
} }
return jsonResponse(relationshipToAPI(foundRelationship)); return jsonResponse(relationshipToApi(foundRelationship));
}, },
); );

View file

@ -20,7 +20,7 @@ export const meta = applyConfig({
oauthPermissions: ["read:follows"], oauthPermissions: ["read:follows"],
}, },
permissions: { permissions: {
required: [RolePermissions.MANAGE_OWN_FOLLOWS], required: [RolePermissions.ManageOwnFollows],
}, },
}); });
@ -41,7 +41,9 @@ export default (app: Hono) =>
const { user: self } = context.req.valid("header"); const { user: self } = context.req.valid("header");
const { id: ids } = context.req.valid("query"); const { id: ids } = context.req.valid("query");
if (!self) return errorResponse("Unauthorized", 401); if (!self) {
return errorResponse("Unauthorized", 401);
}
const idFollowerRelationships = const idFollowerRelationships =
await db.query.Relationships.findMany({ await db.query.Relationships.findMany({
@ -91,6 +93,6 @@ export default (app: Hono) =>
), ),
); );
return jsonResponse(finalUsers.map((o) => o.toAPI())); return jsonResponse(finalUsers.map((o) => o.toApi()));
}, },
); );

View file

@ -105,12 +105,13 @@ export default (app: Hono) =>
} }
// Check if username is valid // Check if username is valid
if (!username?.match(/^[a-z0-9_]+$/)) if (!username?.match(/^[a-z0-9_]+$/)) {
errors.details.username.push({ errors.details.username.push({
error: "ERR_INVALID", error: "ERR_INVALID",
description: description:
"must only contain lowercase letters, numbers, and underscores", "must only contain lowercase letters, numbers, and underscores",
}); });
}
// Check if username doesnt match filters // Check if username doesnt match filters
if ( if (
@ -125,25 +126,28 @@ export default (app: Hono) =>
} }
// Check if username is too long // Check if username is too long
if ((username?.length ?? 0) > config.validation.max_username_size) if ((username?.length ?? 0) > config.validation.max_username_size) {
errors.details.username.push({ errors.details.username.push({
error: "ERR_TOO_LONG", error: "ERR_TOO_LONG",
description: `is too long (maximum is ${config.validation.max_username_size} characters)`, description: `is too long (maximum is ${config.validation.max_username_size} characters)`,
}); });
}
// Check if username is too short // Check if username is too short
if ((username?.length ?? 0) < 3) if ((username?.length ?? 0) < 3) {
errors.details.username.push({ errors.details.username.push({
error: "ERR_TOO_SHORT", error: "ERR_TOO_SHORT",
description: "is too short (minimum is 3 characters)", description: "is too short (minimum is 3 characters)",
}); });
}
// Check if username is reserved // Check if username is reserved
if (config.validation.username_blacklist.includes(username ?? "")) if (config.validation.username_blacklist.includes(username ?? "")) {
errors.details.username.push({ errors.details.username.push({
error: "ERR_RESERVED", error: "ERR_RESERVED",
description: "is reserved", description: "is reserved",
}); });
}
// Check if username is taken // Check if username is taken
if (await User.fromSql(eq(Users.username, username))) { if (await User.fromSql(eq(Users.username, username))) {
@ -158,11 +162,12 @@ export default (app: Hono) =>
!email?.match( !email?.match(
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/, /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/,
) )
) ) {
errors.details.email.push({ errors.details.email.push({
error: "ERR_INVALID", error: "ERR_INVALID",
description: "must be a valid email address", description: "must be a valid email address",
}); });
}
// Check if email is blocked // Check if email is blocked
if ( if (
@ -171,37 +176,42 @@ export default (app: Hono) =>
tempmailDomains.domains.includes( tempmailDomains.domains.includes(
(email ?? "").split("@")[1], (email ?? "").split("@")[1],
)) ))
) ) {
errors.details.email.push({ errors.details.email.push({
error: "ERR_BLOCKED", error: "ERR_BLOCKED",
description: "is from a blocked email provider", description: "is from a blocked email provider",
}); });
}
// Check if email is taken // Check if email is taken
if (await User.fromSql(eq(Users.email, email))) if (await User.fromSql(eq(Users.email, email))) {
errors.details.email.push({ errors.details.email.push({
error: "ERR_TAKEN", error: "ERR_TAKEN",
description: "is already taken", description: "is already taken",
}); });
}
// Check if agreement is accepted // Check if agreement is accepted
if (!agreement) if (!agreement) {
errors.details.agreement.push({ errors.details.agreement.push({
error: "ERR_ACCEPTED", error: "ERR_ACCEPTED",
description: "must be accepted", description: "must be accepted",
}); });
}
if (!locale) if (!locale) {
errors.details.locale.push({ errors.details.locale.push({
error: "ERR_BLANK", error: "ERR_BLANK",
description: `can't be blank`, description: `can't be blank`,
}); });
}
if (!ISO6391.validate(locale ?? "")) if (!ISO6391.validate(locale ?? "")) {
errors.details.locale.push({ errors.details.locale.push({
error: "ERR_INVALID", error: "ERR_INVALID",
description: "must be a valid ISO 639-1 code", description: "must be a valid ISO 639-1 code",
}); });
}
// If any errors are present, return them // If any errors are present, return them
if ( if (

View file

@ -1,7 +1,7 @@
import { afterAll, describe, expect, test } from "bun:test"; import { afterAll, describe, expect, test } from "bun:test";
import { config } from "config-manager"; import { config } from "config-manager";
import { getTestUsers, sendTestRequest } from "~/tests/utils"; import { getTestUsers, sendTestRequest } from "~/tests/utils";
import type { Account as APIAccount } from "~/types/mastodon/account"; import type { Account as apiAccount } from "~/types/mastodon/account";
import { meta } from "./index"; import { meta } from "./index";
const { users, tokens, deleteUsers } = await getTestUsers(5); const { users, tokens, deleteUsers } = await getTestUsers(5);
@ -29,7 +29,7 @@ describe(meta.route, () => {
expect(response.status).toBe(200); expect(response.status).toBe(200);
const data = (await response.json()) as APIAccount[]; const data = (await response.json()) as apiAccount[];
expect(data).toEqual( expect(data).toEqual(
expect.objectContaining({ expect.objectContaining({
id: users[0].id, id: users[0].id,

View file

@ -16,7 +16,7 @@ import {
oneOrMore, oneOrMore,
} from "magic-regexp"; } from "magic-regexp";
import { z } from "zod"; import { z } from "zod";
import { resolveWebFinger } from "~/database/entities/User"; import { resolveWebFinger } from "~/database/entities/user";
import { RolePermissions, Users } from "~/drizzle/schema"; import { RolePermissions, Users } from "~/drizzle/schema";
import { User } from "~/packages/database-interface/user"; import { User } from "~/packages/database-interface/user";
import { LogLevel } from "~/packages/log-manager"; import { LogLevel } from "~/packages/log-manager";
@ -33,7 +33,7 @@ export const meta = applyConfig({
oauthPermissions: [], oauthPermissions: [],
}, },
permissions: { permissions: {
required: [RolePermissions.SEARCH], required: [RolePermissions.Search],
}, },
}); });
@ -84,7 +84,7 @@ export default (app: Hono) =>
domain, domain,
).catch((e) => { ).catch((e) => {
dualLogger.logError( dualLogger.logError(
LogLevel.ERROR, LogLevel.Error,
"WebFinger.Resolve", "WebFinger.Resolve",
e as Error, e as Error,
); );
@ -92,7 +92,7 @@ export default (app: Hono) =>
}); });
if (foundAccount) { if (foundAccount) {
return jsonResponse(foundAccount.toAPI()); return jsonResponse(foundAccount.toApi());
} }
return errorResponse("Account not found", 404); return errorResponse("Account not found", 404);
@ -106,7 +106,7 @@ export default (app: Hono) =>
const account = await User.fromSql(eq(Users.username, username)); const account = await User.fromSql(eq(Users.username, username));
if (account) { if (account) {
return jsonResponse(account.toAPI()); return jsonResponse(account.toApi());
} }
return errorResponse( return errorResponse(

View file

@ -5,8 +5,8 @@ import type { Hono } from "hono";
import { z } from "zod"; import { z } from "zod";
import { import {
createNewRelationship, createNewRelationship,
relationshipToAPI, relationshipToApi,
} from "~/database/entities/Relationship"; } from "~/database/entities/relationship";
import { db } from "~/drizzle/db"; import { db } from "~/drizzle/db";
import { RolePermissions } from "~/drizzle/schema"; import { RolePermissions } from "~/drizzle/schema";
import { User } from "~/packages/database-interface/user"; import { User } from "~/packages/database-interface/user";
@ -23,7 +23,7 @@ export const meta = applyConfig({
oauthPermissions: ["read:follows"], oauthPermissions: ["read:follows"],
}, },
permissions: { permissions: {
required: [RolePermissions.MANAGE_OWN_FOLLOWS], required: [RolePermissions.ManageOwnFollows],
}, },
}); });
@ -46,7 +46,9 @@ export default (app: Hono) =>
const ids = Array.isArray(id) ? id : [id]; const ids = Array.isArray(id) ? id : [id];
if (!self) return errorResponse("Unauthorized", 401); if (!self) {
return errorResponse("Unauthorized", 401);
}
const relationships = await db.query.Relationships.findMany({ const relationships = await db.query.Relationships.findMany({
where: (relationship, { inArray, and, eq }) => where: (relationship, { inArray, and, eq }) =>
@ -62,7 +64,9 @@ export default (app: Hono) =>
for (const id of missingIds) { for (const id of missingIds) {
const user = await User.fromId(id); const user = await User.fromId(id);
if (!user) continue; if (!user) {
continue;
}
const relationship = await createNewRelationship(self, user); const relationship = await createNewRelationship(self, user);
relationships.push(relationship); relationships.push(relationship);
@ -72,6 +76,6 @@ export default (app: Hono) =>
(a, b) => ids.indexOf(a.subjectId) - ids.indexOf(b.subjectId), (a, b) => ids.indexOf(a.subjectId) - ids.indexOf(b.subjectId),
); );
return jsonResponse(relationships.map((r) => relationshipToAPI(r))); return jsonResponse(relationships.map((r) => relationshipToApi(r)));
}, },
); );

View file

@ -1,7 +1,7 @@
import { afterAll, describe, expect, test } from "bun:test"; import { afterAll, describe, expect, test } from "bun:test";
import { config } from "config-manager"; import { config } from "config-manager";
import { getTestUsers, sendTestRequest } from "~/tests/utils"; import { getTestUsers, sendTestRequest } from "~/tests/utils";
import type { Account as APIAccount } from "~/types/mastodon/account"; import type { Account as apiAccount } from "~/types/mastodon/account";
import { meta } from "./index"; import { meta } from "./index";
const { users, tokens, deleteUsers } = await getTestUsers(5); const { users, tokens, deleteUsers } = await getTestUsers(5);
@ -29,7 +29,7 @@ describe(meta.route, () => {
expect(response.status).toBe(200); expect(response.status).toBe(200);
const data = (await response.json()) as APIAccount[]; const data = (await response.json()) as apiAccount[];
expect(data).toEqual( expect(data).toEqual(
expect.arrayContaining([ expect.arrayContaining([
expect.objectContaining({ expect.objectContaining({

View file

@ -16,7 +16,7 @@ import {
} from "magic-regexp"; } from "magic-regexp";
import stringComparison from "string-comparison"; import stringComparison from "string-comparison";
import { z } from "zod"; import { z } from "zod";
import { resolveWebFinger } from "~/database/entities/User"; import { resolveWebFinger } from "~/database/entities/user";
import { RolePermissions, Users } from "~/drizzle/schema"; import { RolePermissions, Users } from "~/drizzle/schema";
import { User } from "~/packages/database-interface/user"; import { User } from "~/packages/database-interface/user";
@ -32,7 +32,7 @@ export const meta = applyConfig({
oauthPermissions: ["read:accounts"], oauthPermissions: ["read:accounts"],
}, },
permissions: { permissions: {
required: [RolePermissions.SEARCH, RolePermissions.VIEW_ACCOUNTS], required: [RolePermissions.Search, RolePermissions.ViewAccounts],
}, },
}); });
@ -81,7 +81,9 @@ export default (app: Hono) =>
context.req.valid("query"); context.req.valid("query");
const { user: self } = context.req.valid("header"); const { user: self } = context.req.valid("header");
if (!self && following) return errorResponse("Unauthorized", 401); if (!self && following) {
return errorResponse("Unauthorized", 401);
}
const [username, host] = q.replace(/^@/, "").split("@"); const [username, host] = q.replace(/^@/, "").split("@");
@ -120,6 +122,6 @@ export default (app: Hono) =>
const result = indexOfCorrectSort.map((index) => accounts[index]); const result = indexOfCorrectSort.map((index) => accounts[index]);
return jsonResponse(result.map((acct) => acct.toAPI())); return jsonResponse(result.map((acct) => acct.toApi()));
}, },
); );

View file

@ -10,9 +10,9 @@ import { MediaBackendType } from "media-manager";
import type { MediaBackend } from "media-manager"; import type { MediaBackend } from "media-manager";
import { LocalMediaBackend, S3MediaBackend } from "media-manager"; import { LocalMediaBackend, S3MediaBackend } from "media-manager";
import { z } from "zod"; import { z } from "zod";
import { getUrl } from "~/database/entities/Attachment"; import { getUrl } from "~/database/entities/attachment";
import { type EmojiWithInstance, parseEmojis } from "~/database/entities/Emoji"; import { type EmojiWithInstance, parseEmojis } from "~/database/entities/emoji";
import { contentToHtml } from "~/database/entities/Status"; import { contentToHtml } from "~/database/entities/status";
import { db } from "~/drizzle/db"; import { db } from "~/drizzle/db";
import { EmojiToUser, RolePermissions } from "~/drizzle/schema"; import { EmojiToUser, RolePermissions } from "~/drizzle/schema";
import { User } from "~/packages/database-interface/user"; import { User } from "~/packages/database-interface/user";
@ -29,7 +29,7 @@ export const meta = applyConfig({
oauthPermissions: ["write:accounts"], oauthPermissions: ["write:accounts"],
}, },
permissions: { permissions: {
required: [RolePermissions.MANAGE_OWN_ACCOUNT], required: [RolePermissions.ManageOwnAccount],
}, },
}); });
@ -116,7 +116,9 @@ export default (app: Hono) =>
fields_attributes, fields_attributes,
} = context.req.valid("form"); } = context.req.valid("form");
if (!user) return errorResponse("Unauthorized", 401); if (!user) {
return errorResponse("Unauthorized", 401);
}
const self = user.data; const self = user.data;
@ -127,7 +129,7 @@ export default (app: Hono) =>
let mediaManager: MediaBackend; let mediaManager: MediaBackend;
switch (config.media.backend as MediaBackendType) { switch (config.media.backend as MediaBackendType) {
case MediaBackendType.LOCAL: case MediaBackendType.Local:
mediaManager = new LocalMediaBackend(config); mediaManager = new LocalMediaBackend(config);
break; break;
case MediaBackendType.S3: case MediaBackendType.S3:
@ -321,8 +323,10 @@ export default (app: Hono) =>
}); });
const output = await User.fromId(self.id); const output = await User.fromId(self.id);
if (!output) return errorResponse("Couldn't edit user", 500); if (!output) {
return errorResponse("Couldn't edit user", 500);
}
return jsonResponse(output.toAPI()); return jsonResponse(output.toApi());
}, },
); );

View file

@ -20,12 +20,14 @@ export default (app: Hono) =>
meta.allowedMethods, meta.allowedMethods,
meta.route, meta.route,
auth(meta.auth, meta.permissions), auth(meta.auth, meta.permissions),
async (context) => { (context) => {
// TODO: Add checks for disabled/unverified accounts // TODO: Add checks for disabled/unverified accounts
const { user } = context.req.valid("header"); const { user } = context.req.valid("header");
if (!user) return errorResponse("Unauthorized", 401); if (!user) {
return errorResponse("Unauthorized", 401);
}
return jsonResponse(user.toAPI(true)); return jsonResponse(user.toApi(true));
}, },
); );

View file

@ -18,7 +18,7 @@ export const meta = applyConfig({
required: false, required: false,
}, },
permissions: { permissions: {
required: [RolePermissions.MANAGE_OWN_APPS], required: [RolePermissions.ManageOwnApps],
}, },
}); });

View file

@ -1,7 +1,7 @@
import { applyConfig, auth } from "@/api"; import { applyConfig, auth } from "@/api";
import { errorResponse, jsonResponse } from "@/response"; import { errorResponse, jsonResponse } from "@/response";
import type { Hono } from "hono"; import type { Hono } from "hono";
import { getFromToken } from "~/database/entities/Application"; import { getFromToken } from "~/database/entities/application";
import { RolePermissions } from "~/drizzle/schema"; import { RolePermissions } from "~/drizzle/schema";
export const meta = applyConfig({ export const meta = applyConfig({
@ -15,7 +15,7 @@ export const meta = applyConfig({
required: true, required: true,
}, },
permissions: { permissions: {
required: [RolePermissions.MANAGE_OWN_APPS], required: [RolePermissions.ManageOwnApps],
}, },
}); });
@ -27,12 +27,18 @@ export default (app: Hono) =>
async (context) => { async (context) => {
const { user, token } = context.req.valid("header"); const { user, token } = context.req.valid("header");
if (!token) return errorResponse("Unauthorized", 401); if (!token) {
if (!user) return errorResponse("Unauthorized", 401); return errorResponse("Unauthorized", 401);
}
if (!user) {
return errorResponse("Unauthorized", 401);
}
const application = await getFromToken(token); const application = await getFromToken(token);
if (!application) return errorResponse("Unauthorized", 401); if (!application) {
return errorResponse("Unauthorized", 401);
}
return jsonResponse({ return jsonResponse({
name: application.name, name: application.name,

View file

@ -19,7 +19,7 @@ export const meta = applyConfig({
oauthPermissions: ["read:blocks"], oauthPermissions: ["read:blocks"],
}, },
permissions: { permissions: {
required: [RolePermissions.MANAGE_OWN_BLOCKS], required: [RolePermissions.ManageOwnBlocks],
}, },
}); });
@ -44,7 +44,9 @@ export default (app: Hono) =>
const { user } = context.req.valid("header"); const { user } = context.req.valid("header");
if (!user) return errorResponse("Unauthorized", 401); if (!user) {
return errorResponse("Unauthorized", 401);
}
const { objects: blocks, link } = await Timeline.getUserTimeline( const { objects: blocks, link } = await Timeline.getUserTimeline(
and( and(
@ -58,7 +60,7 @@ export default (app: Hono) =>
); );
return jsonResponse( return jsonResponse(
blocks.map((u) => u.toAPI()), blocks.map((u) => u.toApi()),
200, 200,
{ {
Link: link, Link: link,

View file

@ -1,7 +1,7 @@
import { applyConfig, auth } from "@/api"; import { applyConfig, auth } from "@/api";
import { jsonResponse } from "@/response"; import { jsonResponse } from "@/response";
import type { Hono } from "hono"; import type { Hono } from "hono";
import { emojiToAPI } from "~/database/entities/Emoji"; import { emojiToApi } from "~/database/entities/emoji";
import { db } from "~/drizzle/db"; import { db } from "~/drizzle/db";
import { RolePermissions } from "~/drizzle/schema"; import { RolePermissions } from "~/drizzle/schema";
@ -16,7 +16,7 @@ export const meta = applyConfig({
required: false, required: false,
}, },
permissions: { permissions: {
required: [RolePermissions.VIEW_EMOJIS], required: [RolePermissions.ViewEmojis],
}, },
}); });
@ -43,7 +43,7 @@ export default (app: Hono) =>
}); });
return jsonResponse( return jsonResponse(
await Promise.all(emojis.map((emoji) => emojiToAPI(emoji))), await Promise.all(emojis.map((emoji) => emojiToApi(emoji))),
); );
}, },
); );

View file

@ -11,8 +11,8 @@ import { zValidator } from "@hono/zod-validator";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import type { Hono } from "hono"; import type { Hono } from "hono";
import { z } from "zod"; import { z } from "zod";
import { getUrl } from "~/database/entities/Attachment"; import { getUrl } from "~/database/entities/attachment";
import { emojiToAPI } from "~/database/entities/Emoji"; import { emojiToApi } from "~/database/entities/emoji";
import { db } from "~/drizzle/db"; import { db } from "~/drizzle/db";
import { Emojis, RolePermissions } from "~/drizzle/schema"; import { Emojis, RolePermissions } from "~/drizzle/schema";
import { config } from "~/packages/config-manager"; import { config } from "~/packages/config-manager";
@ -29,10 +29,7 @@ export const meta = applyConfig({
required: true, required: true,
}, },
permissions: { permissions: {
required: [ required: [RolePermissions.ManageOwnEmojis, RolePermissions.ViewEmojis],
RolePermissions.MANAGE_OWN_EMOJIS,
RolePermissions.VIEW_EMOJIS,
],
}, },
}); });
@ -93,16 +90,18 @@ export default (app: Hono) =>
}, },
}); });
if (!emoji) return errorResponse("Emoji not found", 404); if (!emoji) {
return errorResponse("Emoji not found", 404);
}
// Check if user is admin // Check if user is admin
if ( if (
!user.hasPermission(RolePermissions.MANAGE_EMOJIS) && !user.hasPermission(RolePermissions.ManageEmojis) &&
emoji.ownerId !== user.data.id emoji.ownerId !== user.data.id
) { ) {
return jsonResponse( return jsonResponse(
{ {
error: `You cannot modify this emoji, as it is either global, not owned by you, or you do not have the '${RolePermissions.MANAGE_EMOJIS}' permission to manage global emojis`, error: `You cannot modify this emoji, as it is either global, not owned by you, or you do not have the '${RolePermissions.ManageEmojis}' permission to manage global emojis`,
}, },
403, 403,
); );
@ -133,10 +132,12 @@ export default (app: Hono) =>
} }
if ( if (
!form.shortcode && !(
!form.element && form.shortcode ||
!form.alt && form.element ||
!form.category && form.alt ||
form.category
) &&
form.global === undefined form.global === undefined
) { ) {
return errorResponse( return errorResponse(
@ -146,11 +147,11 @@ export default (app: Hono) =>
} }
if ( if (
!user.hasPermission(RolePermissions.MANAGE_EMOJIS) && !user.hasPermission(RolePermissions.ManageEmojis) &&
form.global form.global
) { ) {
return errorResponse( return errorResponse(
`Only users with the '${RolePermissions.MANAGE_EMOJIS}' permission can make an emoji global or not`, `Only users with the '${RolePermissions.ManageEmojis}' permission can make an emoji global or not`,
401, 401,
); );
} }
@ -207,7 +208,7 @@ export default (app: Hono) =>
)[0]; )[0];
return jsonResponse( return jsonResponse(
emojiToAPI({ emojiToApi({
...newEmoji, ...newEmoji,
instance: null, instance: null,
}), }),
@ -215,7 +216,7 @@ export default (app: Hono) =>
} }
case "GET": { case "GET": {
return jsonResponse(emojiToAPI(emoji)); return jsonResponse(emojiToApi(emoji));
} }
} }
}, },

View file

@ -10,8 +10,8 @@ import { errorResponse, jsonResponse } from "@/response";
import { zValidator } from "@hono/zod-validator"; import { zValidator } from "@hono/zod-validator";
import type { Hono } from "hono"; import type { Hono } from "hono";
import { z } from "zod"; import { z } from "zod";
import { getUrl } from "~/database/entities/Attachment"; import { getUrl } from "~/database/entities/attachment";
import { emojiToAPI } from "~/database/entities/Emoji"; import { emojiToApi } from "~/database/entities/emoji";
import { db } from "~/drizzle/db"; import { db } from "~/drizzle/db";
import { Emojis, RolePermissions } from "~/drizzle/schema"; import { Emojis, RolePermissions } from "~/drizzle/schema";
import { config } from "~/packages/config-manager"; import { config } from "~/packages/config-manager";
@ -28,10 +28,7 @@ export const meta = applyConfig({
required: true, required: true,
}, },
permissions: { permissions: {
required: [ required: [RolePermissions.ManageOwnEmojis, RolePermissions.ViewEmojis],
RolePermissions.MANAGE_OWN_EMOJIS,
RolePermissions.VIEW_EMOJIS,
],
}, },
}); });
@ -79,9 +76,9 @@ export default (app: Hono) =>
return errorResponse("Unauthorized", 401); return errorResponse("Unauthorized", 401);
} }
if (!user.hasPermission(RolePermissions.MANAGE_EMOJIS) && global) { if (!user.hasPermission(RolePermissions.ManageEmojis) && global) {
return errorResponse( return errorResponse(
`Only users with the '${RolePermissions.MANAGE_EMOJIS}' permission can upload global emojis`, `Only users with the '${RolePermissions.ManageEmojis}' permission can upload global emojis`,
401, 401,
); );
} }
@ -148,7 +145,7 @@ export default (app: Hono) =>
)[0]; )[0];
return jsonResponse( return jsonResponse(
emojiToAPI({ emojiToApi({
...emoji, ...emoji,
instance: null, instance: null,
}), }),

View file

@ -18,7 +18,7 @@ export const meta = applyConfig({
required: true, required: true,
}, },
permissions: { permissions: {
required: [RolePermissions.MANAGE_OWN_LIKES], required: [RolePermissions.ManageOwnLikes],
}, },
}); });
@ -43,7 +43,9 @@ export default (app: Hono) =>
const { user } = context.req.valid("header"); const { user } = context.req.valid("header");
if (!user) return errorResponse("Unauthorized", 401); if (!user) {
return errorResponse("Unauthorized", 401);
}
const { objects: favourites, link } = const { objects: favourites, link } =
await Timeline.getNoteTimeline( await Timeline.getNoteTimeline(
@ -60,7 +62,7 @@ export default (app: Hono) =>
return jsonResponse( return jsonResponse(
await Promise.all( await Promise.all(
favourites.map(async (note) => note.toAPI(user)), favourites.map(async (note) => note.toApi(user)),
), ),
200, 200,
{ {

View file

@ -6,12 +6,12 @@ import type { Hono } from "hono";
import { z } from "zod"; import { z } from "zod";
import { import {
checkForBidirectionalRelationships, checkForBidirectionalRelationships,
relationshipToAPI, relationshipToApi,
} from "~/database/entities/Relationship"; } from "~/database/entities/relationship";
import { import {
getRelationshipToOtherUser, getRelationshipToOtherUser,
sendFollowAccept, sendFollowAccept,
} from "~/database/entities/User"; } from "~/database/entities/user";
import { db } from "~/drizzle/db"; import { db } from "~/drizzle/db";
import { Relationships, RolePermissions } from "~/drizzle/schema"; import { Relationships, RolePermissions } from "~/drizzle/schema";
import { User } from "~/packages/database-interface/user"; import { User } from "~/packages/database-interface/user";
@ -27,7 +27,7 @@ export const meta = applyConfig({
required: true, required: true,
}, },
permissions: { permissions: {
required: [RolePermissions.MANAGE_OWN_FOLLOWS], required: [RolePermissions.ManageOwnFollows],
}, },
}); });
@ -46,13 +46,17 @@ export default (app: Hono) =>
async (context) => { async (context) => {
const { user } = context.req.valid("header"); const { user } = context.req.valid("header");
if (!user) return errorResponse("Unauthorized", 401); if (!user) {
return errorResponse("Unauthorized", 401);
}
const { account_id } = context.req.valid("param"); const { account_id } = context.req.valid("param");
const account = await User.fromId(account_id); const account = await User.fromId(account_id);
if (!account) return errorResponse("Account not found", 404); if (!account) {
return errorResponse("Account not found", 404);
}
// Check if there is a relationship on both sides // Check if there is a relationship on both sides
await checkForBidirectionalRelationships(user, account); await checkForBidirectionalRelationships(user, account);
@ -90,8 +94,9 @@ export default (app: Hono) =>
account, account,
); );
if (!foundRelationship) if (!foundRelationship) {
return errorResponse("Relationship not found", 404); return errorResponse("Relationship not found", 404);
}
// Check if accepting remote follow // Check if accepting remote follow
if (account.isRemote()) { if (account.isRemote()) {
@ -99,6 +104,6 @@ export default (app: Hono) =>
await sendFollowAccept(account, user); await sendFollowAccept(account, user);
} }
return jsonResponse(relationshipToAPI(foundRelationship)); return jsonResponse(relationshipToApi(foundRelationship));
}, },
); );

View file

@ -6,12 +6,12 @@ import type { Hono } from "hono";
import { z } from "zod"; import { z } from "zod";
import { import {
checkForBidirectionalRelationships, checkForBidirectionalRelationships,
relationshipToAPI, relationshipToApi,
} from "~/database/entities/Relationship"; } from "~/database/entities/relationship";
import { import {
getRelationshipToOtherUser, getRelationshipToOtherUser,
sendFollowReject, sendFollowReject,
} from "~/database/entities/User"; } from "~/database/entities/user";
import { db } from "~/drizzle/db"; import { db } from "~/drizzle/db";
import { Relationships, RolePermissions } from "~/drizzle/schema"; import { Relationships, RolePermissions } from "~/drizzle/schema";
import { User } from "~/packages/database-interface/user"; import { User } from "~/packages/database-interface/user";
@ -27,7 +27,7 @@ export const meta = applyConfig({
required: true, required: true,
}, },
permissions: { permissions: {
required: [RolePermissions.MANAGE_OWN_FOLLOWS], required: [RolePermissions.ManageOwnFollows],
}, },
}); });
@ -46,13 +46,17 @@ export default (app: Hono) =>
async (context) => { async (context) => {
const { user } = context.req.valid("header"); const { user } = context.req.valid("header");
if (!user) return errorResponse("Unauthorized", 401); if (!user) {
return errorResponse("Unauthorized", 401);
}
const { account_id } = context.req.valid("param"); const { account_id } = context.req.valid("param");
const account = await User.fromId(account_id); const account = await User.fromId(account_id);
if (!account) return errorResponse("Account not found", 404); if (!account) {
return errorResponse("Account not found", 404);
}
// Check if there is a relationship on both sides // Check if there is a relationship on both sides
await checkForBidirectionalRelationships(user, account); await checkForBidirectionalRelationships(user, account);
@ -90,8 +94,9 @@ export default (app: Hono) =>
account, account,
); );
if (!foundRelationship) if (!foundRelationship) {
return errorResponse("Relationship not found", 404); return errorResponse("Relationship not found", 404);
}
// Check if rejecting remote follow // Check if rejecting remote follow
if (account.isRemote()) { if (account.isRemote()) {
@ -99,6 +104,6 @@ export default (app: Hono) =>
await sendFollowReject(account, user); await sendFollowReject(account, user);
} }
return jsonResponse(relationshipToAPI(foundRelationship)); return jsonResponse(relationshipToApi(foundRelationship));
}, },
); );

View file

@ -18,7 +18,7 @@ export const meta = applyConfig({
required: true, required: true,
}, },
permissions: { permissions: {
required: [RolePermissions.MANAGE_OWN_FOLLOWS], required: [RolePermissions.ManageOwnFollows],
}, },
}); });
@ -43,7 +43,9 @@ export default (app: Hono) =>
const { user } = context.req.valid("header"); const { user } = context.req.valid("header");
if (!user) return errorResponse("Unauthorized", 401); if (!user) {
return errorResponse("Unauthorized", 401);
}
const { objects: followRequests, link } = const { objects: followRequests, link } =
await Timeline.getUserTimeline( await Timeline.getUserTimeline(
@ -58,7 +60,7 @@ export default (app: Hono) =>
); );
return jsonResponse( return jsonResponse(
followRequests.map((u) => u.toAPI()), followRequests.map((u) => u.toApi()),
200, 200,
{ {
Link: link, Link: link,

View file

@ -16,6 +16,6 @@ export const meta = applyConfig({
}); });
export default (app: Hono) => export default (app: Hono) =>
app.on(meta.allowedMethods, meta.route, async () => { app.on(meta.allowedMethods, meta.route, () => {
return jsonResponse(config.frontend.settings); return jsonResponse(config.frontend.settings);
}); });

View file

@ -8,7 +8,7 @@ import manifest from "~/package.json";
import { config } from "~/packages/config-manager"; import { config } from "~/packages/config-manager";
import { Note } from "~/packages/database-interface/note"; import { Note } from "~/packages/database-interface/note";
import { User } from "~/packages/database-interface/user"; import { User } from "~/packages/database-interface/user";
import type { Instance as APIInstance } from "~/types/mastodon/instance"; import type { Instance as apiInstance } from "~/types/mastodon/instance";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["GET"], allowedMethods: ["GET"],
@ -96,8 +96,8 @@ export default (app: Hono) =>
id: p.id, id: p.id,
})), })),
}, },
contact_account: contactAccount?.toAPI() || undefined, contact_account: contactAccount?.toApi() || undefined,
} satisfies APIInstance & { } satisfies apiInstance & {
banner: string | null; banner: string | null;
lysand_version: string; lysand_version: string;
sso: { sso: {

View file

@ -20,7 +20,7 @@ export default (app: Hono) =>
meta.allowedMethods, meta.allowedMethods,
meta.route, meta.route,
auth(meta.auth, meta.permissions), auth(meta.auth, meta.permissions),
async (context) => { async (_context) => {
return jsonResponse( return jsonResponse(
config.signups.rules.map((rule, index) => ({ config.signups.rules.map((rule, index) => ({
id: String(index), id: String(index),

View file

@ -6,7 +6,7 @@ import type { Hono } from "hono";
import { z } from "zod"; import { z } from "zod";
import { db } from "~/drizzle/db"; import { db } from "~/drizzle/db";
import { Markers, RolePermissions } from "~/drizzle/schema"; import { Markers, RolePermissions } from "~/drizzle/schema";
import type { Marker as APIMarker } from "~/types/mastodon/marker"; import type { Marker as apiMarker } from "~/types/mastodon/marker";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["GET", "POST"], allowedMethods: ["GET", "POST"],
@ -20,7 +20,7 @@ export const meta = applyConfig({
oauthPermissions: ["read:blocks"], oauthPermissions: ["read:blocks"],
}, },
permissions: { permissions: {
required: [RolePermissions.MANAGE_OWN_ACCOUNT], required: [RolePermissions.ManageOwnAccount],
}, },
}); });
@ -58,7 +58,7 @@ export default (app: Hono) =>
return jsonResponse({}); return jsonResponse({});
} }
const markers: APIMarker = { const markers: apiMarker = {
home: undefined, home: undefined,
notifications: undefined, notifications: undefined,
}; };
@ -132,23 +132,23 @@ export default (app: Hono) =>
case "POST": { case "POST": {
const { const {
"home[last_read_id]": home_id, "home[last_read_id]": homeId,
"notifications[last_read_id]": notifications_id, "notifications[last_read_id]": notificationsId,
} = context.req.valid("query"); } = context.req.valid("query");
const markers: APIMarker = { const markers: apiMarker = {
home: undefined, home: undefined,
notifications: undefined, notifications: undefined,
}; };
if (home_id) { if (homeId) {
const insertedMarker = ( const insertedMarker = (
await db await db
.insert(Markers) .insert(Markers)
.values({ .values({
userId: user.id, userId: user.id,
timeline: "home", timeline: "home",
noteId: home_id, noteId: homeId,
}) })
.returning() .returning()
)[0]; )[0];
@ -166,7 +166,7 @@ export default (app: Hono) =>
); );
markers.home = { markers.home = {
last_read_id: home_id, last_read_id: homeId,
version: totalCount[0].count, version: totalCount[0].count,
updated_at: new Date( updated_at: new Date(
insertedMarker.createdAt, insertedMarker.createdAt,
@ -174,14 +174,14 @@ export default (app: Hono) =>
}; };
} }
if (notifications_id) { if (notificationsId) {
const insertedMarker = ( const insertedMarker = (
await db await db
.insert(Markers) .insert(Markers)
.values({ .values({
userId: user.id, userId: user.id,
timeline: "notifications", timeline: "notifications",
notificationId: notifications_id, notificationId: notificationsId,
}) })
.returning() .returning()
)[0]; )[0];
@ -199,7 +199,7 @@ export default (app: Hono) =>
); );
markers.notifications = { markers.notifications = {
last_read_id: notifications_id, last_read_id: notificationsId,
version: totalCount[0].count, version: totalCount[0].count,
updated_at: new Date( updated_at: new Date(
insertedMarker.createdAt, insertedMarker.createdAt,

View file

@ -7,7 +7,7 @@ import type { MediaBackend } from "media-manager";
import { MediaBackendType } from "media-manager"; import { MediaBackendType } from "media-manager";
import { LocalMediaBackend, S3MediaBackend } from "media-manager"; import { LocalMediaBackend, S3MediaBackend } from "media-manager";
import { z } from "zod"; import { z } from "zod";
import { getUrl } from "~/database/entities/Attachment"; import { getUrl } from "~/database/entities/attachment";
import { RolePermissions } from "~/drizzle/schema"; import { RolePermissions } from "~/drizzle/schema";
import { Attachment } from "~/packages/database-interface/attachment"; import { Attachment } from "~/packages/database-interface/attachment";
@ -23,7 +23,7 @@ export const meta = applyConfig({
oauthPermissions: ["write:media"], oauthPermissions: ["write:media"],
}, },
permissions: { permissions: {
required: [RolePermissions.MANAGE_OWN_MEDIA], required: [RolePermissions.ManageOwnMedia],
}, },
}); });
@ -64,7 +64,7 @@ export default (app: Hono) =>
switch (context.req.method) { switch (context.req.method) {
case "GET": { case "GET": {
if (attachment.data.url) { if (attachment.data.url) {
return jsonResponse(attachment.toAPI()); return jsonResponse(attachment.toApi());
} }
return response(null, 206); return response(null, 206);
} }
@ -77,7 +77,7 @@ export default (app: Hono) =>
let mediaManager: MediaBackend; let mediaManager: MediaBackend;
switch (config.media.backend as MediaBackendType) { switch (config.media.backend as MediaBackendType) {
case MediaBackendType.LOCAL: case MediaBackendType.Local:
mediaManager = new LocalMediaBackend(config); mediaManager = new LocalMediaBackend(config);
break; break;
case MediaBackendType.S3: case MediaBackendType.S3:
@ -105,10 +105,10 @@ export default (app: Hono) =>
thumbnailUrl, thumbnailUrl,
}); });
return jsonResponse(attachment.toAPI()); return jsonResponse(attachment.toApi());
} }
return jsonResponse(attachment.toAPI()); return jsonResponse(attachment.toApi());
} }
} }

View file

@ -9,7 +9,7 @@ import type { MediaBackend } from "media-manager";
import { LocalMediaBackend, S3MediaBackend } from "media-manager"; import { LocalMediaBackend, S3MediaBackend } from "media-manager";
import sharp from "sharp"; import sharp from "sharp";
import { z } from "zod"; import { z } from "zod";
import { getUrl } from "~/database/entities/Attachment"; import { getUrl } from "~/database/entities/attachment";
import { RolePermissions } from "~/drizzle/schema"; import { RolePermissions } from "~/drizzle/schema";
import { Attachment } from "~/packages/database-interface/attachment"; import { Attachment } from "~/packages/database-interface/attachment";
@ -25,7 +25,7 @@ export const meta = applyConfig({
oauthPermissions: ["write:media"], oauthPermissions: ["write:media"],
}, },
permissions: { permissions: {
required: [RolePermissions.MANAGE_OWN_MEDIA], required: [RolePermissions.ManageOwnMedia],
}, },
}); });
@ -104,7 +104,7 @@ export default (app: Hono) =>
let mediaManager: MediaBackend; let mediaManager: MediaBackend;
switch (config.media.backend as MediaBackendType) { switch (config.media.backend as MediaBackendType) {
case MediaBackendType.LOCAL: case MediaBackendType.Local:
mediaManager = new LocalMediaBackend(config); mediaManager = new LocalMediaBackend(config);
break; break;
case MediaBackendType.S3: case MediaBackendType.S3:
@ -141,6 +141,6 @@ export default (app: Hono) =>
// TODO: Add job to process videos and other media // TODO: Add job to process videos and other media
return jsonResponse(newAttachment.toAPI()); return jsonResponse(newAttachment.toApi());
}, },
); );

View file

@ -19,7 +19,7 @@ export const meta = applyConfig({
oauthPermissions: ["read:mutes"], oauthPermissions: ["read:mutes"],
}, },
permissions: { permissions: {
required: [RolePermissions.MANAGE_OWN_MUTES], required: [RolePermissions.ManageOwnMutes],
}, },
}); });
@ -43,7 +43,9 @@ export default (app: Hono) =>
context.req.valid("query"); context.req.valid("query");
const { user } = context.req.valid("header"); const { user } = context.req.valid("header");
if (!user) return errorResponse("Unauthorized", 401); if (!user) {
return errorResponse("Unauthorized", 401);
}
const { objects: mutes, link } = await Timeline.getUserTimeline( const { objects: mutes, link } = await Timeline.getUserTimeline(
and( and(
@ -57,7 +59,7 @@ export default (app: Hono) =>
); );
return jsonResponse( return jsonResponse(
mutes.map((u) => u.toAPI()), mutes.map((u) => u.toApi()),
200, 200,
{ {
Link: link, Link: link,

View file

@ -1,11 +1,11 @@
import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { config } from "config-manager"; import { config } from "config-manager";
import { getTestUsers, sendTestRequest } from "~/tests/utils"; import { getTestUsers, sendTestRequest } from "~/tests/utils";
import type { Notification as APINotification } from "~/types/mastodon/notification"; import type { Notification as apiNotification } from "~/types/mastodon/notification";
import { meta } from "./dismiss"; import { meta } from "./dismiss";
const { users, tokens, deleteUsers } = await getTestUsers(2); const { users, tokens, deleteUsers } = await getTestUsers(2);
let notifications: APINotification[] = []; let notifications: apiNotification[] = [];
// Create some test notifications: follow, favourite, reblog, mention // Create some test notifications: follow, favourite, reblog, mention
beforeAll(async () => { beforeAll(async () => {

View file

@ -19,7 +19,7 @@ export const meta = applyConfig({
oauthPermissions: ["write:notifications"], oauthPermissions: ["write:notifications"],
}, },
permissions: { permissions: {
required: [RolePermissions.MANAGE_OWN_NOTIFICATIONS], required: [RolePermissions.ManageOwnNotifications],
}, },
}); });
@ -39,7 +39,9 @@ export default (app: Hono) =>
const { id } = context.req.valid("param"); const { id } = context.req.valid("param");
const { user } = context.req.valid("header"); const { user } = context.req.valid("header");
if (!user) return errorResponse("Unauthorized", 401); if (!user) {
return errorResponse("Unauthorized", 401);
}
await db await db
.update(Notifications) .update(Notifications)

View file

@ -1,11 +1,11 @@
import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { config } from "config-manager"; import { config } from "config-manager";
import { getTestUsers, sendTestRequest } from "~/tests/utils"; import { getTestUsers, sendTestRequest } from "~/tests/utils";
import type { Notification as APINotification } from "~/types/mastodon/notification"; import type { Notification as apiNotification } from "~/types/mastodon/notification";
import { meta } from "./index"; import { meta } from "./index";
const { users, tokens, deleteUsers } = await getTestUsers(2); const { users, tokens, deleteUsers } = await getTestUsers(2);
let notifications: APINotification[] = []; let notifications: apiNotification[] = [];
// Create some test notifications: follow, favourite, reblog, mention // Create some test notifications: follow, favourite, reblog, mention
beforeAll(async () => { beforeAll(async () => {
@ -114,7 +114,7 @@ describe(meta.route, () => {
expect(response.status).toBe(200); expect(response.status).toBe(200);
const notification = (await response.json()) as APINotification; const notification = (await response.json()) as apiNotification;
expect(notification).toBeDefined(); expect(notification).toBeDefined();
expect(notification.id).toBe(notifications[0].id); expect(notification.id).toBe(notifications[0].id);

View file

@ -3,7 +3,7 @@ import { errorResponse, jsonResponse } from "@/response";
import { zValidator } from "@hono/zod-validator"; import { zValidator } from "@hono/zod-validator";
import type { Hono } from "hono"; import type { Hono } from "hono";
import { z } from "zod"; import { z } from "zod";
import { findManyNotifications } from "~/database/entities/Notification"; import { findManyNotifications } from "~/database/entities/notification";
import { RolePermissions } from "~/drizzle/schema"; import { RolePermissions } from "~/drizzle/schema";
export const meta = applyConfig({ export const meta = applyConfig({
@ -18,7 +18,7 @@ export const meta = applyConfig({
oauthPermissions: ["read:notifications"], oauthPermissions: ["read:notifications"],
}, },
permissions: { permissions: {
required: [RolePermissions.MANAGE_OWN_NOTIFICATIONS], required: [RolePermissions.ManageOwnNotifications],
}, },
}); });
@ -38,7 +38,9 @@ export default (app: Hono) =>
const { id } = context.req.valid("param"); const { id } = context.req.valid("param");
const { user } = context.req.valid("header"); const { user } = context.req.valid("header");
if (!user) return errorResponse("Unauthorized", 401); if (!user) {
return errorResponse("Unauthorized", 401);
}
const notification = ( const notification = (
await findManyNotifications( await findManyNotifications(
@ -51,8 +53,9 @@ export default (app: Hono) =>
) )
)[0]; )[0];
if (!notification) if (!notification) {
return errorResponse("Notification not found", 404); return errorResponse("Notification not found", 404);
}
return jsonResponse(notification); return jsonResponse(notification);
}, },

View file

@ -1,11 +1,11 @@
import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { config } from "config-manager"; import { config } from "config-manager";
import { getTestUsers, sendTestRequest } from "~/tests/utils"; import { getTestUsers, sendTestRequest } from "~/tests/utils";
import type { Notification as APINotification } from "~/types/mastodon/notification"; import type { Notification as apiNotification } from "~/types/mastodon/notification";
import { meta } from "./index"; import { meta } from "./index";
const { users, tokens, deleteUsers } = await getTestUsers(2); const { users, tokens, deleteUsers } = await getTestUsers(2);
let notifications: APINotification[] = []; let notifications: apiNotification[] = [];
// Create some test notifications: follow, favourite, reblog, mention // Create some test notifications: follow, favourite, reblog, mention
beforeAll(async () => { beforeAll(async () => {

View file

@ -17,7 +17,7 @@ export const meta = applyConfig({
oauthPermissions: ["write:notifications"], oauthPermissions: ["write:notifications"],
}, },
permissions: { permissions: {
required: [RolePermissions.MANAGE_OWN_NOTIFICATIONS], required: [RolePermissions.ManageOwnNotifications],
}, },
}); });
@ -28,7 +28,9 @@ export default (app: Hono) =>
auth(meta.auth, meta.permissions), auth(meta.auth, meta.permissions),
async (context) => { async (context) => {
const { user } = context.req.valid("header"); const { user } = context.req.valid("header");
if (!user) return errorResponse("Unauthorized", 401); if (!user) {
return errorResponse("Unauthorized", 401);
}
await db await db
.update(Notifications) .update(Notifications)

View file

@ -1,12 +1,12 @@
import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { config } from "config-manager"; import { config } from "config-manager";
import { getTestStatuses, getTestUsers, sendTestRequest } from "~/tests/utils"; import { getTestStatuses, getTestUsers, sendTestRequest } from "~/tests/utils";
import type { Notification as APINotification } from "~/types/mastodon/notification"; import type { Notification as apiNotification } from "~/types/mastodon/notification";
import { meta } from "./index"; import { meta } from "./index";
const { users, tokens, deleteUsers } = await getTestUsers(2); const { users, tokens, deleteUsers } = await getTestUsers(2);
const statuses = await getTestStatuses(40, users[0]); const statuses = await getTestStatuses(40, users[0]);
let notifications: APINotification[] = []; let notifications: apiNotification[] = [];
// Create some test notifications // Create some test notifications
beforeAll(async () => { beforeAll(async () => {

View file

@ -19,7 +19,7 @@ export const meta = applyConfig({
oauthPermissions: ["write:notifications"], oauthPermissions: ["write:notifications"],
}, },
permissions: { permissions: {
required: [RolePermissions.MANAGE_OWN_NOTIFICATIONS], required: [RolePermissions.ManageOwnNotifications],
}, },
}); });
@ -38,7 +38,9 @@ export default (app: Hono) =>
async (context) => { async (context) => {
const { user } = context.req.valid("header"); const { user } = context.req.valid("header");
if (!user) return errorResponse("Unauthorized", 401); if (!user) {
return errorResponse("Unauthorized", 401);
}
const { "ids[]": ids } = context.req.valid("query"); const { "ids[]": ids } = context.req.valid("query");

View file

@ -1,7 +1,7 @@
import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { config } from "config-manager"; import { config } from "config-manager";
import { getTestStatuses, getTestUsers, sendTestRequest } from "~/tests/utils"; import { getTestStatuses, getTestUsers, sendTestRequest } from "~/tests/utils";
import type { Notification as APINotification } from "~/types/mastodon/notification"; import type { Notification as apiNotification } from "~/types/mastodon/notification";
import { meta } from "./index"; import { meta } from "./index";
const getFormData = (object: Record<string, string | number | boolean>) => const getFormData = (object: Record<string, string | number | boolean>) =>
@ -113,7 +113,7 @@ describe(meta.route, () => {
expect(response.status).toBe(200); expect(response.status).toBe(200);
expect(response.headers.get("content-type")).toBe("application/json"); expect(response.headers.get("content-type")).toBe("application/json");
const objects = (await response.json()) as APINotification[]; const objects = (await response.json()) as apiNotification[];
expect(objects.length).toBe(4); expect(objects.length).toBe(4);
for (const [index, notification] of objects.entries()) { for (const [index, notification] of objects.entries()) {
@ -165,7 +165,7 @@ describe(meta.route, () => {
expect(response.status).toBe(200); expect(response.status).toBe(200);
expect(response.headers.get("content-type")).toBe("application/json"); expect(response.headers.get("content-type")).toBe("application/json");
const objects = (await response.json()) as APINotification[]; const objects = (await response.json()) as apiNotification[];
expect(objects.length).toBe(2); expect(objects.length).toBe(2);
// There should be no element with a status with id of timeline[0].id // There should be no element with a status with id of timeline[0].id

Some files were not shown because too many files have changed in this diff Show more