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,
} from "@lysand-org/federation/types";
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 {
Applications,
type Instances,
Notifications,
Relationships,
type Roles,
Tokens,
type Users,
@ -18,11 +16,6 @@ import {
import type { EmojiWithInstance } from "~/packages/database-interface/emoji";
import { User } from "~/packages/database-interface/user";
import type { Application } from "./application";
import {
type Relationship,
checkForBidirectionalRelationships,
createNewRelationship,
} from "./relationship";
import type { Token } from "./token";
export type UserType = InferSelectModel<typeof Users>;
@ -119,85 +112,6 @@ export const getFromHeader = async (value: string): Promise<AuthData> => {
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) => {
await follower.federateToUser(
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 = (
follower: 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,
"tag": "0030_curvy_vulture",
"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(),
showingReblogs: boolean("showing_reblogs").notNull(),
notifying: boolean("notifying").notNull(),
followedBy: boolean("followed_by").notNull(),
blocking: boolean("blocking").notNull(),
blockedBy: boolean("blocked_by").notNull(),
muting: boolean("muting").notNull(),
mutingNotifications: boolean("muting_notifications").notNull(),
requested: boolean("requested").notNull(),
requestedBy: boolean("requested_by").notNull().default(false),
domainBlocking: boolean("domain_blocking").notNull(),
endorsed: boolean("endorsed").notNull(),
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 {
type UserWithRelations,
findManyUsers,
followRequestToLysand,
} from "~/classes/functions/user";
import { searchManager } from "~/classes/search/search-manager";
import { db } from "~/drizzle/db";
@ -41,6 +42,7 @@ import {
EmojiToUser,
NoteToMentions,
Notes,
Notifications,
type RolePermissions,
UserToPinnedNotes,
Users,
@ -50,6 +52,7 @@ import { BaseInterface } from "./base";
import { Emoji } from "./emoji";
import { Instance } from "./instance";
import type { Note } from "./note";
import { Relationship } from "./relationship";
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(
manager: FederationRequester,
username: string,

View file

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

View file

@ -4,12 +4,8 @@ import type { Hono } from "@hono/hono";
import { zValidator } from "@hono/zod-validator";
import ISO6391 from "iso-639-1";
import { z } from "zod";
import { relationshipToApi } from "~/classes/functions/relationship";
import {
followRequestUser,
getRelationshipToOtherUser,
} from "~/classes/functions/user";
import { RolePermissions } from "~/drizzle/schema";
import { Relationship } from "~/packages/database-interface/relationship";
import { User } from "~/packages/database-interface/user";
export const meta = applyConfig({
@ -69,22 +65,19 @@ export default (app: Hono) =>
return errorResponse("User not found", 404);
}
let relationship = await getRelationshipToOtherUser(
let relationship = await Relationship.fromOwnerAndSubject(
user,
otherUser,
);
if (!relationship.following) {
relationship = await followRequestUser(
user,
otherUser,
relationship.id,
if (!relationship.data.following) {
relationship = await user.followRequest(otherUser, {
reblogs,
notify,
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 type { Hono } from "@hono/hono";
import { zValidator } from "@hono/zod-validator";
import { eq } from "drizzle-orm";
import { z } from "zod";
import { relationshipToApi } from "~/classes/functions/relationship";
import { getRelationshipToOtherUser } from "~/classes/functions/user";
import { db } from "~/drizzle/db";
import { Relationships, RolePermissions } from "~/drizzle/schema";
import { RolePermissions } from "~/drizzle/schema";
import { Relationship } from "~/packages/database-interface/relationship";
import { User } from "~/packages/database-interface/user";
export const meta = applyConfig({
@ -67,28 +64,17 @@ export default (app: Hono) =>
return errorResponse("User not found", 404);
}
const foundRelationship = await getRelationshipToOtherUser(
const foundRelationship = await Relationship.fromOwnerAndSubject(
user,
otherUser,
);
if (!foundRelationship.muting) {
foundRelationship.muting = true;
}
if (notifications ?? true) {
foundRelationship.mutingNotifications = true;
}
await db
.update(Relationships)
.set({
muting: true,
mutingNotifications: notifications ?? true,
})
.where(eq(Relationships.id, foundRelationship.id));
// TODO: Implement duration
await foundRelationship.update({
muting: true,
mutingNotifications: notifications ?? true,
});
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 type { Hono } from "@hono/hono";
import { zValidator } from "@hono/zod-validator";
import { eq } from "drizzle-orm";
import { z } from "zod";
import { relationshipToApi } from "~/classes/functions/relationship";
import { getRelationshipToOtherUser } from "~/classes/functions/user";
import { db } from "~/drizzle/db";
import { Relationships, RolePermissions } from "~/drizzle/schema";
import { RolePermissions } from "~/drizzle/schema";
import { Relationship } from "~/packages/database-interface/relationship";
import { User } from "~/packages/database-interface/user";
export const meta = applyConfig({
@ -60,20 +57,15 @@ export default (app: Hono) =>
return errorResponse("User not found", 404);
}
const foundRelationship = await getRelationshipToOtherUser(
const foundRelationship = await Relationship.fromOwnerAndSubject(
user,
otherUser,
);
foundRelationship.note = comment ?? "";
await foundRelationship.update({
note: comment,
});
await db
.update(Relationships)
.set({
note: foundRelationship.note,
})
.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 type { Hono } from "@hono/hono";
import { zValidator } from "@hono/zod-validator";
import { eq } from "drizzle-orm";
import { z } from "zod";
import { relationshipToApi } from "~/classes/functions/relationship";
import { getRelationshipToOtherUser } from "~/classes/functions/user";
import { db } from "~/drizzle/db";
import { Relationships, RolePermissions } from "~/drizzle/schema";
import { RolePermissions } from "~/drizzle/schema";
import { Relationship } from "~/packages/database-interface/relationship";
import { User } from "~/packages/database-interface/user";
export const meta = applyConfig({
@ -55,22 +52,15 @@ export default (app: Hono) =>
return errorResponse("User not found", 404);
}
const foundRelationship = await getRelationshipToOtherUser(
const foundRelationship = await Relationship.fromOwnerAndSubject(
user,
otherUser,
);
if (!foundRelationship.endorsed) {
foundRelationship.endorsed = true;
}
await foundRelationship.update({
endorsed: true,
});
await db
.update(Relationships)
.set({
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 type { Hono } from "@hono/hono";
import { zValidator } from "@hono/zod-validator";
import { and, eq } from "drizzle-orm";
import { z } from "zod";
import { relationshipToApi } from "~/classes/functions/relationship";
import { getRelationshipToOtherUser } from "~/classes/functions/user";
import { db } from "~/drizzle/db";
import { Relationships, RolePermissions } from "~/drizzle/schema";
import { RolePermissions } from "~/drizzle/schema";
import { Relationship } from "~/packages/database-interface/relationship";
import { User } from "~/packages/database-interface/user";
export const meta = applyConfig({
@ -55,36 +52,22 @@ export default (app: Hono) =>
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,
otherUser,
);
if (foundRelationship.followedBy) {
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));
return jsonResponse(foundRelationship.toApi());
},
);

View file

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

View file

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

View file

@ -3,13 +3,8 @@ import { errorResponse, jsonResponse } from "@/response";
import type { Hono } from "@hono/hono";
import { zValidator } from "@hono/zod-validator";
import { z } from "zod";
import {
createNewRelationship,
relationshipToApi,
} from "~/classes/functions/relationship";
import { db } from "~/drizzle/db";
import { RolePermissions } from "~/drizzle/schema";
import { User } from "~/packages/database-interface/user";
import { Relationship } from "~/packages/database-interface/relationship";
export const meta = applyConfig({
allowedMethods: ["GET"],
@ -50,32 +45,17 @@ export default (app: Hono) =>
return errorResponse("Unauthorized", 401);
}
const relationships = await db.query.Relationships.findMany({
where: (relationship, { inArray, and, eq }) =>
and(
inArray(relationship.subjectId, ids),
eq(relationship.ownerId, self.id),
),
});
const missingIds = ids.filter(
(id) => !relationships.some((r) => r.subjectId === id),
const relationships = await Relationship.fromOwnerAndSubjects(
self,
ids,
);
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(
(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 type { Hono } from "@hono/hono";
import { zValidator } from "@hono/zod-validator";
import { and, eq } from "drizzle-orm";
import { z } from "zod";
import {
checkForBidirectionalRelationships,
relationshipToApi,
} from "~/classes/functions/relationship";
import {
getRelationshipToOtherUser,
sendFollowAccept,
} from "~/classes/functions/user";
import { db } from "~/drizzle/db";
import { Relationships, RolePermissions } from "~/drizzle/schema";
import { sendFollowAccept } from "~/classes/functions/user";
import { RolePermissions } from "~/drizzle/schema";
import { Relationship } from "~/packages/database-interface/relationship";
import { User } from "~/packages/database-interface/user";
export const meta = applyConfig({
@ -58,52 +50,27 @@ export default (app: Hono) =>
return errorResponse("Account not found", 404);
}
// Check if there is a relationship on both sides
await checkForBidirectionalRelationships(user, account);
const oppositeRelationship = await Relationship.fromOwnerAndSubject(
account,
user,
);
// Authorize follow request
await db
.update(Relationships)
.set({
requested: false,
following: true,
})
.where(
and(
eq(Relationships.subjectId, user.id),
eq(Relationships.ownerId, account.id),
),
);
await oppositeRelationship.update({
requested: false,
following: true,
});
// Update followedBy for other user
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(
const foundRelationship = await Relationship.fromOwnerAndSubject(
user,
account,
);
if (!foundRelationship) {
return errorResponse("Relationship not found", 404);
}
// Check if accepting remote follow
if (account.isRemote()) {
// Federate follow accept
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 type { Hono } from "@hono/hono";
import { zValidator } from "@hono/zod-validator";
import { and, eq } from "drizzle-orm";
import { z } from "zod";
import {
checkForBidirectionalRelationships,
relationshipToApi,
} from "~/classes/functions/relationship";
import {
getRelationshipToOtherUser,
sendFollowReject,
} from "~/classes/functions/user";
import { db } from "~/drizzle/db";
import { Relationships, RolePermissions } from "~/drizzle/schema";
import { sendFollowReject } from "~/classes/functions/user";
import { RolePermissions } from "~/drizzle/schema";
import { Relationship } from "~/packages/database-interface/relationship";
import { User } from "~/packages/database-interface/user";
export const meta = applyConfig({
@ -58,52 +50,27 @@ export default (app: Hono) =>
return errorResponse("Account not found", 404);
}
// Check if there is a relationship on both sides
await checkForBidirectionalRelationships(user, account);
const oppositeRelationship = await Relationship.fromOwnerAndSubject(
account,
user,
);
// Reject follow request
await db
.update(Relationships)
.set({
requested: false,
following: false,
})
.where(
and(
eq(Relationships.subjectId, user.id),
eq(Relationships.ownerId, account.id),
),
);
await oppositeRelationship.update({
requested: false,
following: false,
});
// Update followedBy for other user
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(
const foundRelationship = await Relationship.fromOwnerAndSubject(
user,
account,
);
if (!foundRelationship) {
return errorResponse("Relationship not found", 404);
}
// Check if rejecting remote follow
if (account.isRemote()) {
// Federate follow reject
await sendFollowReject(account, user);
}
return jsonResponse(relationshipToApi(foundRelationship));
return jsonResponse(foundRelationship.toApi());
},
);

View file

@ -11,18 +11,16 @@ import {
} from "@lysand-org/federation";
import type { Entity } from "@lysand-org/federation/types";
import type { SocketAddress } from "bun";
import { and, eq } from "drizzle-orm";
import { eq } from "drizzle-orm";
import { matches } from "ip-matching";
import { z } from "zod";
import { type ValidationError, isValidationError } from "zod-validation-error";
import {
getRelationshipToOtherUser,
sendFollowAccept,
} from "~/classes/functions/user";
import { sendFollowAccept } from "~/classes/functions/user";
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 { Note } from "~/packages/database-interface/note";
import { Relationship } from "~/packages/database-interface/relationship";
import { User } from "~/packages/database-interface/user";
export const meta = applyConfig({
@ -228,35 +226,22 @@ export default (app: Hono) =>
}
const foundRelationship =
await getRelationshipToOtherUser(account, user);
await Relationship.fromOwnerAndSubject(
account,
user,
);
if (foundRelationship.following) {
if (foundRelationship.data.following) {
return response("Already following", 200);
}
await db
.update(Relationships)
.set({
following: !user.data.isLocked,
requested: user.data.isLocked,
showingReblogs: true,
notifying: true,
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 foundRelationship.update({
following: !user.data.isLocked,
requested: user.data.isLocked,
showingReblogs: true,
notifying: true,
languages: [],
});
await db.insert(Notifications).values({
accountId: account.id,
@ -280,36 +265,22 @@ export default (app: Hono) =>
}
const foundRelationship =
await getRelationshipToOtherUser(user, account);
await Relationship.fromOwnerAndSubject(
user,
account,
);
if (!foundRelationship.requested) {
if (!foundRelationship.data.requested) {
return response(
"There is no follow request to accept",
200,
);
}
await db
.update(Relationships)
.set({
following: true,
requested: false,
})
.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),
),
);
await foundRelationship.update({
requested: false,
following: true,
});
return response("Follow request accepted", 200);
},
@ -321,36 +292,22 @@ export default (app: Hono) =>
}
const foundRelationship =
await getRelationshipToOtherUser(user, account);
await Relationship.fromOwnerAndSubject(
user,
account,
);
if (!foundRelationship.requested) {
if (!foundRelationship.data.requested) {
return response(
"There is no follow request to reject",
200,
);
}
await db
.update(Relationships)
.set({
requested: 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),
),
);
await foundRelationship.update({
requested: false,
following: false,
});
return response("Follow request rejected", 200);
},