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 { matches } from "ip-matching";
import { z } from "zod"; import { z } from "zod";
import { type ValidationError, isValidationError } from "zod-validation-error"; import { type ValidationError, isValidationError } from "zod-validation-error";
import { Like } from "~/classes/database/like";
import { Note } from "~/classes/database/note"; import { Note } from "~/classes/database/note";
import { Relationship } from "~/classes/database/relationship"; import { Relationship } from "~/classes/database/relationship";
import { User } from "~/classes/database/user"; import { User } from "~/classes/database/user";
import { sendFollowAccept } from "~/classes/functions/user"; import { sendFollowAccept } from "~/classes/functions/user";
import { db } from "~/drizzle/db"; 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 { config } from "~/packages/config-manager";
import { ErrorSchema } from "~/types/api"; import { ErrorSchema } from "~/types/api";
@ -363,6 +364,23 @@ export default apiRoute((app) =>
return context.text("Follow request rejected", 200); 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" is a reserved keyword in JS
delete: async (delete_) => { delete: async (delete_) => {
// Delete the specified object from database, if it exists and belongs to the user // Delete the specified object from database, if it exists and belongs to the user
@ -401,6 +419,24 @@ export default apiRoute((app) =>
break; 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: { default: {
return context.json( 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. * 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 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 * @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 // Check if the user has already liked the note
const existingLike = await Like.fromSql( const existingLike = await Like.fromSql(
and(eq(Likes.likerId, this.id), eq(Likes.likedId, note.id)), 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({ const newLike = await Like.insert({
likerId: this.id, likerId: this.id,
likedId: note.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 // Notify the user that their post has been favourited
await db.insert(Notifications).values({ await db.insert(Notifications).values({
accountId: this.id, accountId: this.id,
@ -472,7 +474,7 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
notifiedId: note.author.id, notifiedId: note.author.id,
noteId: note.id, noteId: note.id,
}); });
} else { } else if (this.isLocal() && note.author.isRemote()) {
// Federate the like // Federate the like
this.federateToFollowers(newLike.toVersia()); this.federateToFollowers(newLike.toVersia());
} }
@ -498,19 +500,19 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
await likeToDelete.delete(); await likeToDelete.delete();
// Remove any eventual notifications for this like if (this.isLocal() && note.author.isLocal()) {
await db // Remove any eventual notifications for this like
.delete(Notifications) await db
.where( .delete(Notifications)
and( .where(
eq(Notifications.accountId, this.id), and(
eq(Notifications.type, "favourite"), eq(Notifications.accountId, this.id),
eq(Notifications.notifiedId, note.author.id), eq(Notifications.type, "favourite"),
eq(Notifications.noteId, note.id), eq(Notifications.notifiedId, note.author.id),
), eq(Notifications.noteId, note.id),
); ),
);
if (this.isLocal() && note.author.isRemote()) { } else if (this.isLocal() && note.author.isRemote()) {
// User is local, federate the delete // User is local, federate the delete
this.federateToFollowers(likeToDelete.unlikeToVersia(this)); 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, "when": 1726491670160,
"tag": "0033_panoramic_sister_grimm", "tag": "0033_panoramic_sister_grimm",
"breakpoints": true "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", { export const Likes = pgTable("Likes", {
id: uuid("id").default(sql`uuid_generate_v7()`).primaryKey().notNull(), id: uuid("id").default(sql`uuid_generate_v7()`).primaryKey().notNull(),
uri: text("uri").unique(),
likerId: uuid("likerId") likerId: uuid("likerId")
.notNull() .notNull()
.references(() => Users.id, { .references(() => Users.id, {