moving some more things to domains

This commit is contained in:
Devin Zuczek
2026-07-09 22:28:24 -04:00
parent 879b5d5905
commit 68d77ee4a4
11 changed files with 433 additions and 228 deletions
+49
View File
@@ -263,3 +263,52 @@ export async function removeFriend(db: D1Database, a: number, b: number): Promis
.bind(a, b)
.run()
}
/** A per-player relationship flag — each is stored on the player's own side of the row. */
export type RelationshipFlag = 'favorited' | 'ignored' | 'muted'
/**
* Set one of `playerId`'s per-side flags (favorited/ignored/muted) on their
* relationship with `otherId`. These flags are stored per player, so the write
* targets the caller's OWN side of the row — `requester_*` when the caller
* initiated the pair, `target_*` otherwise. When the pair has no relationship yet
* (you can ignore/mute someone you aren't friends with) a fresh `None` row is
* created with the caller as requester. Returns the relationship from `playerId`'s
* point of view. The `flag`/side names are a fixed union, so interpolating them
* into the SQL is safe (same pattern as the room interaction toggles).
*/
export async function setRelationshipFlag(
db: D1Database,
playerId: number,
otherId: number,
flag: RelationshipFlag,
value: boolean
): Promise<RelationshipResponse> {
const existing = await findPair(db, playerId, otherId)
const v = value ? 1 : 0
if (!existing) {
// New row: the caller is the requester, so the flag lives on the requester side.
await db
.prepare(
`INSERT INTO relationship (requester_id, target_id, relationship_type, requester_${flag})
VALUES (?1, ?2, ?3, ?4)`
)
.bind(playerId, otherId, RelationshipType.None, v)
.run()
} else {
// Update whichever side the caller is on, leaving the other player's flag alone.
const side = existing.requester_id === playerId ? 'requester' : 'target'
await db
.prepare(
`UPDATE relationship SET ${side}_${flag} = ?3
WHERE (requester_id = ?1 AND target_id = ?2) OR (requester_id = ?2 AND target_id = ?1)`
)
.bind(playerId, otherId, v)
.run()
}
const updated = await findPair(db, playerId, otherId)
return updated
? toResponse(updated, playerId)
: { PlayerID: otherId, RelationshipType: RelationshipType.None, Favorited: 0, Ignored: 0, Muted: 0 }
}
+21
View File
@@ -6,6 +6,7 @@ import {
getRelationshipsForPlayer,
removeFriend,
sendFriendRequest,
setRelationshipFlag,
} from '../relationships-db'
import { authedId, unauthorized } from '../http'
@@ -88,5 +89,25 @@ export const socialRoutes = new Hono<App>({ strict: false })
return c.json(await addFriend(c.env.DB, id, target))
})
// Ignore / mute another player (target arrives as `PlayerId` in the POST body).
// These set a per-player flag on the *caller's* side of the relationship row,
// creating a bare (None) row when the pair aren't otherwise related — so you can
// ignore/mute someone you've never friended. Auth-gated. Returns the resulting
// relationship from the caller's point of view.
.on(['GET', 'POST'], '/api/relationships/v1/ignore', 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, 'ignored', true))
})
.on(['GET', 'POST'], '/api/relationships/v1/mute', 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, 'muted', true))
})
.get('/api/messages/v2/get', (c) => c.json([]))
.get('/api/messages/v1/favoriteFriendOnlineStatus', (c) => c.json([]))
+44
View File
@@ -649,6 +649,8 @@ describe('relationships', () => {
'/api/relationships/v2/acceptfriendrequest',
'/api/relationships/v2/removefriend',
'/api/relationships/v2/addfriend',
'/api/relationships/v1/ignore',
'/api/relationships/v1/mute',
]) {
const res = await exports.default.fetch(`${ORIGIN}${path}?id=1`)
expect(res.status).toBe(401)
@@ -693,4 +695,46 @@ describe('relationships', () => {
test('a self-targeted request is rejected', async () => {
expect((await mutate('/api/relationships/v2/sendfriendrequest', '530', 530)).status).toBe(400)
})
test('v1 ignore/mute set the callers own side of the relationship', async () => {
type FullRel = { PlayerID: number; RelationshipType: number; Ignored: number; Muted: number }
// POST the real client shape: form body `PlayerId=<id>`.
const flag = async (path: string, sub: string, playerId: number) =>
(await (
await exports.default.fetch(`${ORIGIN}${path}`, {
method: 'POST',
headers: {
...(await bearer(sub)),
'Content-Type': 'application/x-www-form-urlencoded',
},
body: `PlayerId=${playerId}`,
})
).json()) as FullRel
// 700 ignores 701 with no prior relationship → a bare None row, the caller's side flagged.
expect(await flag('/api/relationships/v1/ignore', '700', 701)).toMatchObject({
PlayerID: 701,
RelationshipType: 0,
Ignored: 1,
Muted: 0,
})
// 700 then mutes 701 → same row, mute added, the earlier ignore preserved.
expect(await flag('/api/relationships/v1/mute', '700', 701)).toMatchObject({
PlayerID: 701,
Ignored: 1,
Muted: 1,
})
// The tricky case: the caller is the row's TARGET. 710 sends 711 a request
// (710 = requester); 711 ignoring 710 must flag the target side, not the requester's.
await mutate('/api/relationships/v2/sendfriendrequest', '710', 711)
expect(await flag('/api/relationships/v1/ignore', '711', 710)).toMatchObject({
PlayerID: 710,
RelationshipType: 2, // 711 sees 710's request as Received
Ignored: 1,
})
// 710's own side is untouched — the requester never ignored anyone.
const view710 = (await relationships('710')) as unknown as FullRel[]
expect(view710).toEqual([expect.objectContaining({ PlayerID: 711, RelationshipType: 1, Ignored: 0 })])
})
})