mirror of
https://github.com/versia-pub/server.git
synced 2026-03-13 22:09:16 +01:00
Add following
This commit is contained in:
parent
7da7febd00
commit
f56e4f623a
5 changed files with 242 additions and 129 deletions
58
database/entities/Federation.ts
Normal file
58
database/entities/Federation.ts
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import type { User } from "@prisma/client";
|
||||
import type * as Lysand from "lysand-types";
|
||||
import { config } from "config-manager";
|
||||
|
||||
export const objectToInboxRequest = async (
|
||||
object: Lysand.Entity,
|
||||
author: User,
|
||||
userToSendTo: User,
|
||||
): Promise<Request> => {
|
||||
if (!userToSendTo.instanceId || !userToSendTo.endpoints.inbox) {
|
||||
throw new Error("User has no inbox or is a local user");
|
||||
}
|
||||
|
||||
const privateKey = await crypto.subtle.importKey(
|
||||
"pkcs8",
|
||||
Uint8Array.from(atob(author.privateKey ?? ""), (c) => c.charCodeAt(0)),
|
||||
"Ed25519",
|
||||
false,
|
||||
["sign"],
|
||||
);
|
||||
|
||||
const digest = await crypto.subtle.digest(
|
||||
"SHA-256",
|
||||
new TextEncoder().encode(JSON.stringify(object)),
|
||||
);
|
||||
|
||||
const userInbox = new URL(userToSendTo.endpoints.inbox);
|
||||
|
||||
const date = new Date();
|
||||
|
||||
const signature = await crypto.subtle.sign(
|
||||
"Ed25519",
|
||||
privateKey,
|
||||
new TextEncoder().encode(
|
||||
`(request-target): post ${userInbox.pathname}\n` +
|
||||
`host: ${userInbox.host}\n` +
|
||||
`date: ${date.toISOString()}\n` +
|
||||
`digest: SHA-256=${btoa(
|
||||
String.fromCharCode(...new Uint8Array(digest)),
|
||||
)}\n`,
|
||||
),
|
||||
);
|
||||
|
||||
const signatureBase64 = btoa(
|
||||
String.fromCharCode(...new Uint8Array(signature)),
|
||||
);
|
||||
|
||||
return new Request(userInbox, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Date: date.toISOString(),
|
||||
Origin: config.http.base_url,
|
||||
Signature: `keyId="${author.uri}",algorithm="ed25519",headers="(request-target) host date digest",signature="${signatureBase64}"`,
|
||||
},
|
||||
body: JSON.stringify(object),
|
||||
});
|
||||
};
|
||||
|
|
@ -25,6 +25,7 @@ import { emojiToAPI, emojiToLysand, parseEmojis } from "./Emoji";
|
|||
import type { UserWithRelations } from "./User";
|
||||
import { resolveUser, parseMentionsUris, userToAPI } from "./User";
|
||||
import { statusAndUserRelations, userRelations } from "./relations";
|
||||
import { objectToInboxRequest } from "./Federation";
|
||||
|
||||
const statusRelations = Prisma.validator<Prisma.StatusDefaultArgs>()({
|
||||
include: statusAndUserRelations,
|
||||
|
|
@ -262,7 +263,11 @@ export const federateStatus = async (status: StatusWithRelations) => {
|
|||
|
||||
for (const user of toFederateTo) {
|
||||
// TODO: Add queue system
|
||||
const request = await statusToInboxRequest(status, user);
|
||||
const request = await objectToInboxRequest(
|
||||
statusToLysand(status),
|
||||
status.author,
|
||||
user,
|
||||
);
|
||||
|
||||
// Send request
|
||||
const response = await fetch(request);
|
||||
|
|
@ -275,64 +280,6 @@ export const federateStatus = async (status: StatusWithRelations) => {
|
|||
}
|
||||
};
|
||||
|
||||
export const statusToInboxRequest = async (
|
||||
status: StatusWithRelations,
|
||||
user: User,
|
||||
): Promise<Request> => {
|
||||
const output = statusToLysand(status);
|
||||
|
||||
if (!user.instanceId || !user.endpoints.inbox) {
|
||||
throw new Error("User has no inbox or is a local user");
|
||||
}
|
||||
|
||||
const privateKey = await crypto.subtle.importKey(
|
||||
"pkcs8",
|
||||
Uint8Array.from(atob(status.author.privateKey ?? ""), (c) =>
|
||||
c.charCodeAt(0),
|
||||
),
|
||||
"Ed25519",
|
||||
false,
|
||||
["sign"],
|
||||
);
|
||||
|
||||
const digest = await crypto.subtle.digest(
|
||||
"SHA-256",
|
||||
new TextEncoder().encode(JSON.stringify(output)),
|
||||
);
|
||||
|
||||
const userInbox = new URL(user.endpoints.inbox);
|
||||
|
||||
const date = new Date();
|
||||
|
||||
const signature = await crypto.subtle.sign(
|
||||
"Ed25519",
|
||||
privateKey,
|
||||
new TextEncoder().encode(
|
||||
`(request-target): post ${userInbox.pathname}\n` +
|
||||
`host: ${userInbox.host}\n` +
|
||||
`date: ${date.toISOString()}\n` +
|
||||
`digest: SHA-256=${btoa(
|
||||
String.fromCharCode(...new Uint8Array(digest)),
|
||||
)}\n`,
|
||||
),
|
||||
);
|
||||
|
||||
const signatureBase64 = btoa(
|
||||
String.fromCharCode(...new Uint8Array(signature)),
|
||||
);
|
||||
|
||||
return new Request(userInbox, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Date: date.toISOString(),
|
||||
Origin: config.http.base_url,
|
||||
Signature: `keyId="${status.author.uri}",algorithm="ed25519",headers="(request-target) host date digest",signature="${signatureBase64}"`,
|
||||
},
|
||||
body: JSON.stringify(output),
|
||||
});
|
||||
};
|
||||
|
||||
export const getUsersToFederateTo = async (status: StatusWithRelations) => {
|
||||
return await client.user.findMany({
|
||||
where: {
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import { addInstanceIfNotExists } from "./Instance";
|
|||
import { userRelations } from "./relations";
|
||||
import { createNewRelationship } from "./Relationship";
|
||||
import { getBestContentType, urlToContentFormat } from "@content_types";
|
||||
import { objectToInboxRequest } from "./Federation";
|
||||
|
||||
export interface AuthData {
|
||||
user: UserWithRelations | null;
|
||||
|
|
@ -60,40 +61,6 @@ export const getFromRequest = async (req: Request): Promise<AuthData> => {
|
|||
return { user: await retrieveUserFromToken(token), token };
|
||||
};
|
||||
|
||||
export const followUser = async (
|
||||
follower: User,
|
||||
followee: User,
|
||||
relationshipId: string,
|
||||
reblogs = false,
|
||||
notify = false,
|
||||
languages: string[] = [],
|
||||
) => {
|
||||
const relationship = await client.relationship.update({
|
||||
where: { id: relationshipId },
|
||||
data: {
|
||||
following: true,
|
||||
showingReblogs: reblogs,
|
||||
notifying: notify,
|
||||
languages: languages,
|
||||
},
|
||||
});
|
||||
|
||||
if (follower.instanceId === followee.instanceId) {
|
||||
// Notify the user that their post has been favourited
|
||||
await client.notification.create({
|
||||
data: {
|
||||
accountId: follower.id,
|
||||
type: "follow",
|
||||
notifiedId: followee.id,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
// TODO: Add database jobs for federating this
|
||||
}
|
||||
|
||||
return relationship;
|
||||
};
|
||||
|
||||
export const followRequestUser = async (
|
||||
follower: User,
|
||||
followee: User,
|
||||
|
|
@ -102,27 +69,54 @@ export const followRequestUser = async (
|
|||
notify = false,
|
||||
languages: string[] = [],
|
||||
) => {
|
||||
const isRemote = follower.instanceId !== followee.instanceId;
|
||||
|
||||
const relationship = await client.relationship.update({
|
||||
where: { id: relationshipId },
|
||||
data: {
|
||||
requested: true,
|
||||
following: isRemote ? false : !followee.isLocked,
|
||||
requested: isRemote ? true : followee.isLocked,
|
||||
showingReblogs: reblogs,
|
||||
notifying: notify,
|
||||
languages: languages,
|
||||
},
|
||||
});
|
||||
|
||||
if (follower.instanceId === followee.instanceId) {
|
||||
// Notify the user that their post has been favourited
|
||||
await client.notification.create({
|
||||
data: {
|
||||
accountId: follower.id,
|
||||
type: "follow_request",
|
||||
notifiedId: followee.id,
|
||||
},
|
||||
});
|
||||
if (isRemote) {
|
||||
// Federate
|
||||
// TODO: Make database job
|
||||
const request = await objectToInboxRequest(
|
||||
followRequestToLysand(follower, followee),
|
||||
follower,
|
||||
followee,
|
||||
);
|
||||
|
||||
// Send request
|
||||
const response = await fetch(request);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to federate follow request from ${follower.id} to ${followee.uri}`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// TODO: Add database jobs for federating this
|
||||
if (followee.isLocked) {
|
||||
await client.notification.create({
|
||||
data: {
|
||||
accountId: follower.id,
|
||||
type: "follow_request",
|
||||
notifiedId: followee.id,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await client.notification.create({
|
||||
data: {
|
||||
accountId: follower.id,
|
||||
type: "follow",
|
||||
notifiedId: followee.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return relationship;
|
||||
|
|
@ -588,3 +582,68 @@ export const userToLysand = (user: UserWithRelations): Lysand.User => {
|
|||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const followRequestToLysand = (
|
||||
follower: User,
|
||||
followee: User,
|
||||
): Lysand.Follow => {
|
||||
if (follower.instanceId) {
|
||||
throw new Error("Follower must be a local user");
|
||||
}
|
||||
|
||||
if (!followee.instanceId) {
|
||||
throw new Error("Followee must be a remote user");
|
||||
}
|
||||
|
||||
if (!followee.uri) {
|
||||
throw new Error("Followee must have a URI in database");
|
||||
}
|
||||
|
||||
const id = crypto.randomUUID();
|
||||
|
||||
return {
|
||||
type: "Follow",
|
||||
id: id,
|
||||
author: new URL(
|
||||
`/users/${follower.id}`,
|
||||
config.http.base_url,
|
||||
).toString(),
|
||||
followee: followee.uri,
|
||||
created_at: new Date().toISOString(),
|
||||
uri: new URL(`/follows/${id}`, config.http.base_url).toString(),
|
||||
};
|
||||
};
|
||||
|
||||
export const followAcceptToLysand = (
|
||||
follower: User,
|
||||
followee: User,
|
||||
): Lysand.FollowAccept => {
|
||||
if (follower.instanceId) {
|
||||
throw new Error("Follower must be a local user");
|
||||
}
|
||||
|
||||
if (!followee.instanceId) {
|
||||
throw new Error("Followee must be a remote user");
|
||||
}
|
||||
|
||||
if (!followee.uri) {
|
||||
throw new Error("Followee must have a URI in database");
|
||||
}
|
||||
|
||||
const id = crypto.randomUUID();
|
||||
|
||||
return {
|
||||
type: "FollowAccept",
|
||||
id: id,
|
||||
author: new URL(
|
||||
`/users/${followee.id}`,
|
||||
config.http.base_url,
|
||||
).toString(),
|
||||
created_at: new Date().toISOString(),
|
||||
follower: new URL(
|
||||
`/users/${follower.id}`,
|
||||
config.http.base_url,
|
||||
).toString(),
|
||||
uri: new URL(`/follows/${id}`, config.http.base_url).toString(),
|
||||
};
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue