mirror of
https://github.com/versia-pub/server.git
synced 2025-12-06 16:38:19 +01:00
feat(federation): ✨ Implement inbound federation of likes and like deletion
This commit is contained in:
parent
df84572148
commit
0a31b7a8f6
|
|
@ -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(
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
2
drizzle/migrations/0034_jittery_proemial_gods.sql
Normal file
2
drizzle/migrations/0034_jittery_proemial_gods.sql
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
ALTER TABLE "Likes" ADD COLUMN "uri" text;--> statement-breakpoint
|
||||||
|
ALTER TABLE "Likes" ADD CONSTRAINT "Likes_uri_unique" UNIQUE("uri");
|
||||||
2165
drizzle/migrations/meta/0034_snapshot.json
Normal file
2165
drizzle/migrations/meta/0034_snapshot.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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, {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue