refactor: ♻️ Rewrite relationship system

This commit is contained in:
Jesse Wierzbinski 2024-07-27 20:46:19 +02:00
parent 62b68a64ac
commit 3baac85cf7
No known key found for this signature in database
23 changed files with 2634 additions and 596 deletions

View file

@ -1,96 +0,0 @@
import type { Relationship as ApiRelationship } from "@lysand-org/client/types";
import type { InferSelectModel } from "drizzle-orm";
import { db } from "~/drizzle/db";
import { Relationships } from "~/drizzle/schema";
import type { User } from "~/packages/database-interface/user";
export type Relationship = InferSelectModel<typeof Relationships> & {
requestedBy: boolean;
};
/**
* Creates a new relationship between two users.
* @param owner The user who owns the relationship.
* @param other The user who is the subject of the relationship.
* @returns The newly created relationship.
*/
export const createNewRelationship = async (
owner: User,
other: User,
): Promise<Relationship> => {
return {
...(
await db
.insert(Relationships)
.values({
ownerId: owner.id,
subjectId: other.id,
languages: [],
following: false,
showingReblogs: false,
notifying: false,
followedBy: false,
blocking: false,
blockedBy: false,
muting: false,
mutingNotifications: false,
requested: false,
domainBlocking: false,
endorsed: false,
note: "",
updatedAt: new Date().toISOString(),
})
.returning()
)[0],
requestedBy: false,
};
};
export const checkForBidirectionalRelationships = async (
user1: User,
user2: User,
createIfNotExists = true,
): Promise<boolean> => {
const relationship1 = await db.query.Relationships.findFirst({
where: (rel, { and, eq }) =>
and(eq(rel.ownerId, user1.id), eq(rel.subjectId, user2.id)),
});
const relationship2 = await db.query.Relationships.findFirst({
where: (rel, { and, eq }) =>
and(eq(rel.ownerId, user2.id), eq(rel.subjectId, user1.id)),
});
if (!relationship1 && createIfNotExists) {
await createNewRelationship(user1, user2);
}
if (!relationship2 && createIfNotExists) {
await createNewRelationship(user2, user1);
}
return !!relationship1 && !!relationship2;
};
/**
* Converts the relationship to an API-friendly format.
* @returns The API-friendly relationship.
*/
export const relationshipToApi = (rel: Relationship): ApiRelationship => {
return {
blocked_by: rel.blockedBy,
blocking: rel.blocking,
domain_blocking: rel.domainBlocking,
endorsed: rel.endorsed,
followed_by: rel.followedBy,
following: rel.following,
id: rel.subjectId,
muting: rel.muting,
muting_notifications: rel.mutingNotifications,
notifying: rel.notifying,
requested: rel.requested,
requested_by: rel.requestedBy,
showing_reblogs: rel.showingReblogs,
note: rel.note,
};
};

View file

@ -4,13 +4,11 @@ import type {
FollowReject, FollowReject,
} from "@lysand-org/federation/types"; } from "@lysand-org/federation/types";
import { config } from "config-manager"; import { config } from "config-manager";
import { type InferSelectModel, and, eq, sql } from "drizzle-orm"; import { type InferSelectModel, eq, sql } from "drizzle-orm";
import { db } from "~/drizzle/db"; import { db } from "~/drizzle/db";
import { import {
Applications, Applications,
type Instances, type Instances,
Notifications,
Relationships,
type Roles, type Roles,
Tokens, Tokens,
type Users, type Users,
@ -18,11 +16,6 @@ import {
import type { EmojiWithInstance } from "~/packages/database-interface/emoji"; import type { EmojiWithInstance } from "~/packages/database-interface/emoji";
import { User } from "~/packages/database-interface/user"; import { User } from "~/packages/database-interface/user";
import type { Application } from "./application"; import type { Application } from "./application";
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>;
@ -119,85 +112,6 @@ export const getFromHeader = async (value: string): Promise<AuthData> => {
return { user, token, application }; return { user, token, application };
}; };
export const followRequestUser = async (
follower: User,
followee: User,
relationshipId: string,
reblogs = false,
notify = false,
languages: string[] = [],
): Promise<Relationship> => {
const isRemote = followee.isRemote();
await db
.update(Relationships)
.set({
following: isRemote ? false : !followee.data.isLocked,
requested: isRemote ? true : followee.data.isLocked,
showingReblogs: reblogs,
notifying: notify,
languages: languages,
})
.where(eq(Relationships.id, relationshipId));
// Set requested_by on other side
await db
.update(Relationships)
.set({
requestedBy: isRemote ? true : followee.data.isLocked,
followedBy: isRemote ? false : followee.data.isLocked,
})
.where(
and(
eq(Relationships.ownerId, followee.id),
eq(Relationships.subjectId, follower.id),
),
);
const updatedRelationship = await db.query.Relationships.findFirst({
where: (rel, { eq }) => eq(rel.id, relationshipId),
});
if (!updatedRelationship) {
throw new Error("Failed to update relationship");
}
if (isRemote) {
const { ok } = await follower.federateToUser(
followRequestToLysand(follower, followee),
followee,
);
if (!ok) {
await db
.update(Relationships)
.set({
following: false,
requested: false,
})
.where(eq(Relationships.id, relationshipId));
const result = await db.query.Relationships.findFirst({
where: (rel, { eq }) => eq(rel.id, relationshipId),
});
if (!result) {
throw new Error("Failed to update relationship");
}
return result;
}
} else {
await db.insert(Notifications).values({
accountId: follower.id,
type: followee.data.isLocked ? "follow_request" : "follow",
notifiedId: followee.id,
});
}
return updatedRelationship;
};
export const sendFollowAccept = async (follower: User, followee: User) => { export const sendFollowAccept = async (follower: User, followee: User) => {
await follower.federateToUser( await follower.federateToUser(
followAcceptToLysand(follower, followee), followAcceptToLysand(follower, followee),
@ -344,36 +258,6 @@ export const retrieveToken = async (
); );
}; };
/**
* Gets the relationship to another user.
* @param other The other user to get the relationship to.
* @returns The relationship to the other user.
*/
export const getRelationshipToOtherUser = async (
user: User,
other: User,
): Promise<Relationship> => {
await checkForBidirectionalRelationships(user, other);
const foundRelationship = await db.query.Relationships.findFirst({
where: (relationship, { and, eq }) =>
and(
eq(relationship.ownerId, user.id),
eq(relationship.subjectId, other.id),
),
});
if (!foundRelationship) {
// Create new relationship
const newRelationship = await createNewRelationship(user, other);
return newRelationship;
}
return foundRelationship;
};
export const followRequestToLysand = ( export const followRequestToLysand = (
follower: User, follower: User,
followee: User, followee: User,

View file

@ -0,0 +1,3 @@
ALTER TABLE "Relationships" DROP COLUMN IF EXISTS "followed_by";--> statement-breakpoint
ALTER TABLE "Relationships" DROP COLUMN IF EXISTS "blocked_by";--> statement-breakpoint
ALTER TABLE "Relationships" DROP COLUMN IF EXISTS "requested_by";

File diff suppressed because it is too large Load diff

View file

@ -218,6 +218,13 @@
"when": 1721223331975, "when": 1721223331975,
"tag": "0030_curvy_vulture", "tag": "0030_curvy_vulture",
"breakpoints": true "breakpoints": true
},
{
"idx": 31,
"version": "7",
"when": 1722100203904,
"tag": "0031_mature_demogoblin",
"breakpoints": true
} }
] ]
} }

View file

@ -181,13 +181,10 @@ export const Relationships = pgTable("Relationships", {
following: boolean("following").notNull(), following: boolean("following").notNull(),
showingReblogs: boolean("showing_reblogs").notNull(), showingReblogs: boolean("showing_reblogs").notNull(),
notifying: boolean("notifying").notNull(), notifying: boolean("notifying").notNull(),
followedBy: boolean("followed_by").notNull(),
blocking: boolean("blocking").notNull(), blocking: boolean("blocking").notNull(),
blockedBy: boolean("blocked_by").notNull(),
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(),

View file

@ -0,0 +1,295 @@
import type { Relationship as APIRelationship } from "@lysand-org/client/types";
import {
type InferInsertModel,
type InferSelectModel,
type SQL,
and,
desc,
eq,
inArray,
} from "drizzle-orm";
import { db } from "~/drizzle/db";
import { Relationships } from "~/drizzle/schema";
import { BaseInterface } from "./base";
import type { User } from "./user";
export type RelationshipType = InferSelectModel<typeof Relationships>;
export type RelationshipWithOpposite = RelationshipType & {
followedBy: boolean;
blockedBy: boolean;
requestedBy: boolean;
};
export class Relationship extends BaseInterface<
typeof Relationships,
RelationshipWithOpposite
> {
async reload(): Promise<void> {
const reloaded = await Relationship.fromId(this.data.id);
if (!reloaded) {
throw new Error("Failed to reload relationship");
}
this.data = reloaded.data;
}
public static async fromId(
id: string | null,
): Promise<Relationship | null> {
if (!id) {
return null;
}
return await Relationship.fromSql(eq(Relationships.id, id));
}
public static async fromIds(ids: string[]): Promise<Relationship[]> {
return await Relationship.manyFromSql(inArray(Relationships.id, ids));
}
public static async fromOwnerAndSubject(
owner: User,
subject: User,
): Promise<Relationship> {
const found = await Relationship.fromSql(
and(
eq(Relationships.ownerId, owner.id),
eq(Relationships.subjectId, subject.id),
),
);
if (!found) {
// Create a new relationship if one doesn't exist
return await Relationship.insert({
ownerId: owner.id,
subjectId: subject.id,
languages: [],
following: false,
showingReblogs: false,
notifying: false,
blocking: false,
muting: false,
mutingNotifications: false,
requested: false,
domainBlocking: false,
endorsed: false,
note: "",
});
}
return found;
}
public static async fromOwnerAndSubjects(
owner: User,
subjectIds: string[],
): Promise<Relationship[]> {
const found = await Relationship.manyFromSql(
and(
eq(Relationships.ownerId, owner.id),
inArray(Relationships.subjectId, subjectIds),
),
);
const missingSubjectsIds = subjectIds.filter(
(id) => !found.find((rel) => rel.data.subjectId === id),
);
for (const subjectId of missingSubjectsIds) {
await Relationship.insert({
ownerId: owner.id,
subjectId: subjectId,
languages: [],
following: false,
showingReblogs: false,
notifying: false,
blocking: false,
muting: false,
mutingNotifications: false,
requested: false,
domainBlocking: false,
endorsed: false,
note: "",
});
}
return await Relationship.manyFromSql(
and(
eq(Relationships.ownerId, owner.id),
inArray(Relationships.subjectId, subjectIds),
),
);
}
public static async fromSql(
sql: SQL<unknown> | undefined,
orderBy: SQL<unknown> | undefined = desc(Relationships.id),
): Promise<Relationship | null> {
const found = await db.query.Relationships.findFirst({
where: sql,
orderBy,
});
if (!found) {
return null;
}
const opposite = await Relationship.getOpposite(found);
return new Relationship({
...found,
followedBy: opposite.following,
blockedBy: opposite.blocking,
requestedBy: opposite.requested,
});
}
public static async manyFromSql(
sql: SQL<unknown> | undefined,
orderBy: SQL<unknown> | undefined = desc(Relationships.id),
limit?: number,
offset?: number,
extra?: Parameters<typeof db.query.Relationships.findMany>[0],
): Promise<Relationship[]> {
const found = await db.query.Relationships.findMany({
where: sql,
orderBy,
limit,
offset,
with: extra?.with,
});
const opposites = await Promise.all(
found.map((rel) => Relationship.getOpposite(rel)),
);
return found.map((s, i) => {
return new Relationship({
...s,
followedBy: opposites[i].following,
blockedBy: opposites[i].blocking,
requestedBy: opposites[i].requested,
});
});
}
public static async getOpposite(oppositeTo: {
subjectId: string;
ownerId: string;
}): Promise<RelationshipType> {
let output = await db.query.Relationships.findFirst({
where: (rel, { and, eq }) =>
and(
eq(rel.ownerId, oppositeTo.subjectId),
eq(rel.subjectId, oppositeTo.ownerId),
),
});
// If the opposite relationship doesn't exist, create it
if (!output) {
output = (
await db
.insert(Relationships)
.values({
ownerId: oppositeTo.subjectId,
subjectId: oppositeTo.ownerId,
languages: [],
following: false,
showingReblogs: false,
notifying: false,
blocking: false,
domainBlocking: false,
endorsed: false,
note: "",
muting: false,
mutingNotifications: false,
requested: false,
})
.returning()
)[0];
}
return output;
}
async update(
newRelationship: Partial<RelationshipType>,
): Promise<RelationshipWithOpposite> {
await db
.update(Relationships)
.set(newRelationship)
.where(eq(Relationships.id, this.id));
const updated = await Relationship.fromId(this.data.id);
if (!updated) {
throw new Error("Failed to update relationship");
}
this.data = updated.data;
return updated.data;
}
save(): Promise<RelationshipWithOpposite> {
return this.update(this.data);
}
async delete(ids: string[]): Promise<void>;
async delete(): Promise<void>;
async delete(ids?: unknown): Promise<void> {
if (Array.isArray(ids)) {
await db
.delete(Relationships)
.where(inArray(Relationships.id, ids));
} else {
await db.delete(Relationships).where(eq(Relationships.id, this.id));
}
}
public static async insert(
data: InferInsertModel<typeof Relationships>,
): Promise<Relationship> {
const inserted = (
await db.insert(Relationships).values(data).returning()
)[0];
const relationship = await Relationship.fromId(inserted.id);
if (!relationship) {
throw new Error("Failed to insert relationship");
}
// Create opposite relationship if necessary
await Relationship.getOpposite({
subjectId: relationship.data.subjectId,
ownerId: relationship.data.ownerId,
});
return relationship;
}
get id() {
return this.data.id;
}
public toApi(): APIRelationship {
return {
id: this.data.subjectId,
blocked_by: this.data.blockedBy,
blocking: this.data.blocking,
domain_blocking: this.data.domainBlocking,
endorsed: this.data.endorsed,
followed_by: this.data.followedBy,
following: this.data.following,
muting_notifications: this.data.mutingNotifications,
muting: this.data.muting,
note: this.data.note,
notifying: this.data.notifying,
requested_by: this.data.requestedBy,
requested: this.data.requested,
showing_reblogs: this.data.showingReblogs,
};
}
}

View file

@ -34,6 +34,7 @@ import { htmlToText } from "html-to-text";
import { import {
type UserWithRelations, type UserWithRelations,
findManyUsers, findManyUsers,
followRequestToLysand,
} from "~/classes/functions/user"; } from "~/classes/functions/user";
import { searchManager } from "~/classes/search/search-manager"; import { searchManager } from "~/classes/search/search-manager";
import { db } from "~/drizzle/db"; import { db } from "~/drizzle/db";
@ -41,6 +42,7 @@ import {
EmojiToUser, EmojiToUser,
NoteToMentions, NoteToMentions,
Notes, Notes,
Notifications,
type RolePermissions, type RolePermissions,
UserToPinnedNotes, UserToPinnedNotes,
Users, Users,
@ -50,6 +52,7 @@ import { BaseInterface } from "./base";
import { Emoji } from "./emoji"; import { Emoji } from "./emoji";
import { Instance } from "./instance"; import { Instance } from "./instance";
import type { Note } from "./note"; import type { Note } from "./note";
import { Relationship } from "./relationship";
import { Role } from "./role"; import { Role } from "./role";
/** /**
@ -205,6 +208,52 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
); );
} }
public async followRequest(
otherUser: User,
options?: {
reblogs?: boolean;
notify?: boolean;
languages?: string[];
},
): Promise<Relationship> {
const foundRelationship = await Relationship.fromOwnerAndSubject(
this,
otherUser,
);
await foundRelationship.update({
following: otherUser.isRemote() ? false : !otherUser.data.isLocked,
requested: otherUser.isRemote() ? true : otherUser.data.isLocked,
showingReblogs: options?.reblogs,
notifying: options?.notify,
languages: options?.languages,
});
if (otherUser.isRemote()) {
const { ok } = await this.federateToUser(
followRequestToLysand(this, otherUser),
otherUser,
);
if (!ok) {
await foundRelationship.update({
requested: false,
following: false,
});
return foundRelationship;
}
} else {
await db.insert(Notifications).values({
accountId: this.id,
type: otherUser.data.isLocked ? "follow_request" : "follow",
notifiedId: otherUser.id,
});
}
return foundRelationship;
}
static async webFinger( static async webFinger(
manager: FederationRequester, manager: FederationRequester,
username: string, username: string,

View file

@ -2,12 +2,9 @@ import { applyConfig, auth, handleZodError } from "@/api";
import { errorResponse, jsonResponse } from "@/response"; import { errorResponse, jsonResponse } from "@/response";
import type { Hono } from "@hono/hono"; import type { Hono } from "@hono/hono";
import { zValidator } from "@hono/zod-validator"; import { zValidator } from "@hono/zod-validator";
import { eq } from "drizzle-orm";
import { z } from "zod"; import { z } from "zod";
import { relationshipToApi } from "~/classes/functions/relationship"; import { RolePermissions } from "~/drizzle/schema";
import { getRelationshipToOtherUser } from "~/classes/functions/user"; import { Relationship } from "~/packages/database-interface/relationship";
import { db } from "~/drizzle/db";
import { Relationships, 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,22 +52,17 @@ export default (app: Hono) =>
return errorResponse("User not found", 404); return errorResponse("User not found", 404);
} }
const foundRelationship = await getRelationshipToOtherUser( const foundRelationship = await Relationship.fromOwnerAndSubject(
user, user,
otherUser, otherUser,
); );
if (!foundRelationship.blocking) { if (!foundRelationship.data.blocking) {
foundRelationship.blocking = true; await foundRelationship.update({
blocking: true,
});
} }
await db return jsonResponse(foundRelationship.toApi());
.update(Relationships)
.set({
blocking: true,
})
.where(eq(Relationships.id, foundRelationship.id));
return jsonResponse(relationshipToApi(foundRelationship));
}, },
); );

View file

@ -4,12 +4,8 @@ import type { Hono } from "@hono/hono";
import { zValidator } from "@hono/zod-validator"; import { zValidator } from "@hono/zod-validator";
import ISO6391 from "iso-639-1"; import ISO6391 from "iso-639-1";
import { z } from "zod"; import { z } from "zod";
import { relationshipToApi } from "~/classes/functions/relationship";
import {
followRequestUser,
getRelationshipToOtherUser,
} from "~/classes/functions/user";
import { RolePermissions } from "~/drizzle/schema"; import { RolePermissions } from "~/drizzle/schema";
import { Relationship } from "~/packages/database-interface/relationship";
import { User } from "~/packages/database-interface/user"; import { User } from "~/packages/database-interface/user";
export const meta = applyConfig({ export const meta = applyConfig({
@ -69,22 +65,19 @@ export default (app: Hono) =>
return errorResponse("User not found", 404); return errorResponse("User not found", 404);
} }
let relationship = await getRelationshipToOtherUser( let relationship = await Relationship.fromOwnerAndSubject(
user, user,
otherUser, otherUser,
); );
if (!relationship.following) { if (!relationship.data.following) {
relationship = await followRequestUser( relationship = await user.followRequest(otherUser, {
user,
otherUser,
relationship.id,
reblogs, reblogs,
notify, notify,
languages, languages,
); });
} }
return jsonResponse(relationshipToApi(relationship)); return jsonResponse(relationship.toApi());
}, },
); );

View file

@ -2,12 +2,9 @@ import { applyConfig, auth, handleZodError } from "@/api";
import { errorResponse, jsonResponse } from "@/response"; import { errorResponse, jsonResponse } from "@/response";
import type { Hono } from "@hono/hono"; import type { Hono } from "@hono/hono";
import { zValidator } from "@hono/zod-validator"; import { zValidator } from "@hono/zod-validator";
import { eq } from "drizzle-orm";
import { z } from "zod"; import { z } from "zod";
import { relationshipToApi } from "~/classes/functions/relationship"; import { RolePermissions } from "~/drizzle/schema";
import { getRelationshipToOtherUser } from "~/classes/functions/user"; import { Relationship } from "~/packages/database-interface/relationship";
import { db } from "~/drizzle/db";
import { Relationships, 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({
@ -67,28 +64,17 @@ export default (app: Hono) =>
return errorResponse("User not found", 404); return errorResponse("User not found", 404);
} }
const foundRelationship = await getRelationshipToOtherUser( const foundRelationship = await Relationship.fromOwnerAndSubject(
user, user,
otherUser, otherUser,
); );
if (!foundRelationship.muting) { // TODO: Implement duration
foundRelationship.muting = true; await foundRelationship.update({
}
if (notifications ?? true) {
foundRelationship.mutingNotifications = true;
}
await db
.update(Relationships)
.set({
muting: true, muting: true,
mutingNotifications: notifications ?? true, mutingNotifications: notifications ?? true,
}) });
.where(eq(Relationships.id, foundRelationship.id));
// TODO: Implement duration return jsonResponse(foundRelationship.toApi());
return jsonResponse(relationshipToApi(foundRelationship));
}, },
); );

View file

@ -2,12 +2,9 @@ import { applyConfig, auth, handleZodError } from "@/api";
import { errorResponse, jsonResponse } from "@/response"; import { errorResponse, jsonResponse } from "@/response";
import type { Hono } from "@hono/hono"; import type { Hono } from "@hono/hono";
import { zValidator } from "@hono/zod-validator"; import { zValidator } from "@hono/zod-validator";
import { eq } from "drizzle-orm";
import { z } from "zod"; import { z } from "zod";
import { relationshipToApi } from "~/classes/functions/relationship"; import { RolePermissions } from "~/drizzle/schema";
import { getRelationshipToOtherUser } from "~/classes/functions/user"; import { Relationship } from "~/packages/database-interface/relationship";
import { db } from "~/drizzle/db";
import { Relationships, 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({
@ -60,20 +57,15 @@ export default (app: Hono) =>
return errorResponse("User not found", 404); return errorResponse("User not found", 404);
} }
const foundRelationship = await getRelationshipToOtherUser( const foundRelationship = await Relationship.fromOwnerAndSubject(
user, user,
otherUser, otherUser,
); );
foundRelationship.note = comment ?? ""; await foundRelationship.update({
note: comment,
});
await db return jsonResponse(foundRelationship.toApi());
.update(Relationships)
.set({
note: foundRelationship.note,
})
.where(eq(Relationships.id, foundRelationship.id));
return jsonResponse(relationshipToApi(foundRelationship));
}, },
); );

View file

@ -2,12 +2,9 @@ import { applyConfig, auth, handleZodError } from "@/api";
import { errorResponse, jsonResponse } from "@/response"; import { errorResponse, jsonResponse } from "@/response";
import type { Hono } from "@hono/hono"; import type { Hono } from "@hono/hono";
import { zValidator } from "@hono/zod-validator"; import { zValidator } from "@hono/zod-validator";
import { eq } from "drizzle-orm";
import { z } from "zod"; import { z } from "zod";
import { relationshipToApi } from "~/classes/functions/relationship"; import { RolePermissions } from "~/drizzle/schema";
import { getRelationshipToOtherUser } from "~/classes/functions/user"; import { Relationship } from "~/packages/database-interface/relationship";
import { db } from "~/drizzle/db";
import { Relationships, 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,22 +52,15 @@ export default (app: Hono) =>
return errorResponse("User not found", 404); return errorResponse("User not found", 404);
} }
const foundRelationship = await getRelationshipToOtherUser( const foundRelationship = await Relationship.fromOwnerAndSubject(
user, user,
otherUser, otherUser,
); );
if (!foundRelationship.endorsed) { await foundRelationship.update({
foundRelationship.endorsed = true;
}
await db
.update(Relationships)
.set({
endorsed: true, endorsed: true,
}) });
.where(eq(Relationships.id, foundRelationship.id));
return jsonResponse(relationshipToApi(foundRelationship)); return jsonResponse(foundRelationship.toApi());
}, },
); );

View file

@ -2,12 +2,9 @@ import { applyConfig, auth, handleZodError } from "@/api";
import { errorResponse, jsonResponse } from "@/response"; import { errorResponse, jsonResponse } from "@/response";
import type { Hono } from "@hono/hono"; import type { Hono } from "@hono/hono";
import { zValidator } from "@hono/zod-validator"; import { zValidator } from "@hono/zod-validator";
import { and, eq } from "drizzle-orm";
import { z } from "zod"; import { z } from "zod";
import { relationshipToApi } from "~/classes/functions/relationship"; import { RolePermissions } from "~/drizzle/schema";
import { getRelationshipToOtherUser } from "~/classes/functions/user"; import { Relationship } from "~/packages/database-interface/relationship";
import { db } from "~/drizzle/db";
import { Relationships, 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,36 +52,22 @@ export default (app: Hono) =>
return errorResponse("User not found", 404); return errorResponse("User not found", 404);
} }
const foundRelationship = await getRelationshipToOtherUser( const oppositeRelationship = await Relationship.fromOwnerAndSubject(
otherUser,
self,
);
if (oppositeRelationship.data.following) {
await oppositeRelationship.update({
following: false,
});
}
const foundRelationship = await Relationship.fromOwnerAndSubject(
self, self,
otherUser, otherUser,
); );
if (foundRelationship.followedBy) { return jsonResponse(foundRelationship.toApi());
foundRelationship.followedBy = false;
await db
.update(Relationships)
.set({
followedBy: false,
})
.where(eq(Relationships.id, foundRelationship.id));
if (otherUser.isLocal()) {
await db
.update(Relationships)
.set({
following: false,
})
.where(
and(
eq(Relationships.ownerId, otherUser.id),
eq(Relationships.subjectId, self.id),
),
);
}
}
return jsonResponse(relationshipToApi(foundRelationship));
}, },
); );

View file

@ -2,12 +2,9 @@ import { applyConfig, auth, handleZodError } from "@/api";
import { errorResponse, jsonResponse } from "@/response"; import { errorResponse, jsonResponse } from "@/response";
import type { Hono } from "@hono/hono"; import type { Hono } from "@hono/hono";
import { zValidator } from "@hono/zod-validator"; import { zValidator } from "@hono/zod-validator";
import { eq } from "drizzle-orm";
import { z } from "zod"; import { z } from "zod";
import { relationshipToApi } from "~/classes/functions/relationship"; import { RolePermissions } from "~/drizzle/schema";
import { getRelationshipToOtherUser } from "~/classes/functions/user"; import { Relationship } from "~/packages/database-interface/relationship";
import { db } from "~/drizzle/db";
import { Relationships, 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,22 +52,17 @@ export default (app: Hono) =>
return errorResponse("User not found", 404); return errorResponse("User not found", 404);
} }
const foundRelationship = await getRelationshipToOtherUser( const foundRelationship = await Relationship.fromOwnerAndSubject(
user, user,
otherUser, otherUser,
); );
if (foundRelationship.blocking) { if (foundRelationship.data.blocking) {
foundRelationship.blocking = false; await foundRelationship.update({
await db
.update(Relationships)
.set({
blocking: false, blocking: false,
}) });
.where(eq(Relationships.id, foundRelationship.id));
} }
return jsonResponse(relationshipToApi(foundRelationship)); return jsonResponse(foundRelationship.toApi());
}, },
); );

View file

@ -2,12 +2,9 @@ import { applyConfig, auth, handleZodError } from "@/api";
import { errorResponse, jsonResponse } from "@/response"; import { errorResponse, jsonResponse } from "@/response";
import type { Hono } from "@hono/hono"; import type { Hono } from "@hono/hono";
import { zValidator } from "@hono/zod-validator"; import { zValidator } from "@hono/zod-validator";
import { eq } from "drizzle-orm";
import { z } from "zod"; import { z } from "zod";
import { relationshipToApi } from "~/classes/functions/relationship"; import { RolePermissions } from "~/drizzle/schema";
import { getRelationshipToOtherUser } from "~/classes/functions/user"; import { Relationship } from "~/packages/database-interface/relationship";
import { db } from "~/drizzle/db";
import { Relationships, 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,23 +52,17 @@ export default (app: Hono) =>
return errorResponse("User not found", 404); return errorResponse("User not found", 404);
} }
const foundRelationship = await getRelationshipToOtherUser( const foundRelationship = await Relationship.fromOwnerAndSubject(
self, self,
otherUser, otherUser,
); );
if (foundRelationship.following) { if (foundRelationship.data.following) {
foundRelationship.following = false; await foundRelationship.update({
await db
.update(Relationships)
.set({
following: false, following: false,
requested: false, });
})
.where(eq(Relationships.id, foundRelationship.id));
} }
return jsonResponse(relationshipToApi(foundRelationship)); return jsonResponse(foundRelationship.toApi());
}, },
); );

View file

@ -2,12 +2,9 @@ import { applyConfig, auth, handleZodError } from "@/api";
import { errorResponse, jsonResponse } from "@/response"; import { errorResponse, jsonResponse } from "@/response";
import type { Hono } from "@hono/hono"; import type { Hono } from "@hono/hono";
import { zValidator } from "@hono/zod-validator"; import { zValidator } from "@hono/zod-validator";
import { eq } from "drizzle-orm";
import { z } from "zod"; import { z } from "zod";
import { relationshipToApi } from "~/classes/functions/relationship"; import { RolePermissions } from "~/drizzle/schema";
import { getRelationshipToOtherUser } from "~/classes/functions/user"; import { Relationship } from "~/packages/database-interface/relationship";
import { db } from "~/drizzle/db";
import { Relationships, 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,24 +52,18 @@ export default (app: Hono) =>
return errorResponse("User not found", 404); return errorResponse("User not found", 404);
} }
const foundRelationship = await getRelationshipToOtherUser( const foundRelationship = await Relationship.fromOwnerAndSubject(
self, self,
user, user,
); );
if (foundRelationship.muting) { if (foundRelationship.data.muting) {
foundRelationship.muting = false; await foundRelationship.update({
foundRelationship.mutingNotifications = false;
await db
.update(Relationships)
.set({
muting: false, muting: false,
mutingNotifications: false, mutingNotifications: false,
}) });
.where(eq(Relationships.id, foundRelationship.id));
} }
return jsonResponse(relationshipToApi(foundRelationship)); return jsonResponse(foundRelationship.toApi());
}, },
); );

View file

@ -2,12 +2,9 @@ import { applyConfig, auth, handleZodError } from "@/api";
import { errorResponse, jsonResponse } from "@/response"; import { errorResponse, jsonResponse } from "@/response";
import type { Hono } from "@hono/hono"; import type { Hono } from "@hono/hono";
import { zValidator } from "@hono/zod-validator"; import { zValidator } from "@hono/zod-validator";
import { eq } from "drizzle-orm";
import { z } from "zod"; import { z } from "zod";
import { relationshipToApi } from "~/classes/functions/relationship"; import { RolePermissions } from "~/drizzle/schema";
import { getRelationshipToOtherUser } from "~/classes/functions/user"; import { Relationship } from "~/packages/database-interface/relationship";
import { db } from "~/drizzle/db";
import { Relationships, 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,22 +52,17 @@ export default (app: Hono) =>
return errorResponse("User not found", 404); return errorResponse("User not found", 404);
} }
const foundRelationship = await getRelationshipToOtherUser( const foundRelationship = await Relationship.fromOwnerAndSubject(
self, self,
otherUser, otherUser,
); );
if (foundRelationship.endorsed) { if (foundRelationship.data.endorsed) {
foundRelationship.endorsed = false; await foundRelationship.update({
await db
.update(Relationships)
.set({
endorsed: false, endorsed: false,
}) });
.where(eq(Relationships.id, foundRelationship.id));
} }
return jsonResponse(relationshipToApi(foundRelationship)); return jsonResponse(foundRelationship.toApi());
}, },
); );

View file

@ -132,7 +132,7 @@ describe(meta.route, () => {
expect.arrayContaining([ expect.arrayContaining([
expect.objectContaining({ expect.objectContaining({
following: false, following: false,
followed_by: true, followed_by: false,
blocking: false, blocking: false,
muting: false, muting: false,
muting_notifications: false, muting_notifications: false,

View file

@ -3,13 +3,8 @@ import { errorResponse, jsonResponse } from "@/response";
import type { Hono } from "@hono/hono"; import type { Hono } from "@hono/hono";
import { zValidator } from "@hono/zod-validator"; import { zValidator } from "@hono/zod-validator";
import { z } from "zod"; import { z } from "zod";
import {
createNewRelationship,
relationshipToApi,
} from "~/classes/functions/relationship";
import { db } from "~/drizzle/db";
import { RolePermissions } from "~/drizzle/schema"; import { RolePermissions } from "~/drizzle/schema";
import { User } from "~/packages/database-interface/user"; import { Relationship } from "~/packages/database-interface/relationship";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["GET"], allowedMethods: ["GET"],
@ -50,32 +45,17 @@ export default (app: Hono) =>
return errorResponse("Unauthorized", 401); return errorResponse("Unauthorized", 401);
} }
const relationships = await db.query.Relationships.findMany({ const relationships = await Relationship.fromOwnerAndSubjects(
where: (relationship, { inArray, and, eq }) => self,
and( ids,
inArray(relationship.subjectId, ids),
eq(relationship.ownerId, self.id),
),
});
const missingIds = ids.filter(
(id) => !relationships.some((r) => r.subjectId === id),
); );
for (const id of missingIds) {
const user = await User.fromId(id);
if (!user) {
continue;
}
const relationship = await createNewRelationship(self, user);
relationships.push(relationship);
}
relationships.sort( relationships.sort(
(a, b) => ids.indexOf(a.subjectId) - ids.indexOf(b.subjectId), (a, b) =>
ids.indexOf(a.data.subjectId) -
ids.indexOf(b.data.subjectId),
); );
return jsonResponse(relationships.map((r) => relationshipToApi(r))); return jsonResponse(relationships.map((r) => r.toApi()));
}, },
); );

View file

@ -2,18 +2,10 @@ import { applyConfig, auth, handleZodError } from "@/api";
import { errorResponse, jsonResponse } from "@/response"; import { errorResponse, jsonResponse } from "@/response";
import type { Hono } from "@hono/hono"; import type { Hono } from "@hono/hono";
import { zValidator } from "@hono/zod-validator"; import { zValidator } from "@hono/zod-validator";
import { and, eq } from "drizzle-orm";
import { z } from "zod"; import { z } from "zod";
import { import { sendFollowAccept } from "~/classes/functions/user";
checkForBidirectionalRelationships, import { RolePermissions } from "~/drizzle/schema";
relationshipToApi, import { Relationship } from "~/packages/database-interface/relationship";
} from "~/classes/functions/relationship";
import {
getRelationshipToOtherUser,
sendFollowAccept,
} from "~/classes/functions/user";
import { db } from "~/drizzle/db";
import { Relationships, 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({
@ -58,52 +50,27 @@ export default (app: Hono) =>
return errorResponse("Account not found", 404); return errorResponse("Account not found", 404);
} }
// Check if there is a relationship on both sides const oppositeRelationship = await Relationship.fromOwnerAndSubject(
await checkForBidirectionalRelationships(user, account); account,
user,
);
// Authorize follow request await oppositeRelationship.update({
await db
.update(Relationships)
.set({
requested: false, requested: false,
following: true, following: true,
}) });
.where(
and(
eq(Relationships.subjectId, user.id),
eq(Relationships.ownerId, account.id),
),
);
// Update followedBy for other user const foundRelationship = await Relationship.fromOwnerAndSubject(
await db
.update(Relationships)
.set({
followedBy: true,
requestedBy: false,
})
.where(
and(
eq(Relationships.subjectId, account.id),
eq(Relationships.ownerId, user.id),
),
);
const foundRelationship = await getRelationshipToOtherUser(
user, user,
account, account,
); );
if (!foundRelationship) {
return errorResponse("Relationship not found", 404);
}
// Check if accepting remote follow // Check if accepting remote follow
if (account.isRemote()) { if (account.isRemote()) {
// Federate follow accept // Federate follow accept
await sendFollowAccept(account, user); await sendFollowAccept(account, user);
} }
return jsonResponse(relationshipToApi(foundRelationship)); return jsonResponse(foundRelationship.toApi());
}, },
); );

View file

@ -2,18 +2,10 @@ import { applyConfig, auth, handleZodError } from "@/api";
import { errorResponse, jsonResponse } from "@/response"; import { errorResponse, jsonResponse } from "@/response";
import type { Hono } from "@hono/hono"; import type { Hono } from "@hono/hono";
import { zValidator } from "@hono/zod-validator"; import { zValidator } from "@hono/zod-validator";
import { and, eq } from "drizzle-orm";
import { z } from "zod"; import { z } from "zod";
import { import { sendFollowReject } from "~/classes/functions/user";
checkForBidirectionalRelationships, import { RolePermissions } from "~/drizzle/schema";
relationshipToApi, import { Relationship } from "~/packages/database-interface/relationship";
} from "~/classes/functions/relationship";
import {
getRelationshipToOtherUser,
sendFollowReject,
} from "~/classes/functions/user";
import { db } from "~/drizzle/db";
import { Relationships, 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({
@ -58,52 +50,27 @@ export default (app: Hono) =>
return errorResponse("Account not found", 404); return errorResponse("Account not found", 404);
} }
// Check if there is a relationship on both sides const oppositeRelationship = await Relationship.fromOwnerAndSubject(
await checkForBidirectionalRelationships(user, account); account,
user,
);
// Reject follow request await oppositeRelationship.update({
await db
.update(Relationships)
.set({
requested: false, requested: false,
following: false, following: false,
}) });
.where(
and(
eq(Relationships.subjectId, user.id),
eq(Relationships.ownerId, account.id),
),
);
// Update followedBy for other user const foundRelationship = await Relationship.fromOwnerAndSubject(
await db
.update(Relationships)
.set({
followedBy: false,
requestedBy: false,
})
.where(
and(
eq(Relationships.subjectId, account.id),
eq(Relationships.ownerId, user.id),
),
);
const foundRelationship = await getRelationshipToOtherUser(
user, user,
account, account,
); );
if (!foundRelationship) {
return errorResponse("Relationship not found", 404);
}
// Check if rejecting remote follow // Check if rejecting remote follow
if (account.isRemote()) { if (account.isRemote()) {
// Federate follow reject // Federate follow reject
await sendFollowReject(account, user); await sendFollowReject(account, user);
} }
return jsonResponse(relationshipToApi(foundRelationship)); return jsonResponse(foundRelationship.toApi());
}, },
); );

View file

@ -11,18 +11,16 @@ import {
} from "@lysand-org/federation"; } from "@lysand-org/federation";
import type { Entity } from "@lysand-org/federation/types"; import type { Entity } from "@lysand-org/federation/types";
import type { SocketAddress } from "bun"; import type { SocketAddress } from "bun";
import { and, eq } from "drizzle-orm"; 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 { import { sendFollowAccept } from "~/classes/functions/user";
getRelationshipToOtherUser,
sendFollowAccept,
} from "~/classes/functions/user";
import { db } from "~/drizzle/db"; import { db } from "~/drizzle/db";
import { Notes, Notifications, Relationships } from "~/drizzle/schema"; import { Notes, Notifications } from "~/drizzle/schema";
import { config } from "~/packages/config-manager"; import { config } from "~/packages/config-manager";
import { Note } from "~/packages/database-interface/note"; import { Note } from "~/packages/database-interface/note";
import { Relationship } from "~/packages/database-interface/relationship";
import { User } from "~/packages/database-interface/user"; import { User } from "~/packages/database-interface/user";
export const meta = applyConfig({ export const meta = applyConfig({
@ -228,35 +226,22 @@ export default (app: Hono) =>
} }
const foundRelationship = const foundRelationship =
await getRelationshipToOtherUser(account, user); await Relationship.fromOwnerAndSubject(
account,
user,
);
if (foundRelationship.following) { if (foundRelationship.data.following) {
return response("Already following", 200); return response("Already following", 200);
} }
await db await foundRelationship.update({
.update(Relationships)
.set({
following: !user.data.isLocked, following: !user.data.isLocked,
requested: user.data.isLocked, requested: user.data.isLocked,
showingReblogs: true, showingReblogs: true,
notifying: true, notifying: true,
languages: [], languages: [],
}) });
.where(eq(Relationships.id, foundRelationship.id));
// Update other user's requested_by
await db
.update(Relationships)
.set({
requestedBy: user.data.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,
@ -280,36 +265,22 @@ export default (app: Hono) =>
} }
const foundRelationship = const foundRelationship =
await getRelationshipToOtherUser(user, account); await Relationship.fromOwnerAndSubject(
user,
account,
);
if (!foundRelationship.requested) { if (!foundRelationship.data.requested) {
return response( return response(
"There is no follow request to accept", "There is no follow request to accept",
200, 200,
); );
} }
await db await foundRelationship.update({
.update(Relationships)
.set({
following: true,
requested: false, requested: false,
}) following: true,
.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);
}, },
@ -321,36 +292,22 @@ export default (app: Hono) =>
} }
const foundRelationship = const foundRelationship =
await getRelationshipToOtherUser(user, account); await Relationship.fromOwnerAndSubject(
user,
account,
);
if (!foundRelationship.requested) { if (!foundRelationship.data.requested) {
return response( return response(
"There is no follow request to reject", "There is no follow request to reject",
200, 200,
); );
} }
await db await foundRelationship.update({
.update(Relationships)
.set({
requested: false, requested: false,
following: false, following: false,
}) });
.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);
}, },