From 92f5b49879aa10bd9fdb9422f17413285723256d Mon Sep 17 00:00:00 2001 From: Devin Zuczek Date: Mon, 13 Jul 2026 18:36:23 -0400 Subject: [PATCH] add fave/unfave --- apps/api/src/routes/social.ts | 21 ++++++++++++- apps/api/src/test/integration/api.test.ts | 36 +++++++++++++++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/apps/api/src/routes/social.ts b/apps/api/src/routes/social.ts index 7d9bdd3..3a711ff 100644 --- a/apps/api/src/routes/social.ts +++ b/apps/api/src/routes/social.ts @@ -1,5 +1,6 @@ import { Hono } from 'hono' +import { authedId, unauthorized } from '../http' import { acceptFriendRequest, addFriend, @@ -8,7 +9,6 @@ import { sendFriendRequest, setRelationshipFlag, } from '../relationships-db' -import { authedId, unauthorized } from '../http' import type { Context } from 'hono' import type { App } from '../context' @@ -109,5 +109,24 @@ export const socialRoutes = new Hono({ strict: false }) return c.json(await setRelationshipFlag(c.env.DB, id, target, 'muted', true)) }) + // Favorite / unfavorite another player (the client calls these as a GET with the + // target in `?id=`). Same per-side flag mechanics as ignore/mute above: the write + // lands on the *caller's* side of the row, and favoriting someone you have no + // relationship with creates a bare (None) row. Auth-gated. + .on(['GET', 'POST'], '/api/relationships/v1/favorite', async (c) => { + const id = await authedId(c) + if (id === null) return unauthorized(c) + const target = await targetPlayerId(c) + if (target === null || target === id) return c.json({ error: 'invalid player id' }, 400) + return c.json(await setRelationshipFlag(c.env.DB, id, target, 'favorited', true)) + }) + .on(['GET', 'POST'], '/api/relationships/v1/unfavorite', async (c) => { + const id = await authedId(c) + if (id === null) return unauthorized(c) + const target = await targetPlayerId(c) + if (target === null || target === id) return c.json({ error: 'invalid player id' }, 400) + return c.json(await setRelationshipFlag(c.env.DB, id, target, 'favorited', false)) + }) + .get('/api/messages/v2/get', (c) => c.json([])) .get('/api/messages/v1/favoriteFriendOnlineStatus', (c) => c.json([])) diff --git a/apps/api/src/test/integration/api.test.ts b/apps/api/src/test/integration/api.test.ts index 4f78283..63539ab 100644 --- a/apps/api/src/test/integration/api.test.ts +++ b/apps/api/src/test/integration/api.test.ts @@ -1398,6 +1398,8 @@ describe('relationships', () => { '/api/relationships/v2/addfriend', '/api/relationships/v1/ignore', '/api/relationships/v1/mute', + '/api/relationships/v1/favorite', + '/api/relationships/v1/unfavorite', ]) { const res = await exports.default.fetch(`${ORIGIN}${path}?id=1`) expect(res.status).toBe(401) @@ -1504,4 +1506,38 @@ describe('relationships', () => { expect.objectContaining({ PlayerID: 711, RelationshipType: 1, Ignored: 0 }), ]) }) + + test('v1 favorite/unfavorite toggle the caller’s own side, leaving the friendship intact', async () => { + // 720 and 721 are friends; 720 favorites 721 — the real client shape, a GET with `?id=`. + await mutate('/api/relationships/v2/addfriend', '720', 721) + expect( + (await (await mutate('/api/relationships/v1/favorite', '720', 721)).json()) as Rel + ).toMatchObject({ PlayerID: 721, RelationshipType: 3, Favorited: 1 }) + + // Favoriting is one-sided: 721 does not see themselves as having favorited 720. + expect(await relationships('721')).toEqual([ + { PlayerID: 720, RelationshipType: 3, Favorited: 0, Ignored: 0, Muted: 0 }, + ]) + + // Unfavorite clears the flag but keeps the friendship. + expect( + (await (await mutate('/api/relationships/v1/unfavorite', '720', 721)).json()) as Rel + ).toMatchObject({ PlayerID: 721, RelationshipType: 3, Favorited: 0 }) + expect(await relationships('720')).toEqual([ + { PlayerID: 721, RelationshipType: 3, Favorited: 0, Ignored: 0, Muted: 0 }, + ]) + }) + + test('favoriting a player you have no relationship with is allowed', async () => { + // Mirrors ignore/mute: a bare None row is created with the caller's side flagged. + expect( + (await (await mutate('/api/relationships/v1/favorite', '730', 731)).json()) as Rel + ).toMatchObject({ PlayerID: 731, RelationshipType: 0, Favorited: 1 }) + // A None row is not reported as a relationship by v2/get. + expect(await relationships('730')).toEqual([]) + }) + + test('a self-targeted favorite is rejected', async () => { + expect((await mutate('/api/relationships/v1/favorite', '740', 740)).status).toBe(400) + }) })