feat(web): add spaces, grouped channels and file chat

This commit is contained in:
justme committed 2026-10-13 01:39:23 +08:00
1 parent c7d8ef15bc
commit f6cfb12716
111 files changed
+4371 -506

No files matched your search

@@ -0,0 +1,45 @@
import { ChannelSession } from '@webdrop/core';
import { getResource, importResources, listAllResources, listResourceDeletes, type SavedResource } from '../database';
import { openChannelGrant, sealChannelGrant } from './channelGrant';
import { makeLink } from './links';
import { apiFetch, readJson } from './http';
import type { InvitationKeyPair } from './accountCrypto';
import type { ChannelWire } from './wire';
interface AccountChannel { channel_id: string; role: 'owner' | 'member'; payload: string; created_at: number }
export async function recoverAccountChannels(username: string, keys: InvitationKeyPair, signal: AbortSignal): Promise<void> {
const grants = await apiFetch('/accounts/channels', { signal }).then((r) => r.json() as Promise<AccountChannel[]>);
const deletes = new Map((await listResourceDeletes()).map((row) => [row.id, row.modifiedAt]));
const known = new Set(grants.map((row) => row.channel_id));
for (const grant of grants) {
if (signal.aborted) throw new DOMException('账号已切换', 'AbortError');
if ((deletes.get(grant.channel_id) ?? -1) >= grant.created_at || await getResource(grant.channel_id)) continue;
const parsed = await openChannelGrant(grant.payload, username, grant.channel_id, keys.privateKey, grant.role, self.location.origin);
const session = await ChannelSession.open(makeLink(self.location.origin, parsed.id, parsed.keys));
try {
const info = await readJson<ChannelWire>(`/channels/${session.id}`, session.authHeaders('read'));
session.verifyChannelInfo(info);
if (grant.role === 'owner' && info.creator_account !== username) throw new Error('空间所属账号不匹配');
const resource: SavedResource = { id: session.id, kind: 'channel', mode: session.mode, role: grant.role,
creatorAccount: info.creator_account, name: info.settings.name,
link: session.link(self.location.origin), ...(grant.role === 'owner' ? { ownerLink: session.link(self.location.origin, 'owner') } : {}),
endpointSecret: new Uint8Array(32), createdAt: info.created_at, modifiedAt: grant.created_at, expiresAt: info.expires_at,
seats: info.settings.seats, membersCanUpload: info.settings.members_can_upload, maxDownloads: info.settings.file_max_downloads };
if (signal.aborted) throw new DOMException('账号已切换', 'AbortError');
await importResources([resource]);
} finally { session.dispose(); }
}
// Populate owner grants for channels created before account-bound delivery existed.
for (const resource of await listAllResources()) {
if (resource.kind !== 'channel' || resource.role !== 'owner' || !resource.ownerLink || known.has(resource.id)) continue;
const session = await ChannelSession.open(resource.ownerLink, { endpointSecret: resource.endpointSecret });
try {
const payload = await sealChannelGrant(resource.ownerLink, username, keys.publicKey);
await apiFetch(`/channels/${resource.id}/key-package`, { method: 'PUT', signal,
headers: { ...session.authHeaders('owner'), 'Content-Type': 'application/json' }, body: JSON.stringify({ payload }) });
} catch (error) {
if (!(error instanceof Error && 'status' in error && error.status === 404)) throw error;
} finally { session.dispose(); }
}
}