feat(federation): Implement inbound federation of likes and like deletion

This commit is contained in:
Jesse Wierzbinski 2024-10-24 19:08:28 +02:00
parent df84572148
commit 0a31b7a8f6
No known key found for this signature in database
6 changed files with 2230 additions and 17 deletions

View file

@ -12,12 +12,13 @@ import { eq } from "drizzle-orm";
import { matches } from "ip-matching";
import { z } from "zod";
import { type ValidationError, isValidationError } from "zod-validation-error";
import { Like } from "~/classes/database/like";
import { Note } from "~/classes/database/note";
import { Relationship } from "~/classes/database/relationship";
import { User } from "~/classes/database/user";
import { sendFollowAccept } from "~/classes/functions/user";
import { db } from "~/drizzle/db";
import { Notes, Notifications } from "~/drizzle/schema";
import { Likes, Notes, Notifications } from "~/drizzle/schema";
import { config } from "~/packages/config-manager";
import { ErrorSchema } from "~/types/api";
@ -363,6 +364,23 @@ export default apiRoute((app) =>
return context.text("Follow request rejected", 200);
},
"pub.versia:likes/Like": async (like) => {
const author = await User.resolve(like.author);
if (!author) {
return context.json({ error: "Author not found" }, 400);
}
const note = await Note.resolve(like.liked);
if (!note) {
return context.json({ error: "Note not found" }, 400);
}
await author.like(note, like.uri);
return context.text("Like added", 200);
},
// "delete" is a reserved keyword in JS
delete: async (delete_) => {
// Delete the specified object from database, if it exists and belongs to the user
@ -401,6 +419,24 @@ export default apiRoute((app) =>
break;
}
case "pub.versia:likes/Like": {
const like = await Like.fromSql(
eq(Likes.uri, toDelete),
eq(Likes.likerId, user.id),
);
if (like) {
await like.delete();
return context.text("Like deleted", 200);
}
return context.json(
{
error: "Like not found or not owned by user",
},
404,
);
}
default: {
return context.json(
{

View file

@ -447,9 +447,10 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
*
* If the note is already liked, it will return the existing like. Also creates a notification for the author of the note.
* @param note The note to like
* @param uri The URI of the like, if it is remote
* @returns The like object created or the existing like
*/
public async like(note: Note): Promise<Like> {
public async like(note: Note, uri?: string): Promise<Like> {
// Check if the user has already liked the note
const existingLike = await Like.fromSql(
and(eq(Likes.likerId, this.id), eq(Likes.likedId, note.id)),
@ -462,9 +463,10 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
const newLike = await Like.insert({
likerId: this.id,
likedId: note.id,
uri,
});
if (note.author.data.instanceId === this.data.instanceId) {
if (this.isLocal() && note.author.isLocal()) {
// Notify the user that their post has been favourited
await db.insert(Notifications).values({
accountId: this.id,
@ -472,7 +474,7 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
notifiedId: note.author.id,
noteId: note.id,
});
} else {
} else if (this.isLocal() && note.author.isRemote()) {
// Federate the like
this.federateToFollowers(newLike.toVersia());
}
@ -498,19 +500,19 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
await likeToDelete.delete();
// Remove any eventual notifications for this like
await db
.delete(Notifications)
.where(
and(
eq(Notifications.accountId, this.id),
eq(Notifications.type, "favourite"),
eq(Notifications.notifiedId, note.author.id),
eq(Notifications.noteId, note.id),
),
);
if (this.isLocal() && note.author.isRemote()) {
if (this.isLocal() && note.author.isLocal()) {
// Remove any eventual notifications for this like
await db
.delete(Notifications)
.where(
and(
eq(Notifications.accountId, this.id),
eq(Notifications.type, "favourite"),
eq(Notifications.notifiedId, note.author.id),
eq(Notifications.noteId, note.id),
),
);
} else if (this.isLocal() && note.author.isRemote()) {
// User is local, federate the delete
this.federateToFollowers(likeToDelete.unlikeToVersia(this));
}

View file

@ -0,0 +1,2 @@
ALTER TABLE "Likes" ADD COLUMN "uri" text;--> statement-breakpoint
ALTER TABLE "Likes" ADD CONSTRAINT "Likes_uri_unique" UNIQUE("uri");

File diff suppressed because it is too large Load diff

View file

@ -239,6 +239,13 @@
"when": 1726491670160,
"tag": "0033_panoramic_sister_grimm",
"breakpoints": true
},
{
"idx": 34,
"version": "7",
"when": 1729789587213,
"tag": "0034_jittery_proemial_gods",
"breakpoints": true
}
]
}

View file

@ -120,6 +120,7 @@ export const Markers = pgTable("Markers", {
export const Likes = pgTable("Likes", {
id: uuid("id").default(sql`uuid_generate_v7()`).primaryKey().notNull(),
uri: text("uri").unique(),
likerId: uuid("likerId")
.notNull()
.references(() => Users.id, {