mirror of
https://github.com/versia-pub/server.git
synced 2025-12-06 08:28:19 +01:00
refactor(federation): ♻️ Replace WebFinger code with @lysand-org/federation logic, add new debug command
This commit is contained in:
parent
38c8ea24a9
commit
cea9452127
|
|
@ -2,6 +2,8 @@ import { mentionValidator } from "@/api";
|
||||||
import { sanitizeHtml, sanitizeHtmlInline } from "@/sanitization";
|
import { sanitizeHtml, sanitizeHtmlInline } from "@/sanitization";
|
||||||
import markdownItTaskLists from "@hackmd/markdown-it-task-lists";
|
import markdownItTaskLists from "@hackmd/markdown-it-task-lists";
|
||||||
import { getLogger } from "@logtape/logtape";
|
import { getLogger } from "@logtape/logtape";
|
||||||
|
import { SignatureConstructor } from "@lysand-org/federation";
|
||||||
|
import { FederationRequester } from "@lysand-org/federation/requester";
|
||||||
import type { ContentFormat } from "@lysand-org/federation/types";
|
import type { ContentFormat } from "@lysand-org/federation/types";
|
||||||
import { config } from "config-manager";
|
import { config } from "config-manager";
|
||||||
import {
|
import {
|
||||||
|
|
@ -41,7 +43,6 @@ import { objectToInboxRequest } from "./federation";
|
||||||
import {
|
import {
|
||||||
type UserWithInstance,
|
type UserWithInstance,
|
||||||
type UserWithRelations,
|
type UserWithRelations,
|
||||||
resolveWebFinger,
|
|
||||||
transformOutputToUserWithRelations,
|
transformOutputToUserWithRelations,
|
||||||
userExtrasTemplate,
|
userExtrasTemplate,
|
||||||
userRelations,
|
userRelations,
|
||||||
|
|
@ -255,7 +256,10 @@ export const findManyNotes = async (
|
||||||
* @param text The text to parse mentions from.
|
* @param text The text to parse mentions from.
|
||||||
* @returns An array of users mentioned in the text.
|
* @returns An array of users mentioned in the text.
|
||||||
*/
|
*/
|
||||||
export const parseTextMentions = async (text: string): Promise<User[]> => {
|
export const parseTextMentions = async (
|
||||||
|
text: string,
|
||||||
|
author: User,
|
||||||
|
): Promise<User[]> => {
|
||||||
const mentionedPeople = [...text.matchAll(mentionValidator)] ?? [];
|
const mentionedPeople = [...text.matchAll(mentionValidator)] ?? [];
|
||||||
if (mentionedPeople.length === 0) {
|
if (mentionedPeople.length === 0) {
|
||||||
return [];
|
return [];
|
||||||
|
|
@ -310,10 +314,18 @@ export const parseTextMentions = async (text: string): Promise<User[]> => {
|
||||||
|
|
||||||
// Attempt to resolve mentions that were not found
|
// Attempt to resolve mentions that were not found
|
||||||
for (const person of notFoundRemoteUsers) {
|
for (const person of notFoundRemoteUsers) {
|
||||||
const user = await resolveWebFinger(
|
const signatureConstructor = await SignatureConstructor.fromStringKey(
|
||||||
person?.[1] ?? "",
|
author.data.privateKey ?? "",
|
||||||
person?.[2] ?? "",
|
author.getUri(),
|
||||||
);
|
);
|
||||||
|
const manager = new FederationRequester(
|
||||||
|
new URL(`https://${person?.[2] ?? ""}`),
|
||||||
|
signatureConstructor,
|
||||||
|
);
|
||||||
|
|
||||||
|
const uri = await manager.webFinger(person?.[1] ?? "");
|
||||||
|
|
||||||
|
const user = await User.resolve(uri);
|
||||||
|
|
||||||
if (user) {
|
if (user) {
|
||||||
finalList.push(user);
|
finalList.push(user);
|
||||||
|
|
|
||||||
|
|
@ -9,12 +9,12 @@ import { type InferSelectModel, and, eq, sql } from "drizzle-orm";
|
||||||
import { db } from "~/drizzle/db";
|
import { db } from "~/drizzle/db";
|
||||||
import {
|
import {
|
||||||
Applications,
|
Applications,
|
||||||
Instances,
|
type Instances,
|
||||||
Notifications,
|
Notifications,
|
||||||
Relationships,
|
Relationships,
|
||||||
type Roles,
|
type Roles,
|
||||||
Tokens,
|
Tokens,
|
||||||
Users,
|
type Users,
|
||||||
} from "~/drizzle/schema";
|
} from "~/drizzle/schema";
|
||||||
import { User } from "~/packages/database-interface/user";
|
import { User } from "~/packages/database-interface/user";
|
||||||
import type { Application } from "./application";
|
import type { Application } from "./application";
|
||||||
|
|
@ -319,74 +319,6 @@ export const findManyUsers = async (
|
||||||
return output.map((user) => transformOutputToUserWithRelations(user));
|
return output.map((user) => transformOutputToUserWithRelations(user));
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolves a WebFinger identifier to a user.
|
|
||||||
* @param identifier Either a UUID or a username
|
|
||||||
*/
|
|
||||||
export const resolveWebFinger = async (
|
|
||||||
identifier: string,
|
|
||||||
host: string,
|
|
||||||
): Promise<User | null> => {
|
|
||||||
// Check if user not already in database
|
|
||||||
const foundUser = await db
|
|
||||||
.select()
|
|
||||||
.from(Users)
|
|
||||||
.innerJoin(Instances, eq(Users.instanceId, Instances.id))
|
|
||||||
.where(and(eq(Users.username, identifier), eq(Instances.baseUrl, host)))
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if (foundUser[0]) {
|
|
||||||
return await User.fromId(foundUser[0].Users.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
const hostWithProtocol = host.startsWith("http") ? host : `https://${host}`;
|
|
||||||
|
|
||||||
const response = await fetch(
|
|
||||||
new URL(
|
|
||||||
`/.well-known/webfinger?${new URLSearchParams({
|
|
||||||
resource: `acct:${identifier}@${host}`,
|
|
||||||
})}`,
|
|
||||||
hostWithProtocol,
|
|
||||||
),
|
|
||||||
{
|
|
||||||
method: "GET",
|
|
||||||
headers: {
|
|
||||||
Accept: "application/json",
|
|
||||||
},
|
|
||||||
proxy: config.http.proxy.address,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response.status === 404) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = (await response.json()) as {
|
|
||||||
subject: string;
|
|
||||||
links: {
|
|
||||||
rel: string;
|
|
||||||
type: string;
|
|
||||||
href: string;
|
|
||||||
}[];
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!(data.subject && data.links)) {
|
|
||||||
throw new Error(
|
|
||||||
"Invalid WebFinger data (missing subject or links from response)",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const relevantLink = data.links.find((link) => link.rel === "self");
|
|
||||||
|
|
||||||
if (!relevantLink) {
|
|
||||||
throw new Error(
|
|
||||||
"Invalid WebFinger data (missing link with rel: 'self')",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return User.resolve(relevantLink.href);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves a user from a token.
|
* Retrieves a user from a token.
|
||||||
* @param access_token The access token to retrieve the user from.
|
* @param access_token The access token to retrieve the user from.
|
||||||
|
|
|
||||||
51
cli/commands/federation/user/fetch.ts
Normal file
51
cli/commands/federation/user/fetch.ts
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
import { SignatureConstructor } from "@lysand-org/federation";
|
||||||
|
import { FederationRequester } from "@lysand-org/federation/requester";
|
||||||
|
import { Args } from "@oclif/core";
|
||||||
|
import chalk from "chalk";
|
||||||
|
import ora from "ora";
|
||||||
|
import { BaseCommand } from "~/cli/base";
|
||||||
|
import { User } from "~/packages/database-interface/user";
|
||||||
|
|
||||||
|
export default class FederationUserFetch extends BaseCommand<
|
||||||
|
typeof FederationUserFetch
|
||||||
|
> {
|
||||||
|
static override args = {
|
||||||
|
address: Args.string({
|
||||||
|
description: "Address of remote user (name@host.com)",
|
||||||
|
required: true,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
static override description = "Fetch the URL of remote users via WebFinger";
|
||||||
|
|
||||||
|
static override examples = ["<%= config.bin %> <%= command.id %>"];
|
||||||
|
|
||||||
|
static override flags = {};
|
||||||
|
|
||||||
|
public async run(): Promise<void> {
|
||||||
|
const { args } = await this.parse(FederationUserFetch);
|
||||||
|
|
||||||
|
const spinner = ora("Fetching user URI").start();
|
||||||
|
|
||||||
|
const [username, host] = args.address.split("@");
|
||||||
|
|
||||||
|
const requester = await User.getServerActor();
|
||||||
|
|
||||||
|
const signatureConstructor = await SignatureConstructor.fromStringKey(
|
||||||
|
requester.data.privateKey ?? "",
|
||||||
|
requester.getUri(),
|
||||||
|
);
|
||||||
|
const manager = new FederationRequester(
|
||||||
|
new URL(`https://${host}`),
|
||||||
|
signatureConstructor,
|
||||||
|
);
|
||||||
|
|
||||||
|
const uri = await manager.webFinger(username);
|
||||||
|
|
||||||
|
spinner.succeed("Fetched user URI");
|
||||||
|
|
||||||
|
this.log(`URI: ${chalk.blueBright(uri)}`);
|
||||||
|
|
||||||
|
this.exit(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,6 +5,7 @@ import EmojiDelete from "./commands/emoji/delete";
|
||||||
import EmojiImport from "./commands/emoji/import";
|
import EmojiImport from "./commands/emoji/import";
|
||||||
import EmojiList from "./commands/emoji/list";
|
import EmojiList from "./commands/emoji/list";
|
||||||
import FederationInstanceFetch from "./commands/federation/instance/fetch";
|
import FederationInstanceFetch from "./commands/federation/instance/fetch";
|
||||||
|
import FederationUserFetch from "./commands/federation/user/fetch";
|
||||||
import IndexRebuild from "./commands/index/rebuild";
|
import IndexRebuild from "./commands/index/rebuild";
|
||||||
import Start from "./commands/start";
|
import Start from "./commands/start";
|
||||||
import UserCreate from "./commands/user/create";
|
import UserCreate from "./commands/user/create";
|
||||||
|
|
@ -28,6 +29,7 @@ export const commands = {
|
||||||
"emoji:import": EmojiImport,
|
"emoji:import": EmojiImport,
|
||||||
"index:rebuild": IndexRebuild,
|
"index:rebuild": IndexRebuild,
|
||||||
"federation:instance:fetch": FederationInstanceFetch,
|
"federation:instance:fetch": FederationInstanceFetch,
|
||||||
|
"federation:user:fetch": FederationUserFetch,
|
||||||
start: Start,
|
start: Start,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -337,6 +337,11 @@ description = "A Lysand instance"
|
||||||
# URL to your instance banner
|
# URL to your instance banner
|
||||||
# banner = ""
|
# banner = ""
|
||||||
|
|
||||||
|
# Used for federation. If left empty or missing, the server will generate one for you.
|
||||||
|
[instance.keys]
|
||||||
|
public = ""
|
||||||
|
private = ""
|
||||||
|
|
||||||
[permissions]
|
[permissions]
|
||||||
# Control default permissions for users
|
# Control default permissions for users
|
||||||
# Note that an anonymous user having a permission will not allow them
|
# Note that an anonymous user having a permission will not allow them
|
||||||
|
|
|
||||||
|
|
@ -104,7 +104,7 @@
|
||||||
"@json2csv/plainjs": "^7.0.6",
|
"@json2csv/plainjs": "^7.0.6",
|
||||||
"@logtape/logtape": "npm:@jsr/logtape__logtape",
|
"@logtape/logtape": "npm:@jsr/logtape__logtape",
|
||||||
"@lysand-org/client": "^0.2.3",
|
"@lysand-org/client": "^0.2.3",
|
||||||
"@lysand-org/federation": "^2.0.0",
|
"@lysand-org/federation": "^2.1.0",
|
||||||
"@oclif/core": "^4.0.7",
|
"@oclif/core": "^4.0.7",
|
||||||
"@tufjs/canonical-json": "^2.0.0",
|
"@tufjs/canonical-json": "^2.0.0",
|
||||||
"altcha-lib": "^0.3.0",
|
"altcha-lib": "^0.3.0",
|
||||||
|
|
|
||||||
|
|
@ -509,6 +509,15 @@ export const configValidator = z.object({
|
||||||
privacy_policy_path: z.string().optional(),
|
privacy_policy_path: z.string().optional(),
|
||||||
logo: zUrl.optional(),
|
logo: zUrl.optional(),
|
||||||
banner: zUrl.optional(),
|
banner: zUrl.optional(),
|
||||||
|
keys: z
|
||||||
|
.object({
|
||||||
|
public: z.string().min(3).default("").or(z.literal("")),
|
||||||
|
private: z.string().min(3).default("").or(z.literal("")),
|
||||||
|
})
|
||||||
|
.default({
|
||||||
|
public: "",
|
||||||
|
private: "",
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
.default({
|
.default({
|
||||||
name: "Lysand",
|
name: "Lysand",
|
||||||
|
|
@ -518,6 +527,10 @@ export const configValidator = z.object({
|
||||||
privacy_policy_path: undefined,
|
privacy_policy_path: undefined,
|
||||||
logo: undefined,
|
logo: undefined,
|
||||||
banner: undefined,
|
banner: undefined,
|
||||||
|
keys: {
|
||||||
|
public: "",
|
||||||
|
private: "",
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
permissions: z
|
permissions: z
|
||||||
.object({
|
.object({
|
||||||
|
|
|
||||||
|
|
@ -326,7 +326,7 @@ export class Note extends BaseInterface<typeof Notes, StatusWithRelations> {
|
||||||
|
|
||||||
const parsedMentions = [
|
const parsedMentions = [
|
||||||
...(data.mentions ?? []),
|
...(data.mentions ?? []),
|
||||||
...(await parseTextMentions(plaintextContent)),
|
...(await parseTextMentions(plaintextContent, data.author)),
|
||||||
// Deduplicate by .id
|
// Deduplicate by .id
|
||||||
].filter(
|
].filter(
|
||||||
(mention, index, self) =>
|
(mention, index, self) =>
|
||||||
|
|
@ -396,7 +396,7 @@ export class Note extends BaseInterface<typeof Notes, StatusWithRelations> {
|
||||||
* @returns The updated note
|
* @returns The updated note
|
||||||
*/
|
*/
|
||||||
async updateFromData(data: {
|
async updateFromData(data: {
|
||||||
author?: User;
|
author: User;
|
||||||
content?: ContentFormat;
|
content?: ContentFormat;
|
||||||
visibility?: ApiStatus["visibility"];
|
visibility?: ApiStatus["visibility"];
|
||||||
isSensitive?: boolean;
|
isSensitive?: boolean;
|
||||||
|
|
@ -418,7 +418,7 @@ export class Note extends BaseInterface<typeof Notes, StatusWithRelations> {
|
||||||
const parsedMentions = [
|
const parsedMentions = [
|
||||||
...(data.mentions ?? []),
|
...(data.mentions ?? []),
|
||||||
...(plaintextContent
|
...(plaintextContent
|
||||||
? await parseTextMentions(plaintextContent)
|
? await parseTextMentions(plaintextContent, data.author)
|
||||||
: []),
|
: []),
|
||||||
// Deduplicate by .id
|
// Deduplicate by .id
|
||||||
].filter(
|
].filter(
|
||||||
|
|
|
||||||
|
|
@ -123,6 +123,56 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static getServerActor(): User {
|
||||||
|
return new User({
|
||||||
|
id: "00000000-0000-0000-0000-000000000000",
|
||||||
|
username: "actor",
|
||||||
|
avatar: "",
|
||||||
|
createdAt: "2024-01-01T00:00:00.000Z",
|
||||||
|
displayName: "Server Actor",
|
||||||
|
note: "This is a system actor used for server-to-server communication. It is not a real user.",
|
||||||
|
updatedAt: "2024-01-01T00:00:00.000Z",
|
||||||
|
instanceId: null,
|
||||||
|
publicKey: config.instance.keys.public,
|
||||||
|
source: {
|
||||||
|
fields: [],
|
||||||
|
language: null,
|
||||||
|
note: "",
|
||||||
|
privacy: "public",
|
||||||
|
sensitive: false,
|
||||||
|
},
|
||||||
|
fields: [],
|
||||||
|
isAdmin: false,
|
||||||
|
isBot: false,
|
||||||
|
isLocked: false,
|
||||||
|
isDiscoverable: false,
|
||||||
|
endpoints: {
|
||||||
|
dislikes: "",
|
||||||
|
featured: "",
|
||||||
|
likes: "",
|
||||||
|
followers: "",
|
||||||
|
following: "",
|
||||||
|
inbox: "",
|
||||||
|
outbox: "",
|
||||||
|
},
|
||||||
|
disableAutomoderation: false,
|
||||||
|
email: "",
|
||||||
|
emailVerificationToken: "",
|
||||||
|
emojis: [],
|
||||||
|
followerCount: 0,
|
||||||
|
followingCount: 0,
|
||||||
|
header: "",
|
||||||
|
instance: null,
|
||||||
|
password: "",
|
||||||
|
passwordResetToken: "",
|
||||||
|
privateKey: config.instance.keys.private,
|
||||||
|
roles: [],
|
||||||
|
sanctions: [],
|
||||||
|
statusCount: 0,
|
||||||
|
uri: "",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
static getUri(id: string, uri: string | null, baseUrl: string) {
|
static getUri(id: string, uri: string | null, baseUrl: string) {
|
||||||
return uri || new URL(`/users/${id}`, baseUrl).toString();
|
return uri || new URL(`/users/${id}`, baseUrl).toString();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
import { applyConfig, auth, handleZodError } from "@/api";
|
import { applyConfig, auth, handleZodError } from "@/api";
|
||||||
import { errorResponse, jsonResponse } from "@/response";
|
import { errorResponse, jsonResponse } from "@/response";
|
||||||
import { zValidator } from "@hono/zod-validator";
|
import { zValidator } from "@hono/zod-validator";
|
||||||
import { getLogger } from "@logtape/logtape";
|
import { SignatureConstructor } from "@lysand-org/federation";
|
||||||
|
import { FederationRequester } from "@lysand-org/federation/requester";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import type { Hono } from "hono";
|
import type { Hono } from "hono";
|
||||||
import {
|
import {
|
||||||
|
|
@ -16,7 +17,6 @@ import {
|
||||||
oneOrMore,
|
oneOrMore,
|
||||||
} from "magic-regexp";
|
} from "magic-regexp";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { resolveWebFinger } from "~/classes/functions/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";
|
||||||
|
|
||||||
|
|
@ -50,6 +50,7 @@ export default (app: Hono) =>
|
||||||
auth(meta.auth, meta.permissions),
|
auth(meta.auth, meta.permissions),
|
||||||
async (context) => {
|
async (context) => {
|
||||||
const { acct } = context.req.valid("query");
|
const { acct } = context.req.valid("query");
|
||||||
|
const { user } = context.req.valid("header");
|
||||||
|
|
||||||
if (!acct) {
|
if (!acct) {
|
||||||
return errorResponse("Invalid acct parameter", 400);
|
return errorResponse("Invalid acct parameter", 400);
|
||||||
|
|
@ -78,13 +79,22 @@ export default (app: Hono) =>
|
||||||
}
|
}
|
||||||
|
|
||||||
const [username, domain] = accountMatches[0].split("@");
|
const [username, domain] = accountMatches[0].split("@");
|
||||||
const foundAccount = await resolveWebFinger(
|
|
||||||
username,
|
const requester = user ?? User.getServerActor();
|
||||||
domain,
|
|
||||||
).catch((e) => {
|
const signatureConstructor =
|
||||||
getLogger("webfinger").error`${e}`;
|
await SignatureConstructor.fromStringKey(
|
||||||
return null;
|
requester.data.privateKey ?? "",
|
||||||
});
|
requester.getUri(),
|
||||||
|
);
|
||||||
|
const manager = new FederationRequester(
|
||||||
|
new URL(`https://${domain}`),
|
||||||
|
signatureConstructor,
|
||||||
|
);
|
||||||
|
|
||||||
|
const uri = await manager.webFinger(username);
|
||||||
|
|
||||||
|
const foundAccount = await User.resolve(uri);
|
||||||
|
|
||||||
if (foundAccount) {
|
if (foundAccount) {
|
||||||
return jsonResponse(foundAccount.toApi());
|
return jsonResponse(foundAccount.toApi());
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
import { applyConfig, auth, handleZodError } from "@/api";
|
import { applyConfig, auth, handleZodError } from "@/api";
|
||||||
import { errorResponse, jsonResponse } from "@/response";
|
import { errorResponse, jsonResponse } from "@/response";
|
||||||
import { zValidator } from "@hono/zod-validator";
|
import { zValidator } from "@hono/zod-validator";
|
||||||
|
import { SignatureConstructor } from "@lysand-org/federation";
|
||||||
|
import { FederationRequester } from "@lysand-org/federation/requester";
|
||||||
import { eq, like, not, or, sql } from "drizzle-orm";
|
import { eq, like, not, or, sql } from "drizzle-orm";
|
||||||
import type { Hono } from "hono";
|
import type { Hono } from "hono";
|
||||||
import {
|
import {
|
||||||
|
|
@ -16,7 +18,6 @@ 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 "~/classes/functions/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";
|
||||||
|
|
||||||
|
|
@ -90,7 +91,21 @@ export default (app: Hono) =>
|
||||||
const accounts: User[] = [];
|
const accounts: User[] = [];
|
||||||
|
|
||||||
if (resolve && username && host) {
|
if (resolve && username && host) {
|
||||||
const resolvedUser = await resolveWebFinger(username, host);
|
const requester = self ?? User.getServerActor();
|
||||||
|
|
||||||
|
const signatureConstructor =
|
||||||
|
await SignatureConstructor.fromStringKey(
|
||||||
|
requester.data.privateKey ?? "",
|
||||||
|
requester.getUri(),
|
||||||
|
);
|
||||||
|
const manager = new FederationRequester(
|
||||||
|
new URL(`https://${host}`),
|
||||||
|
signatureConstructor,
|
||||||
|
);
|
||||||
|
|
||||||
|
const uri = await manager.webFinger(username);
|
||||||
|
|
||||||
|
const resolvedUser = await User.resolve(uri);
|
||||||
|
|
||||||
if (resolvedUser) {
|
if (resolvedUser) {
|
||||||
accounts.push(resolvedUser);
|
accounts.push(resolvedUser);
|
||||||
|
|
|
||||||
|
|
@ -144,6 +144,14 @@ export default (app: Hono) =>
|
||||||
return jsonResponse(await note.toApi(user), 200);
|
return jsonResponse(await note.toApi(user), 200);
|
||||||
}
|
}
|
||||||
case "PUT": {
|
case "PUT": {
|
||||||
|
if (!user) {
|
||||||
|
return errorResponse("Unauthorized", 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (note.author.id !== user.id) {
|
||||||
|
return errorResponse("Unauthorized", 401);
|
||||||
|
}
|
||||||
|
|
||||||
if (media_ids.length > 0) {
|
if (media_ids.length > 0) {
|
||||||
const foundAttachments =
|
const foundAttachments =
|
||||||
await Attachment.fromIds(media_ids);
|
await Attachment.fromIds(media_ids);
|
||||||
|
|
@ -154,6 +162,7 @@ export default (app: Hono) =>
|
||||||
}
|
}
|
||||||
|
|
||||||
const newNote = await note.updateFromData({
|
const newNote = await note.updateFromData({
|
||||||
|
author: user,
|
||||||
content: statusText
|
content: statusText
|
||||||
? {
|
? {
|
||||||
[content_type]: {
|
[content_type]: {
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
import { applyConfig, auth, handleZodError, userAddressValidator } from "@/api";
|
import { applyConfig, auth, handleZodError, userAddressValidator } from "@/api";
|
||||||
import { errorResponse, jsonResponse } from "@/response";
|
import { errorResponse, jsonResponse } from "@/response";
|
||||||
import { zValidator } from "@hono/zod-validator";
|
import { zValidator } from "@hono/zod-validator";
|
||||||
import { getLogger } from "@logtape/logtape";
|
import { SignatureConstructor } from "@lysand-org/federation";
|
||||||
|
import { FederationRequester } from "@lysand-org/federation/requester";
|
||||||
import { and, eq, inArray, sql } from "drizzle-orm";
|
import { and, eq, inArray, sql } from "drizzle-orm";
|
||||||
import type { Hono } from "hono";
|
import type { Hono } from "hono";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { resolveWebFinger } from "~/classes/functions/user";
|
|
||||||
import { searchManager } from "~/classes/search/search-manager";
|
import { searchManager } from "~/classes/search/search-manager";
|
||||||
import { db } from "~/drizzle/db";
|
import { db } from "~/drizzle/db";
|
||||||
import { Instances, Notes, RolePermissions, Users } from "~/drizzle/schema";
|
import { Instances, Notes, RolePermissions, Users } from "~/drizzle/schema";
|
||||||
|
|
@ -121,13 +121,21 @@ export default (app: Hono) =>
|
||||||
}
|
}
|
||||||
|
|
||||||
if (resolve) {
|
if (resolve) {
|
||||||
const newUser = await resolveWebFinger(
|
const requester = self ?? User.getServerActor();
|
||||||
username,
|
|
||||||
domain,
|
const signatureConstructor =
|
||||||
).catch((e) => {
|
await SignatureConstructor.fromStringKey(
|
||||||
getLogger("webfinger").error`${e}`;
|
requester.data.privateKey ?? "",
|
||||||
return null;
|
requester.getUri(),
|
||||||
});
|
);
|
||||||
|
const manager = new FederationRequester(
|
||||||
|
new URL(`https://${domain}`),
|
||||||
|
signatureConstructor,
|
||||||
|
);
|
||||||
|
|
||||||
|
const uri = await manager.webFinger(username);
|
||||||
|
|
||||||
|
const newUser = await User.resolve(uri);
|
||||||
|
|
||||||
if (newUser) {
|
if (newUser) {
|
||||||
return jsonResponse({
|
return jsonResponse({
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,13 @@
|
||||||
import { getLogger } from "@logtape/logtape";
|
import { getLogger } from "@logtape/logtape";
|
||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
import type { Config } from "~/packages/config-manager";
|
import type { Config } from "~/packages/config-manager";
|
||||||
|
import { User } from "~/packages/database-interface/user";
|
||||||
|
|
||||||
export const checkConfig = async (config: Config) => {
|
export const checkConfig = async (config: Config) => {
|
||||||
await checkOidcConfig(config);
|
await checkOidcConfig(config);
|
||||||
|
|
||||||
|
await checkFederationConfig(config);
|
||||||
|
|
||||||
await checkHttpProxyConfig(config);
|
await checkHttpProxyConfig(config);
|
||||||
|
|
||||||
await checkChallengeConfig(config);
|
await checkChallengeConfig(config);
|
||||||
|
|
@ -127,3 +130,50 @@ const checkOidcConfig = async (config: Config) => {
|
||||||
await Bun.sleep(Number.POSITIVE_INFINITY);
|
await Bun.sleep(Number.POSITIVE_INFINITY);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const checkFederationConfig = async (config: Config) => {
|
||||||
|
const logger = getLogger("server");
|
||||||
|
|
||||||
|
if (!(config.instance.keys.public && config.instance.keys.private)) {
|
||||||
|
logger.fatal`The federation keys are not set in the config`;
|
||||||
|
logger.fatal`Below are generated keys for you to copy in the config at instance.keys.public and instance.keys.private`;
|
||||||
|
|
||||||
|
// Generate a key for them
|
||||||
|
const { public_key, private_key } = await User.generateKeys();
|
||||||
|
|
||||||
|
logger.fatal`Generated public key: ${chalk.gray(public_key)}`;
|
||||||
|
logger.fatal`Generated private key: ${chalk.gray(private_key)}`;
|
||||||
|
|
||||||
|
// Hang until Ctrl+C is pressed
|
||||||
|
await Bun.sleep(Number.POSITIVE_INFINITY);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try and import the key
|
||||||
|
const privateKey = await crypto.subtle
|
||||||
|
.importKey(
|
||||||
|
"pkcs8",
|
||||||
|
Buffer.from(config.instance.keys.private, "base64"),
|
||||||
|
"Ed25519",
|
||||||
|
false,
|
||||||
|
["sign"],
|
||||||
|
)
|
||||||
|
.catch((e) => e as Error);
|
||||||
|
|
||||||
|
// Try and import the key
|
||||||
|
const publicKey = await crypto.subtle
|
||||||
|
.importKey(
|
||||||
|
"spki",
|
||||||
|
Buffer.from(config.instance.keys.public, "base64"),
|
||||||
|
"Ed25519",
|
||||||
|
false,
|
||||||
|
["verify"],
|
||||||
|
)
|
||||||
|
.catch((e) => e as Error);
|
||||||
|
|
||||||
|
if (privateKey instanceof Error || publicKey instanceof Error) {
|
||||||
|
logger.fatal`The federation keys could not be imported! You may generate new ones by removing the old ones from the config and restarting the server.`;
|
||||||
|
|
||||||
|
// Hang until Ctrl+C is pressed
|
||||||
|
await Bun.sleep(Number.POSITIVE_INFINITY);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue