mirror of
https://github.com/versia-pub/server.git
synced 2026-03-13 13:59:16 +01:00
feat(federation): ✨ Implement Share federation support
Some checks failed
CodeQL Scan / Analyze (javascript-typescript) (push) Failing after 0s
Build Docker Images / lint (push) Failing after 6s
Build Docker Images / check (push) Failing after 7s
Build Docker Images / tests (push) Failing after 6s
Build Docker Images / build (server, Dockerfile, ${{ github.repository_owner }}/server) (push) Has been skipped
Build Docker Images / build (worker, Worker.Dockerfile, ${{ github.repository_owner }}/worker) (push) Has been skipped
Deploy Docs to GitHub Pages / build (push) Failing after 0s
Deploy Docs to GitHub Pages / Deploy (push) Has been skipped
Mirror to Codeberg / Mirror (push) Failing after 0s
Nix Build / check (push) Failing after 0s
Some checks failed
CodeQL Scan / Analyze (javascript-typescript) (push) Failing after 0s
Build Docker Images / lint (push) Failing after 6s
Build Docker Images / check (push) Failing after 7s
Build Docker Images / tests (push) Failing after 6s
Build Docker Images / build (server, Dockerfile, ${{ github.repository_owner }}/server) (push) Has been skipped
Build Docker Images / build (worker, Worker.Dockerfile, ${{ github.repository_owner }}/worker) (push) Has been skipped
Deploy Docs to GitHub Pages / build (push) Failing after 0s
Deploy Docs to GitHub Pages / Deploy (push) Has been skipped
Mirror to Codeberg / Mirror (push) Failing after 0s
Nix Build / check (push) Failing after 0s
This commit is contained in:
parent
ec69fc2ac0
commit
cd12ccd6c1
12 changed files with 533 additions and 85 deletions
|
|
@ -739,6 +739,10 @@ export class Note extends BaseInterface<typeof Notes, NoteTypeWithRelations> {
|
|||
`/notes/${status.id}/quotes`,
|
||||
config.http.base_url,
|
||||
).href,
|
||||
"pub.versia:share/Shares": new URL(
|
||||
`/notes/${status.id}/shares`,
|
||||
config.http.base_url,
|
||||
).href,
|
||||
},
|
||||
attachments: status.attachments.map(
|
||||
(attachment) =>
|
||||
|
|
@ -780,6 +784,36 @@ export class Note extends BaseInterface<typeof Notes, NoteTypeWithRelations> {
|
|||
});
|
||||
}
|
||||
|
||||
public toVersiaShare(): VersiaEntities.Share {
|
||||
if (!(this.data.reblogId && this.data.reblog)) {
|
||||
throw new Error("Cannot share a non-reblogged note");
|
||||
}
|
||||
|
||||
return new VersiaEntities.Share({
|
||||
type: "pub.versia:share/Share",
|
||||
id: crypto.randomUUID(),
|
||||
author: this.author.uri.href,
|
||||
uri: new URL(`/shares/${this.id}`, config.http.base_url).href,
|
||||
created_at: new Date().toISOString(),
|
||||
shared: new Note(this.data.reblog as NoteTypeWithRelations).getUri()
|
||||
.href,
|
||||
});
|
||||
}
|
||||
|
||||
public toVersiaUnshare(): VersiaEntities.Delete {
|
||||
return new VersiaEntities.Delete({
|
||||
type: "Delete",
|
||||
id: crypto.randomUUID(),
|
||||
created_at: new Date().toISOString(),
|
||||
author: User.getUri(
|
||||
this.data.authorId,
|
||||
this.data.author.uri ? new URL(this.data.author.uri) : null,
|
||||
).href,
|
||||
deleted_type: "pub.versia:share/Share",
|
||||
deleted: new URL(`/shares/${this.id}`, config.http.base_url).href,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all the ancestors of this post,
|
||||
* i.e. all the posts that this post is a reply to
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import type {
|
|||
Mention as MentionSchema,
|
||||
RolePermission,
|
||||
Source,
|
||||
Status as StatusSchema,
|
||||
} from "@versia/client/schemas";
|
||||
import { db, Media, Notification, PushSubscription } from "@versia/kit/db";
|
||||
import {
|
||||
|
|
@ -52,7 +53,7 @@ import { BaseInterface } from "./base.ts";
|
|||
import { Emoji } from "./emoji.ts";
|
||||
import { Instance } from "./instance.ts";
|
||||
import { Like } from "./like.ts";
|
||||
import type { Note } from "./note.ts";
|
||||
import { Note } from "./note.ts";
|
||||
import { Relationship } from "./relationship.ts";
|
||||
import { Role } from "./role.ts";
|
||||
|
||||
|
|
@ -468,6 +469,123 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
|||
.filter((x) => x !== null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reblog a note.
|
||||
*
|
||||
* If the note is already reblogged, it will return the existing reblog. Also creates a notification for the author of the note.
|
||||
* @param note The note to reblog
|
||||
* @param visibility The visibility of the reblog
|
||||
* @param uri The URI of the reblog, if it is remote
|
||||
* @returns The reblog object created or the existing reblog
|
||||
*/
|
||||
public async reblog(
|
||||
note: Note,
|
||||
visibility: z.infer<typeof StatusSchema.shape.visibility>,
|
||||
uri?: URL,
|
||||
): Promise<Note> {
|
||||
const existingReblog = await Note.fromSql(
|
||||
and(eq(Notes.authorId, this.id), eq(Notes.reblogId, note.id)),
|
||||
undefined,
|
||||
this.id,
|
||||
);
|
||||
|
||||
if (existingReblog) {
|
||||
return existingReblog;
|
||||
}
|
||||
|
||||
const newReblog = await Note.insert({
|
||||
id: randomUUIDv7(),
|
||||
authorId: this.id,
|
||||
reblogId: note.id,
|
||||
visibility,
|
||||
sensitive: false,
|
||||
updatedAt: new Date().toISOString(),
|
||||
applicationId: null,
|
||||
uri: uri?.href,
|
||||
});
|
||||
|
||||
// Refetch the note *again* to get the proper value of .reblogged
|
||||
const finalNewReblog = await Note.fromId(newReblog.id, this?.id);
|
||||
|
||||
if (!finalNewReblog) {
|
||||
throw new Error("Failed to reblog");
|
||||
}
|
||||
|
||||
if (note.author.local) {
|
||||
// Notify the user that their post has been reblogged
|
||||
await note.author.notify("reblog", this, finalNewReblog);
|
||||
}
|
||||
|
||||
if (this.local) {
|
||||
const federatedUsers = await this.federateToFollowers(
|
||||
finalNewReblog.toVersiaShare(),
|
||||
);
|
||||
|
||||
if (
|
||||
note.remote &&
|
||||
!federatedUsers.find((u) => u.id === note.author.id)
|
||||
) {
|
||||
await this.federateToUser(
|
||||
finalNewReblog.toVersiaShare(),
|
||||
note.author,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return finalNewReblog;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unreblog a note.
|
||||
*
|
||||
* If the note is not reblogged, it will return without doing anything. Also removes any notifications for this reblog.
|
||||
* @param note The note to unreblog
|
||||
* @returns
|
||||
*/
|
||||
public async unreblog(note: Note): Promise<void> {
|
||||
const reblogToDelete = await Note.fromSql(
|
||||
and(eq(Notes.authorId, this.id), eq(Notes.reblogId, note.id)),
|
||||
undefined,
|
||||
this.id,
|
||||
);
|
||||
|
||||
if (!reblogToDelete) {
|
||||
return;
|
||||
}
|
||||
|
||||
await reblogToDelete.delete();
|
||||
|
||||
if (note.author.local) {
|
||||
// Remove any eventual notifications for this reblog
|
||||
await db
|
||||
.delete(Notifications)
|
||||
.where(
|
||||
and(
|
||||
eq(Notifications.accountId, this.id),
|
||||
eq(Notifications.type, "reblog"),
|
||||
eq(Notifications.notifiedId, note.data.authorId),
|
||||
eq(Notifications.noteId, note.id),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (this.local) {
|
||||
const federatedUsers = await this.federateToFollowers(
|
||||
reblogToDelete.toVersiaUnshare(),
|
||||
);
|
||||
|
||||
if (
|
||||
note.remote &&
|
||||
!federatedUsers.find((u) => u.id === note.author.id)
|
||||
) {
|
||||
await this.federateToUser(
|
||||
reblogToDelete.toVersiaUnshare(),
|
||||
note.author,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Like a note.
|
||||
*
|
||||
|
|
@ -498,15 +616,17 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
|||
await note.author.notify("favourite", this, note);
|
||||
}
|
||||
|
||||
const federatedUsers = await this.federateToFollowers(
|
||||
newLike.toVersia(),
|
||||
);
|
||||
if (this.local) {
|
||||
const federatedUsers = await this.federateToFollowers(
|
||||
newLike.toVersia(),
|
||||
);
|
||||
|
||||
if (
|
||||
note.remote &&
|
||||
!federatedUsers.find((u) => u.id === note.author.id)
|
||||
) {
|
||||
await this.federateToUser(newLike.toVersia(), note.author);
|
||||
if (
|
||||
note.remote &&
|
||||
!federatedUsers.find((u) => u.id === note.author.id)
|
||||
) {
|
||||
await this.federateToUser(newLike.toVersia(), note.author);
|
||||
}
|
||||
}
|
||||
|
||||
return newLike;
|
||||
|
|
@ -535,19 +655,20 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
|||
await likeToDelete.clearRelatedNotifications();
|
||||
}
|
||||
|
||||
// User is local, federate the delete
|
||||
const federatedUsers = await this.federateToFollowers(
|
||||
likeToDelete.unlikeToVersia(this),
|
||||
);
|
||||
|
||||
if (
|
||||
note.remote &&
|
||||
!federatedUsers.find((u) => u.id === note.author.id)
|
||||
) {
|
||||
await this.federateToUser(
|
||||
if (this.local) {
|
||||
const federatedUsers = await this.federateToFollowers(
|
||||
likeToDelete.unlikeToVersia(this),
|
||||
note.author,
|
||||
);
|
||||
|
||||
if (
|
||||
note.remote &&
|
||||
!federatedUsers.find((u) => u.id === note.author.id)
|
||||
) {
|
||||
await this.federateToUser(
|
||||
likeToDelete.unlikeToVersia(this),
|
||||
note.author,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue