@noverachat/sdk-web 0.0.3 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ko.md +8 -8
- package/README.md +8 -8
- package/dist/index.cjs +386 -258
- package/dist/index.d.cts +163 -170
- package/dist/index.d.ts +163 -170
- package/dist/index.js +386 -258
- package/package.json +1 -1
- package/src/client.ts +50 -157
- package/src/features/room.ts +187 -344
- package/src/generated/rest.ts +480 -0
- package/src/index.ts +1 -0
- package/src/types.ts +59 -177
package/src/features/room.ts
CHANGED
|
@@ -19,6 +19,7 @@ import type {
|
|
|
19
19
|
InviteToken,
|
|
20
20
|
MessageIdStr,
|
|
21
21
|
MessageOut,
|
|
22
|
+
RoomMemberOut,
|
|
22
23
|
RoomUnread,
|
|
23
24
|
SendFileOptions,
|
|
24
25
|
SendParams,
|
|
@@ -34,67 +35,63 @@ import type {
|
|
|
34
35
|
} from "../types.js";
|
|
35
36
|
import { EventBus } from "../internal/event-bus.js";
|
|
36
37
|
import { tempId as newTempId } from "../internal/temp-id.js";
|
|
38
|
+
import {
|
|
39
|
+
parseAnnouncementOut,
|
|
40
|
+
parseBulkDeleteBySenderResult,
|
|
41
|
+
parseBulkDeleteResult,
|
|
42
|
+
parseFileCreateResponse,
|
|
43
|
+
parseFileOut,
|
|
44
|
+
parseInviteTokenOut,
|
|
45
|
+
parseMessageOut,
|
|
46
|
+
parseRoomMemberOut,
|
|
47
|
+
parseRoomUnread,
|
|
48
|
+
} from "../generated/rest.js";
|
|
37
49
|
// Note: previously used `utils/debounce`, replaced with a hand-rolled
|
|
38
50
|
// timer in Room so we can also expose `flushMarkRead()` — the util has
|
|
39
51
|
// no flush primitive.
|
|
40
52
|
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
// (
|
|
44
|
-
// the camelCase
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
id:
|
|
59
|
-
kind:
|
|
60
|
-
mime:
|
|
61
|
-
size:
|
|
62
|
-
name
|
|
63
|
-
width
|
|
64
|
-
height
|
|
65
|
-
duration_sec
|
|
66
|
-
|
|
67
|
-
forward_blocked
|
|
68
|
-
}
|
|
69
|
-
files?: Array<{
|
|
70
|
-
id: string;
|
|
71
|
-
kind: "image" | "video" | "file";
|
|
72
|
-
mime: string;
|
|
73
|
-
size: number;
|
|
74
|
-
name?: string | null;
|
|
75
|
-
width?: number | null;
|
|
76
|
-
height?: number | null;
|
|
77
|
-
duration_sec?: number | null;
|
|
78
|
-
thumbnail_status: "pending" | "ready" | "failed" | "skipped";
|
|
79
|
-
forward_blocked?: boolean;
|
|
80
|
-
}> | null;
|
|
81
|
-
reply_to_id?: string | null;
|
|
82
|
-
reactions?: Array<{ key: string; user_ids: string[]; updated_at: number }>;
|
|
83
|
-
created_at: string;
|
|
84
|
-
created_at_ms: number;
|
|
85
|
-
updated_at?: string | null;
|
|
86
|
-
updated_at_ms?: number | null;
|
|
87
|
-
deleted_at?: string | null;
|
|
88
|
-
deleted_by_user_id?: string | null;
|
|
89
|
-
delete_scope?: MessageOut["deleteScope"] | null;
|
|
90
|
-
edited_count?: number;
|
|
53
|
+
// Inline file metadata as it rides on a message (chat_receive / REST
|
|
54
|
+
// history). The generated REST `MessageOut` leaves `file`/`files` free-form
|
|
55
|
+
// (the OpenAPI schema does), so we fold the snake_case wire shape down to
|
|
56
|
+
// the camelCase `FileInline` here — mirrors `FileInlineWire` in wire.ts.
|
|
57
|
+
function toFileInline(f: {
|
|
58
|
+
id: string;
|
|
59
|
+
kind: FileKind;
|
|
60
|
+
mime: string;
|
|
61
|
+
size: number;
|
|
62
|
+
name?: string | null;
|
|
63
|
+
width?: number | null;
|
|
64
|
+
height?: number | null;
|
|
65
|
+
duration_sec?: number | null;
|
|
66
|
+
thumbnail_status: FileMeta["thumbnailStatus"];
|
|
67
|
+
forward_blocked?: boolean;
|
|
68
|
+
}): FileMeta {
|
|
69
|
+
return {
|
|
70
|
+
id: f.id,
|
|
71
|
+
kind: f.kind,
|
|
72
|
+
mime: f.mime,
|
|
73
|
+
size: f.size,
|
|
74
|
+
name: f.name ?? null,
|
|
75
|
+
width: f.width ?? null,
|
|
76
|
+
height: f.height ?? null,
|
|
77
|
+
durationSec: f.duration_sec ?? null,
|
|
78
|
+
thumbnailStatus: f.thumbnail_status,
|
|
79
|
+
forwardBlocked: Boolean(f.forward_blocked),
|
|
80
|
+
};
|
|
91
81
|
}
|
|
92
82
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
83
|
+
// Enhance the GENERATED `parseMessageOut` with the two client-side
|
|
84
|
+
// extras it can't derive from the OpenAPI schema: parse `file`/`files`
|
|
85
|
+
// into the camelCase `FileInline` shape, and lift sender-attached
|
|
86
|
+
// `_customMeta` out of `data` (Phase D #11). Everything else — key
|
|
87
|
+
// renames, nested sender/reactions — comes straight from the codegen.
|
|
88
|
+
function toMessageOut(j: unknown): MessageOut {
|
|
89
|
+
const base = parseMessageOut(j);
|
|
90
|
+
const raw = j as {
|
|
91
|
+
file?: Parameters<typeof toFileInline>[0] | null;
|
|
92
|
+
files?: Parameters<typeof toFileInline>[0][] | null;
|
|
93
|
+
};
|
|
94
|
+
const dataObj = base.data ?? null;
|
|
98
95
|
const customMeta =
|
|
99
96
|
dataObj && typeof dataObj === "object" && "_customMeta" in dataObj
|
|
100
97
|
? ((dataObj as Record<string, unknown>)._customMeta as
|
|
@@ -102,66 +99,12 @@ function toMessageOut(r: RawMessageOut): MessageOut {
|
|
|
102
99
|
| null)
|
|
103
100
|
: null;
|
|
104
101
|
return {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
userId: r.sender.user_id,
|
|
111
|
-
nickname: r.sender.nickname ?? null,
|
|
112
|
-
profileImageUrl: r.sender.profile_image_url ?? null,
|
|
113
|
-
}
|
|
114
|
-
: null,
|
|
115
|
-
senderId: r.sender_id ?? null,
|
|
116
|
-
messageType: r.message_type,
|
|
117
|
-
customType: r.custom_type ?? null,
|
|
118
|
-
content: r.content ?? null,
|
|
119
|
-
data: dataObj,
|
|
120
|
-
meta: r.meta ?? null,
|
|
102
|
+
...base,
|
|
103
|
+
file: raw.file ? toFileInline(raw.file) : null,
|
|
104
|
+
files:
|
|
105
|
+
raw.files && raw.files.length > 0 ? raw.files.map(toFileInline) : null,
|
|
106
|
+
reactions: base.reactions ?? [],
|
|
121
107
|
customMeta,
|
|
122
|
-
fileId: r.file_id ?? null,
|
|
123
|
-
file: r.file
|
|
124
|
-
? {
|
|
125
|
-
id: r.file.id,
|
|
126
|
-
kind: r.file.kind,
|
|
127
|
-
mime: r.file.mime,
|
|
128
|
-
size: r.file.size,
|
|
129
|
-
name: r.file.name ?? null,
|
|
130
|
-
width: r.file.width ?? null,
|
|
131
|
-
height: r.file.height ?? null,
|
|
132
|
-
durationSec: r.file.duration_sec ?? null,
|
|
133
|
-
thumbnailStatus: r.file.thumbnail_status,
|
|
134
|
-
forwardBlocked: Boolean(r.file.forward_blocked),
|
|
135
|
-
}
|
|
136
|
-
: null,
|
|
137
|
-
files: r.files && r.files.length > 0
|
|
138
|
-
? r.files.map((f) => ({
|
|
139
|
-
id: f.id,
|
|
140
|
-
kind: f.kind,
|
|
141
|
-
mime: f.mime,
|
|
142
|
-
size: f.size,
|
|
143
|
-
name: f.name ?? null,
|
|
144
|
-
width: f.width ?? null,
|
|
145
|
-
height: f.height ?? null,
|
|
146
|
-
durationSec: f.duration_sec ?? null,
|
|
147
|
-
thumbnailStatus: f.thumbnail_status,
|
|
148
|
-
forwardBlocked: Boolean(f.forward_blocked),
|
|
149
|
-
}))
|
|
150
|
-
: null,
|
|
151
|
-
replyToId: r.reply_to_id ?? null,
|
|
152
|
-
reactions: (r.reactions ?? []).map((x) => ({
|
|
153
|
-
key: x.key,
|
|
154
|
-
userIds: x.user_ids,
|
|
155
|
-
updatedAt: x.updated_at,
|
|
156
|
-
})),
|
|
157
|
-
createdAt: r.created_at,
|
|
158
|
-
createdAtMs: r.created_at_ms,
|
|
159
|
-
updatedAt: r.updated_at ?? null,
|
|
160
|
-
updatedAtMs: r.updated_at_ms ?? null,
|
|
161
|
-
deletedAt: r.deleted_at ?? null,
|
|
162
|
-
deletedByUserId: r.deleted_by_user_id ?? null,
|
|
163
|
-
deleteScope: r.delete_scope ?? null,
|
|
164
|
-
editedCount: r.edited_count ?? 0,
|
|
165
108
|
};
|
|
166
109
|
}
|
|
167
110
|
|
|
@@ -446,25 +389,24 @@ export class Room {
|
|
|
446
389
|
// Step 1 — presign. The backend assigns the file_id and the S3 key
|
|
447
390
|
// and returns the URL we should PUT to. ``Content-Type`` is bound
|
|
448
391
|
// into the signature, so the PUT must echo it.
|
|
449
|
-
const
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
// other users can't forward this file to a different room.
|
|
461
|
-
forward_blocked: Boolean(opts.forwardBlocked),
|
|
462
|
-
});
|
|
392
|
+
const created = parseFileCreateResponse(
|
|
393
|
+
await this.http.request<unknown>("POST", "/api/v1/files", {
|
|
394
|
+
kind,
|
|
395
|
+
mime: file.type || "application/octet-stream",
|
|
396
|
+
size: file.size,
|
|
397
|
+
original_name: name,
|
|
398
|
+
// 파일 전달 제한 — uploader-set at presign time. When true,
|
|
399
|
+
// other users can't forward this file to a different room.
|
|
400
|
+
forward_blocked: Boolean(opts.forwardBlocked),
|
|
401
|
+
}),
|
|
402
|
+
);
|
|
463
403
|
const presigned: FilePresignResponse = {
|
|
464
|
-
fileId:
|
|
465
|
-
uploadUrl:
|
|
466
|
-
headers
|
|
467
|
-
|
|
404
|
+
fileId: created.fileId,
|
|
405
|
+
uploadUrl: created.uploadUrl,
|
|
406
|
+
// Presigned S3 headers are always string-valued; the generated
|
|
407
|
+
// model types the JSON object loosely as Record<string, unknown>.
|
|
408
|
+
headers: created.headers as Record<string, string>,
|
|
409
|
+
expiresAtMs: created.expiresAtMs,
|
|
468
410
|
};
|
|
469
411
|
|
|
470
412
|
// Step 2 — S3 PUT via XHR so the caller can show a progress bar.
|
|
@@ -542,24 +484,21 @@ export class Room {
|
|
|
542
484
|
: "file");
|
|
543
485
|
const name = opts.name ?? file.name;
|
|
544
486
|
|
|
545
|
-
const
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
original_name: name,
|
|
555
|
-
forward_blocked: Boolean(opts.forwardBlocked),
|
|
556
|
-
});
|
|
487
|
+
const created = parseFileCreateResponse(
|
|
488
|
+
await this.http.request<unknown>("POST", "/api/v1/files", {
|
|
489
|
+
kind,
|
|
490
|
+
mime: file.type || "application/octet-stream",
|
|
491
|
+
size: file.size,
|
|
492
|
+
original_name: name,
|
|
493
|
+
forward_blocked: Boolean(opts.forwardBlocked),
|
|
494
|
+
}),
|
|
495
|
+
);
|
|
557
496
|
|
|
558
497
|
await new Promise<void>((resolve, reject) => {
|
|
559
498
|
const xhr = new XMLHttpRequest();
|
|
560
|
-
xhr.open("PUT",
|
|
561
|
-
for (const [k, v] of Object.entries(
|
|
562
|
-
xhr.setRequestHeader(k, v);
|
|
499
|
+
xhr.open("PUT", created.uploadUrl);
|
|
500
|
+
for (const [k, v] of Object.entries(created.headers)) {
|
|
501
|
+
xhr.setRequestHeader(k, v as string);
|
|
563
502
|
}
|
|
564
503
|
if (opts.onProgress) {
|
|
565
504
|
xhr.upload.addEventListener("progress", (e) => {
|
|
@@ -585,9 +524,9 @@ export class Room {
|
|
|
585
524
|
});
|
|
586
525
|
|
|
587
526
|
await this.http.request<FileMeta>(
|
|
588
|
-
"POST", `/api/v1/files/${
|
|
527
|
+
"POST", `/api/v1/files/${created.fileId}/commit`, {},
|
|
589
528
|
);
|
|
590
|
-
return
|
|
529
|
+
return created.fileId;
|
|
591
530
|
}
|
|
592
531
|
|
|
593
532
|
/**
|
|
@@ -619,18 +558,7 @@ export class Room {
|
|
|
619
558
|
message_id: MessageIdStr;
|
|
620
559
|
sender_id: string | null;
|
|
621
560
|
created_at_ms: number;
|
|
622
|
-
file:
|
|
623
|
-
id: MessageIdStr;
|
|
624
|
-
kind: FileKind;
|
|
625
|
-
mime: string;
|
|
626
|
-
size_bytes: number;
|
|
627
|
-
original_name: string | null;
|
|
628
|
-
width: number | null;
|
|
629
|
-
height: number | null;
|
|
630
|
-
duration_sec: number | null;
|
|
631
|
-
thumbnail_status: FileMeta["thumbnailStatus"];
|
|
632
|
-
forward_blocked?: boolean;
|
|
633
|
-
};
|
|
561
|
+
file: unknown;
|
|
634
562
|
}>;
|
|
635
563
|
next_cursor: MessageIdStr | null;
|
|
636
564
|
has_more: boolean;
|
|
@@ -644,23 +572,29 @@ export class Room {
|
|
|
644
572
|
},
|
|
645
573
|
);
|
|
646
574
|
return {
|
|
647
|
-
items: raw.items.map((r) =>
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
file
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
575
|
+
items: raw.items.map((r) => {
|
|
576
|
+
// Per-file payload is the generated ``FileOut`` shape — parse it
|
|
577
|
+
// with the codegen'd converter, then fold to the leaner
|
|
578
|
+
// ``FileMeta`` (= FileInline) the SDK surfaces.
|
|
579
|
+
const f = parseFileOut(r.file);
|
|
580
|
+
return {
|
|
581
|
+
messageId: r.message_id,
|
|
582
|
+
senderId: r.sender_id,
|
|
583
|
+
createdAtMs: r.created_at_ms,
|
|
584
|
+
file: {
|
|
585
|
+
id: f.id,
|
|
586
|
+
kind: f.kind,
|
|
587
|
+
mime: f.mime,
|
|
588
|
+
size: f.sizeBytes,
|
|
589
|
+
name: f.originalName ?? null,
|
|
590
|
+
width: f.width ?? null,
|
|
591
|
+
height: f.height ?? null,
|
|
592
|
+
durationSec: f.durationSec ?? null,
|
|
593
|
+
thumbnailStatus: f.thumbnailStatus,
|
|
594
|
+
forwardBlocked: Boolean(f.forwardBlocked),
|
|
595
|
+
},
|
|
596
|
+
};
|
|
597
|
+
}),
|
|
664
598
|
nextCursor: raw.next_cursor,
|
|
665
599
|
hasMore: raw.has_more,
|
|
666
600
|
};
|
|
@@ -706,9 +640,10 @@ export class Room {
|
|
|
706
640
|
messageId: MessageIdStr,
|
|
707
641
|
patch: { content?: string; data?: Record<string, unknown>; meta?: Record<string, unknown> },
|
|
708
642
|
): Promise<MessageOut> {
|
|
709
|
-
|
|
643
|
+
const raw = await this.http.request<unknown>(
|
|
710
644
|
"PUT", `/api/v1/rooms/${this.roomId}/messages/${messageId}`, patch,
|
|
711
645
|
);
|
|
646
|
+
return toMessageOut(raw);
|
|
712
647
|
}
|
|
713
648
|
|
|
714
649
|
async delete(messageId: MessageIdStr, scope: DeleteScope = "ALL"): Promise<void> {
|
|
@@ -733,11 +668,40 @@ export class Room {
|
|
|
733
668
|
* Other members get a `MEMBER_LEFT` [`roomEvent`](../reference/noverachat.md#events).
|
|
734
669
|
* Idempotent if already a non-member; throws 409 if the caller was KICKED
|
|
735
670
|
* (ask an operator to restore first). For GROUP/SUPER_GROUP, re-entry needs
|
|
736
|
-
* an operator add; for ONE, re-creating the 1:1 rejoins with history kept.
|
|
737
|
-
|
|
671
|
+
* an operator add; for ONE, re-creating the 1:1 rejoins with history kept.
|
|
672
|
+
*
|
|
673
|
+
* Optional `deleteMessages` — atomically clean up chat history as part
|
|
674
|
+
* of leaving. Default is `"none"` (messages survive so the room's
|
|
675
|
+
* history keeps making sense to remaining members).
|
|
676
|
+
* - `"none"` — leave only.
|
|
677
|
+
* - `"mine"` — soft-delete every message YOU sent, then leave. Other
|
|
678
|
+
* members' messages are untouched. Broadcasts
|
|
679
|
+
* `messages_cleared_by_sender` so connected clients hide
|
|
680
|
+
* them locally.
|
|
681
|
+
* - `"all"` — wipe every message in the room, then leave. Subject to
|
|
682
|
+
* the app's `allow_member_bulk_delete` gate and per-room
|
|
683
|
+
* OPERATOR check — a plain member gets 403 back. */
|
|
684
|
+
async leave(options?: { deleteMessages?: "none" | "mine" | "all" }): Promise<void> {
|
|
685
|
+
const mode = options?.deleteMessages ?? "none";
|
|
686
|
+
const query = mode === "none"
|
|
687
|
+
? ""
|
|
688
|
+
: `?delete_messages=${encodeURIComponent(mode)}`;
|
|
738
689
|
await this.http.request<void>(
|
|
739
|
-
"POST", `/api/v1/rooms/${this.roomId}/leave`,
|
|
690
|
+
"POST", `/api/v1/rooms/${this.roomId}/leave${query}`,
|
|
691
|
+
);
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
/** Delete every message YOU sent in this room. Standalone version of
|
|
695
|
+
* `leave({ deleteMessages: "mine" })` — useful when a user wants to
|
|
696
|
+
* scrub their history without actually leaving. Other members'
|
|
697
|
+
* messages are untouched; broadcasts `messages_cleared_by_sender`
|
|
698
|
+
* so connected clients hide the vanished ones locally.
|
|
699
|
+
* No OPERATOR gate — you own your messages. */
|
|
700
|
+
async deleteMyMessages(): Promise<{ cleared: number }> {
|
|
701
|
+
const raw = await this.http.request<unknown>(
|
|
702
|
+
"DELETE", `/api/v1/rooms/${this.roomId}/messages/mine`,
|
|
740
703
|
);
|
|
704
|
+
return parseBulkDeleteBySenderResult(raw);
|
|
741
705
|
}
|
|
742
706
|
|
|
743
707
|
/**
|
|
@@ -769,14 +733,10 @@ export class Room {
|
|
|
769
733
|
cleared: number;
|
|
770
734
|
upToMessageId: MessageIdStr | null;
|
|
771
735
|
}> {
|
|
772
|
-
const raw = await this.http.request<
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
return {
|
|
777
|
-
cleared: raw.cleared,
|
|
778
|
-
upToMessageId: raw.up_to_message_id,
|
|
779
|
-
};
|
|
736
|
+
const raw = await this.http.request<unknown>(
|
|
737
|
+
"DELETE", `/api/v1/rooms/${this.roomId}/messages`,
|
|
738
|
+
);
|
|
739
|
+
return parseBulkDeleteResult(raw);
|
|
780
740
|
}
|
|
781
741
|
|
|
782
742
|
async react(messageId: MessageIdStr, key: string): Promise<void> {
|
|
@@ -801,7 +761,7 @@ export class Room {
|
|
|
801
761
|
// camelCase contract here so callers (and the demo's renderMessage)
|
|
802
762
|
// don't see `undefined` on `messageId` / `senderId` / etc.
|
|
803
763
|
const raw = await this.http.request<{
|
|
804
|
-
items:
|
|
764
|
+
items: unknown[]; next_cursor: string | null; has_more: boolean;
|
|
805
765
|
}>("GET", `/api/v1/rooms/${this.roomId}/messages`, undefined, {
|
|
806
766
|
since: String(sinceMessageId), limit,
|
|
807
767
|
});
|
|
@@ -824,7 +784,7 @@ export class Room {
|
|
|
824
784
|
hasMore: boolean;
|
|
825
785
|
}> {
|
|
826
786
|
const raw = await this.http.request<{
|
|
827
|
-
items:
|
|
787
|
+
items: unknown[]; next_cursor: string | null; has_more: boolean;
|
|
828
788
|
}>("GET", `/api/v1/rooms/${this.roomId}/messages`, undefined, { limit });
|
|
829
789
|
return {
|
|
830
790
|
items: raw.items.map(toMessageOut),
|
|
@@ -845,7 +805,7 @@ export class Room {
|
|
|
845
805
|
hasMore: boolean;
|
|
846
806
|
}> {
|
|
847
807
|
const raw = await this.http.request<{
|
|
848
|
-
items:
|
|
808
|
+
items: unknown[]; next_cursor: string | null; has_more: boolean;
|
|
849
809
|
}>("GET", `/api/v1/rooms/${this.roomId}/messages`, undefined, {
|
|
850
810
|
before: beforeMessageId, limit,
|
|
851
811
|
});
|
|
@@ -881,7 +841,7 @@ export class Room {
|
|
|
881
841
|
};
|
|
882
842
|
if (opts?.before) params.before = opts.before;
|
|
883
843
|
const raw = await this.http.request<{
|
|
884
|
-
items:
|
|
844
|
+
items: unknown[]; next_cursor: string | null; has_more: boolean;
|
|
885
845
|
}>("GET", `/api/v1/rooms/${this.roomId}/messages/search`, undefined, params);
|
|
886
846
|
return {
|
|
887
847
|
items: raw.items.map(toMessageOut),
|
|
@@ -895,40 +855,11 @@ export class Room {
|
|
|
895
855
|
* `room.deleteAllMessages()`. Server gates by membership for non-public
|
|
896
856
|
* rooms, so the caller must already be a member to read this.
|
|
897
857
|
*/
|
|
898
|
-
async listMembers(): Promise<
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
lastReadMessageId: MessageIdStr | null;
|
|
904
|
-
joinedAt: string;
|
|
905
|
-
/** Cluster-wide presence dot. `null` when the server didn't resolve it. */
|
|
906
|
-
isOnline: boolean | null;
|
|
907
|
-
/** Per-room mute expiry. `null` = not muted. A far-future sentinel
|
|
908
|
-
* (year 9999) means the operator picked "indefinite" — clients
|
|
909
|
-
* should render that as "무기한" instead of a real timestamp. */
|
|
910
|
-
mutedUntil: string | null;
|
|
911
|
-
}>> {
|
|
912
|
-
const raw = await this.http.request<Array<{
|
|
913
|
-
user_id: string;
|
|
914
|
-
nickname?: string | null;
|
|
915
|
-
role: "OPERATOR" | "MEMBER" | "KICKED";
|
|
916
|
-
push_trigger: "ALL" | "MENTION" | "OFF";
|
|
917
|
-
last_read_message_id: string | null;
|
|
918
|
-
joined_at: string;
|
|
919
|
-
is_online?: boolean | null;
|
|
920
|
-
muted_until?: string | null;
|
|
921
|
-
}>>("GET", `/api/v1/rooms/${this.roomId}/members`);
|
|
922
|
-
return raw.map((m) => ({
|
|
923
|
-
userId: m.user_id,
|
|
924
|
-
nickname: m.nickname ?? null,
|
|
925
|
-
role: m.role,
|
|
926
|
-
pushTrigger: m.push_trigger,
|
|
927
|
-
lastReadMessageId: m.last_read_message_id,
|
|
928
|
-
joinedAt: m.joined_at,
|
|
929
|
-
isOnline: m.is_online ?? null,
|
|
930
|
-
mutedUntil: m.muted_until ?? null,
|
|
931
|
-
}));
|
|
858
|
+
async listMembers(): Promise<RoomMemberOut[]> {
|
|
859
|
+
const raw = await this.http.request<unknown[]>(
|
|
860
|
+
"GET", `/api/v1/rooms/${this.roomId}/members`,
|
|
861
|
+
);
|
|
862
|
+
return raw.map((m) => parseRoomMemberOut(m));
|
|
932
863
|
}
|
|
933
864
|
|
|
934
865
|
// -------- mute --------
|
|
@@ -958,18 +889,10 @@ export class Room {
|
|
|
958
889
|
* newer than the caller's `read_watermark`, plus the room's
|
|
959
890
|
* `last_message_id` so the UI can show a "•" dot regardless of count. */
|
|
960
891
|
async unread(): Promise<RoomUnread> {
|
|
961
|
-
const raw = await this.http.request<
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
last_message_id: string | null;
|
|
966
|
-
}>("GET", `/api/v1/rooms/${this.roomId}/unread`);
|
|
967
|
-
return {
|
|
968
|
-
roomId: raw.room_id,
|
|
969
|
-
unreadCount: raw.unread_count,
|
|
970
|
-
lastReadMessageId: raw.last_read_message_id,
|
|
971
|
-
lastMessageId: raw.last_message_id,
|
|
972
|
-
};
|
|
892
|
+
const raw = await this.http.request<unknown>(
|
|
893
|
+
"GET", `/api/v1/rooms/${this.roomId}/unread`,
|
|
894
|
+
);
|
|
895
|
+
return parseRoomUnread(raw);
|
|
973
896
|
}
|
|
974
897
|
|
|
975
898
|
// -------- freeze --------
|
|
@@ -1007,80 +930,32 @@ export class Room {
|
|
|
1007
930
|
// -------- announcements --------
|
|
1008
931
|
|
|
1009
932
|
async listAnnouncements(): Promise<Announcement[]> {
|
|
1010
|
-
const raw = await this.http.request<
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
created_by: string;
|
|
1015
|
-
created_at: string;
|
|
1016
|
-
updated_at: string;
|
|
1017
|
-
}>>("GET", `/api/v1/rooms/${this.roomId}/announcements`);
|
|
1018
|
-
return raw.map((a) => ({
|
|
1019
|
-
id: a.id,
|
|
1020
|
-
roomId: a.room_id,
|
|
1021
|
-
content: a.content,
|
|
1022
|
-
createdBy: a.created_by,
|
|
1023
|
-
createdAt: a.created_at,
|
|
1024
|
-
updatedAt: a.updated_at,
|
|
1025
|
-
}));
|
|
933
|
+
const raw = await this.http.request<unknown[]>(
|
|
934
|
+
"GET", `/api/v1/rooms/${this.roomId}/announcements`,
|
|
935
|
+
);
|
|
936
|
+
return raw.map((a) => parseAnnouncementOut(a));
|
|
1026
937
|
}
|
|
1027
938
|
|
|
1028
939
|
async getCurrentAnnouncement(): Promise<Announcement | null> {
|
|
1029
|
-
const raw = await this.http.request<
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
content: string;
|
|
1033
|
-
created_by: string;
|
|
1034
|
-
created_at: string;
|
|
1035
|
-
updated_at: string;
|
|
1036
|
-
} | null>("GET", `/api/v1/rooms/${this.roomId}/announcements/current`);
|
|
940
|
+
const raw = await this.http.request<unknown>(
|
|
941
|
+
"GET", `/api/v1/rooms/${this.roomId}/announcements/current`,
|
|
942
|
+
);
|
|
1037
943
|
if (!raw) return null;
|
|
1038
|
-
return
|
|
1039
|
-
id: raw.id,
|
|
1040
|
-
roomId: raw.room_id,
|
|
1041
|
-
content: raw.content,
|
|
1042
|
-
createdBy: raw.created_by,
|
|
1043
|
-
createdAt: raw.created_at,
|
|
1044
|
-
updatedAt: raw.updated_at,
|
|
1045
|
-
};
|
|
944
|
+
return parseAnnouncementOut(raw);
|
|
1046
945
|
}
|
|
1047
946
|
|
|
1048
947
|
async postAnnouncement(content: string): Promise<Announcement> {
|
|
1049
|
-
const raw = await this.http.request<
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
created_by: string;
|
|
1054
|
-
created_at: string;
|
|
1055
|
-
updated_at: string;
|
|
1056
|
-
}>("POST", `/api/v1/rooms/${this.roomId}/announcements`, { content });
|
|
1057
|
-
return {
|
|
1058
|
-
id: raw.id,
|
|
1059
|
-
roomId: raw.room_id,
|
|
1060
|
-
content: raw.content,
|
|
1061
|
-
createdBy: raw.created_by,
|
|
1062
|
-
createdAt: raw.created_at,
|
|
1063
|
-
updatedAt: raw.updated_at,
|
|
1064
|
-
};
|
|
948
|
+
const raw = await this.http.request<unknown>(
|
|
949
|
+
"POST", `/api/v1/rooms/${this.roomId}/announcements`, { content },
|
|
950
|
+
);
|
|
951
|
+
return parseAnnouncementOut(raw);
|
|
1065
952
|
}
|
|
1066
953
|
|
|
1067
954
|
async updateAnnouncement(id: number, content: string): Promise<Announcement> {
|
|
1068
|
-
const raw = await this.http.request<
|
|
1069
|
-
id
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
created_by: string;
|
|
1073
|
-
created_at: string;
|
|
1074
|
-
updated_at: string;
|
|
1075
|
-
}>("PATCH", `/api/v1/rooms/${this.roomId}/announcements/${id}`, { content });
|
|
1076
|
-
return {
|
|
1077
|
-
id: raw.id,
|
|
1078
|
-
roomId: raw.room_id,
|
|
1079
|
-
content: raw.content,
|
|
1080
|
-
createdBy: raw.created_by,
|
|
1081
|
-
createdAt: raw.created_at,
|
|
1082
|
-
updatedAt: raw.updated_at,
|
|
1083
|
-
};
|
|
955
|
+
const raw = await this.http.request<unknown>(
|
|
956
|
+
"PATCH", `/api/v1/rooms/${this.roomId}/announcements/${id}`, { content },
|
|
957
|
+
);
|
|
958
|
+
return parseAnnouncementOut(raw);
|
|
1084
959
|
}
|
|
1085
960
|
|
|
1086
961
|
async deleteAnnouncement(id: number): Promise<void> {
|
|
@@ -1111,51 +986,19 @@ export class Room {
|
|
|
1111
986
|
const body: Record<string, unknown> = {};
|
|
1112
987
|
if (opts && "expiresInHours" in opts) body.expires_in_hours = opts.expiresInHours;
|
|
1113
988
|
if (opts && "maxUses" in opts) body.max_uses = opts.maxUses;
|
|
1114
|
-
const raw = await this.http.request<
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
created_at: string;
|
|
1119
|
-
expires_at: string | null;
|
|
1120
|
-
max_uses: number | null;
|
|
1121
|
-
used_count: number;
|
|
1122
|
-
revoked_at: string | null;
|
|
1123
|
-
}>("POST", `/api/v1/rooms/${this.roomId}/invites`, body);
|
|
1124
|
-
return {
|
|
1125
|
-
token: raw.token,
|
|
1126
|
-
roomId: raw.room_id,
|
|
1127
|
-
createdBy: raw.created_by,
|
|
1128
|
-
createdAt: raw.created_at,
|
|
1129
|
-
expiresAt: raw.expires_at,
|
|
1130
|
-
maxUses: raw.max_uses,
|
|
1131
|
-
usedCount: raw.used_count,
|
|
1132
|
-
revokedAt: raw.revoked_at,
|
|
1133
|
-
};
|
|
989
|
+
const raw = await this.http.request<unknown>(
|
|
990
|
+
"POST", `/api/v1/rooms/${this.roomId}/invites`, body,
|
|
991
|
+
);
|
|
992
|
+
return parseInviteTokenOut(raw);
|
|
1134
993
|
}
|
|
1135
994
|
|
|
1136
995
|
/** OPERATOR-only: list all invite tokens for this room (including
|
|
1137
996
|
* revoked / expired / exhausted), newest first. */
|
|
1138
997
|
async listInvites(): Promise<InviteToken[]> {
|
|
1139
|
-
const raw = await this.http.request<
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
created_at: string;
|
|
1144
|
-
expires_at: string | null;
|
|
1145
|
-
max_uses: number | null;
|
|
1146
|
-
used_count: number;
|
|
1147
|
-
revoked_at: string | null;
|
|
1148
|
-
}>>("GET", `/api/v1/rooms/${this.roomId}/invites`);
|
|
1149
|
-
return raw.map((r) => ({
|
|
1150
|
-
token: r.token,
|
|
1151
|
-
roomId: r.room_id,
|
|
1152
|
-
createdBy: r.created_by,
|
|
1153
|
-
createdAt: r.created_at,
|
|
1154
|
-
expiresAt: r.expires_at,
|
|
1155
|
-
maxUses: r.max_uses,
|
|
1156
|
-
usedCount: r.used_count,
|
|
1157
|
-
revokedAt: r.revoked_at,
|
|
1158
|
-
}));
|
|
998
|
+
const raw = await this.http.request<unknown[]>(
|
|
999
|
+
"GET", `/api/v1/rooms/${this.roomId}/invites`,
|
|
1000
|
+
);
|
|
1001
|
+
return raw.map((r) => parseInviteTokenOut(r));
|
|
1159
1002
|
}
|
|
1160
1003
|
|
|
1161
1004
|
/** OPERATOR-only: revoke a token immediately. Idempotent — revoking
|