mirror of
https://github.com/djdevin/recflare.git
synced 2026-09-08 22:51:30 -07:00
add room invites
This commit is contained in:
@@ -4,16 +4,22 @@ import { describeRoute } from 'hono-openapi'
|
||||
import { getAccountsByIds } from '@repo/domain'
|
||||
import { logger } from '@repo/hono-helpers'
|
||||
|
||||
// The notification-type ids the hub carries (owned by the `notify` worker). Imported
|
||||
// as a value — the enum has no runtime dependencies.
|
||||
import { NotificationType } from '../../../notify/src/notification-types'
|
||||
import { authedId, unauthorized } from '../http'
|
||||
import {
|
||||
AckResponse,
|
||||
AUTHED,
|
||||
ErrorResponse,
|
||||
form,
|
||||
intQuery,
|
||||
json,
|
||||
JsonArray,
|
||||
MutualFriendDto,
|
||||
RelationshipDto,
|
||||
SendMessageRequest,
|
||||
SuccessErrorEnvelope,
|
||||
UNAUTHORIZED_RESPONSE,
|
||||
} from '../openapi'
|
||||
import {
|
||||
@@ -38,9 +44,6 @@ import type {
|
||||
/** The notifications hub is a single global DO instance (see the `notify` worker). */
|
||||
const HUB_INSTANCE = 'global'
|
||||
|
||||
/** NotificationType.RelationshipChanged (see apps/notify/src/notification-types.ts). */
|
||||
const RELATIONSHIP_CHANGED = 1
|
||||
|
||||
/**
|
||||
* Push a `RelationshipChanged` notification carrying `rel` to one player. Hub failures are
|
||||
* logged and swallowed — the DB write has already committed, so a hub hiccup must not fail
|
||||
@@ -54,7 +57,7 @@ async function notifyRelationship(
|
||||
try {
|
||||
await c.env.RECFLARE_NOTIFICATIONS_HUB.getByName(HUB_INSTANCE).notifyPlayer(
|
||||
playerId,
|
||||
RELATIONSHIP_CHANGED,
|
||||
NotificationType.RelationshipChanged,
|
||||
{ ...rel }
|
||||
)
|
||||
} catch (err) {
|
||||
@@ -254,6 +257,77 @@ export const socialRoutes = new Hono<App>({ strict: false })
|
||||
}
|
||||
)
|
||||
|
||||
// A message from one player to another — the "invite me!" style prompts the client
|
||||
// sends. Nothing is stored: the message IS the notification, pushed to the
|
||||
// recipient's hub connection (and queued by the hub if they're offline).
|
||||
.post(
|
||||
'/api/messages/v2/send',
|
||||
describeRoute({
|
||||
tags: ['Social'],
|
||||
summary: 'Send a message to another player',
|
||||
description:
|
||||
'Pushes a `MessageReceived` notification to `ToPlayerId` carrying the message — ' +
|
||||
'the same frame the Coach broadcast sends (see the `notify` worker’s ' +
|
||||
'`coachMessageAll`), except `FromPlayerId` is the caller rather than the Coach ' +
|
||||
'account and it goes to one player. The hub queues it when the recipient is ' +
|
||||
'offline, so it arrives on their next connect.\n\n' +
|
||||
'Nothing is persisted here — there is no message store, the notification is the ' +
|
||||
'whole delivery. The sender is the caller (from the bearer token), NOT a body ' +
|
||||
'field. `Type` is a Message-model type (a different enum from `NotificationType`) ' +
|
||||
'passed through unmapped, defaulting to 0; `Data` is the payload and is commonly ' +
|
||||
'empty.\n\n' +
|
||||
'Answers the same `{ success, error }` envelope as the report / warning writes, ' +
|
||||
'`error` an empty string on success. A hub failure is reported honestly as a 500 ' +
|
||||
'with `success: false` — with no store behind it, a swallowed error would be a ' +
|
||||
'silently dropped message.',
|
||||
security: AUTHED,
|
||||
requestBody: form(SendMessageRequest, 'The message'),
|
||||
responses: {
|
||||
200: json(SuccessErrorEnvelope, '`{ success: true, error: "" }`'),
|
||||
400: json(SuccessErrorEnvelope, 'No `ToPlayerId` in the request'),
|
||||
401: UNAUTHORIZED_RESPONSE,
|
||||
500: json(SuccessErrorEnvelope, 'The notifications hub could not be reached'),
|
||||
},
|
||||
}),
|
||||
async (c) => {
|
||||
const fromPlayerId = await authedId(c)
|
||||
if (fromPlayerId === null) return unauthorized(c)
|
||||
|
||||
const body = (await c.req.parseBody().catch(() => ({}))) as Record<string, unknown>
|
||||
const str = (v: unknown): string | undefined => (typeof v === 'string' ? v : undefined)
|
||||
const toPlayerId = Number.parseInt(str(body.ToPlayerId) ?? '', 10)
|
||||
if (Number.isNaN(toPlayerId)) {
|
||||
return c.json({ success: false, error: 'ToPlayerId is required' }, 400)
|
||||
}
|
||||
|
||||
// The Message the notification carries. Mirrors the coach message's shape with
|
||||
// a real sender and recipient; `Data` stays a string, empty included (the hub
|
||||
// drops only null/undefined from the frame).
|
||||
const message = {
|
||||
FromPlayerId: fromPlayerId,
|
||||
ToPlayerId: toPlayerId,
|
||||
Type: Number.parseInt(str(body.Type) ?? '', 10) || 0,
|
||||
Data: str(body.Data) ?? '',
|
||||
}
|
||||
|
||||
try {
|
||||
await c.env.RECFLARE_NOTIFICATIONS_HUB.getByName(HUB_INSTANCE).notifyPlayer(
|
||||
toPlayerId,
|
||||
NotificationType.MessageReceived,
|
||||
message
|
||||
)
|
||||
} catch (err) {
|
||||
logger.error('failed to push MessageReceived notification', {
|
||||
toPlayerId,
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
})
|
||||
return c.json({ success: false, error: 'Failed to deliver message' }, 500)
|
||||
}
|
||||
|
||||
return c.json({ success: true, error: '' })
|
||||
}
|
||||
)
|
||||
|
||||
// Send a friend request to another player (the target arrives as `?id=`). The
|
||||
// client calls this as a GET; the mutations accept GET or POST (the Go handlers
|
||||
// matched any method). Auth-gated. Returns the resulting relationship from the
|
||||
|
||||
Reference in New Issue
Block a user