95 lines
4.5 KiB
TypeScript
95 lines
4.5 KiB
TypeScript
// QR code REST endpoints (docs/IM-前后端对接文档.md §5.15.1). Confirmed LIVE against the real
|
|
// deployment (2026-09-08, two fresh test accounts) -- `resolveQRCode`'s real response shape nests
|
|
// `relationship`/`can_add`/`action`/`public_account` all under `user`, unlike the doc's own worked
|
|
// example which shows them as siblings of `user` at the top level. Also confirmed live: resolving a
|
|
// code against the SAME account that minted it is rejected (`400 MALFORMED_REQUEST`, `field:"token"`,
|
|
// message "malformed request" -- distinct from a genuinely garbage token, which instead gets message
|
|
// "qr code invalid" with no `details.field` at all) -- cross-account resolution works correctly.
|
|
//
|
|
// The security rationale for why this exists at all instead of just encoding a raw user id (doc line
|
|
// 1299): "二维码只含不透明、签名、带租户和用途约束的短期 Token,不直接放手机号、用户 ID 或登录
|
|
// Token" -- the QR image payload is `qr_payload` (an `im://qr?token=...&purpose=...` URI), not
|
|
// `qr_token` alone, matching the old app's own `im://qr` scheme (im_webapp/src/views/tinode-web.jsx
|
|
// confirmed this exact prefix check before ever sending a scanned string to `resolve`).
|
|
import { imPost } from './client';
|
|
|
|
interface QRCodeDisplayProfile {
|
|
user_id: string;
|
|
display_name: string;
|
|
public_account?: string;
|
|
}
|
|
|
|
export interface MyQRCode {
|
|
qr_token: string;
|
|
/** What actually gets encoded into the QR image -- `im://qr?token=...&purpose=...`. */
|
|
qr_payload: string;
|
|
expires_at: string;
|
|
display_profile: QRCodeDisplayProfile;
|
|
}
|
|
|
|
export function createMyQRCode(expiresInSeconds = 86400) {
|
|
return imPost<MyQRCode>('/me/qrcodes', { purpose: 'add_friend', expires_in_seconds: expiresInSeconds });
|
|
}
|
|
|
|
/** `identifier`: user_id/username/public_account -- viewing a specific friend's own QR card. */
|
|
export function createUserQRCode(identifier: string, expiresInSeconds = 86400) {
|
|
return imPost<MyQRCode>(`/users/${encodeURIComponent(identifier)}/qrcodes`, { purpose: 'add_friend', expires_in_seconds: expiresInSeconds });
|
|
}
|
|
|
|
export interface QRResolveUser {
|
|
user_id: string;
|
|
display_name: string;
|
|
public_account?: string;
|
|
// docs/IM-前后端对接文档.md:1289 -- same real direction-specific enum as friends.ts's
|
|
// UserSearchResult/UserProfile, never a bare "pending".
|
|
relationship: 'none' | 'friend' | 'pending_inbound' | 'pending_outbound' | string;
|
|
can_add: boolean;
|
|
action: string;
|
|
}
|
|
|
|
/** Confirmed LIVE (2026-09-08, same account that minted the code -- a real cross-account "scan a
|
|
* stranger's group code" resolve hasn't been exercised, this session only had one account free):
|
|
* a `purpose:"join_group"` resolve nests the group under `group` (NOT `group_snapshot`, despite
|
|
* the mint response using that key -- resolve and mint use different field names for the same
|
|
* shape), and carries a `status` this specific case's doc example (line 1737, mint-only) never
|
|
* showed: resolving your own group's code (i.e. already a member) comes back `status:"joined"`.
|
|
* What a genuinely-not-yet-a-member resolve returns for `status` is unconfirmed -- treated as
|
|
* "anything other than 'joined'" in QRScannerModal/AddFriendModal, i.e. the general case that still
|
|
* needs a real join-request submission. */
|
|
export interface QRResolveGroup {
|
|
topic: string;
|
|
name: string;
|
|
member_count: number;
|
|
}
|
|
|
|
export interface QRResolveResult {
|
|
purpose: 'add_friend' | 'join_group' | string;
|
|
source: string;
|
|
source_topic: string;
|
|
expires_at: string;
|
|
user?: QRResolveUser;
|
|
group?: QRResolveGroup;
|
|
group_snapshot?: QRResolveGroup;
|
|
/** join_group only -- `"joined"` means the scanning account is already a member (confirmed live);
|
|
* any other value (including absent) is treated as "not a member yet, submit a join request". */
|
|
status?: string;
|
|
}
|
|
|
|
export function resolveQRCode(token: string, purpose: 'add_friend' | 'join_group' | string = 'add_friend') {
|
|
return imPost<QRResolveResult>('/qrcodes/resolve', { token, purpose });
|
|
}
|
|
|
|
/** docs/IM-前后端对接文档.md:1590 -- 群管理员/群主展示的入群二维码 (mirrors createMyQRCode/
|
|
* createUserQRCode's shape, just scoped to a group and carrying `group_snapshot` instead of a
|
|
* personal `display_profile`). */
|
|
export interface GroupQRCode {
|
|
qr_token: string;
|
|
qr_payload: string;
|
|
expires_at: string;
|
|
group_snapshot: QRResolveGroup;
|
|
}
|
|
|
|
export function createGroupQRCode(topic: string, expiresInSeconds = 86400) {
|
|
return imPost<GroupQRCode>(`/groups/${encodeURIComponent(topic)}/qrcodes`, { expires_in_seconds: expiresInSeconds });
|
|
}
|