@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/dist/index.js CHANGED
@@ -1,3 +1,221 @@
1
+ // src/generated/rest.ts
2
+ function parseAppPolicy(j) {
3
+ return {
4
+ appId: j["app_id"],
5
+ allowMemberBulkDelete: j["allow_member_bulk_delete"],
6
+ readReceiptsEnabled: j["read_receipts_enabled"],
7
+ typingIndicatorsEnabled: j["typing_indicators_enabled"],
8
+ allowMemberDeleteAnyMessage: j["allow_member_delete_any_message"]
9
+ };
10
+ }
11
+ function parseUnreadSummary(j) {
12
+ return {
13
+ total: j["total"],
14
+ rooms: j["rooms"].map((e) => parseUnreadRoomItem(e))
15
+ };
16
+ }
17
+ function parseUnreadRoomItem(j) {
18
+ return {
19
+ roomId: j["room_id"],
20
+ name: j["name"],
21
+ roomType: j["room_type"],
22
+ unreadCount: j["unread_count"],
23
+ lastMessageId: j["last_message_id"],
24
+ lastReadMessageId: j["last_read_message_id"],
25
+ lastMessagePreview: j["last_message_preview"],
26
+ memberCount: j["member_count"],
27
+ membersPreview: j["members_preview"] == null ? void 0 : j["members_preview"].map((e) => parseMemberPreview(e))
28
+ };
29
+ }
30
+ function parseMemberPreview(j) {
31
+ return {
32
+ userId: j["user_id"],
33
+ nickname: j["nickname"],
34
+ profileImageUrl: j["profile_image_url"],
35
+ isOnline: j["is_online"]
36
+ };
37
+ }
38
+ function parseRoomUnread(j) {
39
+ return {
40
+ roomId: j["room_id"],
41
+ unreadCount: j["unread_count"],
42
+ lastReadMessageId: j["last_read_message_id"],
43
+ lastMessageId: j["last_message_id"]
44
+ };
45
+ }
46
+ function parseMessageOut(j) {
47
+ return {
48
+ appId: j["app_id"],
49
+ roomId: j["room_id"],
50
+ messageId: j["message_id"],
51
+ sender: j["sender"] == null ? null : parseSenderMini(j["sender"]),
52
+ senderId: j["sender_id"],
53
+ messageType: j["message_type"],
54
+ customType: j["custom_type"],
55
+ content: j["content"],
56
+ data: j["data"],
57
+ meta: j["meta"],
58
+ fileId: j["file_id"],
59
+ file: j["file"],
60
+ files: j["files"],
61
+ replyToId: j["reply_to_id"],
62
+ reactions: j["reactions"] == null ? void 0 : j["reactions"].map((e) => parseReactionEntry(e)),
63
+ createdAt: j["created_at"],
64
+ updatedAt: j["updated_at"],
65
+ deletedAt: j["deleted_at"],
66
+ deletedByUserId: j["deleted_by_user_id"],
67
+ deleteScope: j["delete_scope"],
68
+ editedCount: j["edited_count"],
69
+ createdAtMs: j["created_at_ms"],
70
+ updatedAtMs: j["updated_at_ms"]
71
+ };
72
+ }
73
+ function parseAnnouncementOut(j) {
74
+ return {
75
+ id: j["id"],
76
+ roomId: j["room_id"],
77
+ content: j["content"],
78
+ createdBy: j["created_by"],
79
+ createdAt: j["created_at"],
80
+ updatedAt: j["updated_at"]
81
+ };
82
+ }
83
+ function parseInviteTokenOut(j) {
84
+ return {
85
+ token: j["token"],
86
+ roomId: j["room_id"],
87
+ createdBy: j["created_by"],
88
+ createdAt: j["created_at"],
89
+ expiresAt: j["expires_at"],
90
+ maxUses: j["max_uses"],
91
+ usedCount: j["used_count"],
92
+ revokedAt: j["revoked_at"]
93
+ };
94
+ }
95
+ function parseInviteAcceptResponse(j) {
96
+ return {
97
+ roomId: j["room_id"],
98
+ roomName: j["room_name"],
99
+ wasAlreadyMember: j["was_already_member"]
100
+ };
101
+ }
102
+ function parseDeviceOut(j) {
103
+ return {
104
+ deviceId: j["device_id"],
105
+ tokenType: j["token_type"],
106
+ tokenPreview: j["token_preview"],
107
+ os: j["os"],
108
+ appVersion: j["app_version"],
109
+ pushEnabled: j["push_enabled"],
110
+ lastActiveAt: j["last_active_at"],
111
+ createdAt: j["created_at"]
112
+ };
113
+ }
114
+ function parseDeviceListResponse(j) {
115
+ return {
116
+ items: j["items"].map((e) => parseDeviceOut(e)),
117
+ total: j["total"]
118
+ };
119
+ }
120
+ function parsePushPreferencesOut(j) {
121
+ return {
122
+ pushEnabled: j["push_enabled"]
123
+ };
124
+ }
125
+ function parseFileOut(j) {
126
+ return {
127
+ id: j["id"],
128
+ kind: j["kind"],
129
+ mime: j["mime"],
130
+ sizeBytes: j["size_bytes"],
131
+ originalName: j["original_name"],
132
+ width: j["width"],
133
+ height: j["height"],
134
+ durationSec: j["duration_sec"],
135
+ thumbnailStatus: j["thumbnail_status"],
136
+ uploadStatus: j["upload_status"],
137
+ createdAt: j["created_at"],
138
+ committedAt: j["committed_at"],
139
+ forwardBlocked: j["forward_blocked"]
140
+ };
141
+ }
142
+ function parseFileCreateResponse(j) {
143
+ return {
144
+ fileId: j["file_id"],
145
+ uploadUrl: j["upload_url"],
146
+ headers: j["headers"],
147
+ expiresAtMs: j["expires_at_ms"]
148
+ };
149
+ }
150
+ function parseBlockOut(j) {
151
+ return {
152
+ blockedUserId: j["blocked_user_id"],
153
+ reason: j["reason"],
154
+ createdAt: j["created_at"]
155
+ };
156
+ }
157
+ function parseRoomMemberOut(j) {
158
+ return {
159
+ userId: j["user_id"],
160
+ nickname: j["nickname"],
161
+ role: j["role"],
162
+ pushTrigger: j["push_trigger"],
163
+ lastReadMessageId: j["last_read_message_id"],
164
+ joinedAt: j["joined_at"],
165
+ isOnline: j["is_online"],
166
+ mutedUntil: j["muted_until"]
167
+ };
168
+ }
169
+ function parseBulkDeleteResult(j) {
170
+ return {
171
+ cleared: j["cleared"],
172
+ upToMessageId: j["up_to_message_id"]
173
+ };
174
+ }
175
+ function parseBulkDeleteBySenderResult(j) {
176
+ return {
177
+ cleared: j["cleared"]
178
+ };
179
+ }
180
+ function parseJoinRequestOut(j) {
181
+ return {
182
+ userId: j["user_id"],
183
+ status: j["status"],
184
+ reason: j["reason"],
185
+ requestedAt: j["requested_at"],
186
+ processedAt: j["processed_at"],
187
+ processedByUserId: j["processed_by_user_id"]
188
+ };
189
+ }
190
+ function parseJoinRequestListResponse(j) {
191
+ return {
192
+ items: j["items"].map((e) => parseJoinRequestOut(e)),
193
+ total: j["total"],
194
+ limit: j["limit"],
195
+ offset: j["offset"]
196
+ };
197
+ }
198
+ function parseJoinRequestCreateResponse(j) {
199
+ return {
200
+ status: j["status"],
201
+ roomId: j["room_id"]
202
+ };
203
+ }
204
+ function parseSenderMini(j) {
205
+ return {
206
+ userId: j["user_id"],
207
+ nickname: j["nickname"],
208
+ profileImageUrl: j["profile_image_url"]
209
+ };
210
+ }
211
+ function parseReactionEntry(j) {
212
+ return {
213
+ key: j["key"],
214
+ userIds: j["user_ids"],
215
+ updatedAt: j["updated_at"]
216
+ };
217
+ }
218
+
1
219
  // src/errors.ts
2
220
  var ErrorCode = {
3
221
  INTERNAL: "NC-GEN-001",
@@ -293,64 +511,31 @@ function tempId() {
293
511
  }
294
512
 
295
513
  // src/features/room.ts
296
- function toMessageOut(r) {
297
- const dataObj = r.data ?? null;
514
+ function toFileInline(f) {
515
+ return {
516
+ id: f.id,
517
+ kind: f.kind,
518
+ mime: f.mime,
519
+ size: f.size,
520
+ name: f.name ?? null,
521
+ width: f.width ?? null,
522
+ height: f.height ?? null,
523
+ durationSec: f.duration_sec ?? null,
524
+ thumbnailStatus: f.thumbnail_status,
525
+ forwardBlocked: Boolean(f.forward_blocked)
526
+ };
527
+ }
528
+ function toMessageOut(j) {
529
+ const base = parseMessageOut(j);
530
+ const raw = j;
531
+ const dataObj = base.data ?? null;
298
532
  const customMeta = dataObj && typeof dataObj === "object" && "_customMeta" in dataObj ? dataObj._customMeta : null;
299
533
  return {
300
- appId: r.app_id,
301
- roomId: r.room_id,
302
- messageId: r.message_id,
303
- sender: r.sender ? {
304
- userId: r.sender.user_id,
305
- nickname: r.sender.nickname ?? null,
306
- profileImageUrl: r.sender.profile_image_url ?? null
307
- } : null,
308
- senderId: r.sender_id ?? null,
309
- messageType: r.message_type,
310
- customType: r.custom_type ?? null,
311
- content: r.content ?? null,
312
- data: dataObj,
313
- meta: r.meta ?? null,
314
- customMeta,
315
- fileId: r.file_id ?? null,
316
- file: r.file ? {
317
- id: r.file.id,
318
- kind: r.file.kind,
319
- mime: r.file.mime,
320
- size: r.file.size,
321
- name: r.file.name ?? null,
322
- width: r.file.width ?? null,
323
- height: r.file.height ?? null,
324
- durationSec: r.file.duration_sec ?? null,
325
- thumbnailStatus: r.file.thumbnail_status,
326
- forwardBlocked: Boolean(r.file.forward_blocked)
327
- } : null,
328
- files: r.files && r.files.length > 0 ? r.files.map((f) => ({
329
- id: f.id,
330
- kind: f.kind,
331
- mime: f.mime,
332
- size: f.size,
333
- name: f.name ?? null,
334
- width: f.width ?? null,
335
- height: f.height ?? null,
336
- durationSec: f.duration_sec ?? null,
337
- thumbnailStatus: f.thumbnail_status,
338
- forwardBlocked: Boolean(f.forward_blocked)
339
- })) : null,
340
- replyToId: r.reply_to_id ?? null,
341
- reactions: (r.reactions ?? []).map((x) => ({
342
- key: x.key,
343
- userIds: x.user_ids,
344
- updatedAt: x.updated_at
345
- })),
346
- createdAt: r.created_at,
347
- createdAtMs: r.created_at_ms,
348
- updatedAt: r.updated_at ?? null,
349
- updatedAtMs: r.updated_at_ms ?? null,
350
- deletedAt: r.deleted_at ?? null,
351
- deletedByUserId: r.deleted_by_user_id ?? null,
352
- deleteScope: r.delete_scope ?? null,
353
- editedCount: r.edited_count ?? 0
534
+ ...base,
535
+ file: raw.file ? toFileInline(raw.file) : null,
536
+ files: raw.files && raw.files.length > 0 ? raw.files.map(toFileInline) : null,
537
+ reactions: base.reactions ?? [],
538
+ customMeta
354
539
  };
355
540
  }
356
541
  var Room = class _Room {
@@ -536,20 +721,24 @@ var Room = class _Room {
536
721
  async sendFile(file, opts = {}) {
537
722
  const kind = opts.kind ?? (file.type.startsWith("image/") ? "image" : file.type.startsWith("video/") ? "video" : "file");
538
723
  const name = opts.name ?? file.name;
539
- const presign = await this.http.request("POST", "/api/v1/files", {
540
- kind,
541
- mime: file.type || "application/octet-stream",
542
- size: file.size,
543
- original_name: name,
544
- // 파일 전달 제한 — uploader-set at presign time. When true,
545
- // other users can't forward this file to a different room.
546
- forward_blocked: Boolean(opts.forwardBlocked)
547
- });
724
+ const created = parseFileCreateResponse(
725
+ await this.http.request("POST", "/api/v1/files", {
726
+ kind,
727
+ mime: file.type || "application/octet-stream",
728
+ size: file.size,
729
+ original_name: name,
730
+ // 파일 전달 제한 uploader-set at presign time. When true,
731
+ // other users can't forward this file to a different room.
732
+ forward_blocked: Boolean(opts.forwardBlocked)
733
+ })
734
+ );
548
735
  const presigned = {
549
- fileId: presign.file_id,
550
- uploadUrl: presign.upload_url,
551
- headers: presign.headers,
552
- expiresAtMs: presign.expires_at_ms
736
+ fileId: created.fileId,
737
+ uploadUrl: created.uploadUrl,
738
+ // Presigned S3 headers are always string-valued; the generated
739
+ // model types the JSON object loosely as Record<string, unknown>.
740
+ headers: created.headers,
741
+ expiresAtMs: created.expiresAtMs
553
742
  };
554
743
  await new Promise((resolve, reject) => {
555
744
  const xhr = new XMLHttpRequest();
@@ -604,17 +793,19 @@ var Room = class _Room {
604
793
  async uploadFileOnly(file, opts = {}) {
605
794
  const kind = opts.kind ?? (file.type.startsWith("image/") ? "image" : file.type.startsWith("video/") ? "video" : "file");
606
795
  const name = opts.name ?? file.name;
607
- const presign = await this.http.request("POST", "/api/v1/files", {
608
- kind,
609
- mime: file.type || "application/octet-stream",
610
- size: file.size,
611
- original_name: name,
612
- forward_blocked: Boolean(opts.forwardBlocked)
613
- });
796
+ const created = parseFileCreateResponse(
797
+ await this.http.request("POST", "/api/v1/files", {
798
+ kind,
799
+ mime: file.type || "application/octet-stream",
800
+ size: file.size,
801
+ original_name: name,
802
+ forward_blocked: Boolean(opts.forwardBlocked)
803
+ })
804
+ );
614
805
  await new Promise((resolve, reject) => {
615
806
  const xhr = new XMLHttpRequest();
616
- xhr.open("PUT", presign.upload_url);
617
- for (const [k, v] of Object.entries(presign.headers)) {
807
+ xhr.open("PUT", created.uploadUrl);
808
+ for (const [k, v] of Object.entries(created.headers)) {
618
809
  xhr.setRequestHeader(k, v);
619
810
  }
620
811
  if (opts.onProgress) {
@@ -641,10 +832,10 @@ var Room = class _Room {
641
832
  });
642
833
  await this.http.request(
643
834
  "POST",
644
- `/api/v1/files/${presign.file_id}/commit`,
835
+ `/api/v1/files/${created.fileId}/commit`,
645
836
  {}
646
837
  );
647
- return presign.file_id;
838
+ return created.fileId;
648
839
  }
649
840
  /**
650
841
  * List files shared in this room — newest-first, paginated.
@@ -669,23 +860,26 @@ var Room = class _Room {
669
860
  }
670
861
  );
671
862
  return {
672
- items: raw.items.map((r) => ({
673
- messageId: r.message_id,
674
- senderId: r.sender_id,
675
- createdAtMs: r.created_at_ms,
676
- file: {
677
- id: r.file.id,
678
- kind: r.file.kind,
679
- mime: r.file.mime,
680
- size: r.file.size_bytes,
681
- name: r.file.original_name ?? null,
682
- width: r.file.width ?? null,
683
- height: r.file.height ?? null,
684
- durationSec: r.file.duration_sec ?? null,
685
- thumbnailStatus: r.file.thumbnail_status,
686
- forwardBlocked: Boolean(r.file.forward_blocked)
687
- }
688
- })),
863
+ items: raw.items.map((r) => {
864
+ const f = parseFileOut(r.file);
865
+ return {
866
+ messageId: r.message_id,
867
+ senderId: r.sender_id,
868
+ createdAtMs: r.created_at_ms,
869
+ file: {
870
+ id: f.id,
871
+ kind: f.kind,
872
+ mime: f.mime,
873
+ size: f.sizeBytes,
874
+ name: f.originalName ?? null,
875
+ width: f.width ?? null,
876
+ height: f.height ?? null,
877
+ durationSec: f.durationSec ?? null,
878
+ thumbnailStatus: f.thumbnailStatus,
879
+ forwardBlocked: Boolean(f.forwardBlocked)
880
+ }
881
+ };
882
+ }),
689
883
  nextCursor: raw.next_cursor,
690
884
  hasMore: raw.has_more
691
885
  };
@@ -723,11 +917,12 @@ var Room = class _Room {
723
917
  this.ws.send({ type: "typing", room_id: this.roomId, is_typing: isTyping });
724
918
  }
725
919
  async edit(messageId, patch) {
726
- return this.http.request(
920
+ const raw = await this.http.request(
727
921
  "PUT",
728
922
  `/api/v1/rooms/${this.roomId}/messages/${messageId}`,
729
923
  patch
730
924
  );
925
+ return toMessageOut(raw);
731
926
  }
732
927
  async delete(messageId, scope = "ALL") {
733
928
  await this.http.request(
@@ -752,12 +947,39 @@ var Room = class _Room {
752
947
  * Other members get a `MEMBER_LEFT` [`roomEvent`](../reference/noverachat.md#events).
753
948
  * Idempotent if already a non-member; throws 409 if the caller was KICKED
754
949
  * (ask an operator to restore first). For GROUP/SUPER_GROUP, re-entry needs
755
- * an operator add; for ONE, re-creating the 1:1 rejoins with history kept. */
756
- async leave() {
950
+ * an operator add; for ONE, re-creating the 1:1 rejoins with history kept.
951
+ *
952
+ * Optional `deleteMessages` — atomically clean up chat history as part
953
+ * of leaving. Default is `"none"` (messages survive so the room's
954
+ * history keeps making sense to remaining members).
955
+ * - `"none"` — leave only.
956
+ * - `"mine"` — soft-delete every message YOU sent, then leave. Other
957
+ * members' messages are untouched. Broadcasts
958
+ * `messages_cleared_by_sender` so connected clients hide
959
+ * them locally.
960
+ * - `"all"` — wipe every message in the room, then leave. Subject to
961
+ * the app's `allow_member_bulk_delete` gate and per-room
962
+ * OPERATOR check — a plain member gets 403 back. */
963
+ async leave(options) {
964
+ const mode = options?.deleteMessages ?? "none";
965
+ const query = mode === "none" ? "" : `?delete_messages=${encodeURIComponent(mode)}`;
757
966
  await this.http.request(
758
967
  "POST",
759
- `/api/v1/rooms/${this.roomId}/leave`
968
+ `/api/v1/rooms/${this.roomId}/leave${query}`
969
+ );
970
+ }
971
+ /** Delete every message YOU sent in this room. Standalone version of
972
+ * `leave({ deleteMessages: "mine" })` — useful when a user wants to
973
+ * scrub their history without actually leaving. Other members'
974
+ * messages are untouched; broadcasts `messages_cleared_by_sender`
975
+ * so connected clients hide the vanished ones locally.
976
+ * No OPERATOR gate — you own your messages. */
977
+ async deleteMyMessages() {
978
+ const raw = await this.http.request(
979
+ "DELETE",
980
+ `/api/v1/rooms/${this.roomId}/messages/mine`
760
981
  );
982
+ return parseBulkDeleteBySenderResult(raw);
761
983
  }
762
984
  /**
763
985
  * Set the caller's per-room push preference — like long-pressing a
@@ -783,11 +1005,11 @@ var Room = class _Room {
783
1005
  * hide rendered messages up to (and including) that id.
784
1006
  */
785
1007
  async deleteAllMessages() {
786
- const raw = await this.http.request("DELETE", `/api/v1/rooms/${this.roomId}/messages`);
787
- return {
788
- cleared: raw.cleared,
789
- upToMessageId: raw.up_to_message_id
790
- };
1008
+ const raw = await this.http.request(
1009
+ "DELETE",
1010
+ `/api/v1/rooms/${this.roomId}/messages`
1011
+ );
1012
+ return parseBulkDeleteResult(raw);
791
1013
  }
792
1014
  async react(messageId, key) {
793
1015
  await this.http.request(
@@ -874,17 +1096,11 @@ var Room = class _Room {
874
1096
  * rooms, so the caller must already be a member to read this.
875
1097
  */
876
1098
  async listMembers() {
877
- const raw = await this.http.request("GET", `/api/v1/rooms/${this.roomId}/members`);
878
- return raw.map((m) => ({
879
- userId: m.user_id,
880
- nickname: m.nickname ?? null,
881
- role: m.role,
882
- pushTrigger: m.push_trigger,
883
- lastReadMessageId: m.last_read_message_id,
884
- joinedAt: m.joined_at,
885
- isOnline: m.is_online ?? null,
886
- mutedUntil: m.muted_until ?? null
887
- }));
1099
+ const raw = await this.http.request(
1100
+ "GET",
1101
+ `/api/v1/rooms/${this.roomId}/members`
1102
+ );
1103
+ return raw.map((m) => parseRoomMemberOut(m));
888
1104
  }
889
1105
  // -------- mute --------
890
1106
  /** OPERATOR-only: mute a member so they can't send messages in this
@@ -912,13 +1128,11 @@ var Room = class _Room {
912
1128
  * newer than the caller's `read_watermark`, plus the room's
913
1129
  * `last_message_id` so the UI can show a "•" dot regardless of count. */
914
1130
  async unread() {
915
- const raw = await this.http.request("GET", `/api/v1/rooms/${this.roomId}/unread`);
916
- return {
917
- roomId: raw.room_id,
918
- unreadCount: raw.unread_count,
919
- lastReadMessageId: raw.last_read_message_id,
920
- lastMessageId: raw.last_message_id
921
- };
1131
+ const raw = await this.http.request(
1132
+ "GET",
1133
+ `/api/v1/rooms/${this.roomId}/unread`
1134
+ );
1135
+ return parseRoomUnread(raw);
922
1136
  }
923
1137
  // -------- freeze --------
924
1138
  /** OPERATOR-only moderation: freeze this room. Non-admin messages are
@@ -953,49 +1167,35 @@ var Room = class _Room {
953
1167
  }
954
1168
  // -------- announcements --------
955
1169
  async listAnnouncements() {
956
- const raw = await this.http.request("GET", `/api/v1/rooms/${this.roomId}/announcements`);
957
- return raw.map((a) => ({
958
- id: a.id,
959
- roomId: a.room_id,
960
- content: a.content,
961
- createdBy: a.created_by,
962
- createdAt: a.created_at,
963
- updatedAt: a.updated_at
964
- }));
1170
+ const raw = await this.http.request(
1171
+ "GET",
1172
+ `/api/v1/rooms/${this.roomId}/announcements`
1173
+ );
1174
+ return raw.map((a) => parseAnnouncementOut(a));
965
1175
  }
966
1176
  async getCurrentAnnouncement() {
967
- const raw = await this.http.request("GET", `/api/v1/rooms/${this.roomId}/announcements/current`);
1177
+ const raw = await this.http.request(
1178
+ "GET",
1179
+ `/api/v1/rooms/${this.roomId}/announcements/current`
1180
+ );
968
1181
  if (!raw) return null;
969
- return {
970
- id: raw.id,
971
- roomId: raw.room_id,
972
- content: raw.content,
973
- createdBy: raw.created_by,
974
- createdAt: raw.created_at,
975
- updatedAt: raw.updated_at
976
- };
1182
+ return parseAnnouncementOut(raw);
977
1183
  }
978
1184
  async postAnnouncement(content) {
979
- const raw = await this.http.request("POST", `/api/v1/rooms/${this.roomId}/announcements`, { content });
980
- return {
981
- id: raw.id,
982
- roomId: raw.room_id,
983
- content: raw.content,
984
- createdBy: raw.created_by,
985
- createdAt: raw.created_at,
986
- updatedAt: raw.updated_at
987
- };
1185
+ const raw = await this.http.request(
1186
+ "POST",
1187
+ `/api/v1/rooms/${this.roomId}/announcements`,
1188
+ { content }
1189
+ );
1190
+ return parseAnnouncementOut(raw);
988
1191
  }
989
1192
  async updateAnnouncement(id, content) {
990
- const raw = await this.http.request("PATCH", `/api/v1/rooms/${this.roomId}/announcements/${id}`, { content });
991
- return {
992
- id: raw.id,
993
- roomId: raw.room_id,
994
- content: raw.content,
995
- createdBy: raw.created_by,
996
- createdAt: raw.created_at,
997
- updatedAt: raw.updated_at
998
- };
1193
+ const raw = await this.http.request(
1194
+ "PATCH",
1195
+ `/api/v1/rooms/${this.roomId}/announcements/${id}`,
1196
+ { content }
1197
+ );
1198
+ return parseAnnouncementOut(raw);
999
1199
  }
1000
1200
  async deleteAnnouncement(id) {
1001
1201
  await this.http.request(
@@ -1017,32 +1217,21 @@ var Room = class _Room {
1017
1217
  const body = {};
1018
1218
  if (opts && "expiresInHours" in opts) body.expires_in_hours = opts.expiresInHours;
1019
1219
  if (opts && "maxUses" in opts) body.max_uses = opts.maxUses;
1020
- const raw = await this.http.request("POST", `/api/v1/rooms/${this.roomId}/invites`, body);
1021
- return {
1022
- token: raw.token,
1023
- roomId: raw.room_id,
1024
- createdBy: raw.created_by,
1025
- createdAt: raw.created_at,
1026
- expiresAt: raw.expires_at,
1027
- maxUses: raw.max_uses,
1028
- usedCount: raw.used_count,
1029
- revokedAt: raw.revoked_at
1030
- };
1220
+ const raw = await this.http.request(
1221
+ "POST",
1222
+ `/api/v1/rooms/${this.roomId}/invites`,
1223
+ body
1224
+ );
1225
+ return parseInviteTokenOut(raw);
1031
1226
  }
1032
1227
  /** OPERATOR-only: list all invite tokens for this room (including
1033
1228
  * revoked / expired / exhausted), newest first. */
1034
1229
  async listInvites() {
1035
- const raw = await this.http.request("GET", `/api/v1/rooms/${this.roomId}/invites`);
1036
- return raw.map((r) => ({
1037
- token: r.token,
1038
- roomId: r.room_id,
1039
- createdBy: r.created_by,
1040
- createdAt: r.created_at,
1041
- expiresAt: r.expires_at,
1042
- maxUses: r.max_uses,
1043
- usedCount: r.used_count,
1044
- revokedAt: r.revoked_at
1045
- }));
1230
+ const raw = await this.http.request(
1231
+ "GET",
1232
+ `/api/v1/rooms/${this.roomId}/invites`
1233
+ );
1234
+ return raw.map((r) => parseInviteTokenOut(r));
1046
1235
  }
1047
1236
  /** OPERATOR-only: revoke a token immediately. Idempotent — revoking
1048
1237
  * an already-revoked token returns 204 as well. */
@@ -1052,6 +1241,81 @@ var Room = class _Room {
1052
1241
  `/api/v1/rooms/${this.roomId}/invites/${encodeURIComponent(token)}`
1053
1242
  );
1054
1243
  }
1244
+ // -------- join requests --------
1245
+ /**
1246
+ * Ask to join this room (self). Behavior depends on the room's
1247
+ * `joinPolicy`:
1248
+ * - `OPEN` → the caller is added immediately (`status: "joined"`);
1249
+ * - `APPROVAL_REQUIRED` → the request lands in the operators' inbox
1250
+ * (`status: "pending"`) until [`approveJoinRequest`] / reject.
1251
+ * Idempotent for members (`status: "already_member"`).
1252
+ *
1253
+ * @param reason Optional note shown to operators ("하은이 반 친구 엄마예요").
1254
+ */
1255
+ async requestJoin(reason) {
1256
+ const raw = await this.http.request(
1257
+ "POST",
1258
+ `/api/v1/rooms/${this.roomId}/join-request`,
1259
+ reason ? { reason } : {}
1260
+ );
1261
+ return parseJoinRequestCreateResponse(raw);
1262
+ }
1263
+ /** Cancel the caller's own pending join request. */
1264
+ async cancelJoinRequest() {
1265
+ await this.http.request(
1266
+ "DELETE",
1267
+ `/api/v1/rooms/${this.roomId}/join-request`
1268
+ );
1269
+ }
1270
+ /** OPERATOR-only: list join requests for this room (the approval inbox).
1271
+ * Defaults to pending only — pass a status to see processed history. */
1272
+ async listJoinRequests(opts) {
1273
+ const raw = await this.http.request(
1274
+ "GET",
1275
+ `/api/v1/rooms/${this.roomId}/join-requests`,
1276
+ void 0,
1277
+ {
1278
+ ...opts?.status ? { status: opts.status } : {},
1279
+ ...opts?.limit !== void 0 ? { limit: opts.limit } : {},
1280
+ ...opts?.offset !== void 0 ? { offset: opts.offset } : {}
1281
+ }
1282
+ );
1283
+ return parseJoinRequestListResponse(raw);
1284
+ }
1285
+ /** OPERATOR-only: approve a pending join request — the requester becomes
1286
+ * a MEMBER. [reason] is echoed back to them in later list calls. */
1287
+ async approveJoinRequest(userId, reason) {
1288
+ await this.http.request(
1289
+ "POST",
1290
+ `/api/v1/rooms/${this.roomId}/join-requests/${encodeURIComponent(userId)}/approve`,
1291
+ reason ? { reason } : {}
1292
+ );
1293
+ }
1294
+ /** OPERATOR-only: reject a pending join request. */
1295
+ async rejectJoinRequest(userId, reason) {
1296
+ await this.http.request(
1297
+ "POST",
1298
+ `/api/v1/rooms/${this.roomId}/join-requests/${encodeURIComponent(userId)}/reject`,
1299
+ reason ? { reason } : {}
1300
+ );
1301
+ }
1302
+ // -------- hide / unhide --------
1303
+ /** Hide this room from the caller's own room list (self, reversible —
1304
+ * the KakaoTalk "숨기기"). The room typically reappears via [unhide]
1305
+ * or on new activity, per server policy. Messages are untouched. */
1306
+ async hide() {
1307
+ await this.http.request(
1308
+ "POST",
1309
+ `/api/v1/rooms/${this.roomId}/hide`
1310
+ );
1311
+ }
1312
+ /** Undo [hide] — the room shows up in the caller's list again. */
1313
+ async unhide() {
1314
+ await this.http.request(
1315
+ "POST",
1316
+ `/api/v1/rooms/${this.roomId}/unhide`
1317
+ );
1318
+ }
1055
1319
  // -------- internal dispatch --------
1056
1320
  on(event, fn) {
1057
1321
  return this.bus.on(event, fn);
@@ -1300,13 +1564,7 @@ var NoveraChat = class {
1300
1564
  * destructive buttons to expose. */
1301
1565
  async appPolicy() {
1302
1566
  const raw = await this.http.request("GET", "/api/v1/app/policy");
1303
- return {
1304
- appId: raw.app_id,
1305
- allowMemberBulkDelete: raw.allow_member_bulk_delete,
1306
- readReceiptsEnabled: raw.read_receipts_enabled ?? true,
1307
- typingIndicatorsEnabled: raw.typing_indicators_enabled ?? true,
1308
- deleteAnyMessageEnabled: raw.allow_member_delete_any_message ?? false
1309
- };
1567
+ return parseAppPolicy(raw);
1310
1568
  }
1311
1569
  // ---- device tokens (push) ----
1312
1570
  /**
@@ -1316,19 +1574,23 @@ var NoveraChat = class {
1316
1574
  * safe. Returns the stored device record (token shown only as a prefix).
1317
1575
  */
1318
1576
  async registerDevice(params) {
1319
- const raw = await this.http.request("POST", "/api/v1/users/me/devices", {
1320
- device_id: params.deviceId,
1321
- token: params.token,
1322
- token_type: params.tokenType,
1323
- os: params.os ?? null,
1324
- app_version: params.appVersion ?? null
1325
- });
1326
- return this._toDeviceInfo(raw);
1577
+ const raw = await this.http.request(
1578
+ "POST",
1579
+ "/api/v1/users/me/devices",
1580
+ {
1581
+ device_id: params.deviceId,
1582
+ token: params.token,
1583
+ token_type: params.tokenType,
1584
+ os: params.os ?? null,
1585
+ app_version: params.appVersion ?? null
1586
+ }
1587
+ );
1588
+ return parseDeviceOut(raw);
1327
1589
  }
1328
1590
  /** List the devices registered for the authenticated user. */
1329
1591
  async listDevices() {
1330
1592
  const raw = await this.http.request("GET", "/api/v1/users/me/devices");
1331
- return raw.items.map((d) => this._toDeviceInfo(d));
1593
+ return parseDeviceListResponse(raw).items;
1332
1594
  }
1333
1595
  /**
1334
1596
  * Turn push delivery on/off for one device without unregistering it —
@@ -1366,7 +1628,7 @@ var NoveraChat = class {
1366
1628
  "GET",
1367
1629
  "/api/v1/users/me/push-preferences"
1368
1630
  );
1369
- return { pushEnabled: Boolean(raw.push_enabled) };
1631
+ return parsePushPreferencesOut(raw);
1370
1632
  }
1371
1633
  /**
1372
1634
  * Flip the caller's global push kill switch. Pass `false` to mute
@@ -1379,7 +1641,7 @@ var NoveraChat = class {
1379
1641
  "/api/v1/users/me/push-preferences",
1380
1642
  { push_enabled: enabled }
1381
1643
  );
1382
- return { pushEnabled: Boolean(raw.push_enabled) };
1644
+ return parsePushPreferencesOut(raw);
1383
1645
  }
1384
1646
  // -------------------------------------------------------------------------
1385
1647
  // User blocks — KakaoTalk-style one-way block.
@@ -1403,11 +1665,7 @@ var NoveraChat = class {
1403
1665
  `/api/v1/users/me/blocks/${encodeURIComponent(userId)}`,
1404
1666
  { reason: reason ?? null }
1405
1667
  );
1406
- return {
1407
- blockedUserId: raw.blocked_user_id,
1408
- reason: raw.reason,
1409
- createdAt: raw.created_at
1410
- };
1668
+ return parseBlockOut(raw);
1411
1669
  }
1412
1670
  /** Unblock a user. Idempotent — resolves either way. */
1413
1671
  async unblockUser(userId) {
@@ -1419,23 +1677,7 @@ var NoveraChat = class {
1419
1677
  /** List every user the caller has blocked, newest first. */
1420
1678
  async listBlocks() {
1421
1679
  const raw = await this.http.request("GET", "/api/v1/users/me/blocks");
1422
- return raw.map((r) => ({
1423
- blockedUserId: r.blocked_user_id,
1424
- reason: r.reason,
1425
- createdAt: r.created_at
1426
- }));
1427
- }
1428
- _toDeviceInfo(raw) {
1429
- return {
1430
- deviceId: raw.device_id,
1431
- tokenType: raw.token_type,
1432
- tokenPreview: raw.token_preview,
1433
- os: raw.os,
1434
- appVersion: raw.app_version,
1435
- pushEnabled: raw.push_enabled,
1436
- lastActiveAt: raw.last_active_at,
1437
- createdAt: raw.created_at
1438
- };
1680
+ return raw.map((r) => parseBlockOut(r));
1439
1681
  }
1440
1682
  /**
1441
1683
  * Authorization-gated URL for downloading a file attachment.
@@ -1493,41 +1735,27 @@ var NoveraChat = class {
1493
1735
  * shipping date.
1494
1736
  */
1495
1737
  async getFileMeta(fileId) {
1496
- const raw = await this.http.request("GET", `/api/v1/files/${encodeURIComponent(fileId)}/meta`);
1738
+ const raw = await this.http.request(
1739
+ "GET",
1740
+ `/api/v1/files/${encodeURIComponent(fileId)}/meta`
1741
+ );
1742
+ const f = parseFileOut(raw);
1497
1743
  return {
1498
- id: raw.id,
1499
- kind: raw.kind,
1500
- mime: raw.mime,
1501
- size: raw.size_bytes,
1502
- name: raw.original_name ?? null,
1503
- width: raw.width ?? null,
1504
- height: raw.height ?? null,
1505
- durationSec: raw.duration_sec ?? null,
1506
- thumbnailStatus: raw.thumbnail_status,
1507
- forwardBlocked: Boolean(raw.forward_blocked)
1744
+ id: f.id,
1745
+ kind: f.kind,
1746
+ mime: f.mime,
1747
+ size: f.sizeBytes,
1748
+ name: f.originalName ?? null,
1749
+ width: f.width ?? null,
1750
+ height: f.height ?? null,
1751
+ durationSec: f.durationSec ?? null,
1752
+ thumbnailStatus: f.thumbnailStatus,
1753
+ forwardBlocked: Boolean(f.forwardBlocked)
1508
1754
  };
1509
1755
  }
1510
1756
  async unreadSummary() {
1511
1757
  const raw = await this.http.request("GET", "/api/v1/users/me/unread");
1512
- return {
1513
- total: raw.total,
1514
- rooms: raw.rooms.map((r) => ({
1515
- roomId: r.room_id,
1516
- name: r.name,
1517
- roomType: r.room_type ?? null,
1518
- unreadCount: r.unread_count,
1519
- lastMessageId: r.last_message_id,
1520
- lastMessagePreview: r.last_message_preview ?? null,
1521
- lastReadMessageId: r.last_read_message_id,
1522
- memberCount: r.member_count ?? 0,
1523
- members: (r.members_preview ?? []).map((m) => ({
1524
- userId: m.user_id,
1525
- nickname: m.nickname ?? null,
1526
- profileImageUrl: m.profile_image_url ?? null,
1527
- isOnline: m.is_online ?? null
1528
- }))
1529
- }))
1530
- };
1758
+ return parseUnreadSummary(raw);
1531
1759
  }
1532
1760
  /**
1533
1761
  * Accept an invite link — call this in flows where you receive a
@@ -1545,12 +1773,11 @@ var NoveraChat = class {
1545
1773
  * kicked from the target room.
1546
1774
  */
1547
1775
  async acceptInvite(token) {
1548
- const raw = await this.http.request("POST", `/api/v1/invites/${encodeURIComponent(token)}/accept`);
1549
- return {
1550
- roomId: raw.room_id,
1551
- roomName: raw.room_name,
1552
- wasAlreadyMember: raw.was_already_member
1553
- };
1776
+ const raw = await this.http.request(
1777
+ "POST",
1778
+ `/api/v1/invites/${encodeURIComponent(token)}/accept`
1779
+ );
1780
+ return parseInviteAcceptResponse(raw);
1554
1781
  }
1555
1782
  // ---- internal ----
1556
1783
  dispatch(msg) {