diff --git a/README.md b/README.md index 7190c906..d8de86c8 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,8 @@ Working endpoints are: - `/api/v1/accounts/:id/unfollow` - `/api/v1/accounts/:id/block` - `/api/v1/accounts/:id/unblock` +- `/api/v1/accounts/:id/mute` +- `/api/v1/accounts/:id/unmute` - `/api/v1/accounts/update_credentials` - `/api/v1/accounts/verify_credentials` - `/api/v1/statuses` diff --git a/database/entities/Status.ts b/database/entities/Status.ts index eecfbc98..f3088899 100644 --- a/database/entities/Status.ts +++ b/database/entities/Status.ts @@ -101,6 +101,8 @@ export class Status extends BaseEntity { newStatus.isReblog = false; newStatus.announces = []; + // TODO: Add default language + await newStatus.save(); return newStatus; } diff --git a/server/api/api/v1/accounts/[id]/mute.ts b/server/api/api/v1/accounts/[id]/mute.ts new file mode 100644 index 00000000..22218dcb --- /dev/null +++ b/server/api/api/v1/accounts/[id]/mute.ts @@ -0,0 +1,64 @@ +import { getUserByToken } from "@auth"; +import { parseRequest } from "@request"; +import { errorResponse, jsonResponse } from "@response"; +import { MatchedRoute } from "bun"; +import { Relationship } from "~database/entities/Relationship"; +import { User } from "~database/entities/User"; + +/** + * Follow a user + */ +export default async ( + req: Request, + matchedRoute: MatchedRoute +): Promise => { + const id = matchedRoute.params.id; + + // Check auth token + const token = req.headers.get("Authorization")?.split(" ")[1] || null; + + if (!token) + return errorResponse("This method requires an authenticated user", 422); + + const self = await getUserByToken(token); + + if (!self) return errorResponse("Unauthorized", 401); + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { notifications, duration } = await parseRequest<{ + notifications: boolean; + duration: number; + }>(req); + + const user = await User.findOneBy({ + id, + }); + + if (!user) return errorResponse("User not found", 404); + + // Check if already following + let relationship = await self.getRelationshipToOtherUser(user); + + if (!relationship) { + // Create new relationship + + const newRelationship = await Relationship.createNew(self, user); + + self.relationships.push(newRelationship); + await self.save(); + + relationship = newRelationship; + } + + if (!relationship.muting) { + relationship.muting = true; + } + if (notifications ?? true) { + relationship.muting_notifications = true; + } + + // TODO: Implement duration + + await relationship.save(); + return jsonResponse(await relationship.toAPI()); +}; diff --git a/server/api/api/v1/accounts/[id]/unmute.ts b/server/api/api/v1/accounts/[id]/unmute.ts new file mode 100644 index 00000000..912ae511 --- /dev/null +++ b/server/api/api/v1/accounts/[id]/unmute.ts @@ -0,0 +1,54 @@ +import { getUserByToken } from "@auth"; +import { errorResponse, jsonResponse } from "@response"; +import { MatchedRoute } from "bun"; +import { Relationship } from "~database/entities/Relationship"; +import { User } from "~database/entities/User"; + +/** + * Follow a user + */ +export default async ( + req: Request, + matchedRoute: MatchedRoute +): Promise => { + const id = matchedRoute.params.id; + + // Check auth token + const token = req.headers.get("Authorization")?.split(" ")[1] || null; + + if (!token) + return errorResponse("This method requires an authenticated user", 422); + + const self = await getUserByToken(token); + + if (!self) return errorResponse("Unauthorized", 401); + + const user = await User.findOneBy({ + id, + }); + + if (!user) return errorResponse("User not found", 404); + + // Check if already following + let relationship = await self.getRelationshipToOtherUser(user); + + if (!relationship) { + // Create new relationship + + const newRelationship = await Relationship.createNew(self, user); + + self.relationships.push(newRelationship); + await self.save(); + + relationship = newRelationship; + } + + if (relationship.muting) { + relationship.muting = false; + } + + // TODO: Implement duration + + await relationship.save(); + return jsonResponse(await relationship.toAPI()); +}; diff --git a/server/api/api/v1/statuses/index.ts b/server/api/api/v1/statuses/index.ts index a7fb451c..ba132405 100644 --- a/server/api/api/v1/statuses/index.ts +++ b/server/api/api/v1/statuses/index.ts @@ -102,7 +102,13 @@ export default async (req: Request): Promise => { account: user, application, content: status, - visibility: visibility || "public", + visibility: + visibility || + (config.defaults.visibility as + | "public" + | "unlisted" + | "private" + | "direct"), sensitive: sensitive || false, spoiler_text: spoiler_text || "", emojis: [], diff --git a/tests/api.test.ts b/tests/api.test.ts index 5d289146..2ac8ca3e 100644 --- a/tests/api.test.ts +++ b/tests/api.test.ts @@ -339,6 +339,78 @@ describe("POST /api/v1/accounts/:id/unblock", () => { }); }); +describe("POST /api/v1/accounts/:id/mute with notifications parameter", () => { + test("should mute the specified user and return an APIRelationship object with notifications set to false", async () => { + const response = await fetch( + `${config.http.base_url}/api/v1/accounts/${user2.id}/mute`, + { + method: "POST", + headers: { + Authorization: `Bearer ${token.access_token}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ notifications: true }), + } + ); + + expect(response.status).toBe(200); + expect(response.headers.get("content-type")).toBe("application/json"); + + const account: APIRelationship = await response.json(); + + expect(account.id).toBe(user2.id); + expect(account.muting).toBe(true); + expect(account.muting_notifications).toBe(true); + }); + + test("should mute the specified user and return an APIRelationship object with notifications set to true", async () => { + const response = await fetch( + `${config.http.base_url}/api/v1/accounts/${user2.id}/mute`, + { + method: "POST", + headers: { + Authorization: `Bearer ${token.access_token}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ notifications: false }), + } + ); + + expect(response.status).toBe(200); + expect(response.headers.get("content-type")).toBe("application/json"); + + const account: APIRelationship = await response.json(); + + expect(account.id).toBe(user2.id); + expect(account.muting).toBe(true); + expect(account.muting_notifications).toBe(true); + }); +}); + +describe("POST /api/v1/accounts/:id/unmute", () => { + test("should unmute the specified user and return an APIRelationship object", async () => { + const response = await fetch( + `${config.http.base_url}/api/v1/accounts/${user2.id}/unmute`, + { + method: "POST", + headers: { + Authorization: `Bearer ${token.access_token}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({}), + } + ); + + expect(response.status).toBe(200); + expect(response.headers.get("content-type")).toBe("application/json"); + + const account: APIRelationship = await response.json(); + + expect(account.id).toBe(user2.id); + expect(account.muting).toBe(false); + }); +}); + afterAll(async () => { const activities = await RawActivity.createQueryBuilder("activity") .where("activity.data->>'actor' = :actor", {