Add plain text clipboard transfers and refine input feedback
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
import { inlinePayload } from './clipboardPayload.js';
|
||||
import type { CompleteInput } from '@hop/shared';
|
||||
import type { Server } from 'socket.io';
|
||||
import type { Auth } from './auth.js';
|
||||
import type { Accounts } from './accounts.js';
|
||||
@@ -13,14 +15,7 @@ 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 {
|
||||
parseUrls,
|
||||
type Device,
|
||||
type Item,
|
||||
type Peer,
|
||||
type Space,
|
||||
type TransferRequest,
|
||||
} from '@hop/shared';
|
||||
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;
|
||||
const day = 86400000;
|
||||
@@ -133,7 +128,7 @@ export class Service {
|
||||
.from(s.requests)
|
||||
.where(
|
||||
and(
|
||||
inArray(s.requests.kind, ['url', 'file']),
|
||||
inArray(s.requests.kind, ['url', 'text', 'file']),
|
||||
or(eq(s.requests.senderGuestId, g.id), eq(s.requests.recipientGuestId, g.id)),
|
||||
),
|
||||
)
|
||||
@@ -580,7 +575,7 @@ export class Service {
|
||||
g: Guest,
|
||||
input: {
|
||||
targetDeviceId: string;
|
||||
kind: 'url' | 'file';
|
||||
kind: 'url' | 'text' | 'file';
|
||||
byteSize: number;
|
||||
count: number;
|
||||
files?: { name: string; size: number; type: string }[];
|
||||
@@ -598,7 +593,8 @@ export class Service {
|
||||
)
|
||||
fail(403, 'TARGET_UNAVAILABLE', '이 기기로 보낼 수 없습니다.');
|
||||
if (
|
||||
(input.kind !== 'file' && input.byteSize > this.cfg.maxUrlBytes) ||
|
||||
(input.kind === 'url' && input.byteSize > this.cfg.maxUrlBytes) ||
|
||||
(input.kind === 'text' && input.byteSize > this.cfg.maxTextBytes) ||
|
||||
input.count > this.cfg.maxItems ||
|
||||
input.files?.some((f) => f.size > this.cfg.maxFileBytes)
|
||||
)
|
||||
@@ -756,7 +752,7 @@ export class Service {
|
||||
this.notifyRequests();
|
||||
this.refresh();
|
||||
}
|
||||
complete(g: Guest, id: string, input: { urls: string[] }) {
|
||||
complete(g: Guest, id: string, input: CompleteInput) {
|
||||
const row = this.request(g, id);
|
||||
if (row.kind === 'file')
|
||||
fail(400, 'FILE_UPLOAD_REQUIRED', '파일은 업로드 API를 사용해 주세요.');
|
||||
@@ -770,14 +766,13 @@ export class Service {
|
||||
fail(409, 'REQUEST_STATE', '수락된 유효한 요청이 필요합니다.');
|
||||
const batch = this.db.select().from(s.batches).where(eq(s.batches.requestId, id)).get()!;
|
||||
if (batch.status !== 'waiting') fail(409, 'TRANSFER_STATE', '이미 처리된 전송입니다.');
|
||||
if (row.kind !== 'url')
|
||||
fail(400, 'TYPE_UNSUPPORTED', '파일과 HTTP/HTTPS 링크만 보낼 수 있습니다.');
|
||||
const parsed = parseUrls(input.urls.join('\n'));
|
||||
if (parsed.invalid.length || parsed.urls.length !== input.urls.length)
|
||||
fail(400, 'INVALID_URL', 'HTTP 또는 HTTPS 링크만 보낼 수 있습니다.');
|
||||
const values = parsed.urls;
|
||||
const bytes = Buffer.byteLength(values.join('\n'), 'utf8');
|
||||
if (bytes !== row.byteSize || values.length !== row.count || bytes > this.cfg.maxUrlBytes)
|
||||
const { values, bytes } = inlinePayload(
|
||||
row.kind,
|
||||
input,
|
||||
this.cfg.maxTextBytes,
|
||||
this.cfg.maxUrlBytes,
|
||||
);
|
||||
if (bytes !== row.byteSize || values.length !== row.count)
|
||||
fail(409, 'METADATA_MISMATCH', '내용 크기나 개수가 바뀌었습니다. 새 요청을 보내 주세요.');
|
||||
const now = Date.now();
|
||||
for (const value of values) {
|
||||
@@ -789,7 +784,7 @@ export class Service {
|
||||
batchId: batch.id,
|
||||
kind: row.kind,
|
||||
|
||||
url: value,
|
||||
...(row.kind === 'text' ? { text: value } : { url: value }),
|
||||
createdAt: now,
|
||||
expiresAt: now + this.cfg.urlRetentionDays * day,
|
||||
})
|
||||
@@ -811,7 +806,7 @@ export class Service {
|
||||
.innerJoin(s.batches, eq(s.requests.id, s.batches.requestId))
|
||||
.where(
|
||||
and(
|
||||
inArray(s.requests.kind, ['url', 'file']),
|
||||
inArray(s.requests.kind, ['url', 'text', 'file']),
|
||||
gt(s.requests.createdAt, Date.now() - 7 * day),
|
||||
or(eq(s.requests.senderGuestId, g.id), eq(s.requests.recipientGuestId, g.id)),
|
||||
),
|
||||
@@ -867,7 +862,7 @@ export class Service {
|
||||
.where(
|
||||
and(
|
||||
eq(s.access.guestId, g.id),
|
||||
inArray(s.items.kind, ['url', 'file']),
|
||||
inArray(s.items.kind, ['url', 'text', 'file']),
|
||||
isNull(s.access.deletedAt),
|
||||
gt(s.items.expiresAt, Date.now()),
|
||||
after
|
||||
@@ -896,6 +891,7 @@ export class Service {
|
||||
batchId: r.item.batchId,
|
||||
kind: r.item.kind as 'url' | 'file',
|
||||
url: r.item.url,
|
||||
...(r.item.kind === 'text' ? { text: r.item.text } : {}),
|
||||
file: r.item.fileId
|
||||
? (() => {
|
||||
const f = this.db.select().from(s.files).where(eq(s.files.id, r.item.fileId!)).get()!;
|
||||
|
||||
Reference in New Issue
Block a user