Add automatic nearby discovery and simplify device presence UI
This commit is contained in:
@@ -110,6 +110,72 @@ export async function createApp(cfg: Config, options: { mailer?: Mailer } = {})
|
||||
socket.join(`account:${socket.data.accountId}`);
|
||||
socket.join(`account-session:${socket.data.accountSessionId}`);
|
||||
}
|
||||
const currentGuest = () => {
|
||||
const guest = service.guest(app.parseCookie(socket.request.headers.cookie ?? '').hop_guest);
|
||||
if (!guest) fail(401, 'SESSION_EXPIRED', '기기 세션이 종료되었습니다.');
|
||||
return guest!;
|
||||
};
|
||||
socket.on('location:update', (input, acknowledge) => {
|
||||
try {
|
||||
const guest = currentGuest();
|
||||
service.limiter.hit(`location-guest:${guest.id}`, 6);
|
||||
service.limiter.hit(
|
||||
`location-ip:${socket.data.ip ?? socket.request.socket.remoteAddress}`,
|
||||
120,
|
||||
);
|
||||
if (!cfg.locationDiscoveryEnabled)
|
||||
fail(409, 'LOCATION_DISABLED', '근처 검색을 사용할 수 없습니다.');
|
||||
if (!service.device(guest.id).discoverable || service.activeRoom(guest.id))
|
||||
fail(409, 'LOCATION_UNAVAILABLE', '현재 기기 상태에서는 근처 검색을 사용할 수 없습니다.');
|
||||
const position = z
|
||||
.object({
|
||||
latitude: z.number().finite().min(-90).max(90),
|
||||
longitude: z.number().finite().min(-180).max(180),
|
||||
accuracy: z.number().finite().min(0).max(100000),
|
||||
})
|
||||
.strict()
|
||||
.parse(input);
|
||||
if (position.accuracy > Math.max(100, cfg.locationRadiusMeters))
|
||||
fail(
|
||||
409,
|
||||
'LOCATION_INACCURATE',
|
||||
'위치 오차가 커서 근처 검색을 켜지 못했어요. 다시 확인하거나 QR로 연결해 주세요.',
|
||||
);
|
||||
service.nearby.set(guest.id, socket.id, position);
|
||||
if (typeof acknowledge === 'function') acknowledge({ ok: true });
|
||||
} catch (error) {
|
||||
if (typeof acknowledge === 'function')
|
||||
acknowledge({
|
||||
ok: false,
|
||||
code: error instanceof ApiError ? error.code : 'LOCATION_INVALID',
|
||||
message: error instanceof ApiError ? error.message : '위치를 확인하지 못했습니다.',
|
||||
});
|
||||
}
|
||||
});
|
||||
socket.on('location:accept', (input) => {
|
||||
try {
|
||||
const guest = currentGuest();
|
||||
service.limiter.hit(`location-accept:${guest.id}`, 20);
|
||||
const requestId = z.object({ requestId: z.uuid() }).strict().parse(input).requestId;
|
||||
const request = service.request(guest, requestId);
|
||||
if (
|
||||
request.recipientGuestId === guest.id &&
|
||||
request.status === 'pending' &&
|
||||
request.expiresAt > Date.now() &&
|
||||
cfg.locationDiscoveryEnabled &&
|
||||
service.nearby.nearby(request.senderGuestId, guest.id) &&
|
||||
service.canReceive(request.senderGuestId, guest.id)
|
||||
)
|
||||
service.locationAcceptances.set(requestId, {
|
||||
guestId: guest.id,
|
||||
until: Date.now() + 15000,
|
||||
});
|
||||
} catch {
|
||||
/* Invalid messages cannot grant an acceptance. */
|
||||
}
|
||||
});
|
||||
socket.on('location:clear', () => service.nearby.clear(socket.id));
|
||||
socket.on('disconnect', () => service.nearby.clear(socket.id));
|
||||
});
|
||||
const auth = (req: FastifyRequest) => {
|
||||
const guest = service.guest(req.cookies.hop_guest);
|
||||
@@ -162,7 +228,10 @@ export async function createApp(cfg: Config, options: { mailer?: Mailer } = {})
|
||||
.header('X-Content-Type-Options', 'nosniff')
|
||||
.header('Referrer-Policy', 'no-referrer')
|
||||
.header('X-Frame-Options', 'DENY')
|
||||
.header('Permissions-Policy', 'camera=(), microphone=(), geolocation=(), bluetooth=()');
|
||||
.header(
|
||||
'Permissions-Policy',
|
||||
`camera=(), microphone=(), geolocation=${cfg.locationDiscoveryEnabled ? '(self)' : '()'}, bluetooth=()`,
|
||||
);
|
||||
reply.header(
|
||||
'Content-Security-Policy',
|
||||
"default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; connect-src 'self' ws: wss:; font-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; form-action 'self'",
|
||||
@@ -262,6 +331,11 @@ export async function createApp(cfg: Config, options: { mailer?: Mailer } = {})
|
||||
device: service.device(g.id),
|
||||
devices: service.peers(g),
|
||||
sameNetworkDiscoveryEnabled: cfg.discoveryEnabled,
|
||||
locationDiscovery: {
|
||||
enabled: cfg.locationDiscoveryEnabled,
|
||||
radiusMeters: cfg.locationRadiusMeters,
|
||||
refreshMs: 120000,
|
||||
},
|
||||
requests: r,
|
||||
items: list.items,
|
||||
nextCursor: list.nextCursor,
|
||||
@@ -868,12 +942,19 @@ export async function createApp(cfg: Config, options: { mailer?: Mailer } = {})
|
||||
});
|
||||
}
|
||||
let storageCleanup: Promise<void> | undefined;
|
||||
const cleanup = setInterval(() => {
|
||||
service.cleanup();
|
||||
service.auth.enforceSessions();
|
||||
const cleanStorage = () => {
|
||||
storageCleanup = service.storage
|
||||
.cleanup()
|
||||
.catch(() => app.log.error({ code: 'CLEANUP_FAILED' }, 'Storage cleanup failed'));
|
||||
};
|
||||
cleanStorage();
|
||||
const storageTimer = setInterval(cleanStorage, 60000);
|
||||
storageTimer.unref();
|
||||
const cleanup = setInterval(() => {
|
||||
for (const [id, permission] of service.locationAcceptances)
|
||||
if (permission.until <= Date.now()) service.locationAcceptances.delete(id);
|
||||
service.cleanup();
|
||||
service.auth.enforceSessions();
|
||||
}, 1000);
|
||||
cleanup.unref();
|
||||
app.addHook('preClose', async () => {
|
||||
@@ -882,7 +963,9 @@ export async function createApp(cfg: Config, options: { mailer?: Mailer } = {})
|
||||
});
|
||||
app.addHook('onClose', async () => {
|
||||
clearInterval(cleanup);
|
||||
clearInterval(storageTimer);
|
||||
presence.close();
|
||||
service.nearby.close();
|
||||
service.storage.close();
|
||||
await storageCleanup;
|
||||
await new Promise<void>((resolve) => io.close(() => resolve()));
|
||||
|
||||
Reference in New Issue
Block a user