Clarify receive requests and add retry countdowns and device unblock controls
This commit is contained in:
@@ -14,7 +14,7 @@ import type { DB } from './db/index.js';
|
||||
import * as s from './db/schema.js';
|
||||
import type { Config } from './config.js';
|
||||
import type { Presence } from './presence.js';
|
||||
import { fail, hash, hmac, randomToken, Limiter } from './security.js';
|
||||
import { fail, hash, hmac, randomToken, Limiter, ApiError } from './security.js';
|
||||
import { type Device, type Item, type Peer, type Space, type TransferRequest } from '@hop/shared';
|
||||
export type Guest = typeof s.guests.$inferSelect;
|
||||
export type RequestRow = typeof s.requests.$inferSelect;
|
||||
@@ -599,9 +599,6 @@ export class Service {
|
||||
input.files?.some((f) => f.size > this.cfg.maxFileBytes)
|
||||
)
|
||||
fail(413, 'TRANSFER_LIMIT', '전송 크기 또는 개수 제한을 초과했습니다.');
|
||||
this.limiter.hit(`request-ip:${ip}`, this.cfg.ipRate);
|
||||
this.limiter.hit(`request-guest:${g.id}`, this.cfg.sessionRate);
|
||||
this.limiter.hit(`request-pair:${g.id}:${target!.guestId}`, this.cfg.pairRate);
|
||||
const rejected = this.db
|
||||
.select()
|
||||
.from(s.requests)
|
||||
@@ -613,9 +610,16 @@ export class Service {
|
||||
gt(s.requests.rejectedAt, Date.now() - this.cfg.rejectCooldownMs),
|
||||
),
|
||||
)
|
||||
.orderBy(desc(s.requests.rejectedAt))
|
||||
.get();
|
||||
if (rejected)
|
||||
fail(429, 'REJECTION_COOLDOWN', '상대가 거절했습니다. 잠시 후 다시 시도해 주세요.');
|
||||
throw new ApiError(
|
||||
429,
|
||||
'REJECTION_COOLDOWN',
|
||||
'상대가 거절했어요. 잠시 후 다시 시도해 주세요.',
|
||||
rejected.rejectedAt! + this.cfg.rejectCooldownMs,
|
||||
'pair',
|
||||
);
|
||||
const active = this.db
|
||||
.select()
|
||||
.from(s.requests)
|
||||
@@ -629,6 +633,11 @@ export class Service {
|
||||
)
|
||||
.get();
|
||||
if (active) fail(409, 'TRANSFER_ACTIVE', '진행 중인 전송을 먼저 완료하거나 취소해 주세요.');
|
||||
this.limiter.hitMany([
|
||||
{ key: `request-ip:${ip}`, max: this.cfg.ipRate },
|
||||
{ key: `request-guest:${g.id}`, max: this.cfg.sessionRate },
|
||||
{ key: `request-pair:${g.id}:${target!.guestId}`, max: this.cfg.pairRate },
|
||||
]);
|
||||
const now = Date.now(),
|
||||
id = randomUUID();
|
||||
this.db
|
||||
@@ -752,6 +761,37 @@ export class Service {
|
||||
this.notifyRequests();
|
||||
this.refresh();
|
||||
}
|
||||
listBlocks(g: Guest) {
|
||||
return this.db
|
||||
.select({ deviceId: s.devices.id, name: s.devices.name, code: s.devices.code })
|
||||
.from(s.blocks)
|
||||
.innerJoin(s.devices, eq(s.blocks.targetGuestId, s.devices.guestId))
|
||||
.where(eq(s.blocks.guestId, g.id))
|
||||
.all();
|
||||
}
|
||||
unblock(g: Guest, targetDeviceId: string) {
|
||||
const target = this.deviceGuest(targetDeviceId);
|
||||
if (!target) return;
|
||||
this.db.transaction(() => {
|
||||
const removed = this.db
|
||||
.delete(s.blocks)
|
||||
.where(and(eq(s.blocks.guestId, g.id), eq(s.blocks.targetGuestId, target.guestId)))
|
||||
.run();
|
||||
if (removed.changes)
|
||||
this.db
|
||||
.update(s.requests)
|
||||
.set({ rejectedAt: null })
|
||||
.where(
|
||||
and(
|
||||
eq(s.requests.senderGuestId, target.guestId),
|
||||
eq(s.requests.recipientGuestId, g.id),
|
||||
eq(s.requests.status, 'rejected'),
|
||||
),
|
||||
)
|
||||
.run();
|
||||
});
|
||||
this.refresh();
|
||||
}
|
||||
complete(g: Guest, id: string, input: CompleteInput) {
|
||||
const row = this.request(g, id);
|
||||
if (row.kind === 'file')
|
||||
|
||||
Reference in New Issue
Block a user