@noverachat/sdk-web 0.0.3 → 0.2.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 +485 -258
- package/dist/index.d.cts +212 -169
- package/dist/index.d.ts +212 -169
- package/dist/index.js +485 -258
- package/package.json +1 -1
- package/src/client.ts +50 -157
- package/src/features/room.ts +274 -344
- package/src/generated/rest.ts +529 -0
- package/src/index.ts +4 -0
- package/src/types.ts +62 -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,69 @@ 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
|
+
parseJoinRequestCreateResponse,
|
|
46
|
+
parseJoinRequestListResponse,
|
|
47
|
+
parseMessageOut,
|
|
48
|
+
parseRoomMemberOut,
|
|
49
|
+
parseRoomUnread,
|
|
50
|
+
} from "../generated/rest.js";
|
|
51
|
+
import type {
|
|
52
|
+
JoinRequestCreateResponse,
|
|
53
|
+
JoinRequestListResponse,
|
|
54
|
+
} from "../generated/rest.js";
|
|
37
55
|
// Note: previously used `utils/debounce`, replaced with a hand-rolled
|
|
38
56
|
// timer in Room so we can also expose `flushMarkRead()` — the util has
|
|
39
57
|
// no flush primitive.
|
|
40
58
|
|
|
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;
|
|
59
|
+
// Inline file metadata as it rides on a message (chat_receive / REST
|
|
60
|
+
// history). The generated REST `MessageOut` leaves `file`/`files` free-form
|
|
61
|
+
// (the OpenAPI schema does), so we fold the snake_case wire shape down to
|
|
62
|
+
// the camelCase `FileInline` here — mirrors `FileInlineWire` in wire.ts.
|
|
63
|
+
function toFileInline(f: {
|
|
64
|
+
id: string;
|
|
65
|
+
kind: FileKind;
|
|
66
|
+
mime: string;
|
|
67
|
+
size: number;
|
|
68
|
+
name?: string | null;
|
|
69
|
+
width?: number | null;
|
|
70
|
+
height?: number | null;
|
|
71
|
+
duration_sec?: number | null;
|
|
72
|
+
thumbnail_status: FileMeta["thumbnailStatus"];
|
|
73
|
+
forward_blocked?: boolean;
|
|
74
|
+
}): FileMeta {
|
|
75
|
+
return {
|
|
76
|
+
id: f.id,
|
|
77
|
+
kind: f.kind,
|
|
78
|
+
mime: f.mime,
|
|
79
|
+
size: f.size,
|
|
80
|
+
name: f.name ?? null,
|
|
81
|
+
width: f.width ?? null,
|
|
82
|
+
height: f.height ?? null,
|
|
83
|
+
durationSec: f.duration_sec ?? null,
|
|
84
|
+
thumbnailStatus: f.thumbnail_status,
|
|
85
|
+
forwardBlocked: Boolean(f.forward_blocked),
|
|
86
|
+
};
|
|
91
87
|
}
|
|
92
88
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
89
|
+
// Enhance the GENERATED `parseMessageOut` with the two client-side
|
|
90
|
+
// extras it can't derive from the OpenAPI schema: parse `file`/`files`
|
|
91
|
+
// into the camelCase `FileInline` shape, and lift sender-attached
|
|
92
|
+
// `_customMeta` out of `data` (Phase D #11). Everything else — key
|
|
93
|
+
// renames, nested sender/reactions — comes straight from the codegen.
|
|
94
|
+
function toMessageOut(j: unknown): MessageOut {
|
|
95
|
+
const base = parseMessageOut(j);
|
|
96
|
+
const raw = j as {
|
|
97
|
+
file?: Parameters<typeof toFileInline>[0] | null;
|
|
98
|
+
files?: Parameters<typeof toFileInline>[0][] | null;
|
|
99
|
+
};
|
|
100
|
+
const dataObj = base.data ?? null;
|
|
98
101
|
const customMeta =
|
|
99
102
|
dataObj && typeof dataObj === "object" && "_customMeta" in dataObj
|
|
100
103
|
? ((dataObj as Record<string, unknown>)._customMeta as
|
|
@@ -102,66 +105,12 @@ function toMessageOut(r: RawMessageOut): MessageOut {
|
|
|
102
105
|
| null)
|
|
103
106
|
: null;
|
|
104
107
|
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,
|
|
108
|
+
...base,
|
|
109
|
+
file: raw.file ? toFileInline(raw.file) : null,
|
|
110
|
+
files:
|
|
111
|
+
raw.files && raw.files.length > 0 ? raw.files.map(toFileInline) : null,
|
|
112
|
+
reactions: base.reactions ?? [],
|
|
121
113
|
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
114
|
};
|
|
166
115
|
}
|
|
167
116
|
|
|
@@ -446,25 +395,24 @@ export class Room {
|
|
|
446
395
|
// Step 1 — presign. The backend assigns the file_id and the S3 key
|
|
447
396
|
// and returns the URL we should PUT to. ``Content-Type`` is bound
|
|
448
397
|
// 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
|
-
});
|
|
398
|
+
const created = parseFileCreateResponse(
|
|
399
|
+
await this.http.request<unknown>("POST", "/api/v1/files", {
|
|
400
|
+
kind,
|
|
401
|
+
mime: file.type || "application/octet-stream",
|
|
402
|
+
size: file.size,
|
|
403
|
+
original_name: name,
|
|
404
|
+
// 파일 전달 제한 — uploader-set at presign time. When true,
|
|
405
|
+
// other users can't forward this file to a different room.
|
|
406
|
+
forward_blocked: Boolean(opts.forwardBlocked),
|
|
407
|
+
}),
|
|
408
|
+
);
|
|
463
409
|
const presigned: FilePresignResponse = {
|
|
464
|
-
fileId:
|
|
465
|
-
uploadUrl:
|
|
466
|
-
headers
|
|
467
|
-
|
|
410
|
+
fileId: created.fileId,
|
|
411
|
+
uploadUrl: created.uploadUrl,
|
|
412
|
+
// Presigned S3 headers are always string-valued; the generated
|
|
413
|
+
// model types the JSON object loosely as Record<string, unknown>.
|
|
414
|
+
headers: created.headers as Record<string, string>,
|
|
415
|
+
expiresAtMs: created.expiresAtMs,
|
|
468
416
|
};
|
|
469
417
|
|
|
470
418
|
// Step 2 — S3 PUT via XHR so the caller can show a progress bar.
|
|
@@ -542,24 +490,21 @@ export class Room {
|
|
|
542
490
|
: "file");
|
|
543
491
|
const name = opts.name ?? file.name;
|
|
544
492
|
|
|
545
|
-
const
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
original_name: name,
|
|
555
|
-
forward_blocked: Boolean(opts.forwardBlocked),
|
|
556
|
-
});
|
|
493
|
+
const created = parseFileCreateResponse(
|
|
494
|
+
await this.http.request<unknown>("POST", "/api/v1/files", {
|
|
495
|
+
kind,
|
|
496
|
+
mime: file.type || "application/octet-stream",
|
|
497
|
+
size: file.size,
|
|
498
|
+
original_name: name,
|
|
499
|
+
forward_blocked: Boolean(opts.forwardBlocked),
|
|
500
|
+
}),
|
|
501
|
+
);
|
|
557
502
|
|
|
558
503
|
await new Promise<void>((resolve, reject) => {
|
|
559
504
|
const xhr = new XMLHttpRequest();
|
|
560
|
-
xhr.open("PUT",
|
|
561
|
-
for (const [k, v] of Object.entries(
|
|
562
|
-
xhr.setRequestHeader(k, v);
|
|
505
|
+
xhr.open("PUT", created.uploadUrl);
|
|
506
|
+
for (const [k, v] of Object.entries(created.headers)) {
|
|
507
|
+
xhr.setRequestHeader(k, v as string);
|
|
563
508
|
}
|
|
564
509
|
if (opts.onProgress) {
|
|
565
510
|
xhr.upload.addEventListener("progress", (e) => {
|
|
@@ -585,9 +530,9 @@ export class Room {
|
|
|
585
530
|
});
|
|
586
531
|
|
|
587
532
|
await this.http.request<FileMeta>(
|
|
588
|
-
"POST", `/api/v1/files/${
|
|
533
|
+
"POST", `/api/v1/files/${created.fileId}/commit`, {},
|
|
589
534
|
);
|
|
590
|
-
return
|
|
535
|
+
return created.fileId;
|
|
591
536
|
}
|
|
592
537
|
|
|
593
538
|
/**
|
|
@@ -619,18 +564,7 @@ export class Room {
|
|
|
619
564
|
message_id: MessageIdStr;
|
|
620
565
|
sender_id: string | null;
|
|
621
566
|
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
|
-
};
|
|
567
|
+
file: unknown;
|
|
634
568
|
}>;
|
|
635
569
|
next_cursor: MessageIdStr | null;
|
|
636
570
|
has_more: boolean;
|
|
@@ -644,23 +578,29 @@ export class Room {
|
|
|
644
578
|
},
|
|
645
579
|
);
|
|
646
580
|
return {
|
|
647
|
-
items: raw.items.map((r) =>
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
file
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
581
|
+
items: raw.items.map((r) => {
|
|
582
|
+
// Per-file payload is the generated ``FileOut`` shape — parse it
|
|
583
|
+
// with the codegen'd converter, then fold to the leaner
|
|
584
|
+
// ``FileMeta`` (= FileInline) the SDK surfaces.
|
|
585
|
+
const f = parseFileOut(r.file);
|
|
586
|
+
return {
|
|
587
|
+
messageId: r.message_id,
|
|
588
|
+
senderId: r.sender_id,
|
|
589
|
+
createdAtMs: r.created_at_ms,
|
|
590
|
+
file: {
|
|
591
|
+
id: f.id,
|
|
592
|
+
kind: f.kind,
|
|
593
|
+
mime: f.mime,
|
|
594
|
+
size: f.sizeBytes,
|
|
595
|
+
name: f.originalName ?? null,
|
|
596
|
+
width: f.width ?? null,
|
|
597
|
+
height: f.height ?? null,
|
|
598
|
+
durationSec: f.durationSec ?? null,
|
|
599
|
+
thumbnailStatus: f.thumbnailStatus,
|
|
600
|
+
forwardBlocked: Boolean(f.forwardBlocked),
|
|
601
|
+
},
|
|
602
|
+
};
|
|
603
|
+
}),
|
|
664
604
|
nextCursor: raw.next_cursor,
|
|
665
605
|
hasMore: raw.has_more,
|
|
666
606
|
};
|
|
@@ -706,9 +646,10 @@ export class Room {
|
|
|
706
646
|
messageId: MessageIdStr,
|
|
707
647
|
patch: { content?: string; data?: Record<string, unknown>; meta?: Record<string, unknown> },
|
|
708
648
|
): Promise<MessageOut> {
|
|
709
|
-
|
|
649
|
+
const raw = await this.http.request<unknown>(
|
|
710
650
|
"PUT", `/api/v1/rooms/${this.roomId}/messages/${messageId}`, patch,
|
|
711
651
|
);
|
|
652
|
+
return toMessageOut(raw);
|
|
712
653
|
}
|
|
713
654
|
|
|
714
655
|
async delete(messageId: MessageIdStr, scope: DeleteScope = "ALL"): Promise<void> {
|
|
@@ -733,11 +674,40 @@ export class Room {
|
|
|
733
674
|
* Other members get a `MEMBER_LEFT` [`roomEvent`](../reference/noverachat.md#events).
|
|
734
675
|
* Idempotent if already a non-member; throws 409 if the caller was KICKED
|
|
735
676
|
* (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
|
-
|
|
677
|
+
* an operator add; for ONE, re-creating the 1:1 rejoins with history kept.
|
|
678
|
+
*
|
|
679
|
+
* Optional `deleteMessages` — atomically clean up chat history as part
|
|
680
|
+
* of leaving. Default is `"none"` (messages survive so the room's
|
|
681
|
+
* history keeps making sense to remaining members).
|
|
682
|
+
* - `"none"` — leave only.
|
|
683
|
+
* - `"mine"` — soft-delete every message YOU sent, then leave. Other
|
|
684
|
+
* members' messages are untouched. Broadcasts
|
|
685
|
+
* `messages_cleared_by_sender` so connected clients hide
|
|
686
|
+
* them locally.
|
|
687
|
+
* - `"all"` — wipe every message in the room, then leave. Subject to
|
|
688
|
+
* the app's `allow_member_bulk_delete` gate and per-room
|
|
689
|
+
* OPERATOR check — a plain member gets 403 back. */
|
|
690
|
+
async leave(options?: { deleteMessages?: "none" | "mine" | "all" }): Promise<void> {
|
|
691
|
+
const mode = options?.deleteMessages ?? "none";
|
|
692
|
+
const query = mode === "none"
|
|
693
|
+
? ""
|
|
694
|
+
: `?delete_messages=${encodeURIComponent(mode)}`;
|
|
738
695
|
await this.http.request<void>(
|
|
739
|
-
"POST", `/api/v1/rooms/${this.roomId}/leave`,
|
|
696
|
+
"POST", `/api/v1/rooms/${this.roomId}/leave${query}`,
|
|
697
|
+
);
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
/** Delete every message YOU sent in this room. Standalone version of
|
|
701
|
+
* `leave({ deleteMessages: "mine" })` — useful when a user wants to
|
|
702
|
+
* scrub their history without actually leaving. Other members'
|
|
703
|
+
* messages are untouched; broadcasts `messages_cleared_by_sender`
|
|
704
|
+
* so connected clients hide the vanished ones locally.
|
|
705
|
+
* No OPERATOR gate — you own your messages. */
|
|
706
|
+
async deleteMyMessages(): Promise<{ cleared: number }> {
|
|
707
|
+
const raw = await this.http.request<unknown>(
|
|
708
|
+
"DELETE", `/api/v1/rooms/${this.roomId}/messages/mine`,
|
|
740
709
|
);
|
|
710
|
+
return parseBulkDeleteBySenderResult(raw);
|
|
741
711
|
}
|
|
742
712
|
|
|
743
713
|
/**
|
|
@@ -769,14 +739,10 @@ export class Room {
|
|
|
769
739
|
cleared: number;
|
|
770
740
|
upToMessageId: MessageIdStr | null;
|
|
771
741
|
}> {
|
|
772
|
-
const raw = await this.http.request<
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
return {
|
|
777
|
-
cleared: raw.cleared,
|
|
778
|
-
upToMessageId: raw.up_to_message_id,
|
|
779
|
-
};
|
|
742
|
+
const raw = await this.http.request<unknown>(
|
|
743
|
+
"DELETE", `/api/v1/rooms/${this.roomId}/messages`,
|
|
744
|
+
);
|
|
745
|
+
return parseBulkDeleteResult(raw);
|
|
780
746
|
}
|
|
781
747
|
|
|
782
748
|
async react(messageId: MessageIdStr, key: string): Promise<void> {
|
|
@@ -801,7 +767,7 @@ export class Room {
|
|
|
801
767
|
// camelCase contract here so callers (and the demo's renderMessage)
|
|
802
768
|
// don't see `undefined` on `messageId` / `senderId` / etc.
|
|
803
769
|
const raw = await this.http.request<{
|
|
804
|
-
items:
|
|
770
|
+
items: unknown[]; next_cursor: string | null; has_more: boolean;
|
|
805
771
|
}>("GET", `/api/v1/rooms/${this.roomId}/messages`, undefined, {
|
|
806
772
|
since: String(sinceMessageId), limit,
|
|
807
773
|
});
|
|
@@ -824,7 +790,7 @@ export class Room {
|
|
|
824
790
|
hasMore: boolean;
|
|
825
791
|
}> {
|
|
826
792
|
const raw = await this.http.request<{
|
|
827
|
-
items:
|
|
793
|
+
items: unknown[]; next_cursor: string | null; has_more: boolean;
|
|
828
794
|
}>("GET", `/api/v1/rooms/${this.roomId}/messages`, undefined, { limit });
|
|
829
795
|
return {
|
|
830
796
|
items: raw.items.map(toMessageOut),
|
|
@@ -845,7 +811,7 @@ export class Room {
|
|
|
845
811
|
hasMore: boolean;
|
|
846
812
|
}> {
|
|
847
813
|
const raw = await this.http.request<{
|
|
848
|
-
items:
|
|
814
|
+
items: unknown[]; next_cursor: string | null; has_more: boolean;
|
|
849
815
|
}>("GET", `/api/v1/rooms/${this.roomId}/messages`, undefined, {
|
|
850
816
|
before: beforeMessageId, limit,
|
|
851
817
|
});
|
|
@@ -881,7 +847,7 @@ export class Room {
|
|
|
881
847
|
};
|
|
882
848
|
if (opts?.before) params.before = opts.before;
|
|
883
849
|
const raw = await this.http.request<{
|
|
884
|
-
items:
|
|
850
|
+
items: unknown[]; next_cursor: string | null; has_more: boolean;
|
|
885
851
|
}>("GET", `/api/v1/rooms/${this.roomId}/messages/search`, undefined, params);
|
|
886
852
|
return {
|
|
887
853
|
items: raw.items.map(toMessageOut),
|
|
@@ -895,40 +861,11 @@ export class Room {
|
|
|
895
861
|
* `room.deleteAllMessages()`. Server gates by membership for non-public
|
|
896
862
|
* rooms, so the caller must already be a member to read this.
|
|
897
863
|
*/
|
|
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
|
-
}));
|
|
864
|
+
async listMembers(): Promise<RoomMemberOut[]> {
|
|
865
|
+
const raw = await this.http.request<unknown[]>(
|
|
866
|
+
"GET", `/api/v1/rooms/${this.roomId}/members`,
|
|
867
|
+
);
|
|
868
|
+
return raw.map((m) => parseRoomMemberOut(m));
|
|
932
869
|
}
|
|
933
870
|
|
|
934
871
|
// -------- mute --------
|
|
@@ -958,18 +895,10 @@ export class Room {
|
|
|
958
895
|
* newer than the caller's `read_watermark`, plus the room's
|
|
959
896
|
* `last_message_id` so the UI can show a "•" dot regardless of count. */
|
|
960
897
|
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
|
-
};
|
|
898
|
+
const raw = await this.http.request<unknown>(
|
|
899
|
+
"GET", `/api/v1/rooms/${this.roomId}/unread`,
|
|
900
|
+
);
|
|
901
|
+
return parseRoomUnread(raw);
|
|
973
902
|
}
|
|
974
903
|
|
|
975
904
|
// -------- freeze --------
|
|
@@ -1007,80 +936,32 @@ export class Room {
|
|
|
1007
936
|
// -------- announcements --------
|
|
1008
937
|
|
|
1009
938
|
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
|
-
}));
|
|
939
|
+
const raw = await this.http.request<unknown[]>(
|
|
940
|
+
"GET", `/api/v1/rooms/${this.roomId}/announcements`,
|
|
941
|
+
);
|
|
942
|
+
return raw.map((a) => parseAnnouncementOut(a));
|
|
1026
943
|
}
|
|
1027
944
|
|
|
1028
945
|
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`);
|
|
946
|
+
const raw = await this.http.request<unknown>(
|
|
947
|
+
"GET", `/api/v1/rooms/${this.roomId}/announcements/current`,
|
|
948
|
+
);
|
|
1037
949
|
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
|
-
};
|
|
950
|
+
return parseAnnouncementOut(raw);
|
|
1046
951
|
}
|
|
1047
952
|
|
|
1048
953
|
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
|
-
};
|
|
954
|
+
const raw = await this.http.request<unknown>(
|
|
955
|
+
"POST", `/api/v1/rooms/${this.roomId}/announcements`, { content },
|
|
956
|
+
);
|
|
957
|
+
return parseAnnouncementOut(raw);
|
|
1065
958
|
}
|
|
1066
959
|
|
|
1067
960
|
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
|
-
};
|
|
961
|
+
const raw = await this.http.request<unknown>(
|
|
962
|
+
"PATCH", `/api/v1/rooms/${this.roomId}/announcements/${id}`, { content },
|
|
963
|
+
);
|
|
964
|
+
return parseAnnouncementOut(raw);
|
|
1084
965
|
}
|
|
1085
966
|
|
|
1086
967
|
async deleteAnnouncement(id: number): Promise<void> {
|
|
@@ -1111,51 +992,19 @@ export class Room {
|
|
|
1111
992
|
const body: Record<string, unknown> = {};
|
|
1112
993
|
if (opts && "expiresInHours" in opts) body.expires_in_hours = opts.expiresInHours;
|
|
1113
994
|
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
|
-
};
|
|
995
|
+
const raw = await this.http.request<unknown>(
|
|
996
|
+
"POST", `/api/v1/rooms/${this.roomId}/invites`, body,
|
|
997
|
+
);
|
|
998
|
+
return parseInviteTokenOut(raw);
|
|
1134
999
|
}
|
|
1135
1000
|
|
|
1136
1001
|
/** OPERATOR-only: list all invite tokens for this room (including
|
|
1137
1002
|
* revoked / expired / exhausted), newest first. */
|
|
1138
1003
|
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
|
-
}));
|
|
1004
|
+
const raw = await this.http.request<unknown[]>(
|
|
1005
|
+
"GET", `/api/v1/rooms/${this.roomId}/invites`,
|
|
1006
|
+
);
|
|
1007
|
+
return raw.map((r) => parseInviteTokenOut(r));
|
|
1159
1008
|
}
|
|
1160
1009
|
|
|
1161
1010
|
/** OPERATOR-only: revoke a token immediately. Idempotent — revoking
|
|
@@ -1167,6 +1016,87 @@ export class Room {
|
|
|
1167
1016
|
);
|
|
1168
1017
|
}
|
|
1169
1018
|
|
|
1019
|
+
// -------- join requests --------
|
|
1020
|
+
|
|
1021
|
+
/**
|
|
1022
|
+
* Ask to join this room (self). Behavior depends on the room's
|
|
1023
|
+
* `joinPolicy`:
|
|
1024
|
+
* - `OPEN` → the caller is added immediately (`status: "joined"`);
|
|
1025
|
+
* - `APPROVAL_REQUIRED` → the request lands in the operators' inbox
|
|
1026
|
+
* (`status: "pending"`) until [`approveJoinRequest`] / reject.
|
|
1027
|
+
* Idempotent for members (`status: "already_member"`).
|
|
1028
|
+
*
|
|
1029
|
+
* @param reason Optional note shown to operators ("하은이 반 친구 엄마예요").
|
|
1030
|
+
*/
|
|
1031
|
+
async requestJoin(reason?: string): Promise<JoinRequestCreateResponse> {
|
|
1032
|
+
const raw = await this.http.request<unknown>(
|
|
1033
|
+
"POST", `/api/v1/rooms/${this.roomId}/join-request`,
|
|
1034
|
+
reason ? { reason } : {},
|
|
1035
|
+
);
|
|
1036
|
+
return parseJoinRequestCreateResponse(raw);
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
/** Cancel the caller's own pending join request. */
|
|
1040
|
+
async cancelJoinRequest(): Promise<void> {
|
|
1041
|
+
await this.http.request<void>(
|
|
1042
|
+
"DELETE", `/api/v1/rooms/${this.roomId}/join-request`,
|
|
1043
|
+
);
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
/** OPERATOR-only: list join requests for this room (the approval inbox).
|
|
1047
|
+
* Defaults to pending only — pass a status to see processed history. */
|
|
1048
|
+
async listJoinRequests(opts?: {
|
|
1049
|
+
status?: "PENDING" | "APPROVED" | "REJECTED" | "CANCELLED";
|
|
1050
|
+
limit?: number;
|
|
1051
|
+
offset?: number;
|
|
1052
|
+
}): Promise<JoinRequestListResponse> {
|
|
1053
|
+
const raw = await this.http.request<unknown>(
|
|
1054
|
+
"GET", `/api/v1/rooms/${this.roomId}/join-requests`, undefined, {
|
|
1055
|
+
...(opts?.status ? { status: opts.status } : {}),
|
|
1056
|
+
...(opts?.limit !== undefined ? { limit: opts.limit } : {}),
|
|
1057
|
+
...(opts?.offset !== undefined ? { offset: opts.offset } : {}),
|
|
1058
|
+
},
|
|
1059
|
+
);
|
|
1060
|
+
return parseJoinRequestListResponse(raw);
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
/** OPERATOR-only: approve a pending join request — the requester becomes
|
|
1064
|
+
* a MEMBER. [reason] is echoed back to them in later list calls. */
|
|
1065
|
+
async approveJoinRequest(userId: string, reason?: string): Promise<void> {
|
|
1066
|
+
await this.http.request<void>(
|
|
1067
|
+
"POST",
|
|
1068
|
+
`/api/v1/rooms/${this.roomId}/join-requests/${encodeURIComponent(userId)}/approve`,
|
|
1069
|
+
reason ? { reason } : {},
|
|
1070
|
+
);
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
/** OPERATOR-only: reject a pending join request. */
|
|
1074
|
+
async rejectJoinRequest(userId: string, reason?: string): Promise<void> {
|
|
1075
|
+
await this.http.request<void>(
|
|
1076
|
+
"POST",
|
|
1077
|
+
`/api/v1/rooms/${this.roomId}/join-requests/${encodeURIComponent(userId)}/reject`,
|
|
1078
|
+
reason ? { reason } : {},
|
|
1079
|
+
);
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
// -------- hide / unhide --------
|
|
1083
|
+
|
|
1084
|
+
/** Hide this room from the caller's own room list (self, reversible —
|
|
1085
|
+
* the KakaoTalk "숨기기"). The room typically reappears via [unhide]
|
|
1086
|
+
* or on new activity, per server policy. Messages are untouched. */
|
|
1087
|
+
async hide(): Promise<void> {
|
|
1088
|
+
await this.http.request<void>(
|
|
1089
|
+
"POST", `/api/v1/rooms/${this.roomId}/hide`,
|
|
1090
|
+
);
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
/** Undo [hide] — the room shows up in the caller's list again. */
|
|
1094
|
+
async unhide(): Promise<void> {
|
|
1095
|
+
await this.http.request<void>(
|
|
1096
|
+
"POST", `/api/v1/rooms/${this.roomId}/unhide`,
|
|
1097
|
+
);
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1170
1100
|
// -------- internal dispatch --------
|
|
1171
1101
|
|
|
1172
1102
|
on<K extends keyof RoomEvents>(event: K, fn: (p: RoomEvents[K]) => void): () => void {
|