mirror of
https://github.com/versia-pub/server.git
synced 2025-12-06 16:38:19 +01:00
fix(api): 🐛 Fix incorrect relationships being returned (small rewrite)
This commit is contained in:
parent
20d1a5f39e
commit
c4da7e1484
|
|
@ -61,8 +61,11 @@ export const checkForBidirectionalRelationships = async (
|
||||||
and(eq(rel.ownerId, user2.id), eq(rel.subjectId, user1.id)),
|
and(eq(rel.ownerId, user2.id), eq(rel.subjectId, user1.id)),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!relationship1 && !relationship2 && createIfNotExists) {
|
if (!relationship1 && createIfNotExists) {
|
||||||
await createNewRelationship(user1, user2);
|
await createNewRelationship(user1, user2);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!relationship2 && createIfNotExists) {
|
||||||
await createNewRelationship(user2, user1);
|
await createNewRelationship(user2, user1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,11 @@ import { LogLevel } from "~/packages/log-manager";
|
||||||
import type { Application } from "./Application";
|
import type { Application } from "./Application";
|
||||||
import type { EmojiWithInstance } from "./Emoji";
|
import type { EmojiWithInstance } from "./Emoji";
|
||||||
import { objectToInboxRequest } from "./Federation";
|
import { objectToInboxRequest } from "./Federation";
|
||||||
import { type Relationship, createNewRelationship } from "./Relationship";
|
import {
|
||||||
|
type Relationship,
|
||||||
|
checkForBidirectionalRelationships,
|
||||||
|
createNewRelationship,
|
||||||
|
} from "./Relationship";
|
||||||
import type { Token } from "./Token";
|
import type { Token } from "./Token";
|
||||||
|
|
||||||
export type UserType = InferSelectModel<typeof Users>;
|
export type UserType = InferSelectModel<typeof Users>;
|
||||||
|
|
@ -135,14 +139,22 @@ export const followRequestUser = async (
|
||||||
})
|
})
|
||||||
.where(eq(Relationships.id, relationshipId));
|
.where(eq(Relationships.id, relationshipId));
|
||||||
|
|
||||||
|
// Set requested_by on other side
|
||||||
|
await db
|
||||||
|
.update(Relationships)
|
||||||
|
.set({
|
||||||
|
requestedBy: isRemote ? true : followee.getUser().isLocked,
|
||||||
|
followedBy: isRemote ? false : followee.getUser().isLocked,
|
||||||
|
})
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(Relationships.ownerId, followee.id),
|
||||||
|
eq(Relationships.subjectId, follower.id),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
const updatedRelationship = await db.query.Relationships.findFirst({
|
const updatedRelationship = await db.query.Relationships.findFirst({
|
||||||
where: (rel, { eq }) => eq(rel.id, relationshipId),
|
where: (rel, { eq }) => eq(rel.id, relationshipId),
|
||||||
extras: {
|
|
||||||
requestedBy:
|
|
||||||
sql<boolean>`(SELECT "requested" FROM "Relationships" WHERE "Relationships"."ownerId" = ${followee.id} AND "Relationships"."subjectId" = ${follower.id})`.as(
|
|
||||||
"requested_by",
|
|
||||||
),
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!updatedRelationship) {
|
if (!updatedRelationship) {
|
||||||
|
|
@ -186,12 +198,6 @@ export const followRequestUser = async (
|
||||||
|
|
||||||
const result = await db.query.Relationships.findFirst({
|
const result = await db.query.Relationships.findFirst({
|
||||||
where: (rel, { eq }) => eq(rel.id, relationshipId),
|
where: (rel, { eq }) => eq(rel.id, relationshipId),
|
||||||
extras: {
|
|
||||||
requestedBy:
|
|
||||||
sql<boolean>`(SELECT "requested" FROM "Relationships" WHERE "Relationships"."ownerId" = ${followee.id} AND "Relationships"."subjectId" = ${follower.id})`.as(
|
|
||||||
"requested_by",
|
|
||||||
),
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!result) {
|
if (!result) {
|
||||||
|
|
@ -483,18 +489,14 @@ export const getRelationshipToOtherUser = async (
|
||||||
user: User,
|
user: User,
|
||||||
other: User,
|
other: User,
|
||||||
): Promise<Relationship> => {
|
): Promise<Relationship> => {
|
||||||
|
await checkForBidirectionalRelationships(user, other);
|
||||||
|
|
||||||
const foundRelationship = await db.query.Relationships.findFirst({
|
const foundRelationship = await db.query.Relationships.findFirst({
|
||||||
where: (relationship, { and, eq }) =>
|
where: (relationship, { and, eq }) =>
|
||||||
and(
|
and(
|
||||||
eq(relationship.ownerId, user.id),
|
eq(relationship.ownerId, user.id),
|
||||||
eq(relationship.subjectId, other.id),
|
eq(relationship.subjectId, other.id),
|
||||||
),
|
),
|
||||||
extras: {
|
|
||||||
requestedBy:
|
|
||||||
sql<boolean>`(SELECT "requested" FROM "Relationships" WHERE "Relationships"."ownerId" = ${other.id} AND "Relationships"."subjectId" = ${user.id})`.as(
|
|
||||||
"requested_by",
|
|
||||||
),
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!foundRelationship) {
|
if (!foundRelationship) {
|
||||||
|
|
|
||||||
1
drizzle/0025_violet_susan_delgado.sql
Normal file
1
drizzle/0025_violet_susan_delgado.sql
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
ALTER TABLE "Relationships" ADD COLUMN "requested_by" boolean DEFAULT false NOT NULL;
|
||||||
2100
drizzle/meta/0025_snapshot.json
Normal file
2100
drizzle/meta/0025_snapshot.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -176,6 +176,13 @@
|
||||||
"when": 1717810992701,
|
"when": 1717810992701,
|
||||||
"tag": "0024_lush_aaron_stack",
|
"tag": "0024_lush_aaron_stack",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 25,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1718147685855,
|
||||||
|
"tag": "0025_violet_susan_delgado",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -169,6 +169,7 @@ export const Relationships = pgTable("Relationships", {
|
||||||
muting: boolean("muting").notNull(),
|
muting: boolean("muting").notNull(),
|
||||||
mutingNotifications: boolean("muting_notifications").notNull(),
|
mutingNotifications: boolean("muting_notifications").notNull(),
|
||||||
requested: boolean("requested").notNull(),
|
requested: boolean("requested").notNull(),
|
||||||
|
requestedBy: boolean("requested_by").notNull().default(false),
|
||||||
domainBlocking: boolean("domain_blocking").notNull(),
|
domainBlocking: boolean("domain_blocking").notNull(),
|
||||||
endorsed: boolean("endorsed").notNull(),
|
endorsed: boolean("endorsed").notNull(),
|
||||||
languages: text("languages").array(),
|
languages: text("languages").array(),
|
||||||
|
|
|
||||||
146
server/api/api/v1/accounts/relationships/index.test.ts
Normal file
146
server/api/api/v1/accounts/relationships/index.test.ts
Normal file
|
|
@ -0,0 +1,146 @@
|
||||||
|
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
||||||
|
import { config } from "config-manager";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
import { db } from "~/drizzle/db";
|
||||||
|
import { Users } from "~/drizzle/schema";
|
||||||
|
import { getTestUsers, sendTestRequest } from "~/tests/utils";
|
||||||
|
import { meta } from "./index";
|
||||||
|
|
||||||
|
const { users, tokens, deleteUsers } = await getTestUsers(5);
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
// user0 should be `locked`
|
||||||
|
// user1 should follow user0
|
||||||
|
// user0 should follow user2
|
||||||
|
await db
|
||||||
|
.update(Users)
|
||||||
|
.set({ isLocked: true })
|
||||||
|
.where(eq(Users.id, users[0].id));
|
||||||
|
|
||||||
|
const res1 = await sendTestRequest(
|
||||||
|
new Request(
|
||||||
|
new URL(
|
||||||
|
`/api/v1/accounts/${users[0].id}/follow`,
|
||||||
|
config.http.base_url,
|
||||||
|
),
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${tokens[1].accessToken}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({}),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(res1.ok).toBe(true);
|
||||||
|
|
||||||
|
const res2 = await sendTestRequest(
|
||||||
|
new Request(
|
||||||
|
new URL(
|
||||||
|
`/api/v1/accounts/${users[2].id}/follow`,
|
||||||
|
config.http.base_url,
|
||||||
|
),
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${tokens[0].accessToken}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({}),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(res2.ok).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await deleteUsers();
|
||||||
|
});
|
||||||
|
|
||||||
|
// /api/v1/accounts/relationships
|
||||||
|
describe(meta.route, () => {
|
||||||
|
test("should return 401 if not authenticated", async () => {
|
||||||
|
const response = await sendTestRequest(
|
||||||
|
new Request(
|
||||||
|
new URL(
|
||||||
|
`${meta.route}?id[]=${users[2].id}`,
|
||||||
|
config.http.base_url,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(response.status).toBe(401);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should return relationships", async () => {
|
||||||
|
const response = await sendTestRequest(
|
||||||
|
new Request(
|
||||||
|
new URL(
|
||||||
|
`${meta.route}?id[]=${users[2].id}`,
|
||||||
|
config.http.base_url,
|
||||||
|
),
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${tokens[0].accessToken}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
|
||||||
|
const body = await response.json();
|
||||||
|
expect(body).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
id: users[2].id,
|
||||||
|
following: true,
|
||||||
|
followed_by: false,
|
||||||
|
blocking: false,
|
||||||
|
muting: false,
|
||||||
|
muting_notifications: false,
|
||||||
|
requested: false,
|
||||||
|
domain_blocking: false,
|
||||||
|
endorsed: false,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should be requested_by user1", async () => {
|
||||||
|
const response = await sendTestRequest(
|
||||||
|
new Request(
|
||||||
|
new URL(
|
||||||
|
`${meta.route}?id[]=${users[1].id}`,
|
||||||
|
config.http.base_url,
|
||||||
|
),
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${tokens[0].accessToken}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
|
||||||
|
const body = await response.json();
|
||||||
|
expect(body).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
following: false,
|
||||||
|
followed_by: true,
|
||||||
|
blocking: false,
|
||||||
|
muting: false,
|
||||||
|
muting_notifications: false,
|
||||||
|
requested_by: true,
|
||||||
|
domain_blocking: false,
|
||||||
|
endorsed: false,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import { applyConfig, auth, handleZodError, qsQuery } from "@/api";
|
import { applyConfig, auth, handleZodError, qsQuery } from "@/api";
|
||||||
import { errorResponse, jsonResponse } from "@/response";
|
import { errorResponse, jsonResponse } from "@/response";
|
||||||
import { zValidator } from "@hono/zod-validator";
|
import { zValidator } from "@hono/zod-validator";
|
||||||
import { sql } from "drizzle-orm";
|
|
||||||
import type { Hono } from "hono";
|
import type { Hono } from "hono";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import {
|
import {
|
||||||
|
|
@ -9,7 +8,7 @@ import {
|
||||||
relationshipToAPI,
|
relationshipToAPI,
|
||||||
} from "~/database/entities/Relationship";
|
} from "~/database/entities/Relationship";
|
||||||
import { db } from "~/drizzle/db";
|
import { db } from "~/drizzle/db";
|
||||||
import { Relationships, RolePermissions } from "~/drizzle/schema";
|
import { RolePermissions } from "~/drizzle/schema";
|
||||||
import { User } from "~/packages/database-interface/user";
|
import { User } from "~/packages/database-interface/user";
|
||||||
|
|
||||||
export const meta = applyConfig({
|
export const meta = applyConfig({
|
||||||
|
|
@ -55,12 +54,6 @@ export default (app: Hono) =>
|
||||||
inArray(relationship.subjectId, ids),
|
inArray(relationship.subjectId, ids),
|
||||||
eq(relationship.ownerId, self.id),
|
eq(relationship.ownerId, self.id),
|
||||||
),
|
),
|
||||||
extras: {
|
|
||||||
requestedBy:
|
|
||||||
sql<boolean>`(SELECT "requested" FROM "Relationships" WHERE "Relationships"."ownerId" = ${Relationships.id} AND "Relationships"."subjectId" = ${self.id})`.as(
|
|
||||||
"requested_by",
|
|
||||||
),
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const missingIds = ids.filter(
|
const missingIds = ids.filter(
|
||||||
|
|
|
||||||
|
|
@ -76,6 +76,7 @@ export default (app: Hono) =>
|
||||||
.update(Relationships)
|
.update(Relationships)
|
||||||
.set({
|
.set({
|
||||||
followedBy: true,
|
followedBy: true,
|
||||||
|
requestedBy: false,
|
||||||
})
|
})
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
|
|
|
||||||
|
|
@ -76,6 +76,7 @@ export default (app: Hono) =>
|
||||||
.update(Relationships)
|
.update(Relationships)
|
||||||
.set({
|
.set({
|
||||||
followedBy: false,
|
followedBy: false,
|
||||||
|
requestedBy: false,
|
||||||
})
|
})
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import {
|
||||||
SignatureValidator,
|
SignatureValidator,
|
||||||
} from "@lysand-org/federation";
|
} from "@lysand-org/federation";
|
||||||
import type { SocketAddress } from "bun";
|
import type { SocketAddress } from "bun";
|
||||||
import { eq } from "drizzle-orm";
|
import { and, eq } from "drizzle-orm";
|
||||||
import type { Hono } from "hono";
|
import type { Hono } from "hono";
|
||||||
import { matches } from "ip-matching";
|
import { matches } from "ip-matching";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
@ -246,6 +246,19 @@ export default (app: Hono) =>
|
||||||
})
|
})
|
||||||
.where(eq(Relationships.id, foundRelationship.id));
|
.where(eq(Relationships.id, foundRelationship.id));
|
||||||
|
|
||||||
|
// Update other user's requested_by
|
||||||
|
await db
|
||||||
|
.update(Relationships)
|
||||||
|
.set({
|
||||||
|
requestedBy: user.getUser().isLocked,
|
||||||
|
})
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(Relationships.subjectId, account.id),
|
||||||
|
eq(Relationships.ownerId, user.id),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
await db.insert(Notifications).values({
|
await db.insert(Notifications).values({
|
||||||
accountId: account.id,
|
accountId: account.id,
|
||||||
type: user.getUser().isLocked
|
type: user.getUser().isLocked
|
||||||
|
|
@ -285,6 +298,20 @@ export default (app: Hono) =>
|
||||||
})
|
})
|
||||||
.where(eq(Relationships.id, foundRelationship.id));
|
.where(eq(Relationships.id, foundRelationship.id));
|
||||||
|
|
||||||
|
// Update other user's data
|
||||||
|
await db
|
||||||
|
.update(Relationships)
|
||||||
|
.set({
|
||||||
|
followedBy: true,
|
||||||
|
requestedBy: false,
|
||||||
|
})
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(Relationships.subjectId, account.id),
|
||||||
|
eq(Relationships.ownerId, user.id),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
return response("Follow request accepted", 200);
|
return response("Follow request accepted", 200);
|
||||||
},
|
},
|
||||||
followReject: async (followReject) => {
|
followReject: async (followReject) => {
|
||||||
|
|
@ -312,6 +339,20 @@ export default (app: Hono) =>
|
||||||
})
|
})
|
||||||
.where(eq(Relationships.id, foundRelationship.id));
|
.where(eq(Relationships.id, foundRelationship.id));
|
||||||
|
|
||||||
|
// Update other user's data
|
||||||
|
await db
|
||||||
|
.update(Relationships)
|
||||||
|
.set({
|
||||||
|
followedBy: false,
|
||||||
|
requestedBy: false,
|
||||||
|
})
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(Relationships.subjectId, account.id),
|
||||||
|
eq(Relationships.ownerId, user.id),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
return response("Follow request rejected", 200);
|
return response("Follow request rejected", 200);
|
||||||
},
|
},
|
||||||
undo: async (undo) => {
|
undo: async (undo) => {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue